/* The register and its cover.
 *
 * The page is paper: flat, ruled, dense, and the only colour that speaks is
 * the colour of a verdict. The one dark surface is the chrome at the top —
 * the cover of the register — which carries identity, navigation and the
 * pulse of the fetch queue, and never a figure.
 *
 * Sections follow the order a page is built: reset, shell, panels, controls,
 * then one screen at a time.
 */

/* --- Reset and page ------------------------------------------------------ */

*, *::before, *::after { box-sizing: border-box; }

/* `hidden` is how every panel, trace and empty state on this page says it has
   nothing to show, and it is a *presentation* hint: the browser implements it
   as `[hidden] { display: none }` in the user-agent sheet, which any class
   rule setting `display` outranks. A trace panel styled `display: grid` was
   therefore painted over its own card while `element.hidden` sat true and
   every test agreed it was hidden. Restated here, and with `!important`,
   because the whole point is to outrank whatever the component declares. */
[hidden] { display: none !important; }

body {
  margin: 0;
  background: var(--paper);
  color: var(--ink);
  font-family: var(--font-sans);
  font-size: var(--text-base);
  line-height: 1.5;
  -webkit-font-smoothing: antialiased;
}

/* Every heading is set here, once. The display face never appears below
   --text-lg: Cormorant's hairlines vanish at label size, and a serif nobody
   can tell is a serif is a fallback that arrived on purpose. */
h1, h2, h3 { font-family: var(--font-display); letter-spacing: var(--track-display); font-weight: 700; }
h1 { margin: 0; font-size: var(--text-2xl); }
/* Every section heading is an h2: the page went h1 straight to h3, which
   reads to a screen reader as a level that was skipped, and looked identical
   either way. More air above a heading than below it — the gap belongs to the
   break, not to the title. */
h2 { margin: 0 0 var(--space); font-size: var(--text-xl); }
h3 { margin: calc(var(--space) * 3) 0 var(--space); font-size: var(--text-lg); }

/* One focus ring, declared once, on everything focusable. Two elements
   carried their own copy and every other control was left to whatever the
   browser drew — including the ones with no border and no background, where
   that is the only thing separating "here" from "somewhere on this page". */
:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }
/* On the cover the violet reaches 2.7:1 and stops being a ring. */
.app-topbar :focus-visible { outline-color: var(--accent-on-chrome); }

.icon { width: 1em; height: 1em; flex: none; }

/* For a label whose text is already on screen as the panel's own header.
   Removing the label instead would leave the field with no accessible name;
   clipping it keeps the name and drops the second copy of the words. */
.visually-hidden {
  position: absolute;
  width: 1px;
  height: 1px;
  margin: -1px;
  padding: 0;
  overflow: hidden;
  clip-path: inset(50%);
  white-space: nowrap;
}

/* --- App shell ----------------------------------------------------------- */

.app-shell { display: flex; flex-direction: column; min-height: 100vh; min-height: 100dvh; }

/* The page scrolls under the bar rather than pushing it away: the queue pulse
   and the way back to the watchlist are the two things that must never end up
   somewhere above the fold. */
.app-topbar {
  position: sticky;
  top: 0;
  z-index: var(--z-chrome);
  display: flex;
  align-items: center;
  flex-wrap: wrap;
  gap: var(--space);
  /* A floor, not a height: the search field drops to a second line on a phone,
     and a fixed height would cut it off rather than let the bar grow. */
  min-height: var(--topbar-height);
  padding: var(--space-half) calc(var(--space) * 3);
  background: linear-gradient(160deg, var(--chrome) 0%, var(--chrome-deep) 100%);
  border-bottom: 1px solid var(--chrome-rule);
}

/* The one violet line in the app, and the only place the accent is a surface
   rather than a mark of state: it is the gilt edge of the cover, and it is
   one pixel, which is the difference between an edge and a decoration. */
.app-topbar::after {
  content: "";
  position: absolute;
  inset: auto 0 -1px 0;
  height: 1px;
  background: linear-gradient(90deg, transparent, var(--accent) 35%, var(--accent-on-chrome) 50%, var(--accent) 65%, transparent);
}

.brand { display: inline-flex; align-items: center; gap: var(--space); color: var(--on-chrome); text-decoration: none; }
.brand-mark { width: 20px; height: 20px; color: var(--accent-on-chrome); }
.brand-name {
  font-family: var(--font-display);
  font-size: var(--text-xl);
  font-weight: 700;
  letter-spacing: 0.03em;
  white-space: nowrap;
}

.topbar-nav { display: flex; align-items: center; gap: var(--space-half); margin-left: var(--space); }

.nav-link {
  position: relative;
  display: inline-flex;
  align-items: center;
  gap: 6px;
  padding: var(--space-half) var(--space);
  border-radius: var(--radius);
  color: var(--on-chrome-muted);
  text-decoration: none;
  font-size: var(--text-sm);
  font-weight: 600;
  transition: color var(--fast) var(--ease), background-color var(--fast) var(--ease);
}
.nav-link .icon { width: 15px; height: 15px; }
.nav-link:hover { color: var(--on-chrome); background: var(--chrome-hover); }

/* The current page is named twice: full-strength text, and a rule under it.
   Colour alone would leave the state to a 1.4:1 difference in grey. */
.nav-link.is-current { color: var(--on-chrome); }
.nav-link.is-current::after {
  content: "";
  position: absolute;
  inset: auto var(--space) 2px var(--space);
  height: 2px;
  border-radius: 2px;
  background: var(--accent-on-chrome);
}

.topbar-right { display: flex; align-items: center; gap: var(--space); margin-left: auto; }
.signed-in { color: var(--on-chrome-muted); font-size: var(--text-xs); }

/* --- Search, in the bar --------------------------------------------------- */
/* Adding a stock was the first panel of the stack: a panel's height spent, on
   every screen, on the one gesture a periodic review never makes. It is a
   gesture, so it lives in the bar with the other gestures.
   The bar's rule still holds — no figure and no verdict up here. Exchange,
   currency and the coverage warning all land in the paper panel below. */

/* Centred in the slack. Left against the navigation it read as a fourth item
   of the nav; the bar's two clusters now bracket it. */
.topbar-search { position: relative; display: flex; align-items: center; flex: 1 1 16rem; max-width: 34rem; margin-inline: auto; }

.search-field {
  width: 100%;
  padding: var(--space-half) var(--space) var(--space-half) calc(var(--space) * 4);
  border: 1px solid var(--chrome-rule);
  border-radius: var(--radius);
  /* White at 7% over the cover: --on-chrome reads 12.3:1 on it and the
     placeholder's --on-chrome-muted 4.6:1, both above the floor. */
  background: var(--chrome-hover);
  color: var(--on-chrome);
  font: inherit;
  font-size: var(--text-sm);
  transition: border-color var(--fast) var(--ease), background-color var(--fast) var(--ease);
}
.search-field::placeholder { color: var(--on-chrome-muted); }
.search-field:hover, .search-field:focus { border-color: var(--accent-on-chrome); background: var(--chrome-rule); }

/* A submit button wearing a magnifier, not a magnifier drawn beside a field:
   Enter is the only other way to run a search, and a thumb has no Enter. */
.search-go {
  position: absolute;
  left: var(--space-half);
  display: inline-flex;
  padding: var(--space-half);
  border: none;
  border-radius: var(--radius);
  background: transparent;
  color: var(--on-chrome-muted);
  cursor: pointer;
  transition: color var(--fast) var(--ease);
}
.search-go:hover { color: var(--on-chrome); }
.search-go .icon { width: 15px; height: 15px; }

/* --- The queue pulse ----------------------------------------------------- */
/* The one animated thing in the app, and the only always-on report of a
   subsystem the user cannot otherwise see. A stopped drain loop is
   indistinguishable from an empty queue; this says which, on every page.
   A paper pill on the dark cover, because a status colour laid straight on
   the chrome reaches 2.7:1 — inside the pill it is back on --surface. */

.pulse {
  display: inline-flex;
  align-items: center;
  gap: 6px;
  padding: 3px var(--space);
  border: 1px solid var(--rule);
  border-radius: var(--radius-pill);
  background: var(--surface);
  color: var(--muted);
  font-family: var(--font-mono);
  font-size: var(--text-2xs);
  font-weight: 500;
  white-space: nowrap;
  text-decoration: none;
  transition: border-color var(--base) var(--ease);
}
.pulse:hover { border-color: var(--accent); }

.pulse-dot { width: 7px; height: 7px; border-radius: 50%; background: var(--status-na); flex: none; }
.pulse.is-idle .pulse-dot { background: var(--status-met); }
.pulse.is-working { color: var(--accent-text); border-color: var(--accent); }
.pulse.is-working .pulse-dot { background: var(--accent); animation: pulse-ring 1.8s var(--ease) infinite; }
.pulse.is-failing { color: var(--status-broken); border-color: var(--status-broken); }
.pulse.is-failing .pulse-dot { background: var(--status-broken); }
.pulse.is-stopped { color: var(--status-broken); border-color: var(--status-broken); background: var(--wash-broken); }
.pulse.is-stopped .pulse-dot { background: var(--status-broken); animation: pulse-ring 1.8s var(--ease) infinite; }

/* Decays outward from an already-visible dot: the ring is work in flight, not
   an entrance. */
@keyframes pulse-ring {
  0% { box-shadow: 0 0 0 0 var(--accent-halo); }
  70% { box-shadow: 0 0 0 6px transparent; }
  100% { box-shadow: 0 0 0 0 transparent; }
}

/* --- Page body and the panel stack --------------------------------------- */

/* No measure. The old 1180px cap was written for prose, and this screen is not
   prose: it is a grid of cards read in blocks, not in lines. Capped, a 1920px
   window drew three columns where it holds six, and fifty stocks — the stated
   volume — became seventeen rows nobody could see the end of. */
.app-main {
  flex: 1;
  width: 100%;
  padding: calc(var(--space) * 3) calc(var(--space) * 3) calc(var(--space) * 8);
}

/* The vertical rhythm lives here and nowhere else: a panel dropped into the
   stack is spaced without knowing it is in one, and no panel carries a margin
   of its own to double the gap. */
.panel-stack { display: flex; flex-direction: column; gap: var(--gap-panel); }

.panel { border: 1px solid var(--rule); border-radius: var(--radius-panel); background: var(--surface); }
.panel:empty { display: none; }

.panel-head {
  display: flex;
  align-items: center;
  gap: var(--space);
  padding: calc(var(--space) * 1.5) calc(var(--space) * 2);
  border-bottom: 1px solid var(--rule-faint);
}
.panel-head .icon { width: 16px; height: 16px; color: var(--muted); }

/* A ruled column header, not a title: uppercase and tracked, in the sans.
   This is where the register speaks at small sizes — the display face is
   reserved for headings big enough to show that it is one. */
.panel-title {
  margin: 0;
  font-family: var(--font-sans);
  font-size: var(--text-2xs);
  font-weight: 700;
  letter-spacing: var(--track-caps);
  text-transform: uppercase;
  color: var(--ink);
}
/* Beside the title, not flushed to the far edge. With the page's width cap
   gone, `margin-left: auto` threw a single digit eighteen hundred pixels away
   from the words that say what it counts — a figure nobody can pair with its
   label is a figure that says nothing. The count belongs to the heading here
   exactly as it does on `.section-count`. */
.panel-note { font-family: var(--font-mono); font-size: var(--text-2xs); color: var(--muted); }
.panel-body { padding: calc(var(--space) * 2); }
/* For lists and tables that rule to the panel's own edge. */
.panel-body-flush { padding: 0; }

/* The banner PRODUCT.md calls the real home screen. The only panel whose
   header takes the accent — not decoration: it is the one panel that says
   "start here", and it is absent from the page entirely when it has nothing
   to say. */
.panel-lead .panel-head { border-bottom-color: var(--rule); }
.panel-lead .panel-title { color: var(--accent-text); }
.panel-lead .panel-head .icon { color: var(--accent); }

/* --- Buttons and controls ------------------------------------------------ */

.btn {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: 6px;
  padding: var(--space) calc(var(--space) * 2);
  border: 1px solid var(--rule);
  border-radius: var(--radius);
  background: var(--surface);
  color: var(--ink);
  font: inherit;
  font-size: var(--text-sm);
  font-weight: 600;
  white-space: nowrap;
  cursor: pointer;
  transition: border-color var(--fast) var(--ease), background-color var(--fast) var(--ease), color var(--fast) var(--ease);
}
.btn:hover:not(:disabled) { border-color: var(--accent); color: var(--accent-text); }
.btn:disabled { cursor: not-allowed; opacity: 0.55; }
.btn .icon { width: 15px; height: 15px; }

.btn-primary { border-color: var(--accent); background: var(--accent); color: var(--on-accent); }
.btn-primary:hover:not(:disabled) { background: var(--accent-text); border-color: var(--accent-text); color: var(--on-accent); }

/* Available, not advertised: the actions at the foot of a card. */
.btn-quiet {
  padding: var(--space-half) var(--space);
  color: var(--muted);
  font-size: var(--text-2xs);
  font-weight: 600;
}
.btn-danger:hover:not(:disabled) { border-color: var(--status-broken); color: var(--status-broken); background: var(--wash-broken); }
/* A button mid-action is not a button that failed: the cursor says wait. */
.btn.is-busy { cursor: progress; }

/* A button that acts like a link, because signing out is a POST: rendering it
   as an <a> would invite a browser to prefetch it. */
.link-button {
  display: inline-flex;
  align-items: center;
  gap: 5px;
  padding: var(--space-half) var(--space);
  border: 1px solid transparent;
  border-radius: var(--radius);
  background: transparent;
  color: var(--on-chrome-muted);
  font: inherit;
  font-size: var(--text-xs);
  font-weight: 600;
  cursor: pointer;
  transition: color var(--fast) var(--ease), background-color var(--fast) var(--ease);
}
.link-button:hover { color: var(--on-chrome); background: var(--chrome-hover); }
.link-button .icon { width: 15px; height: 15px; }

/* Three distinct buttons, never a cycling toggle: a stray click lands on a
   deliberate value rather than the next one in a cycle. Joined into one
   control so the three read as one question with three answers. */
.state-group {
  display: inline-flex;
  overflow: hidden;
  border: 1px solid var(--rule);
  border-radius: var(--radius);
  background: var(--surface);
}
.state-button {
  min-width: 44px;
  padding: var(--space-half) var(--space);
  border: none;
  border-left: 1px solid var(--rule);
  background: transparent;
  color: var(--muted);
  font: inherit;
  font-size: var(--text-sm);
  font-weight: 600;
  cursor: pointer;
  transition: background-color var(--fast) var(--ease), color var(--fast) var(--ease);
}
.state-button:first-child { border-left: none; }
/* `.is-accept` is excluded rather than left to a later rule: this selector is
   (0,4,0) and `.state-button.is-accept` only (0,2,0), so declaration order
   never got a say — hovering the one filled button in the group turned it grey,
   dropping the primary action's affordance exactly under the cursor. */
.state-button:hover:not(:disabled):not(.is-selected):not(.is-accept) { background: var(--sunken); color: var(--ink); }

/* A selected answer wears the status it produces, not the accent. `yes` grades
   `met`, `no` grades `broken`, and no answer at all grades `na` — that mapping
   is the domain's (see ANSWER_STATUS in api/dashboard.py), and borrowing the
   accent for it made a stock nobody has judged yet render as twelve blocks of
   solid violet: a wall of "chosen" over a column of unanswered questions.
   Weight and an inset ring carry the selection alongside the colour, so the
   state does not rest on hue. */
.state-button.is-selected { font-weight: 700; box-shadow: inset 0 0 0 1px currentColor; }
.state-button[data-answer="yes"].is-selected { background: var(--wash-met); color: var(--status-met); }
.state-button[data-answer="no"].is-selected { background: var(--wash-broken); color: var(--status-broken); }
.state-button[data-answer="unanswered"].is-selected { background: var(--wash-na); color: var(--status-na); }
.state-button:disabled { cursor: not-allowed; opacity: 0.55; }
/* The group clips its own corners, so an outset ring would be clipped too. */
.state-button:focus-visible { outline-offset: -3px; }

/* --- Sign in ------------------------------------------------------------- */
/* The cover, full bleed, with the register's own card laid on it. This page
   carried no class and no rule of its own: it rendered in raw browser
   defaults — Times, full-bleed inputs, a grey system button — which is the
   first thing anyone sees of this app. */

.login-page {
  display: grid;
  place-items: center;
  min-height: 100vh;
  min-height: 100dvh;
  padding: calc(var(--space) * 3);
  background: linear-gradient(160deg, var(--chrome) 0%, var(--chrome-deep) 100%);
}

.login-panel { width: min(23rem, 100%); }
.login-brand {
  display: flex;
  align-items: center;
  justify-content: center;
  gap: var(--space);
  margin-bottom: calc(var(--space) * 3);
  color: var(--on-chrome);
}
.login-brand .icon { width: 26px; height: 26px; color: var(--accent-on-chrome); }
.login-wordmark { font-family: var(--font-display); font-size: var(--text-2xl); font-weight: 700; letter-spacing: 0.03em; }

/* Flat, like everything at rest. A drop shadow here would be the app's only
   inert elevation, on the one screen that sets the expectation for the rest. */
#login-form {
  display: grid;
  gap: calc(var(--space) * 2);
  padding: calc(var(--space) * 3);
  border: 1px solid var(--rule);
  border-radius: var(--radius-panel);
  background: var(--surface);
}

#login-form label {
  display: grid;
  gap: var(--space-half);
  color: var(--muted);
  font-size: var(--text-2xs);
  font-weight: 700;
  letter-spacing: var(--track-caps);
  text-transform: uppercase;
}

.field {
  padding: var(--space) calc(var(--space) * 1.5);
  border: 1px solid var(--rule);
  border-radius: var(--radius);
  background: var(--surface);
  color: var(--ink);
  font: inherit;
  font-size: var(--text-base);
  transition: border-color var(--fast) var(--ease);
}
.field:hover { border-color: var(--muted); }
.field::placeholder { color: var(--muted); }

/* Empty until something goes wrong: an always-present error line is a line
   nobody reads when it finally says something. */
#login-error {
  margin: 0;
  padding: var(--space) calc(var(--space) * 1.5);
  border-radius: var(--radius);
  background: var(--wash-broken);
  color: var(--status-broken);
  font-size: var(--text-sm);
}
#login-error:empty { display: none; }

/* --- Search results ------------------------------------------------------- */
/* Paper, hanging off the bar. The shadow is licit here and only here among the
   search's parts: the panel exists because somebody asked, which is the whole
   test this design system applies to elevation. */

.search-panel {
  position: absolute;
  z-index: var(--z-reply);
  top: calc(100% + var(--space));
  left: 0;
  right: 0;
  /* Wider than the field on a desktop, flush with it on a phone: ten results
     with their coverage warnings need room the bar does not have. */
  min-width: min(24rem, 100%);
  max-height: min(70vh, 32rem);
  overflow-y: auto;
  padding: var(--space);
  border: 1px solid var(--rule);
  border-radius: var(--radius-panel);
  background: var(--surface);
  box-shadow: 0 12px 32px var(--shadow-deep);
}
.search-results { list-style: none; margin: 0; padding: 0; display: grid; gap: var(--space-half); }

/* Ruled, not boxed. Framing each row inside a panel that is itself framed is a
   container drawn around containers — the one thing the rule exists to avoid.
   The hover is the row's own affordance and needs no second edge. */
.search-result {
  display: flex;
  align-items: baseline;
  gap: var(--space);
  flex-wrap: wrap;
  padding: var(--space);
  border-bottom: 1px solid var(--rule-faint);
  border-radius: var(--radius);
  transition: background-color var(--fast) var(--ease);
}
.search-result:last-child { border-bottom: none; }
.search-result:hover { background: var(--accent-wash); }

.search-add {
  display: inline-flex;
  align-items: baseline;
  gap: var(--space);
  padding: 0;
  border: none;
  background: none;
  color: var(--accent-text);
  font: inherit;
  font-weight: 600;
  text-align: left;
  cursor: pointer;
}
.search-add:disabled { color: var(--muted); cursor: not-allowed; }
.search-ticker { font-family: var(--font-mono); font-weight: 600; }
.search-name { font-weight: 400; }

.search-venue { margin-left: auto; color: var(--muted); font-family: var(--font-mono); font-size: var(--text-xs); }
.search-etf {
  padding: 1px 6px;
  border: 1px solid var(--rule);
  border-radius: var(--radius-pill);
  color: var(--muted);
  font-size: var(--text-2xs);
  font-weight: 700;
  letter-spacing: var(--track-caps);
}

/* Shown before the user commits, not after: the whole point of the warning is
   that it arrives while adding the ticker is still a decision. */
.search-warning { flex-basis: 100%; margin: var(--space-half) 0 0; color: var(--status-borderline); font-size: var(--text-sm); }
.search-status { margin: 0; color: var(--muted); font-size: var(--text-sm); }
.search-error { margin: var(--space-half) 0 0; color: var(--status-broken); font-size: var(--text-sm); }

/* --- The two lists ------------------------------------------------------- */
/* Owned above, watched below. The two sections are separated by air rather
   than by a box: a second border inside a page already ruled at every level
   would be a container drawn around containers. */

.stock-section + .stock-section { margin-top: calc(var(--space) * 4); }
.section-head { display: flex; align-items: baseline; gap: var(--space); }
/* Mono, like every other figure meant to be compared with the one below it. */
.section-count { font-family: var(--font-mono); font-size: var(--text-sm); font-weight: 400; color: var(--muted); }

/* min() and not a bare 18rem: below 288px of usable width the track keeps its
   minimum and the card pushes the whole page sideways — which is every phone
   in portrait, the device this list is actually read on.
   Eighteen and not twenty-two: with the page's width cap gone, this is what
   turns three columns into six. Each card grows a little — twenty-three pills
   wrap onto more rows at this width — and the grid still loses about half its
   rows, which is the figure that decides. */
.stock-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(min(18rem, 100%), 1fr)); gap: var(--gap-panel); }
.grid-empty { margin: 0; color: var(--muted); }

/* An empty state that says what to do next, not that there is nothing here. */
.empty-state {
  display: grid;
  justify-items: center;
  gap: var(--space);
  padding: calc(var(--space) * 6) calc(var(--space) * 2);
  border: 1px dashed var(--rule);
  border-radius: var(--radius-panel);
  background: var(--surface);
  text-align: center;
}
.empty-state .icon { width: 28px; height: 28px; color: var(--muted); }
.empty-state p { margin: 0; max-width: 38ch; color: var(--muted); }
.empty-state strong { display: block; color: var(--ink); font-family: var(--font-display); font-size: var(--text-lg); font-weight: 700; }

/* --- The card ------------------------------------------------------------ */
/* Flat at rest: no shadow on a card, one rule, and that rule is the verdict.
 *
 * The verdict used to be a 3px band across the top edge. It is the whole
 * card's outline now, for two reasons. The band is the single most
 * recognisable tell of a generated interface, and — the reason that actually
 * decides it — this design system says the one-pixel rule is its only
 * structural tool. A three-pixel bar is a second one, doing the same job
 * louder. Colouring the rule the card already has costs nothing, reads across
 * a grid of twenty, and leaves the loud carrier where it belongs: the word in
 * the header, on its wash. */

/* A column: the grid stretches every card in a row to the tallest of them, and
   a card with no divergence to report was leaving its action bar stranded
   halfway up a white rectangle. Pushed to the foot, the bars line up across
   the row and the empty space lands where empty space belongs. */
.stock-card {
  position: relative;
  display: flex;
  flex-direction: column;
  /* One gap for the whole column instead of a margin on each block. Every
     child then spaces itself, including the ones a criterion catalog change
     adds later, and the auto margin at the foot has a floor to sit above. */
  gap: var(--space);
  padding: calc(var(--space) * 2);
  border: 1px solid var(--rule);
  border-radius: var(--radius-panel);
  background: var(--surface);
}

.stock-card.is-ok { border-color: var(--status-met); }
.stock-card.is-borderline { border-color: var(--status-borderline); }
.stock-card.is-ko { border-color: var(--status-broken); }
/* Nothing is being claimed about an incomplete stock yet, and the card says so
   before the word does: a provisional edge, all the way round. */
.stock-card.is-incomplete { border-color: var(--status-na); border-style: dashed; }

.card-head { display: flex; align-items: baseline; gap: var(--space); flex-wrap: wrap; }

.card-title {
  display: inline-flex;
  align-items: baseline;
  gap: var(--space);
  /* Both min-widths are load-bearing: a flex item's floor is its content, so
     without them the company name refuses to shrink, the header wraps, and the
     score drops onto a second line on every card with a long name.
     The basis is 4rem and not `auto` for the same reason, one step earlier: a
     wrapping flex container assigns items to lines by their *unshrunk* size, so
     a title basing itself on its content wraps before it ever gets the chance
     to shrink. At 18rem tracks that put the score on a second line on every
     card whose verdict word was long — a ragged run of heads across the grid,
     one and two lines deep with nothing but the name deciding which. */
  flex: 1 1 4rem;
  min-width: 0;
  padding: 0;
  border: none;
  background: none;
  color: inherit;
  font: inherit;
  text-align: left;
  text-decoration: none;
  cursor: pointer;
}
/* The ticker is what the eye travels down a column on, so it is mono and it is
   first; the company name is the gloss, and it is neither. */
.card-ticker { flex: none; font-family: var(--font-mono); font-size: var(--text-base); font-weight: 600; }
.card-name { min-width: 0; overflow: hidden; color: var(--muted); font-size: var(--text-sm); text-overflow: ellipsis; white-space: nowrap; }
.card-title:hover .card-ticker { color: var(--accent-text); }

/* The verdict is a field, not a word in colour: at fifty cards the eye finds
   the shape before it reads anything. The word stays — colour is never the
   only carrier of a status here. */
.card-status {
  padding: 2px var(--space);
  border-radius: var(--radius-pill);
  font-size: var(--text-2xs);
  font-weight: 700;
  letter-spacing: var(--track-caps);
  text-transform: uppercase;
  white-space: nowrap;
}
.card-status.status-ok { color: var(--status-met); background: var(--wash-met); }
.card-status.status-borderline { color: var(--status-borderline); background: var(--wash-borderline); }
.card-status.status-ko { color: var(--status-broken); background: var(--wash-broken); }
.card-status.status-incomplete { color: var(--status-na); background: var(--wash-na); }
/* Two scores are only comparable when their digits line up. */
.card-score { margin-left: auto; font-family: var(--font-mono); font-size: var(--text-sm); color: var(--muted); }

/* wrap, not scroll: a criterion added to the catalog adds a pill, and it must
   fall onto a second line rather than overflow the card. */
.pill-row { position: relative; display: flex; flex-wrap: wrap; gap: var(--space-half); }
/* Deliberately not the positioned ancestor: see .metric-tooltip. */
.metric-tooltip-holder { position: static; }

.pill {
  display: inline-flex;
  align-items: baseline;
  gap: var(--space-half);
  padding: 2px 7px;
  border: 1px solid var(--rule);
  border-radius: var(--radius);
  background: var(--surface);
  font: inherit;
  font-size: var(--text-2xs);
  font-weight: 600;
  cursor: pointer;
  transition: background-color var(--fast) var(--ease);
}
.metric-trigger-value { font-family: var(--font-mono); font-weight: 500; }
.metric-trigger-status { font-size: var(--text-2xs); font-weight: 700; text-transform: uppercase; }
.pill.status-met { color: var(--status-met); border-color: var(--status-met); }
.pill.status-borderline { color: var(--status-borderline); border-color: var(--status-borderline); }
.pill.status-broken { color: var(--status-broken); border-color: var(--status-broken); }
.pill.status-na { color: var(--status-na); border-color: var(--status-na); }
.pill:hover { background: var(--paper); }
/* An open pill is ringed in its own status, so a panel spanning the whole card
   is never ambiguous about which of twenty pills it belongs to. */
.pill[aria-expanded="true"] { background: var(--paper); outline: 1px solid currentColor; outline-offset: -2px; }

/* The shadow is a reply: the tooltip appears because the user asked.
   Anchored to the pill row, not to the pill: a 20rem panel hanging off a
   six-pixel pill in the last column ran clean off the right edge of the
   viewport, and the trace it was clipping is the whole reason the pill is a
   button. Spanning the card instead, it opens under the row it belongs to,
   inside the card, at every width — and the card can never be off-screen. */
.metric-tooltip {
  position: absolute;
  z-index: var(--z-reply);
  top: calc(100% + var(--space-half));
  left: 0;
  right: 0;
  display: grid;
  gap: 3px;
  padding: calc(var(--space) * 1.5);
  border: 1px solid var(--rule);
  border-radius: var(--radius);
  background: var(--surface);
  box-shadow: 0 6px 24px var(--shadow);
  font-size: var(--text-xs);
  line-height: 1.55;
  overflow-wrap: anywhere;
}
.tooltip-title { font-weight: 700; }
.tooltip-verdict { font-family: var(--font-mono); font-weight: 700; }
.tooltip-reason { color: var(--status-na); }
.tooltip-divergence { color: var(--status-borderline); font-weight: 600; }
.tooltip-period, .tooltip-threshold { color: var(--muted); }
/* A step wraps rather than being cut: half of "CFO 265,817,658 - capex
   132,908,829" is not a smaller version of the calculation, it is a different
   number. */
.tooltip-step { font-family: var(--font-mono); color: var(--ink); overflow-wrap: anywhere; }
.tooltip-source {
  padding-top: var(--space-half);
  border-top: 1px solid var(--rule-faint);
  color: var(--muted);
  font-family: var(--font-mono);
  font-size: var(--text-2xs);
}
.tooltip-source-name { color: var(--ink); font-weight: 600; }

.card-blocking, .card-divergence, .card-nodata, .card-error {
  display: flex;
  align-items: flex-start;
  gap: 6px;
  margin: 0;
  font-size: var(--text-sm);
}
.card-blocking .icon, .card-divergence .icon, .card-nodata .icon, .card-error .icon { width: 15px; height: 15px; margin-top: 3px; }
.card-blocking { color: var(--status-broken); }
.card-divergence { color: var(--status-borderline); }
.card-nodata { color: var(--status-na); }
/* The exact message, next to the button that produced it and left on screen
   while the user decides what to do about it. */
.card-error { color: var(--status-broken); }

/* The actions sit at the foot of the card, quiet: they are available, not
   advertised. The verdict is what the card is for. */
/* `auto` takes whatever slack the stretched card has and puts it above the
   rule; the column's own gap is what guarantees a floor when there is none. */
.card-actions-holder {
  display: flex;
  flex-direction: column;
  gap: var(--space);
  margin-top: auto;
  padding-top: calc(var(--space) * 1.5);
  border-top: 1px solid var(--rule-faint);
}
.card-actions { display: flex; gap: var(--space-half); flex-wrap: wrap; }

/* --- What changed, and the ETFs ------------------------------------------ */

.delta-list, .etf-list { list-style: none; margin: 0; padding: 0; }

/* The banner folds, and opens on arrival. PRODUCT.md calls it the real home
   screen, so the fold is a courtesy offered to the reader — never a default
   that hides the one thing this screen exists to say. */
.changes-banner > summary { cursor: pointer; list-style: none; }
.changes-banner > summary::-webkit-details-marker { display: none; }
/* Closed, the summary's rule would sit one pixel above the panel's own bottom
   edge and read as a thicker line for no reason. */
.changes-banner:not([open]) > summary { border-bottom: none; }
.changes-banner .chevron {
  width: 14px;
  height: 14px;
  color: var(--accent);
  transition: transform var(--fast) var(--ease);
}
/* One glyph, rotated. Two drawings could disagree about which state is which,
   and it turns from an already-visible position: nothing enters animating. */
.changes-banner[open] > summary .chevron { transform: rotate(90deg); }

/* Movements flow into columns, separated by white rather than by a rule. In an
   auto-fill grid `:last-child` is a single cell, so a bottom rule would draw
   under every cell of the last row except one — a rule that lies about the
   structure it claims to describe. The mono ticker opens each cell and stands
   in for its left edge.
   The ceiling stays: at fifty stocks an earnings season scrolls inside the
   banner rather than pushing the list it comments on below the fold. */
.delta-list {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(min(24rem, 100%), 1fr));
  gap: var(--space) calc(var(--space) * 3);
  padding: calc(var(--space) * 1.5) calc(var(--space) * 2);
  max-height: 40vh;
  overflow-y: auto;
}

/* A two-column grid rather than a wrapping row: as a flex line the whole block
   of movements is one item, so a stock with four of them dropped the lot onto
   a second line and left its ticker alone on the first. The ticker keeps its
   column, the movements wrap inside theirs, and every row starts at the same
   x whatever it carries. Six rem and not seven: inside a 24rem cell, seven
   left the movements two words wide. */
.delta-list li {
  display: grid;
  grid-template-columns: 6rem 1fr;
  align-items: baseline;
  gap: var(--space);
}
.etf-list li {
  display: flex;
  align-items: baseline;
  gap: var(--space);
  flex-wrap: wrap;
  padding: var(--space) calc(var(--space) * 2);
  border-bottom: 1px solid var(--rule-faint);
}
.etf-list li:last-child { border-bottom: none; }

.delta-ticker { font-family: var(--font-mono); font-weight: 600; }
.delta-moves { display: flex; flex-wrap: wrap; gap: var(--space-half) calc(var(--space) * 2); }
/* Each criterion keeps its own reading: one stock can improve on one line and
   break on the next, and a single colour for the row would have to pick. */
.delta { display: inline-flex; align-items: baseline; gap: 5px; font-size: var(--text-sm); }
.delta-label { color: var(--muted); }
.delta-arrow { color: var(--muted); }

.chip {
  padding: 1px 6px;
  border-radius: var(--radius-pill);
  font-size: var(--text-2xs);
  font-weight: 700;
  letter-spacing: 0.02em;
  white-space: nowrap;
}
.chip.status-met { color: var(--status-met); background: var(--wash-met); }
.chip.status-borderline { color: var(--status-borderline); background: var(--wash-borderline); }
.chip.status-broken { color: var(--status-broken); background: var(--wash-broken); }
.chip.status-na { color: var(--status-na); background: var(--wash-na); }
/* Where a value came *from* is history, not a verdict: outlined, so the two
   chips on one line cannot be read as two current states. */
.chip.is-past { background: transparent; box-shadow: inset 0 0 0 1px currentColor; font-weight: 600; }

.etf-ticker { min-width: 6rem; font-family: var(--font-mono); font-weight: 600; }
.etf-name { color: var(--muted); font-size: var(--text-sm); }
.etf-owned {
  margin-left: auto;
  padding: 1px 6px;
  border-radius: var(--radius-pill);
  background: var(--accent-wash);
  color: var(--accent-text);
  font-size: var(--text-2xs);
  font-weight: 700;
  letter-spacing: var(--track-caps);
  text-transform: uppercase;
}

/* --- Fetch queue --------------------------------------------------------- */

.worker-line { display: flex; align-items: center; gap: var(--space); flex-wrap: wrap; margin: 0; }
.worker-state { display: inline-flex; align-items: center; gap: 6px; color: var(--status-met); font-weight: 600; }
.worker-line.is-stopped .worker-state { color: var(--status-broken); }
/* The exact message and the streak: one failure is weather, twenty is a wall,
   and a truncated message tells neither. */
.worker-error { color: var(--status-broken); font-family: var(--font-mono); font-size: var(--text-xs); overflow-wrap: anywhere; }

.quota-list { display: grid; grid-template-columns: repeat(auto-fit, minmax(min(15rem, 100%), 1fr)); gap: var(--space); list-style: none; margin: 0; padding: 0; }
.quota-item { display: grid; gap: 6px; padding: var(--space) calc(var(--space) * 1.5); border: 1px solid var(--rule); border-radius: var(--radius); }
.quota-head { display: flex; align-items: baseline; gap: var(--space); }
.quota-provider { font-size: var(--text-sm); font-weight: 600; }
.quota-count { margin-left: auto; color: var(--muted); font-family: var(--font-mono); font-size: var(--text-sm); }
/* The budget decides whether a fetch happens today, so it is drawn as well as
   counted: a track, and what is left of it. */
.quota-meter { height: 4px; overflow: hidden; border-radius: var(--radius-pill); background: var(--sunken); }
.quota-fill { height: 100%; background: var(--status-met); }
.quota-item.is-low .quota-fill { background: var(--status-borderline); }
.quota-item.is-exhausted { border-color: var(--status-broken); }
.quota-item.is-exhausted .quota-fill { background: var(--status-broken); }
.quota-item.is-exhausted .quota-count { color: var(--status-broken); }
.quota-uncapped { color: var(--muted); font-family: var(--font-mono); font-size: var(--text-xs); }

/* The table scrolls inside its own box; the page never does. Six columns do
   not fit a phone, and a page that scrolls sideways loses the header too. */
.table-scroll { overflow-x: auto; }
.queue-table { width: 100%; border-collapse: collapse; font-size: var(--text-sm); }
.queue-table th, .queue-table td { padding: var(--space) calc(var(--space) * 2); text-align: left; vertical-align: top; }
.queue-table thead th {
  position: sticky;
  top: 0;
  background: var(--sunken);
  border-bottom: 1px solid var(--rule);
  color: var(--muted);
  font-size: var(--text-2xs);
  font-weight: 700;
  letter-spacing: var(--track-caps);
  text-transform: uppercase;
  white-space: nowrap;
}
.queue-table tbody tr { border-bottom: 1px solid var(--rule-faint); }
.queue-table tbody tr:last-child { border-bottom: none; }
.queue-table tbody tr:hover { background: var(--paper); }
.job-ticker { font-family: var(--font-mono); font-weight: 600; }
.job-provider { color: var(--muted); }
/* Every figure meant to be read against another figure is mono. */
.job-attempts { font-family: var(--font-mono); text-align: right; }
/* The exact message wraps rather than being clipped: truncating it would throw
   away the part that distinguishes a quota ceiling from a dead key. */
.job-error { min-width: 16rem; color: var(--muted); font-size: var(--text-xs); }
.job-error-text { white-space: pre-wrap; overflow-wrap: anywhere; }
.queue-empty { color: var(--muted); }

/* --- Company analysis ---------------------------------------------------- */

.company-main { max-width: 82rem; margin-inline: auto; }
.company-sheet {
  overflow: hidden;
  border: 1px solid var(--rule);
  border-radius: var(--radius-panel);
  background: var(--surface);
}
.company-head {
  display: grid;
  grid-template-columns: minmax(0, 1fr) auto;
  gap: calc(var(--space) * 2);
  align-items: end;
  padding: calc(var(--space) * 3);
  border-bottom: 1px solid var(--rule);
}
.company-overview { min-width: 0; }
.company-back {
  grid-column: 1 / -1;
  width: fit-content;
  color: var(--muted);
  font-size: var(--text-xs);
  font-weight: 600;
  text-decoration-thickness: 1px;
  text-underline-offset: 3px;
}
.company-summary { min-width: 0; }
.company-identity { display: flex; align-items: baseline; gap: calc(var(--space) * 2); min-width: 0; }
.company-ticker { flex: none; font-family: var(--font-mono); }
.company-name { min-width: 0; margin: 0; overflow: hidden; color: var(--muted); text-overflow: ellipsis; white-space: nowrap; }
.company-facts {
  display: flex;
  flex-wrap: wrap;
  gap: var(--space-half) calc(var(--space) * 2);
  margin: var(--space) 0 0;
  font-size: var(--text-xs);
}
.company-facts dt { color: var(--muted); }
.company-facts dd { margin: 0; font-family: var(--font-mono); }
.company-verdicts {
  display: flex;
  flex-wrap: wrap;
  gap: var(--space-half) calc(var(--space) * 2);
  margin-top: calc(var(--space) * 2);
  font-size: var(--text-xs);
}
.company-verdict { white-space: nowrap; }
.company-verdict strong { font-family: var(--font-mono); }
.company-verdict.status-met { color: var(--status-met); }
.company-verdict.status-borderline { color: var(--status-borderline); }
.company-verdict.status-broken { color: var(--status-broken); }
.company-verdict.status-na { color: var(--status-na); }
.company-indicator-disclosure { border-bottom: 1px solid var(--rule); }
.company-indicator-disclosure > summary {
  display: flex;
  align-items: baseline;
  gap: var(--space);
  padding: calc(var(--space) * 1.5) calc(var(--space) * 3);
  color: var(--muted);
  font-size: var(--text-xs);
  font-weight: 700;
  cursor: pointer;
  list-style: none;
}
.company-indicator-disclosure > summary::-webkit-details-marker { display: none; }
.company-disclosure-chevron {
  width: calc(var(--space) * 1.75);
  height: calc(var(--space) * 1.75);
  color: var(--accent-text);
  transition: transform var(--fast) var(--ease);
}
.company-indicator-disclosure[open] .company-disclosure-chevron { transform: rotate(90deg); }
.indicator-count { color: var(--ink); font-family: var(--font-mono); }
.company-indicators {
  position: relative;
  display: flex;
  flex-wrap: wrap;
  gap: var(--space-half);
  padding: 0 calc(var(--space) * 3) calc(var(--space) * 2);
}
.company-actions .card-actions-holder { margin: 0; }
.analysis-family { display: grid; grid-template-columns: minmax(13rem, 0.32fr) minmax(0, 1fr); border-bottom: 1px solid var(--rule); }
.analysis-family:last-child { border-bottom: none; }
.analysis-head { padding: calc(var(--space) * 3); border-right: 1px solid var(--rule); }
.analysis-head h2 { margin-bottom: var(--space-half); }
.analysis-head p { max-width: 32ch; margin: 0; color: var(--muted); font-size: var(--text-sm); }
.criteria-sheet { min-width: 0; padding: 0 calc(var(--space) * 3) calc(var(--space) * 3); }
.criteria-sheet h3:first-of-type { margin-top: calc(var(--space) * 3); }

/* A write launched from a row has nowhere of its own to fail out loud, so the
   page keeps one line above the audit sheet for it. */
.company-error {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--space);
  margin: 0 0 calc(var(--space) * 2);
  padding: var(--space-half) var(--space);
  border-radius: var(--radius);
  background: var(--wash-broken);
  color: var(--status-broken);
  font-size: var(--text-sm);
}

.criterion-row {
  position: relative;
  display: grid;
  grid-template-columns: 1fr auto;
  gap: var(--space);
  align-items: start;
  padding: calc(var(--space) * 1.5) 0;
  border-bottom: 1px solid var(--rule-faint);
}
.criterion-label { font-size: var(--text-sm); }
.criterion-label.is-blocking::after {
  content: " blocking";
  margin-left: 6px;
  padding: 1px 6px;
  border-radius: var(--radius-pill);
  background: var(--wash-broken);
  color: var(--status-broken);
  font-size: var(--text-2xs);
  font-weight: 700;
  letter-spacing: var(--track-caps);
  text-transform: uppercase;
  white-space: nowrap;
}
.criterion-row.is-retired { opacity: 0.55; }
.criterion-error { grid-column: 1 / -1; margin: 0; color: var(--status-broken); font-size: var(--text-xs); }

/* A verdict nobody can click on. Sits where the buttons would be — the
   second, `auto` grid column — so the eye travels down one column whether a
   criterion is judged or calculated. */
.criterion-status {
  align-self: center;
  padding: var(--space-half) var(--space);
  border-radius: var(--radius-pill);
  font-family: var(--font-mono);
  font-size: var(--text-sm);
}
.criterion-status.metric-trigger { border: 0; cursor: pointer; }

/* The status vocabulary is coloured everywhere else it appears — on a pill, on
   a card chip — and had to be here too. `.pill.status-met` and
   `.card-status.status-ok` are both scoped to their own element, so the class
   alone carries no colour: an uncoloured verdict would read as neutral text
   next to a card whose pills say the same thing in hue. */
.criterion-status.status-met { color: var(--status-met); background: var(--wash-met); }
.criterion-status.status-borderline { color: var(--status-borderline); background: var(--wash-borderline); }
.criterion-status.status-broken { color: var(--status-broken); background: var(--wash-broken); }
.criterion-status.status-na { color: var(--status-na); background: var(--wash-na); }

.criterion-note-readonly {
  grid-column: 1 / -1;
  margin: 0;
  color: var(--muted);
  font-size: var(--text-xs);
}

.criterion-note {
  grid-column: 1 / -1;
  min-height: 2.4em;
  padding: var(--space) calc(var(--space) * 1.5);
  border: 1px solid var(--rule);
  border-radius: var(--radius);
  background: var(--surface);
  color: var(--ink);
  font: inherit;
  font-size: var(--text-sm);
  resize: vertical;
  transition: border-color var(--fast) var(--ease);
}
.criterion-note:hover:not(:disabled) { border-color: var(--muted); }
.criterion-note::placeholder { color: var(--muted); }
/* Sans, not mono: a date in a sentence is prose, and mono here is reserved for
   figures and for identifiers read down a column. */
.criterion-stamp { grid-column: 1 / -1; color: var(--muted); font-size: var(--text-2xs); }

/* --- AI import ------------------------------------------------------------ */
/* The paste box. Accent, never a status hue: an import is a gesture, not a
   verdict about the company. */
.ai-import {
  margin-bottom: var(--space);
  padding: var(--space);
  border: 1px solid var(--rule);
  border-radius: var(--radius);
  background: var(--surface);
}

/* The feature's only entry point. Left unstyled it rendered in the browser's
   default face with no pointer and no focus ring, in a file that tokenises
   every other control. */
.ai-import summary {
  cursor: pointer;
  font-size: var(--text-sm);
  font-weight: 600;
}

.ai-paste {
  width: 100%;
  min-height: 6em;
  margin: var(--space) 0;
  padding: var(--space-half) var(--space);
  border: 1px solid var(--rule);
  border-radius: var(--radius);
  background: var(--surface);
  color: var(--ink);
  font-family: var(--font-mono);
  font-size: var(--text-sm);
  resize: vertical;
}

.ai-actions { display: flex; gap: var(--space-half); flex-wrap: wrap; }

.ai-errors {
  margin: var(--space) 0 0;
  padding-left: calc(var(--space) * 2);
  color: var(--status-broken);
  font-size: var(--text-sm);
}

/* Read next to "Import AI answers": the date the model says it worked from,
   not stored, so a stale sheet is caught here before anything is endorsed. */
.ai-stamp { display: block; margin-top: var(--space); color: var(--muted); font-size: var(--text-2xs); }

/* A row a proposal sits on, marked even where it has scrolled out from under
   the (possibly collapsed) box above it. */
.criterion-row.has-proposal { border-left: 3px solid var(--accent); padding-left: var(--space); }

/* An inset ring, not a border: `.state-button` declares `border: none` on three
   sides and `:first-child` drops the fourth, so `border-color` marked nothing
   at all on the Yes button. Written through `.state-group` to reach (0,3,0),
   because `[data-answer="yes"].is-selected` is (0,3,0) too and comes earlier —
   below that, a value proposed that is already the recorded one rendered with
   no mark whatsoever, which is precisely the case where the user needs to see
   there is nothing here to accept. The ring stacks over the selection's wash
   instead of replacing it: recorded and proposed are two facts, not one. */
.state-group .state-button.is-proposed { box-shadow: inset 0 0 0 2px var(--accent); }
/* The accent word is for a proposal the user has not already made themselves;
   a selected button keeps the status colour it earned. */
.state-button.is-proposed:not(.is-selected) { color: var(--accent-text); }
.state-button.is-accept { background: var(--accent); color: var(--on-accent); }
/* A halo rather than a second fill: no new colour pair to hold to contrast. */
.state-button.is-accept:hover:not(:disabled) { box-shadow: inset 0 0 0 2px var(--accent-halo); }

/* The "you: … · AI: …" line under a row where a proposal disagrees with what
   is already recorded. Scoped rather than global: a bare .status-met matches
   no rule elsewhere in this file (.pill.status-met, .card-status.status-ok
   and .criterion-status.status-met are all compound selectors on their own
   element), so the recorded side needs its own colour here or it renders in
   plain ink next to an AI side that is correctly accent. */
.criterion-conflict { grid-column: 1 / -1; font-family: var(--font-mono); font-size: var(--text-xs); }
.criterion-conflict .status-met { color: var(--status-met); }
.criterion-conflict .status-borderline { color: var(--status-borderline); }
.criterion-conflict .status-broken { color: var(--status-broken); }
.criterion-conflict .status-na { color: var(--status-na); }
.is-proposed-text { color: var(--accent-text); }

/* --- Legend -------------------------------------------------------------- */

.legend { display: flex; align-items: baseline; gap: var(--space) calc(var(--space) * 2); flex-wrap: wrap; padding: calc(var(--space) * 2); }
.legend-items { display: flex; gap: var(--space-half); flex-wrap: wrap; }
.legend p { margin: 0; color: var(--muted); font-size: var(--text-xs); }

/* --- Narrow screens ------------------------------------------------------ */
/* The phone is a stated device, not a courtesy: the watchlist gets read
   standing up. Only what breaks is restated here. */

@media (max-width: 46rem) {
  /* 16px of gutter on each side of a 375px screen is 9% of the readable
     width; 24px was 13% spent on nothing. */
  .app-main { padding: calc(var(--space) * 2) var(--space) calc(var(--space) * 6); }
  .app-topbar { gap: var(--space-half); padding: var(--space-half) var(--space); }
  /* The field takes a line of its own. It cannot share the first: at 375px that
     line already carries the mark, two navigation glyphs, the queue pulse and
     the way out, and a field squeezed in beside them would not show a ticker. */
  .topbar-search { order: 3; flex-basis: 100%; max-width: none; margin-bottom: var(--space-half); }
  /* The brand shrinks to its mark, and the nav to its icons: the pulse and the
     way between the two screens are what the bar is for on a phone, and two
     words of wordmark would crowd both out. */
  .brand-name { display: none; }
  .nav-link span { display: none; }
  .nav-link { padding: var(--space-half); }
  .nav-link.is-current::after { inset: auto var(--space-half) 2px var(--space-half); }
  .signed-in { display: none; }
  .link-button span { display: none; }
  .panel-body { padding: calc(var(--space) * 1.5); }
  .delta-list, .etf-list li, .queue-table th, .queue-table td { padding-right: calc(var(--space) * 1.5); padding-left: calc(var(--space) * 1.5); }
  /* 96px of ticker column out of 390 leaves the movements two words wide.
     Stacked, the ticker labels the block underneath it instead. */
  .delta-list li { grid-template-columns: 1fr; gap: var(--space-half); }
  /* The error column is off-screen at this width — the table scrolls to it —
     but its wrapped text still sets the row's height, so a five-line provider
     message left five blank lines in the columns that *are* on screen. Capped
     and scrollable rather than clipped: the exact message is the only thing
     that tells a quota ceiling from a dead key, and it is still all there. */
  .job-error { min-width: 12rem; }
  .job-error-text { max-height: 5.5em; overflow-y: auto; }
  .company-head { grid-template-columns: 1fr; padding: calc(var(--space) * 2); }
  .company-identity { display: grid; gap: var(--space-half); }
  .company-name { white-space: normal; }
  .company-actions .card-actions { justify-content: flex-start; }
  .company-indicator-disclosure > summary { padding-right: calc(var(--space) * 2); padding-left: calc(var(--space) * 2); }
  .company-indicators { padding-right: calc(var(--space) * 2); padding-left: calc(var(--space) * 2); }
  .company-indicators .metric-tooltip { width: 100%; }
  .analysis-family { grid-template-columns: 1fr; }
  .analysis-head { padding: calc(var(--space) * 2); border-right: none; border-bottom: 1px solid var(--rule-faint); }
  .criteria-sheet { padding: 0 calc(var(--space) * 2) calc(var(--space) * 2); }
  .criterion-row { grid-template-columns: 1fr; }
}

/* --- Touch --------------------------------------------------------------- */
/* Density is the point of this screen and it survives on a mouse. A finger is
   not a cursor: the same pill is a 20px target, and the trace behind it is the
   only way to see why a stock is disqualified. Grown on coarse pointers only,
   so the desktop reading loses nothing. */

@media (pointer: coarse) {
  .pill { padding: var(--space-half) var(--space); font-size: var(--text-xs); }
  .btn-quiet { padding: var(--space) calc(var(--space) * 1.5); font-size: var(--text-xs); }
  .state-button { min-height: 44px; }
  .card-title { padding: var(--space-half) 0; }
  .search-add { padding: var(--space-half) 0; }
  /* The one control on the bar a finger has to hit precisely. */
  .search-field { padding-top: var(--space); padding-bottom: var(--space); }
  .search-go { padding: var(--space); }
}

/* --- Reduced motion ------------------------------------------------------ */
/* The pulse is the only loop in the app and it is exactly what this setting
   exists to stop. The dot keeps its colour, so the state it reports survives
   the animation being switched off. */

@media (prefers-reduced-motion: reduce) {
  *, *::before, *::after {
    animation-duration: 0.001ms !important;
    animation-iteration-count: 1 !important;
    transition-duration: 0.001ms !important;
  }
}

/* --- Thresholds screen ---------------------------------------------------- */

.settings-warning {
  color: var(--muted);
  font-size: var(--text-sm);
  margin: 0;
}

.settings-error {
  color: var(--status-broken);
  font-size: var(--text-sm);
  margin: 0;
}

.settings-row {
  display: flex;
  align-items: center;
  justify-content: space-between;
  flex-wrap: wrap;
  gap: var(--space);
  padding: calc(var(--space) * 1.5) 0;
  border-bottom: 1px solid var(--rule-faint);
}
.settings-row:last-child { border-bottom: none; }

.settings-label { display: flex; align-items: baseline; flex-wrap: wrap; gap: var(--space); }
.settings-name { font-weight: 600; }
.settings-catalog { color: var(--muted); font-family: var(--font-mono); font-size: var(--text-sm); }

.settings-control { display: flex; align-items: center; gap: var(--space); }
.settings-comparator { color: var(--muted); font-family: var(--font-mono); }
.settings-input {
  width: 7rem;
  padding: var(--space-half) var(--space);
  border: 1px solid var(--rule);
  border-radius: var(--radius);
  background: var(--surface);
  color: var(--ink);
  font: inherit;
  font-family: var(--font-mono);
  text-align: right;
}
.settings-input:hover:not(:disabled) { border-color: var(--muted); }
.settings-input:disabled { cursor: not-allowed; opacity: 0.55; }

.settings-row.is-overridden .settings-name { text-decoration: underline dotted; }

/* A row whose last save was refused: the field still holds what the user
   typed (settings.js's `refusals` table), and the row itself says so even
   after the error line above has scrolled out of view or been read once. */
.settings-row.is-refused { background: var(--wash-broken); }
.settings-row.is-refused .settings-input { border-color: var(--status-broken); }
