/* TickerWorth — TITAN visual language (Phase 1: the token layer).
   ---------------------------------------------------------------------------
   Every custom property below KEEPS ITS NAME and changes only its value, so
   nothing downstream needs to know the palette moved — the proven house move
   from the Apple-Stocks redesign (REDESIGN-STOCKS.md: "keep every existing
   token name so nothing downstream breaks").

   Titan is monochrome paper-and-ink: warm cream grounds, near-black ink,
   hairlines instead of shadows, and exactly two families — Geist (sans) and
   Geist Mono (figures, eyebrows, pill labels). LIGHT is now the shipped
   default; dark is a pure token swap and needs no compositional change.

   TWO DENSITY REGISTERS, both named (see the REGISTERS block below). Titan is a
   marketing system on a 96px band rhythm; this is an application, and an
   application on a 96px rhythm is forty screens tall. So the marketing rhythm
   and the in-app rhythm are DECLARED, not improvised per surface.

   Where Titan has nothing, the token says so. Titan documents default states
   only (DESIGN.md: "Hover states are not documented"), has no text tier that
   passes WCAG AA below 24px, and has no progress, badge or overlay primitives.
   Those gaps are filled here once, in tokens, so no surface invents its own.
   ========================================================================= */
/* `hidden` means the element is not relevant — the UA says so with `[hidden]{display:none}`
   at specificity (0,1,0), which any single class beats. This sheet had already lost that
   fight three times and fixed it three times, one selector at a time (.usage-stat, .adv-osc,
   .adv-lock). The fourth was #hero-badge: the founding-member pill rendered as an empty
   28x12 grey box above the headline on the live home page, hidden by attribute and visible
   to the eye. Stating it once, hard, ends the whack-a-mole. Verified safe: nothing in
   app.js reveals a [hidden] element by setting inline display. */
[hidden] { display: none !important; }

/* ============================================================================
   FONTS — Geist + Geist Mono, self-hosted, SIL Open Font License 1.1.
   See web/fonts/README.md for provenance and hashes.

   latin loads always (51.2 KB for the pair); latin-ext carries a unicode-range
   so it only downloads on a page that actually contains extended-Latin
   characters, which on this product is essentially never.

   THE WEIGHT RANGE IS 100-900, NOT the prototype's 400-600. Both files carry a
   full `wght 100..900` fvar axis (verified with fontTools), and clamping the
   declaration to 400-600 does not save a byte — it just makes the browser
   SYNTHESISE anything outside it. This file sets `font-weight: 700` in 133
   rules and 300 on the hero; at 400-600 every one of those would have rendered
   as a faux bold or a faux light. Declared at the real range they are real
   instances of the real face.
   ========================================================================= */
@font-face {
  font-family: "Geist";
  font-style: normal;
  font-weight: 100 900;
  font-display: swap;
  src: url("/fonts/Geist-latin.woff2") format("woff2");
  unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA,
    U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193,
    U+2212, U+2215, U+FEFF, U+FFFD;
}
@font-face {
  font-family: "Geist";
  font-style: normal;
  font-weight: 100 900;
  font-display: swap;
  src: url("/fonts/Geist-latin-ext.woff2") format("woff2");
  unicode-range: U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF,
    U+0304, U+0308, U+0329, U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF, U+2020,
    U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF;
}
@font-face {
  font-family: "Geist Mono";
  font-style: normal;
  font-weight: 100 900;
  font-display: swap;
  src: url("/fonts/GeistMono-latin.woff2") format("woff2");
  unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA,
    U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193,
    U+2212, U+2215, U+FEFF, U+FFFD;
}
@font-face {
  font-family: "Geist Mono";
  font-style: normal;
  font-weight: 100 900;
  font-display: swap;
  src: url("/fonts/GeistMono-latin-ext.woff2") format("woff2");
  unicode-range: U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF,
    U+0304, U+0308, U+0329, U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF, U+2020,
    U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF;
}

/* METRIC-MATCHED FALLBACKS — MEASURED, NOT DERIVED.
   ---------------------------------------------------------------------------
   Deriving `size-adjust` from a single glyph advance is what broke this in
   Phase 0: Geist 'n' is 0.581 against Arial's 0.556, which says 104.5%, and at
   104.5% the fallback run came out 3.37% WIDER than Geist — worse than the
   2.80% NARROWER you get by doing nothing at all. A one-glyph ratio is not the
   ratio of a page.

   These numbers come from measuring 2,442 distinct real text runs — every
   rendered string on /, a company page, /methodology and /pricing, each at its
   own computed size and weight — in Geist against each fallback candidate:

     Arial            0.9829 of Geist  ->  size-adjust 101.7%
     Helvetica Neue   0.9904 of Geist  ->  size-adjust 101.0%
     Menlo (mono)     1.0034 of Geist Mono -> size-adjust 99.7%

   Arial is listed first because it exists on macOS AND Windows, so one factor
   holds on both. Geist's own box measures ascent 1.005em / descent 0.295em
   (canvas fontBoundingBox at 2000px); the overrides divide by size-adjust so
   the adjusted em still yields Geist's own line box.

   ONE HONEST CAVEAT. Geist Mono is 0.600 advance on every glyph; Menlo measures
   99.7%. The Windows mono fallback (Consolas) is NOT installed on the machine
   these were measured on, so its ratio could not be measured here and is not
   claimed — a Windows reader will see figures reflow slightly on swap. Mono is
   only used for eyebrows, pill labels and figures, all short runs in fixed
   cells, so the practical cost is small. */
@font-face {
  font-family: "Geist Fallback";
  src: local("Arial"), local("Helvetica Neue"), local("Roboto");
  size-adjust: 101.7%;
  ascent-override: 98.8%;
  descent-override: 29.0%;
  line-gap-override: 0%;
}
@font-face {
  font-family: "Geist Mono Fallback";
  src: local("Menlo"), local("Consolas"), local("DejaVu Sans Mono");
  size-adjust: 99.7%;
  ascent-override: 100.8%;
  descent-override: 29.6%;
  line-gap-override: 0%;
}

:root {
  /* ---- LIGHT: Titan paper. -------------------------------------------------
     Titan's own light grounds are canvas #ffffff and canvas-soft (cream)
     #f3efeb, and its card is a cream fill with a sand hairline. This app's
     structure is cards floating on a grouped ground, so the two map cleanly the
     other way round: the PAGE is cream and the CARDS are canvas.

     That is also the fix for the defect Phase 0 hit — a cream card on a cream
     band is the same #f3efeb as the band, so the unit reads as a stray 1px
     rectangle, which violates Titan's own rule that "a cream fill OR a single
     hairline is enough to define a unit". Cards are canvas here, so the fill
     and the hairline each do half the work, as intended. */
  --bg: #f3efeb;                 /* Titan canvas-soft — the page ground */
  --surface: #ffffff;            /* Titan canvas — cards float on the cream */
  --surface-2: #f3efeb;          /* one step back toward the ground */
  --surface-3: #dfddda;          /* Titan hairline-warm as a FILL — recessed wells.
                                    Titan's `sand` (#d8d3cc) is too dark to carry the
                                    --muted text `.chg.flat` sets on it (4.33:1); on
                                    #dfddda the same token measures 4.75:1. */
  --line: #d8d3cc;               /* Titan sand — the card hairline Titan actually draws */
  --line-soft: #e9eaeb;          /* Titan hairline */
  --line-strong: #c8c1b6;        /* INVENTED: Titan light stops at sand, and this app
                                    needs one step past it for pressed/active edges.
                                    Sand darkened by the same ratio sand:hairline-warm. */
  --text: #111111;               /* Titan ink */
  /* THE TEXT RAMP, AND THE ONE PLACE TITAN HAD TO BE EXTENDED.
     DESIGN.md assigns `stone` (#8a8a87) to "tertiary/muted text" and `mist`
     (#b3b3b3) to "de-emphasized inline copy and metadata". Measured on Titan's
     own grounds, stone is 3.46:1 on canvas and 3.03:1 on cream, and mist is
     2.10 / 1.83 — so two of Titan's four text tiers fail WCAG AA for the copy
     they are specified for, and every caption in this product is 12-14px.

     Phase 0 solved this by resolving tertiary to `slate`, which passes but
     collapses two tiers into one colour. This product sets --muted and --faint
     ADJACENTLY on ~100 nodes (label over value, caption under caption), so that
     flattening is visible here in a way it was not in a three-surface
     prototype. Instead --faint takes the lightest value on the slate->stone
     line that still clears 4.5:1 on the WORST light ground small text sits on
     (cream): #696764 at 4.93:1 on cream, 5.64:1 on canvas — a real third tier
     with ~0.4 of margin, against slate's 5.63 / 6.44 above it. */
  --muted: #615e5b;              /* Titan slate — secondary label */
  --faint: #696764;              /* tertiary label. See above: MEASURED, not a Titan token. */
  /* accent — Titan has no chromatic brand accent at all, so the accent IS the ink */
  --accent: #111111;
  --accent-bright: #111111;
  --accent-soft: rgba(17, 17, 17, 0.06);
  --accent-rim: rgba(17, 17, 17, 0.16);
  /* ---- DIRECTION INK — DUOTONE IS THE DEFAULT. -----------------------------
     TO SHIP TITAN-PURE MONOCHROME INSTEAD, THIS IS ONE LINE: set
     `--ink-mode: mono` on :root (below). Nothing else changes, in either theme.

     Why duotone ships by default: Titan says, flatly and twice, that there is
     no chromatic accent — so this is an EXCEPTION to Titan, not a reading of
     it. It is taken because in mono "up" is plain ink, the same colour as every
     other figure in the row, so a market-cap-sorted screener (71 down, 29 up on
     the real distribution) reads as a wall of red with no visible good news —
     factually misleading on a product whose pitch is honesty. Mono also
     collapses two meanings into one colour: black means "up" in the direction
     column and "this is a number" three columns left.

     `#2e6648` was chosen by searching for a green whose contrast MATCHES
     brick's rather than one that merely passes — 6.75 vs 6.60 on canvas, 5.90
     vs 5.77 on cream — so neither direction shouts louder than the other.

     THE RULE THAT HOLDS IN BOTH MODES: a verdict is NEVER colour alone. The
     sign and the ▲/▼ glyph always ship, so switching to mono loses elegance,
     never information. */
  --ink-mode: duotone;           /* a LABEL, not the switch — see below */
  --up: #2e6648;                 /* ledger green — AA 6.75 canvas / 5.90 cream */
  --up-soft: rgba(46, 102, 72, 0.12);
  --down: #b3241a;               /* Titan `error` brick — the one chromatic Titan licenses */
  --down-soft: rgba(179, 36, 26, 0.11);
  /* the third direction. A price series that did not move is neither green nor red, and the
     chart line has to be able to say so. HEX, not rgba: the chart gradient builds its stops
     by appending an alpha pair to this value. Titan `stone`. */
  --flat: #8a8a87;
  /* TEXT variants. In the Apple palette these had to differ from the graphics colours,
     which were tuned for fills. Titan's direction inks were picked to clear AA as small
     text in the first place, so the text and graphics values are the same colour here —
     the token names are kept so a future divergence stays a one-value change. */
  --up-text: #2e6648;
  --down-text: #b3241a;
  --blue: #111111;               /* link colour — Titan has no blue; links are ink */
  /* ---- Score grade scale. --------------------------------------------------
     Deliberately its own scale so a good grade never reads as an up-tick. Titan
     has exactly ONE non-direction chromatic (`error`), so the weak end and the
     down-tick land on the same brick — which is a real collapse and is flagged
     in the Phase 1 report, not hidden. The tokens stay distinct so separating
     them again is a one-value change.
     Every value here also has to work as a BADGE FILL with --bg drawn on top:
     16.51 / 5.63 / 5.77 against cream. */
  --score-strong: #111111;       /* A / B / high confidence / positive driver — ink */
  --score-mid: #615e5b;          /* C / moderate / medium — slate */
  /* NOT brick. Brick is --down, and pointing the weak band at it was tried and
     LOOKED WRONG: a company with four middling pillars painted four coral bars and
     a coral grade badge, so an ordinary mid-cap read as an alarm — the same "wall of
     red" failure that duotone exists to prevent on the screener, moved onto the score.
     This is brick one step deeper and quieter (the same value --warn takes, and for
     the same reason: caution is not error). Still Titan's one chromatic family, still
     6.69:1 on cream and legible as a badge fill, and visibly not a down-tick. */
  --score-weak: #8a3b2e;         /* D / F / weak / low / negative driver */
  /* Degraded data, not danger. The live price feed being down is a caveat on one number,
     not a failure of the product. Titan offers no amber, so rather than borrow the error
     red this is brick ONE STEP DEEPER AND QUIETER — the same hue family, a different
     voice. 6.69:1 on cream, and clearly not --down-text. */
  --warn: #8a3b2e;
  /* ---- FROM THE BRAND KIT (TickerWorth-Titan-Kit/tokens.css) ----------------
     Three Titan tokens this app had no need for until the landing gained a dark
     band and a dark footer. Values are the kit's, verbatim.
     `--mist` is deliberately NOT adopted: Titan assigns it to "de-emphasized
     inline copy and metadata" and it measures 2.10:1 on canvas, so there is no
     size at which it may carry text here. */
  --surface-dark: #111111;        /* the dark band / footer ground, in LIGHT theme */
  --on-dark-muted: #8a8a87;       /* secondary copy ON that ground — 5.45:1 */
  --hairline-on-dark: #26241f;

  /* ============ THE PLATE'S OWN INK ======================================
     The hero photograph now has a light twin, and everything painted OVER the
     plate has to follow the plate rather than assume the dark one. Before this
     there was no choice to make: eighteen rules wrote `#fff` and a wash wrote
     `rgba(16,15,13,…)` directly, because there was only ever one plate.

     `--on-plate` is the ink that sits on the photograph. `--plate-wash` is the
     wash painted over it — the same gradient geometry in both themes, only the
     colour swapped, so the stops that were measured in PIXELS against the text
     column keep working exactly as they did.

     Light is the default here rather than an override, because the product is
     light-first and the pre-paint seed always stamps `data-theme`. */
  --on-plate: #111111;
  --on-plate-2: rgba(17, 17, 17, 0.78);
  --on-plate-3: rgba(17, 17, 17, 0.66);
  --on-plate-fill: rgba(17, 17, 17, 0.06);
  --on-plate-rim: rgba(17, 17, 17, 0.16);
  --on-plate-rim-2: rgba(17, 17, 17, 0.22);
  --on-plate-rim-3: rgba(17, 17, 17, 0.26);
  --plate-wash: 243, 239, 235;    /* rules on it; `slate` measures 2.93 and cannot be used */

  /* ---- ENGRAVING OPACITY, TUNED PER GROUND -----------------------------------
     The kit ships its line art at 0.42 and records that this is "effectively
     invisible once scaled to page size", wanting roughly 1.85x on light grounds
     and 1.45x reversed. Checked rather than believed, and it took two wrong
     instruments to get there: the strongest-2%-of-ink statistic said 0.42 was
     fine (1.67:1) and the mean-over-inked-pixels statistic put every case in a
     flat 1.10-1.39 band that decided nothing. What settled it was rendering the
     plates at the size the page actually uses — 620px for the rosette, 760px for
     the curves — and looking.

     On cream at 620px, 0.42 is a smudge and 0.78 reads as engraving without
     competing with the headline in front of it. Reversed on near-black, 0.61
     reads. On pure CANVAS the ink needs less than on cream, because white gives
     it more to work against — which is the kit's own point about tuning per
     ground rather than sharing one number. */
  --engrave-on-cream: 0.78;       /* 0.42 x 1.85 — the kit's figure, confirmed */
  --engrave-on-canvas: 0.62;      /* white carries it further; less boost needed */
  --engrave-on-dark: 0.61;        /* 0.42 x 1.45 — the kit's figure, confirmed */
  /* SCALE is the variable the kit does not name, and it matters as much as the ground.
     The three figures above were measured on DISCRETE ornaments — the rosette at 620px,
     the curves at 760px — sitting in negative space. `wave-field` is a FIELD: at 1400px
     across a whole band it covers roughly ten times the area, and at the same opacity it
     stops being the "quiet ground for large flat sections" the kit calls it and starts
     competing with the numbered steps in front of it. Measured the same way, at band
     width, on both grounds. */
  --engrave-field: 0.30;

  --shadow: 0 12px 28px -20px rgba(17, 17, 17, 0.22);

  /* Typography. TWO families, per Titan: Geist and Geist Mono, nothing else.
     `--serif` keeps its name and now resolves to the same sans — Titan ships no
     serif, and a third family would be the one Titan "don't" with no upside.
     Every consumer of --serif is therefore now Geist; see the report. */
  --serif: "Geist", "Geist Fallback", -apple-system, "Segoe UI", Roboto, sans-serif;
  --mono: "Geist Mono", "Geist Mono Fallback", "SF Mono", ui-monospace, Menlo, monospace;
  --sans: "Geist", "Geist Fallback", -apple-system, "Segoe UI", Roboto, sans-serif;

  /* Radius — Titan's own steps, and the two registers use different ones.
     Titan: none 0 / xs 6 / sm 20 / md 32 / pill. */
  --r: 20px;      /* Titan `sm` — the in-app card/panel radius (density register) */
  --r-sm: 6px;    /* Titan `xs` — controls, inputs, chips */
  --r-lg: 32px;   /* Titan `md` — hero / grouped-card outer (marketing register) */
  --r-full: 9999px;  /* Titan `full` — discs, avatars, the numbered step markers */
  --r-pill: 140px;   /* Titan `pill` — the search field and the CTA capsules */
  --wrap-pad: 22px;  /* the container's own gutter; see .wrap */

  /* ============ ONE SHELL, TWO REGISTERS =================================
     Miraaj, three times now, most recently on News at 1920: two narrow columns
     in the middle and "enormous empty margins on both sides". Measured, the
     container used 58% of the window at 1920 and 44% at 2560 — a 720px gutter
     down each edge — and the tile count stayed at three from 1280 all the way
     to 2560. Both halves were fixed: the width AND the number of columns.

     It got that way honestly. Four separate blocks each widened the container
     for their own page (`#view`, `#dashboard`+`.mylist`, `#screener`,
     `#discover`), all four to the same `min(1560px, 100% - 40px)`, and news and
     portfolio were in none of them — which is why News sat at 1120 while the
     dashboard sat at 1560. The stylesheet's own comment described the mechanism:
     "the dashboard and the screener each opt in from their own block".

     So there are two registers and they derive from here, not from four places:

       READ  — prose wants a measure. The landing, pricing, methodology, about
               and the SEO pages keep 1120: stretching a paragraph across 1920
               would be worse than the dead space, not better.
       TILES — a workbench uses the window. Every tiled region grows with the
               viewport to a ceiling, and its column COUNT follows the width
               available rather than a breakpoint.
     ====================================================================== */
  --shell-read: 1120px;
  --shell-tiles: 2160px;      /* even a workbench has a ceiling */
  --shell-gutter: clamp(16px, 2.2vw, 40px);
  /* Measured by syncScrollbarWidth() in app.js. 0 on overlay-scrollbar platforms, ~15px
     where the scrollbar takes real width — and `100vw` counts it while the percentage
     margins that centre the shell do not. Without this the shell is a scrollbar too wide
     and, being centred, loses content off BOTH edges at once. */
  --sbw: 0px;
  /* THE COLUMN WIDTH IS FLUID, AND THAT IS THE WHOLE TRICK. The obvious reading of
     "use the window" is more columns; measured, it is the wrong one. With six to ten
     tiles on these surfaces, every extra column costs more in multicol balancing holes
     than it saves in gutter. Total unused area (side gutters + in-grid voids) on News:

         --tile-col     1440        1920        2560
            340px    21.5% (3c)  41.1% (5c)  49.6% (6c)
            400px    21.5% (3c)  31.6% (4c)  43.5% (5c)
            460px    25.1% (2c)  24.1% (3c)  37.6% (4c)
            600px    25.1% (2c)  24.1% (3c)  31.2% (3c)

     No single fixed value is best at more than one width — which is exactly why a fixed
     one was wrong before. A fluid column tracks it: `30vw` lands on three columns from
     1280 to 2560, two from 768 to 1024 and one on a phone, and the COLUMNS THEMSELVES
     grow from ~360px to ~700px as the window does. The tiles resize; the count changes
     where changing it actually helps. */
  --tile-col: clamp(340px, 30vw, 700px);
  --prose-measure: 74ch;      /* running text, wherever it lives */
  --ease: cubic-bezier(0.16, 1, 0.3, 1);

  --glow: rgba(17, 17, 17, 0.03);

  /* ============ MOTION SYSTEM ============================================
     One vocabulary for every transition in the app, so nothing feels bolted
     on. Only transform/opacity/colour are animated (compositor-friendly);
     nothing here animates layout, and nothing blocks first paint.
     Titan documents no motion at all, so this system is kept intact — it was
     audited (plans 008-012) and there is nothing in Titan to replace it with. */
  --ease-out: cubic-bezier(0.16, 1, 0.3, 1);      /* decelerate — the house curve */
  --ease-in-out: cubic-bezier(0.65, 0, 0.35, 1);  /* symmetric, for reversible states */
  --ease-spring: cubic-bezier(0.34, 1.4, 0.64, 1); /* gentle overshoot, used sparingly */
  --dur-1: 120ms;   /* press / colour — must feel instant */
  --dur-2: 200ms;   /* hover, small state changes */
  --dur-3: 320ms;   /* panels, sheets, accordions */
  --dur-4: 520ms;   /* the hero reveal */
  --dur-5: 900ms;   /* the score count-up */

  /* ============ STATE MATRIX =============================================
     Titan documents DEFAULT STATES ONLY — DESIGN.md says so in as many words:
     "Hover states are not documented." That is fine for a marketing site and
     useless for an application, where every control has five. Declaring them
     once here is what stops each surface inventing its own.

     The shape of the answer is Titan's, not Apple's: with no shadows to raise
     and no accent to tint with, a state change is a change of FILL WEIGHT or
     HAIRLINE WEIGHT. Hover deepens the ground one step; active deepens it
     again and removes travel; disabled drops opacity and kills the pointer.
     Selected is the ink inverted, which is the only "loud" state Titan has. */
  --state-hover-fill: rgba(17, 17, 17, 0.045);
  --state-hover-line: var(--line-strong);
  --state-active-fill: rgba(17, 17, 17, 0.085);
  --state-active-line: var(--text);
  --state-selected-fill: var(--text);
  --state-selected-ink: var(--bg);
  /* 0.55, and it was measured rather than picked. WCAG exempts disabled controls from
     contrast, but this product's whole stance is that a reader can always tell what they
     are looking at, and an unavailable button they cannot READ is a worse answer than a
     legible one. On the ink button against cream: 0.42 puts the label at 2.71:1 on its own
     fill, 0.55 puts it at 4.01:1, and 0.60 reaches 4.72 but is hard to tell from enabled
     at a glance. 0.55 is unmistakably inactive (4.01 against the enabled 16.51) and still
     legible. It replaces the 0.6 and 0.7 that two surfaces had improvised. */
  --state-disabled-opacity: 0.55;
  /* The focus ring is NOT restyled. It is the audited two-ring
     (3px halo + 1px edge) that the whole app already shares through --ring, and
     it survives a palette change unchanged because both rings derive from
     --accent. Do not replace it with an outline. */
  --ring: 0 0 0 3px color-mix(in srgb, var(--accent) 20%, transparent),
          0 0 0 1px color-mix(in srgb, var(--accent) 55%, transparent);

  /* ============ TYPE SCALE ==============================================
     Titan's ramp: display 60 / h1 40 / h2 32 / h3 24 / body-lg 20 / body 18 /
     body-sm 16 / caption 14 / fine 12 / eyebrow 11.

     The two registers split it. The MARKETING register runs from the top of
     Titan's ramp (display, h1, body 18). The IN-APP register starts at Titan's
     body-sm (16) and works down, because a data product sets four numbers where
     a brochure sets one sentence. Both ends are Titan values; only which end a
     surface reads from is a decision. */
  /* The hero cap is Miraaj's call from 2026-08-19 and is deliberately LOUDER
     than Titan's 60px display: "restraint in colour and decoration earns the
     right to be loud in size". Titan makes that restraint total, so the
     argument only gets stronger. Cap is 5.5rem = 88px. */
  --t-hero: clamp(3rem, 2rem + 5vw, 5.5rem);
  --t-display: clamp(2rem, 1.5rem + 1.6vw, 2.5rem);   /* Titan h2 32 -> h1 40 */
  --t-title: clamp(1.25rem, 1.15rem + 0.45vw, 1.5rem); /* Titan body-lg 20 -> h3 24 */
  --t-body: 16px;    /* Titan body-sm — the in-app body step */
  --t-small: 14px;   /* Titan caption */
  --t-meta: 12px;    /* Titan fine — the data-dense step, the most-used size in this file */
  --t-micro: 11px;   /* Titan eyebrow */
  --t-eyebrow: 11px; /* Titan eyebrow — was 10px, which is below anything Titan sets */
  /* Titan sets its mono eyebrow at 0.33px of tracking on an 11px face — 0.03em,
     which is very nearly none. The old value here was 0.14em, from a different
     system. This is the one place Titan's own number contradicts a shipped
     assertion (tests/test_typography_optical), and Titan wins: the eyebrow is a
     mono label, and mono is already wide. */
  --track-eyebrow: 0.03em;
  /* the ONE named exception to the eyebrow: a column header in a dense table has to
     fit a column rather than a card. Nine tables had nine settings between them. */
  --t-eyebrow-dense: 10px;
  --track-eyebrow-dense: 0.06em;
  --track-tight: -0.025em;   /* Titan sets ~-0.027em across its whole display ramp */

  /* ---- the heading bands: tracking and leading move together ------------
     apple-design §15: tracking is size-specific (large type reads too loose as it
     grows, so it gets pulled in) and leading runs inversely (tight on display,
     comfortable on body). The two were previously decided in separate places, so
     they drifted apart; they are declared as PAIRS and applied at the one place
     the size is decided.

     Titan's own tracking is close to constant in em (-1.6px at 60, -1.2px at 40,
     -0.85px at 32, -0.6px at 24 => -0.027 / -0.030 / -0.027 / -0.025em), so the
     monotonic band shape survives the re-point almost untouched — only the title
     band moves, from -0.008em (a value from the previous system) to Titan's
     -0.025em. */
  --track-hero:    -0.03em;   /* .hero h1, 48-88px — unchanged; tests/test_hero_headline.py
                                 computes its headroom assuming ZERO tracking, so this
                                 can only ever add clearance. Do not loosen it. */
  --lead-hero:      1.05;
  --track-display: -0.028em;  /* page h1 / display h2, 32-40px — sits between Titan's h1
                                 (-0.030em) and h2 (-0.027em) */
  --lead-display:   1.1;      /* Titan h1/h2 line-height, exactly */
  --track-title:   -0.025em;  /* Titan h3, exactly (-0.6px at 24px) */
  --lead-title:     1.15;     /* Titan h3 line-height, exactly */
  --track-title-serif: -0.025em;  /* Kept as a NAME, not as a distinction. This existed
                                 because `.meth h2` / `.gl-cat` were set in a serif that
                                 carried visibly more sidebearing than the UI sans at the
                                 same size, so the two faces needed different tracking at
                                 one size. Titan ships ONE face, so the two are now the
                                 same value by construction. See
                                 tests/test_editorial_heading_fit.py, re-measured in Geist. */
  /* Body and meta deliberately have NO tracking token. Body is already `normal`
     everywhere it matters, and 0 is not worth a name. */

  /* ============ SPACING RHYTHM — AND THE TWO REGISTERS ===================
     The values are Titan's own eight-step ramp (4 / 6 / 12 / 20 / 32 / 40 / 56
     / 96), which happens to be exactly eight steps, so it maps one-for-one onto
     the eight names this file already has.

     Stated in rem, not px, and the two are NOT interchangeable here.
     apple-design §15: "Respect the user's text-size setting. Scale layout WITH
     the text — spacing in rem/em, not fixed px — so a larger font doesn't break
     the layout." A reader who sets a 20px browser default used to get larger
     type inside boxes that stayed exactly the same size, so the page crowded and
     then clipped. In rem the rhythm grows with them.

     The divisor is 16 (the browser default this page never overrides — there is
     deliberately no `html { font-size: <px> }` rule, which would opt the page out
     of the setting entirely). Every Titan step divides exactly: 4/16=0.25,
     6/16=0.375, 12/16=0.75, 20/16=1.25, 32/16=2, 40/16=2.5, 56/16=3.5, 96/16=6.

     This is the OUTER rhythm only. Internal chip and control padding stays in px
     on purpose — a 10px pill that grew with the root size would balloon away from
     the glyphs it wraps. */
  --s-1: 0.25rem;  --s-2: 0.375rem; --s-3: 0.75rem;  --s-4: 1.25rem;
  --s-5: 2rem;     --s-6: 2.5rem;   --s-7: 3.5rem;   --s-8: 6rem;

  /* THE TWO REGISTERS, NAMED.
     --rhythm-marketing is Titan's band rhythm, unmodified: 96px between bands on
     the landing, the pitch, /pricing, /about, /methodology — surfaces where the
     whitespace IS the luxury signal and there are six things on the page.
     --rhythm-app is the in-app section rhythm: a company view has 35 panels and
     at 96px it would be forty screens tall. 48px is the value this app already
     shipped and it sits deliberately off the --s-N ramp; the density register
     below (12-20px padding, ~40px rows) is what carries it. */
  --space-section: 3rem;            /* 48px — the in-app section rhythm; deliberately
                                       off the --s-N ramp. This is the long-standing
                                       name and stays the literal. */
  --rhythm-marketing: var(--s-8);   /* 96px — Titan's band rhythm, unmodified */
  --rhythm-app: var(--space-section);  /* 48px — the register's own name for the above */

  /* ============ ELEVATION — TITAN HAS NONE, AND THAT IS THE POINT =========
     DESIGN.md: "Don't add drop shadows." Titan's elevation is a fill plus a
     hairline, and the whole system holds together because nothing floats.

     So --el-1 and --el-2 are `none`: every resting surface in this app already
     carries `border: 1px solid var(--line)` (verified on .panel, .grp, .card,
     .metrics, .dtile), so removing the shadow leaves the hairline doing exactly
     the job Titan says it should. Hover no longer raises a card — it deepens the
     ground, via the state matrix above.

     --el-3 and --el-hero survive, quietly, and the reason is not a compromise:
     they are only ever worn by DETACHED overlays — dropdowns, menus, popovers,
     modals, the tip sheet. Titan has no overlays at all, so it has no rule to
     break here, and an unshadowed popover over running text is simply unreadable.
     They are now a single soft shadow rather than a four-layer stack. */
  --sh-tint: 17, 17, 17;
  --el-1: none;
  --el-2: none;
  --el-3: 0 10px 24px -12px rgba(var(--sh-tint), 0.22);
  --el-hero: 0 18px 44px -20px rgba(var(--sh-tint), 0.26);
  /* The lit-top-edge rim belonged to a glossy system. Titan's edge is the
     hairline itself; the rims are neutralised rather than removed so every
     `box-shadow: ..., var(--rim)` in this file stays valid. */
  --rim: 0 0 0 0 transparent;
  --rim-strong: inset 0 0 0 1px var(--line);

  /* Grain: an SVG feTurbulence tile as a data-URI. Static, cached, ~0 cost. On
     Titan's cream it does the job the guilloché does at large scale — it keeps a
     flat fill from reading as a screen. Dropped from 0.055 to 0.03: a warm paper
     ground shows noise far more than a cool grey one did. */
  --grain: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='140' height='140'%3E%3Cfilter id='n'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='0.85' numOctaves='3' stitchTiles='stitch'/%3E%3CfeColorMatrix type='saturate' values='0'/%3E%3C/filter%3E%3Crect width='140' height='140' filter='url(%23n)' opacity='0.03'/%3E%3C/svg%3E");
}

/* MONO IS ONE LINE. To ship Titan-pure monochrome instead of duotone, add
   `data-ink="mono"` to the <html> tag in web/index.html — THAT IS THE WHOLE
   CHANGE, one attribute, and it covers both themes. Direction is then carried
   by the ▲/▼ glyph, the sign and weight alone, and brick stays reserved for
   negative, which is the one chromatic value Titan itself licenses (`error`).
   Duotone ships without the attribute because it is the default. */
:root[data-ink="mono"] {
  --ink-mode: mono;
  --up: var(--text);
  --up-text: var(--text);
  --up-soft: rgba(17, 17, 17, 0.06);
}

/* ---- DARK: Titan's dark ground. A pure token swap — Phase 0 measured zero
   contrast failures in dark across every combination, with more margin than
   light has. No compositional change at any width. */
:root[data-theme="dark"] {
  --bg: #100f0d;                 /* Titan dark canvas */
  --surface: #1b1916;            /* Titan dark canvas-soft — cards */
  --surface-2: #211f1b;          /* Titan dark hairline-warm as a fill */
  --surface-3: #2a2621;          /* Titan dark sand — wells, tracks */
  --line: #2a2621;               /* Titan sand — the card hairline Titan actually draws */
  --line-soft: #211f1b;          /* Titan hairline-warm */
  --line-strong: #3a352e;        /* INVENTED, mirroring light: one step past sand */
  --text: #ffffff;               /* Titan dark ink */
  --muted: #b8b4b0;              /* Titan dark slate — 8.51:1 on the card */
  /* Dark is where Titan's own ramp works: `stone` #939392 measures 6.23 on the
     page ground and 5.70 on the card, so the tertiary tier needs no invention
     here — only light did. */
  --faint: #939392;
  --accent: #ffffff;
  --accent-bright: #ffffff;
  --accent-soft: rgba(255, 255, 255, 0.09);
  --accent-rim: rgba(255, 255, 255, 0.22);
  /* direction — the dark lift was picked the same way as light: 6.77 against
     coral's 6.86 on the page ground, within 0.09, so neither shouts louder */
  --up: #4dab79;
  --up-soft: rgba(77, 171, 121, 0.16);
  --down: #ff6b5e;               /* Titan dark `error` */
  --down-soft: rgba(255, 107, 94, 0.16);
  --flat: #939392;               /* Titan dark stone */
  --up-text: #4dab79;
  --down-text: #ff6b5e;
  --blue: #ffffff;
  --score-strong: #ffffff;
  --score-mid: #b8b4b0;
  --score-weak: #e0917f;         /* see the light block — caution, not the down-tick */
  --warn: #e0917f;               /* the same one-step-quieter brick, lifted for dark */
  /* white line art on near-black carries further than ink on cream, so the field
     steps down again — the same relationship --engrave-on-dark has to --on-cream. */
  --engrave-field: 0.22;
  --surface-dark: #080808;        /* Titan's deepest ground */
  --on-dark-muted: #9a9793;
  --hairline-on-dark: #171613;
  --on-plate: #ffffff;
  /* 0.86, not the 0.78 it mirrors in light. Measured at 390 over the dark plate, where
     the mobile wash is at its strongest and the usage stat sits lowest in the gradient,
     0.78 lands at 4.30:1 — under AA. Nothing had ever caught it because tools/titan/
     contrast.py deliberately does not score text over an image (it reports those nodes as
     "over image/video — flagged, not scored"), and tools/titan/heroplate.py is the first
     probe to measure this class at all. 0.86 reads 5.21:1 and is still visibly secondary
     to the headline's solid white. */
  --on-plate-2: rgba(255, 255, 255, 0.86);
  --on-plate-3: rgba(255, 255, 255, 0.66);
  --on-plate-fill: rgba(255, 255, 255, 0.12);
  --on-plate-rim: rgba(255, 255, 255, 0.16);
  --on-plate-rim-2: rgba(255, 255, 255, 0.22);
  --on-plate-rim-3: rgba(255, 255, 255, 0.26);
  --plate-wash: 16, 15, 13;
  --shadow: 0 14px 34px -18px rgba(0, 0, 0, 0.7);
  --glow: rgba(255, 255, 255, 0.025);
  --sh-tint: 0, 0, 0;
  --el-1: none;
  --el-2: none;
  --el-3: 0 10px 24px -12px rgba(0, 0, 0, 0.6);
  --el-hero: 0 18px 44px -20px rgba(0, 0, 0, 0.66);
  --rim: 0 0 0 0 transparent;
  --rim-strong: inset 0 0 0 1px var(--line);
  --ring: 0 0 0 3px color-mix(in srgb, var(--accent) 24%, transparent),
          0 0 0 1px color-mix(in srgb, var(--accent) 60%, transparent);
  /* state matrix, dark: the ground deepens by LIFTING here, not darkening */
  --state-hover-fill: rgba(255, 255, 255, 0.055);
  --state-hover-line: var(--line-strong);
  --state-active-fill: rgba(255, 255, 255, 0.1);
  --state-active-line: var(--text);
  --state-selected-fill: var(--text);
  --state-selected-ink: var(--bg);
  /* dark hides grain, so the tile carries slightly more alpha */
  --grain: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='140' height='140'%3E%3Cfilter id='n'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='0.85' numOctaves='3' stitchTiles='stitch'/%3E%3CfeColorMatrix type='saturate' values='0'/%3E%3C/filter%3E%3Crect width='140' height='140' filter='url(%23n)' opacity='0.06'/%3E%3C/svg%3E");
}
:root[data-theme="dark"][data-ink="mono"] {
  --up: var(--text);
  --up-text: var(--text);
  --up-soft: rgba(255, 255, 255, 0.1);
}

/* ============================================================================
   TILE DENSITY — the in-app register, stated as numbers
   ---------------------------------------------------------------------------
   One density, four surfaces: the Discover tiles, the ticker rows (watchlist,
   screener, dashboard, portfolio), the portfolio analysis panels, and the
   company page's accordion sections. Tokens rather than four sets of hardcoded
   paddings, because "site-wide density" that lives in four places is a setting
   that drifts.

   EVERY VALUE HERE IS A TITAN SPACING STEP — xs 6, sm 12, md 20. That is the
   whole point of naming this a register rather than treating it as an
   exception: the in-app surfaces are not "Titan with the padding cheated down",
   they are Titan read from the bottom of its own ramp instead of the top. The
   marketing surfaces read from the top (--rhythm-marketing, 96px bands, 32px
   radius); these read 12-20px padding and a ~40px row.

   The row height is the number that had to be chosen rather than looked up:
   12px of block padding around a 16px line box is 40px, which clears nothing in
   Titan but is the standard dense-table row and still leaves the 44px thumb
   floor to the tap-target rules.

   The rule for what gets cut: CHROME, NEVER A CHECKABLE NUMBER. Padding,
   leading, a second line of company name and a footer's border all go. The
   price a Discover tile's upside is measured from does NOT, because a
   percentage with nothing to stand on is exactly what this product doesn't
   ship — it just loses the rule and the 20px of box around it.
   ========================================================================= */
:root {
  --tile-pad-y: 12px;      /* .dtile block padding — Titan sm */
  --tile-pad-x: 12px;      /* …and inline — Titan sm */
  --tile-min-h: 118px;     /* the floor, not the height */
  --tile-gap: 6px;         /* value column ↔ sparkline — Titan xs. Was 8px; the two
                              px back are the ones `.wl-price` was clipping. */
  --row-pad-y: 12px;       /* .wl-row block padding — Titan sm; 12 + 16 + 12 = a 40px row */
  --panel-pad-y: 12px;     /* .pa-panel / .pa-item — Titan sm */
  --panel-pad-x: 20px;     /* …— Titan md */
  --sect-pad-y: 12px;      /* .sect-sum — Titan sm */
  --sect-pad-x: 20px;      /* …— Titan md */
  --sect-min-h: 52px;      /* still well over the 44px thumb floor */
}

* { box-sizing: border-box; }
/* iOS Safari inflates text inside block containers on its own ("text autosizing"), most
   visibly when a phone is turned landscape — which is exactly the width at which Discover
   switches from the compact list to tiles. An inflated 19px fair-value number outgrew the
   column it was laid out in and painted under the sparkline beside it. Pinning the adjust
   to 100% makes the rendered size the size we specified, on every device. */
html { scroll-behavior: smooth; -webkit-text-size-adjust: 100%; text-size-adjust: 100%; }
body {
  margin: 0;
  background:
    radial-gradient(1100px 600px at 82% -8%, var(--glow), transparent 60%),
    var(--bg);
  color: var(--text);
  font-family: var(--sans);
  -webkit-font-smoothing: antialiased;
  line-height: 1.5;
  min-height: 100vh;
}
a { color: var(--blue); text-decoration: none; }
/* An underline is the DEFAULT hover, and it is not enough on its own: a link that is
   already underlined in its resting state — body prose on /methodology, the "our Apple
   analysis" sample link — gets nothing back from it, so eighteen links on one page were
   measurably inert under the pointer. The underline still does the work where a link is
   bare; `text-decoration-thickness` covers the rest, and costs nothing where it is the
   only change a reader notices. */
a:hover { text-decoration: underline; text-decoration-thickness: 2px; }
.mono { font-family: var(--mono); font-variant-numeric: tabular-nums; }
.tnum { font-variant-numeric: tabular-nums; }

/* ---------- shell ---------- */
/* `--wrap-pad` is declared here and CONSUMED here, so the full-bleed landing bands
   (which escape the container with a negative margin of exactly this size) cannot drift
   away from it. It steps down at the same two breakpoints .wrap always did. */
.wrap { max-width: var(--shell-read); margin: 0 auto; padding: 0 var(--wrap-pad); }
header.top {
  position: sticky; top: 0; z-index: 20;
  backdrop-filter: blur(14px);
  background: color-mix(in srgb, var(--bg) 78%, transparent);
  border-bottom: 1px solid var(--line-soft);
}
.top-inner { display: flex; align-items: center; gap: 18px; padding: 14px 22px; max-width: 1120px; margin: 0 auto; }
/* THE LOCKUP — seal + wordmark, from the brand kit.
   Titan's spec puts "a circular engraved seal to the left of the wordmark" on inner
   pages, and the TickerWorth mark IS that seal: a guilloché rosette, the anti-counterfeit
   engraving of banknotes and share certificates. Nothing had to be invented for the two
   systems to agree.

   ALIGNMENT MOVED FROM `baseline` TO `center`. With a text-only wordmark, baseline was
   right. A 26px circular seal has no baseline to share — aligned to one it hangs low. */
.brand { display: flex; align-items: center; gap: 10px; cursor: pointer; user-select: none; }
.brand .mark {
  /* -0.03em, not -0.02em. THE KIT IS AUTHORITATIVE and this is the one place it and the
     shipped stylesheet disagreed: the kit sets the wordmark at heading-1's tracking. */
  font-family: var(--sans); font-size: 22px; letter-spacing: -0.03em; color: var(--text);
  font-weight: 400;
}
/* REDUCTION IS A TIER SYSTEM, NOT A SCALE — the kit says so and the files prove it:
   `tw-seal.svg` is 14 ellipses at stroke-width 1.9, `tw-seal-micro.svg` is 8 at 3.8. At
   26px the standard plate turns to grey mush, so the header takes the MICRO tier. Drawn
   as a mask over `currentColor` rather than an <img>, so one file serves both themes and
   the seal inherits the wordmark's ink. */
.brand-seal {
  width: 26px; height: 26px; flex: 0 0 auto; display: block;
  background-color: currentColor;
  -webkit-mask: url("/assets/brand/tw-seal-micro.svg") center / contain no-repeat;
  mask: url("/assets/brand/tw-seal-micro.svg") center / contain no-repeat;
  color: var(--text);
}
/* TITAN SHIPS NO ITALIC. Geist is upright-only in the faces we self-host, so every
   `font-style: italic` below this line would render as a SYNTHETIC oblique — a
   mechanically slanted upright, which at 11.5px disclaimer size is worse than no
   emphasis at all. Titan's own answer is explicit (DESIGN.md: "Weight, not color,
   creates hierarchy... emphasis comes from the 400/500 weight step"), so italic is
   replaced by weight, by the mono face, or by nothing where the surrounding rule
   already carries the distinction. */
.brand .mark b { color: var(--accent); font-weight: 500; }

/* Same rule, applied to the MARKUP this stylesheet does not own. `<em>` appears in
   web/app.js and in app/pages.py (which is frozen this phase), and the UA default for
   em/i/cite/dfn is `font-style: italic` — which the browser can only satisfy by
   slanting an upright face. Stating it once here converts every one of them to the
   weight step without touching a single template. `<i>` is also used as a decorative
   bar/pip element in several components; neither declaration affects an empty box. */
em, i, cite, dfn { font-style: normal; font-weight: 500; }
.brand .tag { font-family: var(--mono); font-size: 10.5px; letter-spacing: 0.18em; text-transform: uppercase; color: var(--faint); }

.search {
  margin-left: auto; display: flex; align-items: center; gap: 8px;
  background: var(--surface); border: 1px solid var(--line); border-radius: var(--r-full);
  padding: 7px 8px 7px 16px; transition: border-color 0.2s, box-shadow 0.2s;
  min-width: 260px;
}
.search:focus-within { border-color: var(--accent); box-shadow: 0 0 0 3px var(--accent-soft); }
.search input {
  background: none; border: none; outline: none; color: var(--text);
  font-family: var(--mono); font-size: 14px; letter-spacing: 0.06em; text-transform: uppercase;
  width: 100%; min-width: 60px;
}
.search input::placeholder { color: var(--faint); letter-spacing: 0.04em; }
/* THE "VALUE IT" PILL IS GONE, AND SO IS ITS STYLING. This block skinned a text button
   labelled "Value it" that sat nested inside the search pill: accent fill, mono 700 at
   12.5px, 7px/15px padding, a brightness+lift on hover. The markup became a 30px round
   icon disc (`.search-go`) and the label became nothing at all — but this ruleset stayed,
   and it is the reported misalignment.

   `.search button` is (0,1,1); `.search-go` is (0,1,0). Type selectors do not care about
   source order, so `padding: 7px 15px` here beat `padding: 0` there no matter that
   .search-go is declared 8,500 lines further down. On a 30px border-box button that
   leaves a content box 0px wide, and the 17px glyph — itself perfectly centred, ink
   bbox 2.95..21.05 around dead-centre 12,12 — got laid out from the 15px padding edge
   and hung 2px past the right rim. Measured on prod: offset [15, 7] where centred is
   [6.5, 6.5]. Vertical was right all along, which is why it read as a subtle lean
   rather than an obvious break.

   Deleting beats overriding here: the label these rules describe does not exist, so
   bumping `.search-go`'s specificity would just leave five dead declarations aimed at a
   button with no text, waiting to win some other property later. `.search button`
   matches `.search-go` and nothing else — the autocomplete rows inside `.search` are
   divs, not buttons — so `.search-go` now owns the whole control, which is what its own
   rule already assumed. The disc's colours, hover and coarse-pointer bump all live
   there. */
.theme-btn {
  background: var(--surface); border: 1px solid var(--line); color: var(--muted);
  border-radius: var(--r-full); width: 34px; height: 34px; cursor: pointer; font-size: 15px;
  display: grid; place-items: center; transition: color 0.2s, border-color 0.2s;
}
.theme-btn:hover { color: var(--accent); border-color: var(--accent); }
/* the touch floor, the same way `.search-go` takes it — a 34px disc is under --tap on a phone */
@media (pointer: coarse) { .theme-btn { width: var(--tap); height: var(--tap); } }

/* ---------- hero / landing ---------- */
.hero { padding: 58px 0 26px; }
/* THE HEADLINE MUST NEVER SIT AT THE CROSSING. That is the fix, and it is a design
   decision before it is a bug fix.

   THE ORIGINAL BUG (kept, because the rule only makes sense with it). The old copy, "Is
   this company actually worth it?", measured ~15.1em, and the old sizing set that single
   line at 0.99x-1.09x of its container across the entire 860-1440px range. A headline that
   fits by one percent fits only on the machine it was measured on: `--serif` was a STACK
   back then (Iowan / Georgia / Times) and resolved differently per OS — the same string was
   988px in Iowan and 977px in Georgia at 65.6px, against an 891px fallback — and a
   fractional zoom can round -0.03em toward zero, worth ~9% of the line across 34
   characters. Either one put "it?" outside the box.

   Tuning the clamp cannot fix this, only move it: the required width grows at ~51% of the
   viewport while the container grows at ~100%, so they cross exactly once, and wherever
   they cross there is a band where one engine keeps the line and another breaks it. So the
   rule is: the headline must either CLEAR its box comfortably or OVERRUN it clearly. The
   band between must not exist. Below 700px the 8em cap guarantees the overrun and every
   engine wraps; at 700px and up the container clears the line by at least 1.09x.

   WHAT TITAN CHANGED (2026-08-20). Half the original hazard is gone: `--serif` is no
   longer a per-OS stack. It resolves to self-hosted Geist for every reader, so the 11%
   face-substitution swing that this comment was written about cannot happen. The remaining
   swing is the metric-matched fallback shown during the swap, which measures 9.064em
   against Geist's 8.884em at weight 300 — so THE FALLBACK IS NOW THE WIDER FACE and is
   what the arithmetic in tests/test_hero_headline.py sizes against. Re-measure both if the
   copy or the face changes; the suite recomputes everything else from this file. */
.hero h1 {
  /* WEIGHT 300, not 400, and a line-height under 1 — see the .hero-h1 block at the end of
     this file for why the size carries the headline rather than the weight. This is a REAL
     instance now, not a synthetic light: the shipped Geist carries a full wght 100..900
     axis and the @font-face declares it. The prototype's 400-600 range would have rendered
     this as 400. */
  font-family: var(--serif); font-weight: 300;
  font-size: var(--t-hero);
  /* Leading is NOT declared here. It was — at 1.06, dead, losing to the 1.02 in the
     display-type block below, which is the same "declared twice, decided somewhere nobody
     would look" defect that block's own comment describes fixing for the TRACKING. The
     sibling property was missed. The hero band token now decides it, in one place. */
  margin: 0 0 16px;
  /* THE MEASURE, and why it needs a breakpoint rather than one number.

     An em-based cap is scale-invariant: `cap / line-width` is a constant, so a single value
     cannot both force a break on a phone and allow one line on desktop. Leaving it to the
     container instead reintroduces the crossing this whole file exists to remove — measured
     with the new copy, 560px and 620px landed at 0.989x and 1.049x of the container, inside
     the band where a font substitution decides whether the last word clips.

     So the break is explicit. Below 700px the cap binds at 8em, which is 0.88x of the
     9.1em single line and therefore ALWAYS overruns: every engine wraps, none is deciding.
     At 700px and up the container clears the line by at least 1.09x. */
  max-width: 16em;
  text-wrap: balance;
  /* Last resort, for absurd zoom on a narrow phone: break inside a word before spilling. */
  overflow-wrap: break-word;
}
/* THE EMPHASIS IS WEIGHT, NOT ITALIC — and that is Titan's own rule, stated in its
   DESIGN.md: "Weight, not color, creates hierarchy... emphasis comes from the 400/500
   weight step." It also has to be, because the shipped Geist has no italic and a browser
   asked for one can only slant the upright. The old copy coloured this word too, which was
   a second signal for a word that already had one; at 88px the 300->500 step is
   unmistakable on its own. */
.hero h1 em { font-style: normal; font-weight: 500; }
.hero p.lede { max-width: 640px; color: var(--muted); font-size: clamp(1rem, 0.95rem + 0.4vw, 1.18rem); margin: 0 0 26px; }
.hero .big-search { display: flex; gap: 10px; max-width: 540px; }
.hero .big-search input {
  flex: 1; background: var(--surface); border: 1px solid var(--line); border-radius: var(--r-sm);
  padding: 15px 18px; color: var(--text); font-family: var(--mono); font-size: 17px;
  letter-spacing: 0.08em; text-transform: uppercase; outline: none; transition: border-color 0.2s, box-shadow 0.2s;
}
.hero .big-search input:focus { border-color: var(--accent); box-shadow: 0 0 0 3px var(--accent-soft); }
.hero .big-search button {
  border: none; cursor: pointer; border-radius: var(--r-sm); padding: 0 26px;
  background: var(--accent); color: var(--bg); font-family: var(--mono); font-weight: 700; font-size: 15px;
  letter-spacing: 0.04em; transition: transform 0.15s var(--ease), filter 0.15s;
}
.hero .big-search button:hover { filter: brightness(1.08); transform: translateY(-1px); }
.hint { margin-top: 12px; font-family: var(--mono); font-size: 12px; color: var(--faint); }
.hint b { color: var(--muted); font-weight: 600; }

/* section label */
.eyebrow {
  font-family: var(--mono); font-size: var(--t-eyebrow); letter-spacing: var(--track-eyebrow);
  text-transform: uppercase;
  color: var(--faint); display: flex; align-items: center; gap: 12px; margin: 40px 0 18px;
}
.eyebrow::after { content: ""; flex: 1; height: 1px; background: var(--line-soft); }

/* ---------- autocomplete dropdown ---------- */
.search, .hero .big-search { position: relative; }
.ac-dropdown {
  position: absolute; top: calc(100% + 6px); left: 0; right: 0; z-index: 40;
  background: var(--surface); border: 1px solid var(--line); border-radius: var(--r-sm);
  box-shadow: var(--shadow); overflow: hidden; max-height: 340px; overflow-y: auto;
  /* Entrance and exit both live in the popover block near the bottom of this file
     (search "F5. Popovers"). This rule used to also declare `animation: fade`, which
     was already dead — the F5 block overrode it — and would have come back to life
     when 006 removed the keyframe it was overridden by. Deleted rather than shadowed. */
}
.hero .big-search .ac-dropdown { right: 116px; }  /* align under the input, not the Run-it button */
.ac-item {
  display: grid; grid-template-columns: auto minmax(0, 1fr) auto; align-items: center; gap: 10px;
  padding: 10px 14px; cursor: pointer; border-bottom: 1px solid var(--line-soft);
  transition: background 0.12s;
}
.ac-item:last-child { border-bottom: none; }
/* fill removed: the Phase 2 wash is the one hover fill in the app. `.active` keeps
   its own, because a highlighted autocomplete row is a SELECTION, not a hover. */
.ac-item.active { background: var(--surface-2); }
.ac-tk { font-family: var(--mono); font-weight: 700; font-size: 13px; color: var(--accent); }
.ac-nm { font-size: 13.5px; color: var(--text); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.ac-ex { font-family: var(--mono); font-size: 10.5px; color: var(--muted); white-space: nowrap; }
@media (max-width: 620px) { .hero .big-search .ac-dropdown { right: 0; } }

/* ---------- analysed stat + sector grouping ---------- */
.analysed-stat { font-family: var(--mono); font-size: 13px; color: var(--muted); margin: 8px 0 4px; }
/* Its counts merged into the single #usage-stat sentence; keep the node (the desktop grid and
   the hero rhythm both reference it) but never let an empty row take vertical space. */
.analysed-stat:empty { display: none; }
.analysed-stat .stat-num { color: var(--accent); font-weight: 700; font-size: 15px; }
.models-head { display: flex; align-items: center; gap: 16px; flex-wrap: wrap; margin: 40px 0 16px; }
.models-head .eyebrow::after { display: none; }
.sector-filter { display: flex; gap: 7px; flex-wrap: wrap; }
.sector-chip {
  font-family: var(--mono); font-size: 11.5px; letter-spacing: 0.02em; color: var(--muted);
  background: var(--surface); border: 1px solid var(--line); border-radius: var(--r-full);
  padding: 5px 12px; cursor: pointer; transition: color 0.2s, border-color 0.2s, background 0.2s;
}
.sector-chip:hover { color: var(--accent); border-color: var(--accent); }
.sector-chip.active { color: var(--bg); background: var(--accent); border-color: var(--accent); font-weight: 600; }
.sector-group { margin-bottom: 26px; }
.sector-head {
  display: flex; align-items: center; gap: 10px; margin: 0 0 12px;
  font-family: var(--mono); font-size: 12px; letter-spacing: 0.14em; text-transform: uppercase; color: var(--text);
}
.sector-head .sector-count {
  font-size: 10.5px; color: var(--muted); background: var(--surface-2); border: 1px solid var(--line-soft);
  border-radius: var(--r-full); padding: 1px 8px; letter-spacing: 0.02em;
}

/* ---------- company grid ----------
   `.grid` is live and holds `.grp grp-list` rows on a phone and `.dtile` tiles on desktop.
   The `.card` tile it was originally written for is gone: measured 2026-08-10, zero
   elements carry a bare `card` class anywhere in the product, so its ~20 rules were
   deleted rather than left reading as live. */
.grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(232px, 1fr)); gap: 14px; }
.bar { height: 5px; border-radius: var(--r-sm); background: var(--surface-2); margin-top: 13px; overflow: hidden; position: relative; }
.bar i { position: absolute; top: 0; bottom: 0; left: 50%; width: 1px; background: var(--faint); }
.bar span { position: absolute; top: 0; bottom: 0; border-radius: var(--r-sm); }
.pill {
  font-family: var(--mono); font-size: 10.5px; font-weight: 700; padding: 3px 8px; border-radius: var(--r-full);
  letter-spacing: 0.03em; display: inline-block;
}
.pill.up { color: var(--up-text); background: var(--up-soft); }
.pill.down { color: var(--down-text); background: var(--down-soft); }
.pill.flat { color: var(--muted); background: var(--surface-2); }
.pill.parked { color: var(--accent); background: var(--accent-soft); }

/* ---------- company view ---------- */
#view { padding-bottom: 80px; }
.back {
  font-family: var(--mono); font-size: 12px; color: var(--muted); cursor: pointer;
  display: inline-flex; align-items: center; gap: 7px; margin: 26px 0 18px; background: none; border: none;
}
.back:hover { color: var(--accent); }
.co-head { display: flex; align-items: baseline; gap: 16px; flex-wrap: wrap; margin-bottom: 6px; }
.co-head h2 { font-family: var(--serif); font-weight: 400; font-size: clamp(1.8rem, 1.2rem + 2.4vw, 2.9rem); letter-spacing: -0.02em; margin: 0; }
.co-head .tk-big { font-family: var(--mono); color: var(--accent); font-size: 15px; letter-spacing: 0.08em; margin-top: 8px; }
.badges { display: flex; gap: 8px; margin: 10px 0 4px; flex-wrap: wrap; align-items: center; }
.badge {
  font-family: var(--mono); font-size: 10.5px; letter-spacing: 0.1em; text-transform: uppercase;
  padding: 4px 10px; border-radius: var(--r-full); border: 1px solid var(--line); color: var(--muted);
}
.badge.modeled { color: var(--text); border-color: color-mix(in srgb, var(--text) 30%, var(--line)); }
.badge.auto { color: var(--accent); border-color: color-mix(in srgb, var(--accent) 40%, var(--line)); }
.badge.automodeled { color: var(--blue); border-color: color-mix(in srgb, var(--blue) 40%, var(--line)); }
/* small honest tag on auto-modeled grid cards */
.tier-tag { font-family: var(--mono); font-size: 8.5px; font-weight: 700; letter-spacing: 0.06em; text-transform: uppercase; padding: 2px 5px; border-radius: var(--r-sm); margin-left: 7px; vertical-align: middle; }
.tier-tag.auto { color: var(--blue); background: color-mix(in srgb, var(--blue) 12%, transparent); border: 1px solid color-mix(in srgb, var(--blue) 30%, var(--line)); }

.panel { background: var(--surface); border: 1px solid var(--line); border-radius: var(--r); padding: 22px; margin-top: 16px; }
.panel h3 { font-family: var(--mono); font-size: var(--t-eyebrow); letter-spacing: var(--track-eyebrow); text-transform: uppercase; color: var(--muted); margin: 0 0 16px; }
.notice {
  border: 1px solid var(--accent-soft); background: color-mix(in srgb, var(--accent-soft) 40%, transparent);
  border-radius: var(--r); padding: 12px 15px; font-size: 13px; color: var(--text); margin-top: 16px;
}
.notice b { color: var(--accent); }

/* DEGRADED MODE. Deliberately warmer than a plain .notice and deliberately NOT red: the
   product is working and the valuation is unaffected — it is the live price feed that is
   down. An alarm would overstate it; the accent-coloured notice it otherwise inherits would
   understate it into invisibility, which is how a stale number gets read as a live one. */
.notice.degraded {
  border-color: color-mix(in srgb, var(--warn, #c9821f) 45%, transparent);
  background: color-mix(in srgb, var(--warn, #c9821f) 11%, transparent);
}
.notice.degraded b { color: var(--warn, #c9821f); letter-spacing: 0.01em; }

/* the duel */
/* THE ONE PLACE A ZERO FLOOR IS WRONG. Giving every `fr` track `minmax(0, …)` stops a
   grid overflowing its card, and on nineteen templates that was purely a fix. Here it cut
   "$194.63" in half at 1024 — the price and the fair value, side by side, are the two most
   checkable numbers on the page, and a number that is cut is worse than a number that
   spills: spilling is ugly and complete, cutting silently changes what it says.

   So this keeps a `min-content` floor — the grid will widen before it will ever truncate a
   figure — and fixes the actual cause, which is that the figure was sized against the
   VIEWPORT (`clamp(2rem, 1rem + 4vw, 3.3rem)`) while living in a card whose width does not
   track it. At 1024 the company page is two columns, so a 4vw figure was being asked to fit
   a 163px track. Sized against its own container instead, it fits by construction at every
   width and the floor never has to fire. */
.duel {
  display: grid;
  grid-template-columns: minmax(min-content, 1fr) auto minmax(min-content, 1fr);
  align-items: center; gap: 20px;
  container-type: inline-size;
}
.duel .side { text-align: center; }
.duel .side .lab { font-family: var(--mono); font-size: 10.5px; letter-spacing: 0.16em; text-transform: uppercase; color: var(--faint); }
.duel .side .num { font-family: var(--mono); font-size: clamp(1.5rem, 9.4cqi, 3.3rem); font-weight: 700; font-variant-numeric: tabular-nums; letter-spacing: -0.02em; line-height: 1.1; }
.duel .side.value .num { color: var(--accent); }
.duel .vs { font-family: var(--mono); color: var(--faint); font-size: 16px; text-transform: uppercase; letter-spacing: var(--track-eyebrow); }
.verdict { text-align: center; margin-top: 14px; }
.verdict .big { font-family: var(--mono); font-size: 30px; font-weight: 700; font-variant-numeric: tabular-nums; }
.verdict .cap { font-size: 13px; color: var(--muted); margin-top: 2px; }

/* WACC slider hero */
.slider-hero { position: relative; }
.sh-top { display: flex; justify-content: space-between; align-items: flex-end; gap: 16px; flex-wrap: wrap; margin-bottom: 4px; }
.sh-read .lab { font-family: var(--mono); font-size: 10.5px; letter-spacing: 0.16em; text-transform: uppercase; color: var(--faint); }
.sh-read .v { font-family: var(--mono); font-size: clamp(2.4rem, 1.4rem + 4vw, 3.6rem); font-weight: 700; font-variant-numeric: tabular-nums; letter-spacing: -0.02em; line-height: 1; color: var(--accent); }
.sh-up { text-align: right; font-family: var(--mono); }
.sh-up .v { font-size: 26px; font-weight: 700; font-variant-numeric: tabular-nums; }
.sh-up .lab { font-size: 10.5px; letter-spacing: 0.14em; text-transform: uppercase; color: var(--faint); }
.chart-box { position: relative; margin: 10px 0 4px; }
#curve { width: 100%; height: 210px; display: block; }
#price-canvas { width: 100%; height: 200px; display: block; }

/* ---------- price history chart ---------- */
.range-tabs { display: flex; gap: 6px; margin: 2px 0 4px; }
.range-tab {
  font-family: var(--mono); font-size: 11.5px; letter-spacing: 0.04em; color: var(--muted);
  background: var(--surface); border: 1px solid var(--line); border-radius: var(--r-full);
  padding: 5px 13px; cursor: pointer; transition: color 0.2s, border-color 0.2s, background 0.2s;
}
.range-tab:hover { color: var(--accent); border-color: var(--accent); }
.range-tab.active { color: var(--bg); background: var(--accent); border-color: var(--accent); font-weight: 700; }
.range-tab.pro-range.locked { color: var(--faint); }
.price-meta {
  display: flex; flex-wrap: wrap; gap: 6px 16px; align-items: center; margin-top: 10px;
  font-family: var(--mono); font-size: 12.5px; color: var(--muted);
}
.price-meta b { color: var(--text); }
.pm-intrinsic { color: var(--accent); }
.price-lock { display: flex; flex-wrap: wrap; align-items: center; gap: 12px; font-family: var(--mono); font-size: 12.5px; color: var(--muted); }
.mini-upgrade {
  font-family: var(--mono); font-size: 12px; font-weight: 700; color: var(--bg); background: var(--accent);
  border: none; border-radius: var(--r-full); padding: 6px 14px; cursor: pointer; transition: filter 0.15s, transform 0.15s;
}
.mini-upgrade:hover { filter: brightness(1.08); transform: translateY(-1px); }
.wacc-row { display: flex; align-items: center; gap: 16px; margin-top: 14px; }
/* tabular-nums: the percentage re-renders on every step of the drag, and proportional
   digits change width as they change value, so the readout shimmers sideways. Inherited
   by #wacc-num, which is the span that actually holds the live figure. */
.wacc-val { font-family: var(--mono); font-size: 22px; font-weight: 700; color: var(--text); min-width: 92px; font-variant-numeric: tabular-nums; }
.wacc-val small { color: var(--faint); font-size: 12px; font-weight: 400; letter-spacing: 0.1em; }
/* [F5] Skip link — the one genuinely missing keyboard affordance. Sits off-screen until it takes
   focus, then lands over the header so the first Tab can jump the whole nav into the content.
   NOTE: a general focus ring is NOT added here. The app already ships one further down
   (`:where(button, a, summary, input, [tabindex]):focus-visible` → `box-shadow: var(--ring)`),
   verified rendering as a 3px + 1px ring on keyboard focus. The only addition is the WACC slider,
   where a box-shadow ring hugging a 6px track is easy to miss — that rule out-specifies the
   :where() one deliberately. */
.skip-link {
  position: fixed; top: 8px; left: 8px; z-index: 200;
  transform: translateY(-160%);
  background: var(--text); color: var(--bg);
  padding: 10px 16px; border-radius: var(--r-full);
  font-family: var(--mono); font-size: 13px; text-decoration: none;
  transition: transform 0.18s ease;
}
.skip-link:focus { transform: translateY(0); }

input[type="range"].wacc:focus-visible {
  outline: 2px solid var(--accent);
  outline-offset: 4px;
}

input[type="range"].wacc {
  -webkit-appearance: none; appearance: none; flex: 1; height: 6px; border-radius: var(--r-sm);
  background: linear-gradient(90deg, var(--up) 0%, var(--accent) 55%, var(--down) 100%);
  outline: none; cursor: pointer;
}
/* Grab and release are deliberately asymmetric. The grab is the user deciding, and it has
   to feel caught the instant they touch it (110ms); the release is the system answering,
   and it can be unhurried without feeling slow (220ms). Matched timings read mechanical.
   Firefox got no press feedback at all until the ::-moz- pair below. */
input[type="range"].wacc::-webkit-slider-thumb {
  -webkit-appearance: none; appearance: none; width: 26px; height: 26px; border-radius: 50%;
  /* No shadow. It was `0 3px 12px rgba(0,0,0,0.5)` — half-opaque black under a 26px thumb,
     the heaviest shadow in the product, on the control Titan's flat register is most visible
     on. Two things settle it beyond the kit: the `::-moz-range-thumb` rule immediately below
     never had one, so Chrome and Firefox have been drawing this dial differently the whole
     time and nobody has missed it in Firefox; and the thumb is already a 4px accent ring
     against a 6px track, which is the contrast doing the work. */
  background: var(--text); border: 4px solid var(--accent); cursor: grab;
  transition: transform 220ms var(--ease-out);
}
input[type="range"].wacc::-webkit-slider-thumb:active {
  transform: scale(1.18); cursor: grabbing;
  transition: transform 110ms var(--ease-out);
}
input[type="range"].wacc::-moz-range-thumb {
  width: 26px; height: 26px; border-radius: 50%; background: var(--text); border: 4px solid var(--accent); cursor: grab;
  transition: transform 220ms var(--ease-out);
}
input[type="range"].wacc::-moz-range-thumb:active {
  transform: scale(1.18); cursor: grabbing;
  transition: transform 110ms var(--ease-out);
}
/* The blanket reduced-motion `*` rule near the top of this file cannot reach these: `*`
   matches elements, and a slider thumb is a shadow pseudo-element, so it is the one
   transition in the app the global floor silently misses. Collapse it explicitly. The
   press feedback itself survives — the thumb still grows on grab, it just stops tweening. */
@media (prefers-reduced-motion: reduce) {
  input[type="range"].wacc::-webkit-slider-thumb,
  input[type="range"].wacc::-webkit-slider-thumb:active { transition-duration: 0.001ms; }
  input[type="range"].wacc::-moz-range-thumb,
  input[type="range"].wacc::-moz-range-thumb:active { transition-duration: 0.001ms; }
}
.wacc-meta { display: flex; gap: 18px; flex-wrap: wrap; margin-top: 14px; font-family: var(--mono); font-size: 12px; color: var(--muted); }
.wacc-meta b { color: var(--text); font-variant-numeric: tabular-nums; }
.reset-w { font-family: var(--mono); font-size: 11px; color: var(--faint); background: none; border: 1px solid var(--line); border-radius: var(--r-full); padding: 4px 11px; cursor: pointer; }
.reset-w:hover { color: var(--accent); border-color: var(--accent); }

/* metric strip */
/* 176px, not 150. Geist Mono is a 0.600 advance on every glyph; the mono face this
   app previously RESOLVED to on macOS was SF Mono at 0.495. Measured: "$2,081,655M"
   at 21px is 114.3px in SF Mono and 138.6px in Geist Mono — 21% wider — so a cell
   sized for the old face clipped the new one by 19px. The figure is the point of
   this component, so the cell grows; it does not get a smaller number. */
/* HAIRLINE MOSAICS DRAW THEIR LINES PER CELL, NOT AS A CONTAINER GROUND.
   The old technique — container background var(--line) showing through a 1px gap — is
   correct only while the item count happens to fill the last row. Four metrics in three
   auto-fit columns left two empty tracks, and the "hairline" ground painted them as a
   full-height sand SLAB beside the fourth cell (measured on the company page at 1440).
   Each cell now draws its own 1px line with a 0-blur ring shadow into the same 1px gap —
   a hairline, not an elevation — so an unfilled track shows the card surface and simply
   is not there. Applies to every auto-fit mosaic: .metrics, .opt-facts, .an-kpis. */
.metrics { display: grid; grid-template-columns: repeat(auto-fit, minmax(176px, 1fr)); gap: 1px; background: var(--surface); border: 1px solid var(--line); border-radius: var(--r); overflow: hidden; }
.metric { background: var(--surface); padding: 16px 18px; box-shadow: 0 0 0 1px var(--line); }
.metric .lab { font-family: var(--mono); font-size: 10px; letter-spacing: 0.14em; text-transform: uppercase; color: var(--faint); }
/* A FIGURE MAY WRAP. A FIGURE MAY NEVER BE SILENTLY CUT. `overflow-wrap: anywhere` is
   the guarantee, not the fix: whatever the track width and whatever the number, no digit
   is ever hidden. It exists because `$2,066,925M` needs 127px and the phone grid handed
   this cell 123px of content — four pixels short, the "M" gone, and the page still
   reporting a market cap. Everything below is what keeps the guarantee from firing. */
.metric .val { font-family: var(--mono); font-size: 20px; font-weight: 500; font-variant-numeric: tabular-nums; margin-top: 6px; overflow-wrap: anywhere; }
.metric .sub { font-size: 11px; color: var(--muted); margin-top: 3px; }

/* two-col */
.cols { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 16px; }
@media (max-width: 780px) { .cols { grid-template-columns: minmax(0, 1fr); } .duel { grid-template-columns: minmax(0, 1fr); } .duel .vs { display: none; } }

/* heatmap.
   The cell ink was a hardcoded #0c0f12 — the exact class the contrast probe caught on the
   change pills (near-black tuned for light fills, painted onto dark-theme fills). The cell
   fills are now theme-token mixes (see heatColor in app.js), so the ink is the theme's own.
   `td` declares its mono HERE, at table.heat td strength, because the figures are figures —
   the running-prose sweep's blanket `.heat td` sans rule would otherwise flatten them. */
table.heat { border-collapse: collapse; width: 100%; font-family: var(--mono); font-size: 12px; }
table.heat th { color: var(--faint); font-weight: 600; padding: 6px; font-size: 10.5px; letter-spacing: 0.06em; }
table.heat td { padding: 9px 5px; text-align: center; font-family: var(--mono); font-variant-numeric: tabular-nums; border-radius: var(--r-sm); color: var(--text); font-weight: 500; }
table.heat td.axis { background: none; color: var(--muted); font-weight: 400; }
table.heat td.base { outline: 2px solid var(--text); outline-offset: -2px; }

/* assumptions table */
table.assum { border-collapse: collapse; width: 100%; font-size: 13px; }
table.assum tr { border-bottom: 1px solid var(--line-soft); }
table.assum td { padding: 9px 8px; vertical-align: top; }
table.assum td.k { color: var(--muted); width: 40%; }
table.assum td.v { font-family: var(--mono); font-variant-numeric: tabular-nums; text-align: right; white-space: nowrap; }
.kind { font-family: var(--mono); font-size: 9px; letter-spacing: 0.08em; text-transform: uppercase; padding: 2px 6px; border-radius: var(--r-sm); margin-left: 8px; }
/* REPORTED was green. Provenance is not direction: the duotone is licensed so a screener
   can say up from down, and a green REPORTED chip both spends that license on decoration
   and teaches the reader that green sometimes means "sourced" — which devalues it where it
   means "up". The hierarchy survives in monochrome the way Titan says it should ("weight,
   not color"): reported = ink with a defined hairline (strongest claim), guidance = ink
   tint, estimate = slate (weakest). The stocks index's neutral "Hand-researched" chip was
   already doing this; now every provenance chip agrees with it. */
.kind.reported { color: var(--text); background: var(--accent-soft); border: 1px solid color-mix(in srgb, var(--text) 30%, var(--line)); }
.kind.guidance { color: var(--blue); background: color-mix(in srgb, var(--blue) 15%, transparent); }
.kind.estimate { color: var(--muted); background: var(--surface-2); }
/* This line carries raw XBRL concept names — `us-gaap:RevenueFromContractWithCustomer...`
   is one 346px unbreakable token. In a 156px box it overflows its container silently.
   `overflow-wrap: anywhere` is the only value that breaks a token with no break
   opportunity in it; `word-break: break-word` was already set on phones and does not. */
.src { display: block; color: var(--faint); font-size: 11px; font-family: var(--sans); text-align: right; margin-top: 2px; max-width: 100%; overflow-wrap: anywhere; }

/* news */
.news-item { border-left: 2px solid var(--line); padding: 4px 0 16px 16px; position: relative; }
.news-item:last-child { padding-bottom: 0; }
.news-item .dt { font-family: var(--mono); font-size: 11px; color: var(--accent); letter-spacing: 0.04em; }
.news-item .hl { font-weight: 600; margin: 3px 0; }
.news-item .bd { color: var(--muted); font-size: 13px; }
.bull-bear { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 10px; margin-top: 8px; font-size: 12.5px; }
.bull-bear .bx { padding: 8px 11px; border-radius: var(--r-sm); }
.bull-bear .bull { background: var(--up-soft); }
.bull-bear .bear { background: var(--down-soft); }
.bull-bear .bx b { font-family: var(--mono); font-size: 10px; letter-spacing: 0.1em; text-transform: uppercase; display: block; margin-bottom: 3px; }
.bull-bear .bull b { color: var(--up-text); } .bull-bear .bear b { color: var(--down-text); }

/* explainer prose */
.prose { color: var(--text); font-size: 14.5px; line-height: 1.7; }
.prose h1, .prose h2, .prose h3 { font-family: var(--serif); font-weight: 400; letter-spacing: -0.01em; margin: 22px 0 8px; }
.prose hr { border: none; border-top: 1px solid var(--line-soft); margin: 20px 0; }
.prose h1 { font-size: 22px; } .prose h2 { font-size: 19px; } .prose h3 { font-size: 16px; }
.prose p { margin: 0 0 12px; color: var(--muted); }
.prose strong { color: var(--text); }
.prose code { font-family: var(--mono); background: var(--surface-2); padding: 1px 5px; border-radius: var(--r-sm); font-size: 0.9em; }
.prose ul { color: var(--muted); padding-left: 20px; }

.disclaimer { font-size: 12px; color: var(--faint); text-align: center; margin: 34px auto 0; max-width: 640px; line-height: 1.6; }
/* THE KIT'S HAIRLINE RULE IS GROUND-DEPENDENT, and this is the handful of places the
   port did not follow it. The kit README: *"Elevation is fill + 1px hairline (`--sand` on
   cream, `--hairline` elsewhere)."* So `--line` (the kit's `--sand`, warm #d8d3cc) belongs
   on the cream page and `--line-soft` (the kit's `--hairline`, cool #e9eaeb) belongs on a
   white card — they are two GROUNDS, not two weights, and a cool grey line on warm cream
   is the one thing a monochrome system cannot absorb.

   Measured with tools/titan/hairlines.py, which samples the pixels just outside every
   soft-hairline border across ten surfaces: 1,048 of 1,084 were already correct. These
   sixteen were not. The footer is the one that matters — its top rule separates the footer
   from the cream page on every page in the product. */
.footer { border-top: 1px solid var(--line); margin-top: 50px; padding: 26px 0; text-align: center; color: var(--faint); font-family: var(--mono); font-size: 11px; letter-spacing: 0.05em; }

/* states */
.loader { text-align: center; padding: 70px 0; color: var(--muted); font-family: var(--mono); font-size: 13px; }
.spin { width: 28px; height: 28px; border: 2px solid var(--line); border-top-color: var(--accent); border-radius: 50%; margin: 0 auto 16px; animation: sp 0.8s linear infinite; }
@keyframes sp { to { transform: rotate(360deg); } }
.err-card { text-align: center; padding: 50px 24px; }
.err-card .ico { font-size: 34px; }
.err-card h3 { font-family: var(--serif); font-weight: 400; font-size: 24px; margin: 14px 0 8px; letter-spacing: normal; text-transform: none; color: var(--text); }
.err-card p { color: var(--muted); max-width: 460px; margin: 0 auto; }
.hidden { display: none !important; }
/* Set by the before-first-paint script in index.html when the URL carries any hash, and
   cleared by `Router.applyHash` the moment routing resolves. It exists so a deep link never
   paints the ~40,000px home shell only to have the router collapse it to a ~3,700px company
   view — a 36,000px reflow that moved the footer, nav and search on every indexed URL.
   This stylesheet is render-blocking, so the rule is in force for that first paint. */
html.route-deep #home { display: none !important; }
/* Keep the main column at least a viewport tall so the FOOTER starts below the fold.
   The residual shift at every width was the footer sitting near the fold while `#view` was
   still empty (docH was ~= viewport) and then being shoved off-screen as panels landed.
   Measured at 1024: footer top 704 -> off-screen as docH went 965 -> 3763.

   UNCONDITIONAL, and that is the point — an earlier attempt scoped this to `html.route-deep`
   and it made 768 WORSE (0.115 -> 0.151), because dropping the reservation at routing time
   let the footer jump back UP into view before the content had grown past it, charging two
   shifts instead of none. A reservation that is removed mid-load is worse than none; this one
   is never removed. Home and a loaded company view are both far taller, so it only ever binds
   during the empty moment it exists for. */
#main { min-height: 100vh; }
/* `.fade` itself is declared ONCE, further down beside the comment that explains its
   stagger (search "Staggered reveal"). A second declaration used to sit here at equal
   specificity and lost the cascade outright — measured, the element computed 0.5s/both
   from the later rule while this one claimed 0.4s with no fill — so it was dead code that
   read like the truth to anyone scanning the file. Same defect class as the duplicate
   @keyframes the last audit fixed one level down. The keyframe below is still live and is
   called by several rules directly. */
@keyframes fade { from { opacity: 0; transform: translateY(8px); } to { opacity: 1; transform: none; } }

/* ---------- prominent download CTA ---------- */
.dl-cta-row { display: flex; align-items: center; gap: 12px; flex-wrap: wrap; margin: 4px 0 2px; }
.dl-cta {
  display: inline-flex; align-items: center; gap: 8px; background: var(--accent); color: var(--bg);
  border: none; border-radius: var(--r-full); padding: 12px 20px; cursor: pointer;
  font-family: var(--mono); font-weight: 700; font-size: 14px; text-decoration: none;
  transition: filter 0.15s, transform 0.15s var(--ease);
}
.dl-cta:hover { filter: brightness(1.08); transform: translateY(-1px); text-decoration: none; }
.dl-cta.ghost { background: var(--surface-2); color: var(--text); border: 1px solid var(--line); font-weight: 600; }
.dl-cta.ghost:hover { border-color: var(--accent); color: var(--accent); }
.dl-cta.locked { background: var(--surface-2); color: var(--text); border: 1px solid color-mix(in srgb, var(--accent) 55%, var(--line)); }
.dl-cta.locked:hover { border-color: var(--accent); color: var(--accent); background: var(--accent-soft); }
.dl-cta-note { font-family: var(--mono); font-size: 11.5px; color: var(--faint); }

/* ---------- DCF model breakdown (Pro) ---------- */
.model-intro { color: var(--muted); font-size: 13px; margin: -4px 0 16px; }
.model-intro b { color: var(--text); }
.model-scroll { overflow-x: auto; border-radius: var(--r-sm); }
table.waterfall { border-collapse: collapse; width: 100%; min-width: 720px; font-family: var(--mono); font-size: 12px; }
table.waterfall th { text-align: right; padding: 8px 9px; color: var(--faint); font-weight: 600; font-size: 10px; letter-spacing: 0.04em; text-transform: uppercase; border-bottom: 1px solid var(--line); white-space: nowrap; }
table.waterfall th:first-child, table.waterfall td:first-child { text-align: left; }
table.waterfall td { text-align: right; padding: 9px 9px; font-variant-numeric: tabular-nums; border-bottom: 1px solid var(--line-soft); white-space: nowrap; }
table.waterfall td.yr { color: var(--accent); font-weight: 600; }
table.waterfall td.muted { color: var(--muted); }
table.waterfall td.strong { font-weight: 600; color: var(--text); }
table.waterfall td.accent { color: var(--accent); }
table.waterfall tr.tv-row td { border-top: 1px solid var(--line); background: color-mix(in srgb, var(--accent-soft) 22%, transparent); }
.bridge { margin-top: 18px; max-width: 460px; margin-left: auto; }
.brow { display: flex; justify-content: space-between; padding: 7px 0; font-family: var(--mono); font-size: 13px; color: var(--muted); border-bottom: 1px solid var(--line-soft); }
.brow .bval { color: var(--text); font-variant-numeric: tabular-nums; }
.brow.sum { color: var(--text); font-weight: 600; }
.brow.final { border-top: 2px solid var(--line); border-bottom: none; margin-top: 4px; padding-top: 12px; font-family: var(--serif); font-size: 17px; color: var(--text); }
.brow.final .bval { font-family: var(--mono); font-size: 20px; font-weight: 700; }
.dl-row { display: flex; align-items: center; gap: 10px; flex-wrap: wrap; margin-top: 22px; padding-top: 18px; border-top: 1px solid var(--line-soft); }
.dl-lab { font-family: var(--mono); font-size: 11px; letter-spacing: 0.08em; text-transform: uppercase; color: var(--faint); }
.dl-btn { font-family: var(--mono); font-size: 12.5px; font-weight: 600; color: var(--text); background: var(--surface-2); border: 1px solid var(--line); border-radius: var(--r-full); padding: 8px 15px; transition: border-color 0.2s, color 0.2s, transform 0.15s var(--ease); }
.dl-btn:hover { border-color: var(--accent); color: var(--accent); transform: translateY(-1px); text-decoration: none; }
.model-teaser { font-family: var(--mono); font-size: 13px; color: var(--muted); padding: 20px; line-height: 1.7; }

/* ---------- gating / locked / upgrade ---------- */
/* ---- THE LOCKED PANEL, SIZED BY ITS OVERLAY AND NOT BY WHAT IT HIDES ----
   Miraaj, on the "Go deeper" panel: the lock icon sits over the eyebrow text, the buttons
   straddle the panel edge, and the blur "looks like a rendering fault". Three symptoms,
   one cause, and it is geometric.

   `.locked-cta` was `position: absolute; inset: 0` — so the region's height came from the
   BLURRED CONTENT UNDERNEATH, and the overlay had to fit whatever that happened to be.
   Measured on the company page: a 594x84 region hosting an icon, a title, a two-line
   subtitle and a button — about 200px of content in 84px of box. Everything above and
   below it was overlapped, and the button hung out of the bottom edge.

   Grid-stacking both children in one cell inverts that: the region is as tall as its
   TALLEST child, which is the overlay, and the teaser stretches to fill. A locked panel can
   no longer be shorter than the thing it has to say.

   And the blur goes. It was `blur(6px)` at 0.5 — heavy enough to read as a failed render,
   and on the ratios lens it was blurring literal `••` placeholders, which is worse than
   ugly: it makes fake data look like real data behind frosted glass, on a product whose
   whole argument is that it does not do that. What the layer is FOR is showing the shape of
   what you get, so it keeps the shape and loses the pretence — a faint ghost of the layout,
   obviously decorative, with nothing to mistake for a number. ---- */
/* THE STACK IS FOR A TEASER, AND ONLY FOR A TEASER.
   `.locked` means two different things in this app. On a teaser it means "these are two
   LAYERS" — a dimmed preview under an unlock CTA — and stacking them in one grid cell is
   the whole point. On the screener's advanced filters it means "these controls are
   disabled": `class="scr-controls scr-adv locked"`, where `.scr-controls` supplies a
   five-column grid and the children are PEERS, not layers.

   `.locked > *` could not tell the two apart, so it put all four filter fields in cell
   1/1 — Max P/E, Min mkt cap, Min div yield and Health printed on top of one another,
   four labels and four controls in the same 1042x61 box. Measured: six pairwise overlaps,
   every child at the same x, y, width and height, all at full opacity.

   `:has(> .locked-cta)` is the test for "is this a teaser", because a teaser is exactly
   the thing that carries an unlock CTA. A disabled control group has none, keeps its own
   layout, and just goes quiet. */
.locked:has(> .locked-cta) { display: grid; }
.locked:has(> .locked-cta) > .locked-blur,
.locked:has(> .locked-cta) > .locked-cta { grid-area: 1 / 1; min-width: 0; }
.locked-blur { opacity: 0.16; filter: none; pointer-events: none; user-select: none; }
.locked-cta {
  display: flex; flex-direction: column; align-items: center;
  justify-content: center; text-align: center; gap: 6px;
  padding: var(--s-5) var(--s-4);
}
.lock-ico { font-size: 26px; }
.lock-title { font-family: var(--serif); font-size: 20px; }
.lock-sub { color: var(--muted); font-size: 13px; max-width: 380px; }
.lock-btn {
  margin-top: 10px; background: var(--accent); color: var(--bg); border: none; border-radius: var(--r-full);
  padding: 11px 22px; font-family: var(--mono); font-weight: 700; font-size: 13px; cursor: pointer;
  transition: filter 0.15s, transform 0.15s var(--ease);
}
.lock-btn:hover { filter: brightness(1.08); transform: translateY(-1px); }
.upgrade-card .lock-btn { margin-top: 18px; }
.upgrade-sub { font-size: 12px; color: var(--faint); margin-top: 14px; }
.free-meter {
  font-family: var(--mono); font-size: 11.5px; color: var(--muted);
  display: inline-flex; align-items: center; gap: 6px; padding: 5px 11px;
  background: var(--surface); border: 1px solid var(--line); border-radius: var(--r-full);
}
.free-meter b { color: var(--accent); }
.free-meter a { color: var(--accent); cursor: pointer; }
.free-meter-wrap { margin: 6px 0 34px; min-height: 1px; }

/* ---------- footer + legal pages ---------- */
.footer-disc { color: var(--muted); font-family: var(--sans); font-size: 12.5px; margin-bottom: 8px; }
.footer-disc b { color: var(--text); }
.footer-links a { color: var(--faint); }
.footer-links a:hover { color: var(--accent); }
.legal { max-width: 760px; padding-bottom: 80px; }
/* The loosest display heading on the site until now: -0.02em at up to 44.8px, while a
   20px h2 two lines below it carried -0.028em. Leading is deliberately NOT declared here —
   the `h1` band rule supplies it, which is the whole point of having a band. */
.legal h1 { font-family: var(--serif); font-weight: 400; font-size: clamp(2rem, 1.4rem + 2vw, 2.8rem); letter-spacing: var(--track-display); margin: 8px 0 12px; }
.legal h2 { font-family: var(--serif); font-weight: 400; font-size: 20px; margin: 28px 0 8px; }
.legal p, .legal li { color: var(--muted); font-size: 15px; line-height: 1.7; }
.legal b { color: var(--text); }
.legal ul { padding-left: 20px; }
.legal a { color: var(--accent); }
.legal .updated { font-family: var(--mono); font-size: 12px; color: var(--faint); }
.legal .legal-note { background: var(--accent-soft); color: var(--text); border-radius: var(--r-sm); padding: 12px 15px; font-size: 13.5px; }
.legal .callout { border: 1px solid color-mix(in srgb, var(--accent) 40%, var(--line)); border-radius: var(--r-sm); padding: 16px 18px; margin: 20px 0; color: var(--text); font-size: 14.5px; line-height: 1.65; }
.legal .legal-foot { margin-top: 40px; padding-top: 20px; border-top: 1px solid var(--line-soft); font-family: var(--mono); font-size: 13px; }

/* ---------- account + auth modal ---------- */
.account { display: flex; align-items: center; }
.signin-btn {
  font-family: var(--mono); font-size: 13px; font-weight: 600; letter-spacing: 0.03em;
  color: var(--text); background: var(--surface); border: 1px solid var(--line);
  border-radius: var(--r-full); padding: 8px 16px; cursor: pointer; transition: border-color 0.2s, color 0.2s, transform 0.15s var(--ease);
}
.signin-btn:hover { border-color: var(--accent); color: var(--accent); transform: translateY(-1px); }
.acct-wrap { position: relative; }
.acct-btn {
  display: flex; align-items: center; gap: 8px; background: var(--surface); border: 1px solid var(--line);
  border-radius: var(--r-full); padding: 6px 12px; cursor: pointer; color: var(--text);
  font-family: var(--mono); font-size: 12.5px; transition: border-color 0.2s;
}
.acct-btn:hover { border-color: var(--accent); }
@media (pointer: coarse) { .acct-btn { min-height: var(--tap); } }
/* THE BAR SHOWS AN AVATAR, NOT AN ADDRESS — at every width, not only on the phone.
   The markup has always carried both; the phone swapped to the initial at 620px and the
   desktop kept the full email. Two reasons the initial now wins everywhere. It is the
   profile affordance the redesign asks for, and a translucent bar over a hero has no room
   for a 150px string. And an email in a persistent chrome element is read by everyone who
   sees the screen — over a shoulder, in a screenshot, on a shared call — which is a poor
   trade for information the reader already knows.

   Nothing is lost to a screen reader: `.acct-btn` carries the full address in `aria-label`,
   and the initial is `aria-hidden`, so the accessible name is unchanged. */
.acct-email { display: none; }
.acct-initial {
  display: inline-flex; align-items: center; justify-content: center;
  width: 26px; height: 26px; border-radius: var(--r-full);
  background: var(--accent-soft); color: var(--accent);
  font-size: var(--t-meta); font-weight: 600; line-height: 1;
}
.pro-badge { font-family: var(--mono); font-size: 9.5px; font-weight: 700; letter-spacing: 0.1em; color: var(--bg); background: var(--accent); padding: 2px 6px; border-radius: var(--r-sm); }
.acct-menu {
  /* Material comes from the Phase B popover group (search "FROSTED GLASS") — it was the
     one popover built before Phase B and left opaque. */
  position: absolute; right: 0; top: calc(100% + 8px); border: 1px solid var(--line);
  border-radius: var(--r-sm); box-shadow: var(--shadow); overflow: hidden; min-width: 190px; z-index: 30;
  /* min-width is a floor, not a promise — at 320px it would be wider than the screen and
     the JS clamp could only choose which edge to lose. This lets it give way first. */
  max-width: calc(100vw - 16px);
  /* motion: see the F5 popover block */
}
.acct-menu button {
  display: block; width: 100%; text-align: left; background: none; border: none; cursor: pointer;
  padding: 11px 15px; color: var(--text); font-family: var(--sans); font-size: 13px; transition: background 0.15s;
}
.acct-menu button:hover { background: var(--surface-2); }
.acct-menu button.upgrade { color: var(--accent); font-weight: 600; }

.modal-backdrop {
  position: fixed; inset: 0; z-index: 100; display: grid; place-items: center; padding: 20px;
  /* Phase B, sheet tier: the largest surface in the app reads thickest (apple-design §12).
     The scrim dims, the blur pushes back — Apple pairs both; 4px was barely a blur at all. */
  background: rgba(4, 6, 8, 0.66);
  -webkit-backdrop-filter: var(--mat-blur-strong); backdrop-filter: var(--mat-blur-strong);
  animation: fade 0.25s var(--ease);
}
/* AN OVERLAY THAT IS TALLER THAN THE VIEWPORT MUST SCROLL, OR IT IS BROKEN.
   Measured at 844x390 — a phone in landscape, or a phone with the software keyboard up,
   which is the state this modal is MOST often in: the card is 447px, so 77px of it hung
   below the fold with `overflow: visible` on both the card and its backdrop. What was
   below the fold was the submit button, the legal line and the forgot-password link. The
   sign-up form was unusable and nothing on screen said so.

   `max-height` + `overflow-y: auto` here rather than on the backdrop: the backdrop is the
   scrim and centring it while it scrolls fights the `place-items: center`. `100dvh` and
   not `100vh` for the same reason the defect showed up on a phone — the dynamic unit is
   the one that accounts for the browser chrome that appears and disappears. */
.modal {
  position: relative; width: 100%; max-width: 400px; background: var(--surface);
  border: 1px solid var(--line); border-radius: var(--r); padding: 30px 28px 24px; box-shadow: var(--shadow);
  animation: modalIn 0.3s var(--ease);
  max-height: calc(100dvh - 40px);   /* 40px == the backdrop's own 20px padding, twice */
  overflow-y: auto;
  overscroll-behavior: contain;      /* the page behind must not scroll with it */
}
@keyframes modalIn { from { opacity: 0; transform: translateY(14px) scale(0.98); } to { opacity: 1; transform: none; } }
.modal-close { position: absolute; top: 14px; right: 14px; background: none; border: none; color: var(--faint); font-size: 15px; cursor: pointer; transition: color 0.2s; }
.modal-close:hover { color: var(--text); }
.auth-tabs { display: flex; gap: 4px; background: var(--surface-2); border-radius: var(--r-full); padding: 4px; margin-bottom: 20px; }
.auth-tab { flex: 1; background: none; border: none; cursor: pointer; padding: 8px; border-radius: var(--r-full); color: var(--muted); font-family: var(--mono); font-size: 12.5px; font-weight: 600; transition: background 0.2s, color 0.2s; }
.auth-tab.active { background: var(--accent); color: var(--bg); }
.auth-h { font-family: var(--serif); font-weight: 400; font-size: 24px; margin: 0 0 4px; letter-spacing: -0.01em; }
.auth-sub { color: var(--muted); font-size: 13px; margin: 0 0 20px; }
/* THE STUDENT OFFER LINE. Sits between the subtitle and the Google button, so the reader
   meets the offer and the button that grants it as one thought. Tinted rather than plain
   muted text because it has to survive being skimmed past on the way to the form — but a
   TINT, not a banner: it is a sentence about eligibility, not a promotion, and the honest
   sentence it carries would be undercut by anything that looked like a sale. Uses --line for
   the rule and --surface-2 for the ground, so it inherits both themes with no second block
   and no colour of its own to keep in sync. */
.auth-student {
  color: var(--text); font-size: 12.5px; line-height: 1.45;
  margin: -8px 0 18px; padding: 10px 12px;
  background: var(--surface-2); border: 1px solid var(--line);
  border-radius: var(--r-sm);
}
.fld { display: block; margin-bottom: 14px; }
.fld span { display: block; font-family: var(--mono); font-size: 11px; letter-spacing: 0.08em; text-transform: uppercase; color: var(--faint); margin-bottom: 6px; }
.fld input {
  width: 100%; background: var(--bg); border: 1px solid var(--line); border-radius: var(--r-sm);
  padding: 12px 14px; color: var(--text); font-family: var(--sans); font-size: 15px; outline: none;
  transition: border-color 0.2s, box-shadow 0.2s;
}
.fld input:focus { border-color: var(--accent); box-shadow: 0 0 0 3px var(--accent-soft); }
/* Inline validation, which is the whole point of the pseudo-class: `:user-invalid` fires
   only AFTER the reader has interacted with the field, where a bare `:invalid` would paint
   an empty required input red before it has been typed in — scolding someone for not having
   started yet (apple-design §16: "validate inline, not on submit"). The browser already
   tracked this state; nothing in the stylesheet was reading it.

   :focus keeps its own rule above and wins while the field is focused, so the red does not
   fight the focus ring mid-typing — it lands when the reader moves on. No new animation:
   `.fld input` already transitions border-color and box-shadow, so this inherits it and the
   reduced-motion floor already collapses it. */
.fld input:user-invalid { border-color: var(--down); box-shadow: 0 0 0 3px var(--down-soft); }
/* :user-valid used to paint the border green. Removed: the duotone is licensed for
   DIRECTION (a screener needs up and down), not for decoration — a correctly-typed email
   is not an up-tick, and silence is what "fine" looks like in a near-monochrome system.
   Error keeps its voice above; validity speaks by not speaking. */
.auth-error { background: var(--down-soft); color: var(--down-text); font-size: 13px; padding: 9px 12px; border-radius: var(--r-sm); margin-bottom: 14px; }
/* The success box was green (--up-soft / --up): same call as :user-valid above — success
   is not an up-tick, and the sentence inside it already says the good news. Neutral ink. */
.auth-ok { background: var(--accent-soft); color: var(--text); font-size: 14px; padding: 12px 14px; border-radius: var(--r-sm); margin-bottom: 14px; }
/* The static shell pages (404 / 500 / reset) — their headings were inline 28-30px with
   -0.01em tracking, off the ramp every other heading reads from. Same treatment as
   `.legal h1`, one size down for a page whose whole job is one sentence. */
.shell h1 { font-family: var(--serif); font-weight: 400; font-size: clamp(1.75rem, 1.4rem + 1.2vw, 2rem); letter-spacing: var(--track-display); margin: 8px 0 10px; }
.shell .shell-sub { color: var(--muted); font-size: 14px; margin-bottom: 22px; line-height: 1.55; }
.shell .shell-foot { margin-top: 18px; font-size: 13px; }
.shell .shell-foot a { color: var(--accent); }
/* A CTA is a pill (`--r-pill`), and this was the last 6px rectangle among them — it
   survived every sweep because it lives in the auth modal and on /reset, /404 and /500,
   none of which the pill probe ever opened. Label typography comes from the shared
   button-primary rule near the end of this file. */
/* ---------- the signup choice (app/card_trial.py) ---------- */
/* Two doors, drawn as two equal-weight cards. THE FREE ONE IS NOT SMALLER, not greyer and not
   a link under the button: it gets the same box, the same type ramp and the same hit area as
   the trial. `app/card_trial.py` forbids dark patterns in the flow; this is that rule in CSS,
   where "which option looks like the real one" is actually decided. The only difference the
   selected state draws is WHICH ONE IS SELECTED. */
.trial-choice { border: 0; padding: 0; margin: 0 0 14px; min-width: 0; }
.trial-choice-legend {
  display: block; padding: 0; float: left; width: 100%;   /* float: the legend box ignores margin-bottom */
  font-family: var(--mono); font-size: 11px; letter-spacing: 0.08em;
  text-transform: uppercase; color: var(--faint); margin-bottom: 6px;
}
.trial-opt {
  display: flex; gap: 10px; align-items: flex-start; clear: both;
  background: var(--bg); border: 1px solid var(--line); border-radius: var(--r-sm);
  padding: 11px 13px; margin-bottom: 8px; cursor: pointer;
  transition: border-color 0.2s, background 0.2s;
}
.trial-opt:hover { border-color: var(--state-hover-line); }
/* The whole card reads as chosen, not just the dot — the dot is 12px of signal on a box the
   reader is actually clicking. Progressive: without :has() the radio still shows the state. */
.trial-opt:has(input:checked) { border-color: var(--accent-rim); background: var(--accent-soft); }
/* The ring lands on the CARD when the radio inside it is focused by keyboard, because the
   radio itself is a 13px target inside a box the size of a button — a focus ring drawn only
   around the dot is a ring nobody sees. */
.trial-opt:has(input:focus-visible) { box-shadow: var(--ring); }
.trial-opt input { margin: 3px 0 0; accent-color: var(--accent); flex: none; }
.trial-opt input:focus-visible { outline: none; }   /* the card carries the ring, above */
.trial-opt-body { display: block; min-width: 0; }
.trial-opt-title { display: block; font-size: 14px; color: var(--text); margin-bottom: 3px; }
.trial-opt-note { display: block; font-size: 12px; line-height: 1.5; color: var(--muted); }
/* Who holds the card, under both options — it is true whichever is picked, and repeating it
   inside the trial box would weight that option's wall of text against the free one. */
.trial-choice-card { font-size: 11.5px; line-height: 1.5; color: var(--faint); margin: 0 0 4px; }

.auth-submit {
  width: 100%; background: var(--accent); color: var(--bg); border: none; border-radius: var(--r-pill);
  padding: 13px; cursor: pointer;
  transition: filter 0.15s, transform 0.15s var(--ease);
}
.auth-submit:hover { filter: brightness(1.07); transform: translateY(-1px); }
/* The opacity and the cursor now come from the state matrix at the end of this file —
   they were improvised here at 0.6 and, on `.dt-watch`, at 0.7, which is how two
   disabled controls end up looking differently disabled. `transform: none` stays: it is
   about this button's press animation, not about being disabled. */
.auth-submit:disabled { transform: none; }
/* Google's branding guidelines require their mark on a neutral surface at full contrast —
   so this deliberately does NOT take `--accent` like `.auth-submit` does. It is a secondary
   control in our palette and a compliant Google button at the same time. */
.auth-google {
  display: flex; align-items: center; justify-content: center; gap: 10px;
  width: 100%; box-sizing: border-box;
  padding: 12px; margin-bottom: 4px;
  border: 1px solid var(--line); border-radius: var(--r-pill);
  color: var(--text); text-decoration: none; font-size: 14px;
  transition: border-color 0.15s, background 0.15s, transform 0.15s var(--ease);
}
.auth-google:hover { border-color: var(--text); transform: translateY(-1px); }
.auth-google:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }
.auth-google-g { flex: none; }
/* The "or" rule. A flex row rather than a pseudo-element on a border, so the label sits in
   the gap at any width instead of on top of the line at narrow ones. */
.auth-or {
  display: flex; align-items: center; gap: 12px;
  margin: 14px 0 12px; font-size: 11px; letter-spacing: 0.08em;
  text-transform: uppercase; color: var(--faint); font-family: var(--mono);
}
.auth-or::before, .auth-or::after {
  content: ""; flex: 1; height: 1px; background: var(--line-soft);
}
.auth-foot { display: flex; justify-content: space-between; align-items: center; margin-top: 16px; font-size: 12px; }
.auth-foot a { color: var(--muted); }
.auth-note { font-family: var(--mono); font-size: 11px; color: var(--faint); }

/* ---------- lens: multiples & health ---------- */
.lens-groups { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 22px 32px; }
.lens-gtitle { font-family: var(--mono); font-size: 11px; letter-spacing: 0.12em; text-transform: uppercase; color: var(--accent); margin-bottom: 10px; }
.lens-row { display: grid; grid-template-columns: 12px minmax(0, 1fr) auto; grid-template-areas: "dot lab val" ". note note"; align-items: center; gap: 2px 8px; padding: 7px 0; border-bottom: 1px solid var(--line-soft); }
.lens-dot { grid-area: dot; width: 8px; height: 8px; border-radius: 50%; }
.lens-lab { grid-area: lab; font-size: 13px; color: var(--text); }
.lens-val { grid-area: val; font-family: var(--mono); font-variant-numeric: tabular-nums; font-size: 14px; font-weight: 600; }
.lens-note { grid-area: note; font-size: 11.5px; color: var(--muted); line-height: 1.4; }
.lens-src { margin-top: 16px; font-family: var(--mono); font-size: 11px; color: var(--faint); }
@media (max-width: 620px) { .lens-groups { grid-template-columns: minmax(0, 1fr); } }

/* ---------- motion polish ---------- */
/* Smooth theme cross-fade — targeted surfaces only, so toggling light/dark glides. */
body, header.top, .panel, .metric, .search, .hero .big-search input,
.badge, table.assum tr, .metrics {
  transition: background-color 0.35s var(--ease), border-color 0.35s var(--ease),
              color 0.35s var(--ease);
}


/* Staggered reveal for the company view — each panel eases up a beat after the last.
   JS sets --i on the fade elements; keeps the cascade cheap and declarative.

   THE ONLY declaration of `.fade` in this file, and it should stay that way: it decorates
   ~35 panels per company page plus the home grid's sector cards, so a second declaration
   further down would silently retune all of them. 0.5s was hardcoded, the one shared
   entrance in the motion system that skipped the scale; --dur-3 is the token whose stated
   purpose is "panels, sheets, accordions", which is exactly what this is, and it brings
   each step inside the UI budget. The stagger, its 60ms interval, `both` and var(--ease)
   are deliberate and untouched — this shortens each step, it does not restage the cascade. */
.fade { animation: fade var(--dur-3) var(--ease) both; animation-delay: calc(var(--i, 0) * 60ms); }

/* Slider value feedback: a soft pulse when the number changes. */
.sh-read .v.pulse { animation: pulse 0.28s var(--ease); }
@keyframes pulse { 0% { transform: scale(1); } 40% { transform: scale(1.035); } 100% { transform: scale(1); } }

/* Skeleton shimmer while the grid loads (esp. a cold backend). */
.skel { background: var(--surface); border: 1px solid var(--line); border-radius: var(--r); height: 128px; position: relative; overflow: hidden; }
.skel::after {
  content: ""; position: absolute; inset: 0;
  background: linear-gradient(90deg, transparent, color-mix(in srgb, var(--line) 60%, transparent), transparent);
  transform: translateX(-100%); animation: shimmer 1.3s infinite;
}
@keyframes shimmer { 100% { transform: translateX(100%); } }

.back { transition: color 0.2s ease, transform 0.2s var(--ease); }
.back:hover { transform: translateX(-3px); }
.badge { transition: border-color 0.25s, color 0.25s, transform 0.2s var(--ease); }

/* ---------- mobile ---------- */
@media (max-width: 620px) {
  .top-inner { flex-wrap: wrap; gap: 10px; padding: 12px 16px; }
  /* The brand is the ONE thing in this header that must never shrink. `flex: 1; min-width: 0`
     made it the only flexible item, so with a signed-in account the bell + PRO badge + email
     + theme toggle took what they needed and left the brand 35px to hold a 95px mark —
     "TickerWorth" rendered as "TickerWo". It now sizes to its content, and the account button
     gives up the width instead by showing just the initial at phone widths. */
  .brand { flex: 0 0 auto; min-width: max-content; }
  .brand .tag { display: none; }
  .brand .mark { font-size: 19px; }
  .search { order: 3; flex-basis: 100%; margin-left: 0; min-width: 0; }
  .search input { min-width: 0; }
  /* bigger, thumb-friendly header controls */
  .theme-btn { width: 40px; height: 40px; font-size: 16px; }
  .signin-btn { padding: 9px 16px; font-size: 13.5px; }
  /* the email/initial swap is now the default at every width — see `.acct-initial` */
  .wrap { --wrap-pad: 16px; }
  .hero { padding: 34px 0 20px; }
  .hero .big-search { flex-direction: column; }
  .hero .big-search button { padding: 15px; }
  /* the free-lookup pill reads as a left-aligned block, not a squished pill */
  .free-meter { display: flex; align-items: flex-start; line-height: 1.55; border-radius: var(--r-sm); padding: 10px 13px; }
  .duel { gap: 8px; }
  .co-head h2 { font-size: 2rem; }
  .co-head { gap: 10px; }
  .watch-btn { margin-left: 0; }
  /* Two hard tracks with a `minmax(0, ...)` floor — a track allowed to shrink below its
     own content. At 375px this resolved to 159px tracks holding 123px of content against
     a 127px figure. The count stays (one metric per row reads as a list, not a mosaic),
     but the cells are given the room back: 13px side padding and an 18px figure put
     `$2,066,925M` at 114px inside 133px. tools/titan/starve.py holds the line. */
  .metrics { grid-template-columns: repeat(2, minmax(0, 1fr)); }
  .metric { padding: 16px 13px; }
  .metric .val { font-size: 18px; }
  .cols { grid-template-columns: minmax(0, 1fr); }
  .wacc-row { flex-wrap: wrap; row-gap: 12px; }
  .wacc-val { flex: 0 0 auto; }
  .wacc-slider, .wacc { flex: 1 1 160px; }
  .reset-w { order: 3; }
  .panel { padding: 18px 15px; }
  table.assum { table-layout: fixed; }
  table.assum td.k { width: 54%; word-break: break-word; }
  table.assum td.v { width: 46%; white-space: normal; word-break: break-word; }
  .src { max-width: 100%; word-break: break-word; }
  .duel .side .num { font-size: 2.2rem; }
  .sh-read .v { font-size: 2.4rem; }
  /* newer components: comfortable one-column layouts on phones */
  .dash-head { margin-bottom: 18px; }
  .dash-head h2, .hw-pagetitle { font-size: 1.7rem; }
  .brief-cols { gap: 16px; }
  .brief-summary { font-size: 13.5px; }
  .rev-big { font-size: 30px; }
  .rev-cmp { gap: 10px; }
  .rev-cmp .v { font-size: 19px; }
  .lens-groups { gap: 14px; }
}

/* very small phones (≤400px): trim the last bit of cramping */
@media (max-width: 400px) {
  .wrap { --wrap-pad: 13px; }
  .brand .mark { font-size: 18px; }
  .hero h1 { margin-bottom: 12px; }
  .ov-item { padding: 6px 11px; font-size: 12px; }
  .peer-table, table.heat { font-size: 11.5px; }
}

/* ---------- earnings & economic calendar ---------- */
#calendar { padding-bottom: 80px; }
.cal-cols { display: grid; grid-template-columns: minmax(0, 1.4fr) minmax(0, 1fr); gap: 26px; align-items: start; }
.cal-day { margin-bottom: 16px; }
.cal-date { font-family: var(--mono); font-size: 11px; letter-spacing: 0.08em; text-transform: uppercase; color: var(--accent); font-weight: 700; padding-bottom: 6px; border-bottom: 1px solid var(--line-soft); margin-bottom: 4px; }
.cal-row { display: flex; align-items: baseline; gap: 9px; padding: 8px 4px; cursor: pointer; border-radius: var(--r-sm); transition: background 0.15s; }
.cal-row:hover { background: var(--surface-2); }
.cal-row:hover .cal-tk { color: var(--accent); }
.cal-tk { font-family: var(--mono); font-weight: 700; font-size: 13px; letter-spacing: 0.03em; flex: 0 0 auto; }
.cal-watch { color: var(--accent); }
.cal-nm { color: var(--muted); font-size: 12px; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; flex: 1 1 auto; }
.cal-toggle { font-family: var(--mono); font-size: 10.5px; letter-spacing: normal; text-transform: none; color: var(--muted); cursor: pointer; margin-left: 6px; font-weight: 400; }
.cal-macro-col .cal-macro { display: flex; gap: 12px; padding: 10px 0; border-top: 1px solid var(--line); }
.cal-macro-list { background: var(--surface); border: 1px solid var(--line); border-radius: var(--r); padding: 4px 16px; }
.cal-mdate { font-family: var(--mono); font-size: 12px; color: var(--accent); font-weight: 700; flex: 0 0 44px; }
.cal-mname { font-size: 13px; color: var(--text); }
/* metadata captions, not figures — "usually first Friday" is a phrase, and Geist Mono is
   never set on running prose. The times keep their tabular alignment in sans. */
.cal-mcat { font-family: var(--sans); font-variant-numeric: tabular-nums; font-size: 10.5px; color: var(--faint); margin-top: 2px; }
.cal-macro-note { font-family: var(--sans); font-size: 11px; color: var(--faint); margin-top: 10px; line-height: 1.5; }
@media (max-width: 720px) { .cal-cols { grid-template-columns: minmax(0, 1fr); gap: 28px; } }

/* ---------- watchlist alert notifications ---------- */
.account { gap: 10px; }
.notif-wrap { position: relative; }
.notif-bell {
  position: relative; background: var(--surface); border: 1px solid var(--line); color: var(--muted);
  border-radius: var(--r-full); width: 36px; height: 36px; cursor: pointer; font-size: 15px;
  display: grid; place-items: center; transition: color 0.2s, border-color 0.2s;
}
.notif-bell:hover { color: var(--accent); border-color: var(--accent); }
@media (pointer: coarse) { .notif-bell { width: var(--tap); height: var(--tap); } }
.notif-badge {
  position: absolute; top: -4px; right: -4px; min-width: 17px; height: 17px; padding: 0 4px;
  border-radius: var(--r-full); background: var(--down); color: #111; font-family: var(--mono);
  font-size: 10px; font-weight: 700; display: grid; place-items: center; line-height: 1;
}
.notif-dropdown {
  position: absolute; right: 0; top: calc(100% + 8px); width: 340px; max-width: 82vw; max-height: 420px;
  overflow-y: auto; background: var(--surface); border: 1px solid var(--line); border-radius: var(--r-sm);
  box-shadow: var(--shadow); z-index: 40;   /* motion: see the F5 popover block */
}
.notif-head { font-family: var(--mono); font-size: 10.5px; letter-spacing: 0.14em; text-transform: uppercase; color: var(--faint); padding: 12px 15px 8px; }
.notif-empty { padding: 22px 18px; color: var(--muted); font-size: var(--t-small); line-height: 1.6; }
.notif-item { display: flex; gap: 11px; padding: 11px 15px; border-top: 1px solid var(--line-soft); cursor: pointer; transition: background 0.15s; }
.notif-item:hover { background: var(--surface-2); }
.notif-item.unread { background: color-mix(in srgb, var(--accent) 7%, transparent); }
.notif-ico { font-size: 14px; color: var(--accent); flex: 0 0 auto; margin-top: 1px; }
.notif-msg { font-size: 13px; color: var(--text); line-height: 1.4; }
.notif-time { font-family: var(--mono); font-size: 10.5px; color: var(--faint); margin-top: 3px; }
.notif-foot { font-family: var(--mono); font-size: 11px; color: var(--muted); padding: 11px 15px; border-top: 1px solid var(--line-soft); line-height: 1.5; }
.notif-foot a { color: var(--accent); cursor: pointer; }

/* portfolio alerts: a header that carries the settings toggle, non-linking rows for
   portfolio-level alerts (macro belongs to the book, not to a ticker), and the settings
   panel itself. Single column at every width — the dropdown never splits. */
.notif-head { display: flex; align-items: center; justify-content: space-between; gap: 8px; }
.notif-gear {
  background: none; border: 0; color: var(--faint); cursor: pointer; padding: 2px 5px;
  font-family: var(--mono); font-size: 10px; letter-spacing: 0.1em; text-transform: uppercase;
  line-height: 1; border-radius: var(--r-sm); transition: color 0.15s, background 0.15s;
}
.notif-gear:hover { color: var(--accent); background: var(--surface-2); }
.notif-item.flat { cursor: default; }
.notif-item.flat:hover { background: none; }
.notif-item.flat.unread:hover { background: color-mix(in srgb, var(--accent) 7%, transparent); }
.notif-note { display: block; margin-top: 8px; font-size: 11.5px; color: var(--faint); }

.notif-set { display: flex; align-items: flex-start; gap: 12px; padding: 12px 15px; border-top: 1px solid var(--line-soft); }
.notif-set.master { border-top: 0; }
.notif-set.muted .notif-set-txt { opacity: 0.5; }
.notif-set-txt { flex: 1 1 auto; min-width: 0; }
.notif-set-lbl { font-size: 12.5px; font-weight: 600; color: var(--text); line-height: 1.35; }
.notif-set-help { font-size: 11.5px; color: var(--muted); line-height: 1.45; margin-top: 3px; }
.notif-switch {
  flex: 0 0 auto; margin-top: 2px; width: 38px; height: 22px; padding: 0; cursor: pointer;
  border-radius: var(--r-full); border: 1px solid var(--line); background: var(--surface-2);
  position: relative; transition: background 0.18s var(--ease), border-color 0.18s var(--ease);
}
.notif-switch .notif-knob {
  position: absolute; top: 2px; left: 2px; width: 16px; height: 16px; border-radius: 50%;
  background: var(--muted); transition: transform 0.18s var(--ease), background 0.18s var(--ease);
}
.notif-switch.on { background: color-mix(in srgb, var(--accent) 26%, transparent); border-color: var(--accent); }
.notif-switch.on .notif-knob { transform: translateX(16px); background: var(--accent); }
.notif-switch:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }
@media (prefers-reduced-motion: reduce) { .notif-switch, .notif-switch .notif-knob { transition: none; } }

/* Phones: the bell does not sit at the right edge of the header, so a panel anchored to it
   with `right: 0` hangs 150px off the LEFT of a 375px viewport — which is where the settings
   labels went ("…ings", "…ur holdings"). Below 640px it stops being a dropdown and becomes a
   sheet pinned under the header, full width less a gutter. Still one column. */
@media (max-width: 640px) {
  /* `top` is refined from the bell's own rect when the sheet opens (Notifs.place) so it
     stays put if the header ever changes height. The literal here is the fallback that
     keeps the sheet on screen regardless — without it, the base rule's `top: calc(100% + 8px)`
     would resolve against the VIEWPORT once this is fixed-positioned, parking the panel a
     full screen below the fold. */
  .notif-dropdown {
    position: fixed; top: 76px; left: 12px; right: 12px;
    width: auto; max-width: none; max-height: min(70vh, 560px);
  }
  .notif-set-help, .notif-foot { font-size: 12px; }
}

/* ---------- news: morning brief + trading news ----------
   Tidy tiles, one column at every width. The feed is a list of hairline-separated articles
   rather than a card grid: headlines are variable-length text, and a grid of them turns into
   ragged boxes the moment two of them differ in length. */
.nw-brief { display: grid; gap: var(--s-3); }
.nw-tone {
  margin-left: 8px; padding: 2px 8px; border-radius: var(--r-full);
  font-family: var(--mono); font-size: 10px; letter-spacing: 0.08em; text-transform: uppercase;
  border: 1px solid var(--line); color: var(--muted);
}
.nw-risk-on { border-color: var(--up); color: var(--up); }
.nw-risk-off { border-color: var(--down); color: var(--down); }
.nw-summary { font-size: 15px; line-height: 1.55; color: var(--text); margin: 0; }

.nw-strip { display: flex; flex-wrap: wrap; gap: 8px; }
.nw-chip {
  display: inline-flex; align-items: center; gap: 6px; padding: 5px 11px;
  border: 1px solid var(--line-soft); border-radius: var(--r-full);
  font-family: var(--mono); font-size: 11.5px; color: var(--muted);
}
.nw-chip b { color: var(--faint); font-weight: 600; }

/* A CONTAINER query, not a viewport one. This two-column block used to sit in a panel
   that was half the page wide, so a viewport breakpoint was a good enough proxy for its
   own width. Under the multi-column flow the panel is one third of the page at 1440 and
   one half at 900, so the proxy broke: measured at 1440 this needed 521px inside a 299px
   box and pushed 198px of horizontal scroll onto the whole document. What decides the
   layout here is the PANEL's width, so that is what is asked. */
.nw-brief, .nw-feed, .mc-digest, .mc-week, .mc-horizon, .mc-earn { container-type: inline-size; }
.nw-mvs { display: grid; gap: var(--s-3); grid-template-columns: repeat(2, minmax(0, 1fr)); }
@container (max-width: 460px) { .nw-mvs { grid-template-columns: minmax(0, 1fr); } }
.nw-mv-lab { font-size: 10.5px; letter-spacing: 0.14em; text-transform: uppercase; color: var(--faint); margin-bottom: 6px; }
.nw-mv-row {
  display: flex; align-items: baseline; gap: 8px; width: 100%; padding: 7px 0;
  background: none; border: 0; border-top: 1px solid var(--line-soft); cursor: pointer;
  text-align: left; color: inherit;
}
.nw-mv-row:hover .nw-mv-t { color: var(--accent); }
.nw-mv-t { font-family: var(--mono); font-size: 12.5px; font-weight: 700; flex: 0 0 auto; }
.nw-mv-n { font-size: 12px; color: var(--muted); flex: 1 1 auto; min-width: 0;
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.nw-mv-c { font-family: var(--mono); font-size: 12px; flex: 0 0 auto; }

.nw-sub { font-size: 10.5px; letter-spacing: 0.14em; text-transform: uppercase; color: var(--faint); margin-bottom: 8px; }
.nw-ylist { display: flex; flex-wrap: wrap; gap: 8px; }
.nw-y {
  display: inline-flex; align-items: center; gap: 8px; padding: 6px 12px; min-height: 34px;
  border: 1px solid var(--line); border-radius: var(--r-full); background: var(--surface);
  cursor: pointer; font-family: var(--mono); font-size: 12px; color: var(--text);
}
.nw-y:hover { border-color: var(--accent); }
.nw-y-t { font-weight: 700; }
.nw-signin, .nw-note {
  font-size: 12.5px; color: var(--muted); line-height: 1.5;
  padding-top: var(--s-2); border-top: 1px solid var(--line-soft);
}
.nw-signin a, .nw-note a { color: var(--accent); cursor: pointer; }

.nw-feed { margin-top: var(--s-4); }
.nw-list { display: grid; }
.nw-item { padding: 14px 0; border-top: 1px solid var(--line-soft); display: grid; gap: 7px; }
.nw-item:first-child { border-top: 0; }
.nw-item.mine { border-left: 2px solid var(--accent); padding-left: 12px; }
.nw-meta { display: flex; flex-wrap: wrap; align-items: center; gap: 8px; font-size: 10.5px; color: var(--faint); }
.nw-tk {
  background: none; border: 1px solid var(--line); border-radius: var(--r-full); cursor: pointer;
  padding: 2px 8px; font-family: var(--mono); font-size: 10.5px; font-weight: 700; color: var(--text);
}
.nw-tk:hover { border-color: var(--accent); color: var(--accent); }
.nw-mine { color: var(--accent); letter-spacing: 0.08em; text-transform: uppercase; }
.nw-hl {
  font-size: 14.5px; line-height: 1.45; color: var(--text); text-decoration: none;
  font-weight: 500;
}
.nw-hl:hover { color: var(--accent); text-decoration: underline; text-underline-offset: 3px; }
.nw-sent {
  display: inline-block; padding: 2px 8px; border-radius: var(--r-full);
  font-family: var(--mono); font-size: 10px; letter-spacing: 0.06em;
  border: 1px solid var(--line-soft); color: var(--muted);
}
.nw-sent.up { color: var(--up); border-color: color-mix(in srgb, var(--up) 40%, transparent); }
.nw-sent.down { color: var(--down); border-color: color-mix(in srgb, var(--down) 40%, transparent); }
.nw-method, .nw-disc {
  font-size: 11.5px; color: var(--faint); line-height: 1.5;
}
.nw-method { margin-top: var(--s-3); padding-top: var(--s-2); border-top: 1px solid var(--line-soft); }
/* The shell above aligns its LEFT EDGE with the tiles; this keeps its LINES readable.
   Without the cap a footnote in the tiles shell sets 1405px lines at 11.5px type. */
.nw-disc { margin: var(--s-3) 0 var(--s-5); max-width: var(--shell-read); }

@media (max-width: 720px) {
  .nw-mvs { grid-template-columns: minmax(0, 1fr); }
  .nw-hl { font-size: 15px; }          /* the thing you actually read gets the larger size */
  .nw-y { min-height: var(--tap); }
}

/* ---------- stock screener ---------- */
.screener-link { color: var(--accent); cursor: pointer; font-weight: 600; }
.screener-link:hover { text-decoration: underline; }
#screener { padding-bottom: 80px; }
.scr-controls { display: grid; grid-template-columns: repeat(auto-fit, minmax(150px, 1fr)); gap: 12px; margin: 8px 0 4px; }
.scr-field { display: flex; flex-direction: column; gap: 5px; }
.scr-field label { font-family: var(--mono); font-size: 10px; letter-spacing: 0.1em; text-transform: uppercase; color: var(--faint); }
.scr-field input, .scr-field select {
  font-family: var(--mono); font-size: 13px; color: var(--text); background: var(--surface);
  border: 1px solid var(--line); border-radius: var(--r-sm); padding: 9px 11px; outline: none;
}
.scr-field input:focus, .scr-field select:focus { border-color: var(--accent); box-shadow: 0 0 0 3px var(--accent-soft); }
.scr-adv-head { font-family: var(--mono); font-size: 11px; letter-spacing: 0.14em; text-transform: uppercase; color: var(--muted); margin: 20px 0 8px; display: flex; align-items: center; gap: 8px; }
.scr-pro-tag { font-family: var(--mono); font-size: 9px; font-weight: 700; color: var(--bg); background: var(--accent); padding: 2px 6px; border-radius: var(--r-sm); }
.scr-adv.locked { opacity: 0.55; }
.scr-adv.locked input, .scr-adv.locked select { cursor: not-allowed; }
.scr-upsell { font-family: var(--mono); font-size: 12.5px; color: var(--muted); display: flex; align-items: center; gap: 12px; flex-wrap: wrap; margin: 10px 0 0; }
/* The capped-result footer. It sits under the ten rows a free account gets and states the
   remainder, because a list that stops at ten without saying so is the dishonest version of
   this feature. Flat elevation: fill + one hairline, no shadow. */
.scr-more { display: flex; align-items: center; justify-content: space-between; gap: 14px; flex-wrap: wrap; margin: 14px 0 0; padding: 14px 18px; border: 1px solid var(--line); border-radius: var(--r); background: var(--surface); font-family: var(--mono); font-size: 12.5px; color: var(--muted); }
.scr-actions { display: flex; align-items: center; gap: 12px; flex-wrap: wrap; margin: 22px 0 16px; }
.scr-run { font-family: var(--mono); font-weight: 700; font-size: 13px; color: var(--bg); background: var(--accent); border: none; border-radius: var(--r-full); padding: 10px 20px; cursor: pointer; transition: filter 0.15s, transform 0.15s; }
.scr-run:hover { filter: brightness(1.08); transform: translateY(-1px); }
.scr-export { font-family: var(--mono); font-size: 12.5px; color: var(--muted); background: var(--surface); border: 1px solid var(--line); border-radius: var(--r-full); padding: 9px 16px; cursor: pointer; transition: color 0.2s, border-color 0.2s; }
.scr-export:hover { color: var(--accent); border-color: var(--accent); }
.scr-count { font-family: var(--mono); font-size: 12px; color: var(--faint); }
.scr-table th, .scr-table td { text-align: left; }
.scr-table th:not(.pc-co), .scr-table td:not(.pc-co) { text-align: right; }
.scr-nm { color: var(--muted); font-size: 10.5px; margin-top: 2px; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; max-width: 180px; }
/* "researched" was green — same call as `.kind.reported` above: provenance is not
   direction, and the stocks index already says "Hand-researched" in neutral ink. */
.tier-tag.res { color: var(--text); background: var(--accent-soft); border: 1px solid color-mix(in srgb, var(--text) 30%, var(--line)); }
.tier-tag.trend { color: var(--muted); background: var(--surface-2); border: 1px solid var(--line); }

/* ---------- most undervalued (per our DCF model) ---------- */
.uv-sub { font-family: var(--mono); font-size: 10px; letter-spacing: 0; text-transform: none; color: var(--muted); }
.uv-list { background: var(--surface); border: 1px solid var(--line); border-radius: var(--r); overflow: hidden; }
/* In tile mode the strip is a grid of cards, not one bordered list box — keeping the
   wrapper's surface and hairline would draw a second frame around the tiles. The phone
   still gets the framed single-column list, unchanged. */
.uv-list.dtile-grid { background: none; border: none; border-radius: 0; overflow: visible; }
.uv-row { display: flex; align-items: baseline; gap: 10px; padding: 10px 15px; cursor: pointer; border-top: 1px solid var(--line-soft); transition: background 0.15s; }
.uv-row:first-child { border-top: none; }
.uv-row:hover { background: var(--surface-2); }
.uv-row:hover .uv-tk { color: var(--accent); }
.uv-tk { font-family: var(--mono); font-weight: 700; font-size: 13px; letter-spacing: 0.03em; flex: 0 0 auto; }
.uv-nm { color: var(--muted); font-size: 12px; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; flex: 1 1 auto; }
/* "+102.8% to fair value" is a figure INSIDE a phrase — same call as the /stocks gap
   column: the span holding the phrase goes sans (tabular figures keep their alignment),
   because Geist Mono is never set on running prose. */
.uv-up { font-family: var(--sans); font-size: 13px; font-weight: 700; color: var(--up-text); font-variant-numeric: tabular-nums; flex: 0 0 auto; }
.uv-note { font-family: var(--mono); font-size: 10.5px; color: var(--faint); line-height: 1.5; margin-top: 10px; }

/* --- sectors, ranked by our fair-value gap -------------------------------------------------
   A ZERO-CENTRED DIVERGING BAR. The centre rule is the market's price; a bar to the right
   means our models sit above it. Bars are scaled to the widest gap in the set rather than
   to a fixed percentage, so the chart reads as a comparison between sectors — which is what
   it is — instead of implying an absolute scale it does not have.

   No new hue (DESIGN.md §9): the two directions reuse --up / --down exactly as the price
   marks do, and the figure beside every bar carries a SIGN, so the direction survives with
   no colour at all (§8, §2 — a verdict is never rendered in colour alone). */
.sg-list { background: var(--surface); border: 1px solid var(--line); border-radius: var(--r); overflow: hidden; }
.sg-row { display: grid; grid-template-columns: minmax(88px, 1fr) minmax(0, 2.4fr) auto;
          align-items: center; gap: 12px; padding: 11px 14px; border-bottom: 1px solid var(--line-soft); }
.sg-row:last-child { border-bottom: 0; }
.sg-name { font-weight: 560; font-size: 13.5px; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.sg-track { position: relative; height: 16px; }
/* The zero rule. Drawn on the track rather than between the bars so it stays put when a
   set happens to be all-positive or all-negative. */
.sg-track::before { content: ""; position: absolute; left: 50%; top: -2px; bottom: -2px;
                    width: 1px; background: var(--line); }
/* SQUARE ENDS, deliberately. A rounded cap on a 12px bar eats a visible slice of the
   shortest bars, which is the one thing a length encoding must not do — and Titan's
   radius register has nothing between 0 and 6px anyway. */
.sg-bar { position: absolute; top: 2px; height: 12px; }
.sg-bar.pos { left: 50%; background: var(--up); }
.sg-bar.neg { right: 50%; background: var(--down); }
.sg-fig { font-family: var(--mono); font-size: 12.5px; font-variant-numeric: tabular-nums;
          text-align: right; white-space: nowrap; }
.sg-fig.pos { color: var(--up-text); }
.sg-fig.neg { color: var(--down-text); }
.sg-n { font-family: var(--mono); font-size: 10px; color: var(--faint); margin-left: 6px; }
/* The refusals are content, not chrome: same reading width, quieter voice. */
.sg-refused { margin-top: 10px; display: grid; gap: 6px; }
.sg-ref { font-family: var(--mono); font-size: 10.5px; color: var(--faint); line-height: 1.5; }
.sg-ref b { color: var(--muted); font-weight: 600; }
@media (max-width: 560px) {
  .sg-row { grid-template-columns: minmax(72px, 1fr) minmax(0, 1.6fr) auto; gap: 8px; padding: 10px; }
  .sg-name { font-size: 12.5px; }
  .sg-n { display: none; }   /* the count survives in the note below rather than crowding the row */
}

/* movers-with-context + live headlines */
.mover-main { display: flex; align-items: baseline; gap: 10px; min-width: 0; }
.mover-reason { font-size: 11px; color: var(--muted); margin-top: 4px; line-height: 1.4; min-width: 0; }
.news-live-panel .hl-src, .hl-src { font-family: var(--mono); font-size: 10px; font-weight: 400; color: var(--faint); text-transform: none; letter-spacing: 0.02em; }
.hl-item { padding: 10px 0; border-top: 1px solid var(--line-soft); }
.hl-item:first-of-type { border-top: none; }
.hl-link { font-size: 14px; color: var(--text); line-height: 1.45; }
.hl-link:hover { color: var(--accent); }
.hl-meta { font-family: var(--mono); font-size: 10.5px; color: var(--faint); margin-top: 3px; }
.hl-disc { font-family: var(--mono); font-size: 10.5px; color: var(--faint); margin-top: 12px; border-top: 1px solid var(--line-soft); padding-top: 10px; line-height: 1.5; }

/* ---------- what these headlines mean (app/market_recap.py) ----------
   Deliberately the same furniture as the headline list directly above it — same hairline
   rows, same mono meta, same disclaimer rule — because it is the same feed read a second
   way, and a differently-styled box would read as a differently-sourced one.
   The one addition is the rule down the left of the explanation: it marks the sentence as
   OURS rather than the publisher's, which is the only distinction in this panel that a
   reader must not get wrong. */
.mr-story { padding: 12px 0; border-top: 1px solid var(--line-soft); }
.mr-story:first-of-type { border-top: none; padding-top: 2px; }
.mr-head { font-size: 14px; color: var(--text); line-height: 1.45; }
.mr-head:hover { color: var(--accent); }
.mr-why {
  font-size: 13px; color: var(--muted); line-height: 1.6;
  border-left: 2px solid var(--line); padding-left: 11px; margin: 7px 0 6px;
}
.mr-meta { font-family: var(--mono); font-size: 10.5px; color: var(--faint); margin-top: 3px; }
.mr-thin, .mr-asof {
  font-family: var(--mono); font-size: 10.5px; color: var(--faint); line-height: 1.5;
  margin-top: 10px;
}
.mr-asof { margin-top: 6px; }

/* ---------- top movers ---------- */
/* The scope qualifier rides inside the eyebrow (so it inherits the rule that draws the hairline
   after it); the note sits under the grid, where a full sentence has room to be a sentence. */
.movers-scope { font-family: var(--mono); font-size: 11px; letter-spacing: 0; text-transform: none; color: var(--muted); }
.movers-note { font-family: var(--mono); font-size: 10.5px; color: var(--faint); line-height: 1.5; margin-top: 8px; }
.movers-note:empty { display: none; }
.movers-more { font-family: var(--mono); font-size: 11px; letter-spacing: 0; text-transform: none; color: var(--muted); }
.movers-more a { color: var(--accent); cursor: pointer; }
.movers-grid { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 16px; }
.movers-col {
  background: var(--surface); border: 1px solid var(--line); border-radius: var(--r); overflow: hidden;
}
.movers-h {
  font-family: var(--mono); font-size: var(--t-eyebrow); letter-spacing: var(--track-eyebrow); text-transform: uppercase;
  padding: 10px 14px; border-bottom: 1px solid var(--line-soft);
}
.movers-h.up { color: var(--up-text); }
.movers-h.down { color: var(--down-text); }
.mover {
  /* A COLUMN, not a row. This container was written as a horizontal flex when a row was
     four inline fields; `.mover-main` and the headline `.mover-reason` were added later as
     two children and the container was never changed. So flex laid them side by side and
     handed the headline whatever width the four fields left over.

     On the phone that remainder is 46px. A 60-character headline in a 46px column sets one
     short word per line, runs ten lines deep, drives the row to 350px — four rows filled
     Miraaj's entire screen — and every word longer than 46px is cut mid-word by the card's
     own `overflow: hidden`: "Wedbu / Reitera / Outpe / Telix".

     Two probes were guarding this and neither could see it. clipsweep asks
     `scrollWidth > clientWidth` and the element measured 46/46 — a starved column WRAPS
     rather than overflows, so it is never "clipped". The page-level overflow check returned
     0 because `.movers-col` clips the overspill itself. tools/titan/starve.py is the
     predicate that catches it, and `.mover-reason`'s own `margin-top: 4px` — meaningless in
     a row — is the evidence this was always meant to stack. */
  display: flex; flex-direction: column; align-items: stretch; gap: 0;
  padding: 9px 14px; cursor: pointer;
  border-bottom: 1px solid var(--line-soft); transition: background 0.15s;
}
.mover:last-child { border-bottom: none; }
/* fill removed — the state matrix at the end of this file is the one hover fill in
   the app. This one survived the first pass because the browser probe was happy:
   the row DID respond, just with a different fill from every other row. The
   ticker's colour change on the line below composes with the wash and stays. */
.mover:hover .mover-tk { color: var(--accent); }
.mover-tk { font-family: var(--mono); font-weight: 700; font-size: 13px; letter-spacing: 0.03em; flex: 0 0 auto; }
/* `min-width: 0` is what lets the ellipsis happen. A flex item's default shrink floor is
   `min-width: auto` = min-content, and for `white-space: nowrap` text min-content is the
   WHOLE STRING — so without this the name refuses to shrink and pushes the price and the
   change out of the row instead of truncating. Same floor as the `minmax(0, 1fr)` grid rule. */
.mover-nm { color: var(--muted); font-size: 11.5px; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; flex: 1 1 auto; min-width: 0; }
.mover-px { font-family: var(--mono); font-size: 12px; color: var(--muted); flex: 0 0 auto; }
.mover-dc { font-family: var(--mono); font-size: 12.5px; font-weight: 700; font-variant-numeric: tabular-nums; flex: 0 0 auto; min-width: 58px; text-align: right; }
.mover-empty { padding: 14px; color: var(--muted); font-size: var(--t-small); line-height: 1.6; }
@media (max-width: 560px) { .movers-grid { grid-template-columns: minmax(0, 1fr); } }

/* ---------- overnight world-markets strip ---------- */
.overnight-strip { display: flex; flex-wrap: wrap; gap: 10px; }
.ov-item {
  display: inline-flex; align-items: center; gap: 8px;
  background: var(--surface); border: 1px solid var(--line); border-radius: var(--r-full);
  padding: 7px 14px; font-family: var(--mono); font-size: 12.5px;
}
.ov-flag { font-size: 13px; }
.ov-nm { color: var(--muted); }
.ov-v { font-variant-numeric: tabular-nums; font-weight: 700; }

/* ---------- peer comparison table (Pro) ---------- */
.pc-kind { font-family: var(--mono); font-size: 9px; font-weight: 700; letter-spacing: 0.1em; text-transform: uppercase; padding: 3px 7px; border-radius: var(--r-sm); margin-left: 8px; color: var(--muted); background: var(--surface-2); border: 1px solid var(--line); vertical-align: middle; }
.pc-kind.sector { color: var(--accent); background: var(--accent-soft); border-color: color-mix(in srgb, var(--accent) 35%, var(--line)); }
.peer-scroll { overflow-x: auto; }
.peer-table { width: 100%; border-collapse: collapse; font-family: var(--mono); font-size: 12.5px; font-variant-numeric: tabular-nums; }
.peer-table th { text-align: right; font-size: 10px; letter-spacing: 0.06em; text-transform: uppercase; color: var(--faint); padding: 0 10px 10px; font-weight: 600; white-space: nowrap; }
.peer-table th.pc-co { text-align: left; }
.peer-table td { text-align: right; padding: 10px; border-top: 1px solid var(--line-soft); white-space: nowrap; }
.peer-table td.pc-co { text-align: left; }
.peer-table tbody tr { cursor: pointer; transition: background 0.15s; }
.peer-table tbody tr:hover { background: var(--surface-2); }
.pc-tk { font-weight: 700; color: var(--accent); letter-spacing: 0.04em; }
.pc-nm { color: var(--muted); font-size: 10.5px; margin-left: 8px; }
.peer-table tr.pc-subj { background: color-mix(in srgb, var(--accent) 8%, transparent); }
.peer-table tr.pc-subj td { border-top: 1px solid color-mix(in srgb, var(--accent) 30%, var(--line)); }
.peer-table tr.pc-subj .pc-tk { color: var(--accent); }
.peer-table tr.pc-median { cursor: default; }
.peer-table tr.pc-median:hover { background: none; }
.peer-table tr.pc-median td { color: var(--muted); border-top: 1.5px solid var(--line); font-weight: 500; }
.peer-src { font-family: var(--mono); font-size: 10.5px; color: var(--faint); margin-top: 12px; }

/* ---------- TickerWorth Score (free composite) ---------- */
.score-panel { background: linear-gradient(180deg, var(--surface-2), var(--surface)); }
.score-head { display: flex; align-items: center; gap: 24px; flex-wrap: wrap; }
.score-gauge { position: relative; width: 116px; height: 116px; border-radius: 50%; border: 4px solid var(--accent); display: grid; place-items: center; flex: 0 0 auto; }
.score-num { font-family: var(--mono); font-size: 40px; font-weight: 700; line-height: 1; font-variant-numeric: tabular-nums; }
.score-outof { font-family: var(--mono); font-size: 10px; color: var(--faint); margin-top: 2px; }
/* ============ THE GRADE BADGE IS ONE SHAPE ==========================================
   A square at the control radius, with a hairline, carrying the grade band in the TEXT and
   BORDER colour — never in the fill. Sized to its context: 34px beside the score panel,
   30px in a list row, 26px in a watchlist row, 22px in the condensed header. SCALE is what
   separates the hero from the list; the vocabulary does not change with it.

   It was two vocabularies. `.wl-score` (236 instances across the landing, the dashboard and
   the calendar) and `.hw-grade` were already hairline squares with the band in the text.
   `.score-grade` and `.mini-grade` — 2 instances, both on the company page — were FILLED
   CIRCLES with white text and a drop shadow, so the product's signature number was drawn two
   different ways, and both ways appear on the company page at once: the disc at the top, and
   `.wl-score` squares in any list below it.

   236 against 2 decided the direction. The rest is Titan: `--r-full` is reserved for discs,
   avatars and step markers — identity objects — and a grade is data. A filled disc also
   spends the band colour on a background, which is why it needed white text and a shadow to
   stay legible; the hairline form spends it on the glyph, which is the thing being read.
   The drop shadow went with it: the kit's elevation is a fill plus a hairline, and this was
   one of only two real shadows in the product. */
.score-grade { position: absolute; bottom: -10px; right: -6px; width: 34px; height: 34px;
  border-radius: var(--r-sm); border: 1px solid currentColor; background: var(--surface);
  font-family: var(--mono); font-weight: 700; font-size: 14px;
  display: grid; place-items: center; }
.score-info { flex: 1 1 260px; min-width: 220px; }
.score-comps { display: flex; flex-direction: column; gap: 7px; margin-bottom: 12px; }
.sc-comp { display: grid; grid-template-columns: 78px minmax(0, 1fr) 34px; align-items: center; gap: 10px; }
.sc-clab { font-family: var(--mono); font-size: 11px; color: var(--muted); }
.sc-cbar { height: 7px; border-radius: var(--r-sm); background: var(--surface-2); overflow: hidden; }
/* No width transition: JS renders the bar with its final inline width, so there is
   no start value to animate from — the declaration was dead, and `width` is a
   layout property we don't animate anyway. */
.sc-cbar span { display: block; height: 100%; border-radius: var(--r-sm); }
.sc-cval { font-family: var(--mono); font-size: 12.5px; font-weight: 700; text-align: right; font-variant-numeric: tabular-nums; }
.score-meth { font-family: var(--mono); font-size: 10.5px; color: var(--faint); }
.score-checked { font-family: var(--mono); font-size: 11px; color: var(--up-text); margin-bottom: 8px; font-weight: 600; }
/* broadened composite — per-component breakdown cards */
.score-comps2 { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 16px; margin-top: 20px; }
.sc-comp2 { border-left: 2px solid var(--line); padding-left: 14px; }
.sc-c2head { display: flex; align-items: baseline; gap: 8px; margin-bottom: 6px; }
.sc-c2head .sc-clab { font-family: var(--mono); font-size: 11px; letter-spacing: 0.06em; text-transform: uppercase; color: var(--text); font-weight: 700; }
.sc-cwt { font-family: var(--mono); font-size: 10px; color: var(--faint); }
.sc-c2head .sc-cval { margin-left: auto; font-family: var(--mono); font-size: 15px; font-weight: 700; font-variant-numeric: tabular-nums; }
.sc-comp2 .sc-cbar { display: block; height: 7px; border-radius: var(--r-sm); background: var(--line); overflow: hidden; margin-bottom: 7px; }
.sc-comp2 .sc-cbar span { display: block; height: 100%; border-radius: var(--r-sm); }
.sc-why { font-size: 12px; line-height: 1.5; color: var(--muted); }
@media (max-width: 640px) { .score-comps2 { grid-template-columns: minmax(0, 1fr); gap: 14px; } }

/* ---------- score v2: confidence, drivers, honest suppression ---------- */
/* A score shown as a BAND when confidence is low — never a false-precise point. */
.score-num-band { font-size: 24px; letter-spacing: -0.01em; }
.sc-bandlab { font-size: 15px; line-height: 1.45; color: var(--text); font-weight: 600; margin-bottom: 8px; }

.sc-meaning { font-size: 12.5px; line-height: 1.5; color: var(--muted); margin: -3px 0 9px; }
.sc-conf { display: inline-flex; align-items: center; gap: 8px; }
/* The two axes behind the grade, shown together so neither can be read as the whole story. */
.sc-axes { display: flex; flex-wrap: wrap; gap: 8px 18px; margin: 7px 0 2px; }
/* Each axis is a small stack: LABEL + number on one line, then the pillars it averages.
   The caption is what stops "Business 51" and the "Quality 73" pillar below from reading as
   two answers to one question. */
.sc-ax { display: inline-grid; grid-template-columns: auto auto; align-items: baseline; gap: 0 6px; }
.sc-axk { font-family: var(--mono); font-size: 10px; letter-spacing: 0.07em; text-transform: uppercase; color: var(--muted); }
.sc-axv { font-family: var(--mono); font-size: 13px; font-weight: 700; }
.sc-axsub { grid-column: 1 / -1; font-size: 10.5px; line-height: 1.35; color: var(--muted); opacity: 0.85; margin-top: 1px; }

/* The meter has to show its CAPACITY, not just its fill. Three dark dashes floating in
   whitespace can be read as "three of however many" — which is how a full 3-of-3 meter got
   reported as looking almost empty next to a HIGH CONFIDENCE label. Every slot is drawn;
   an unfilled one is a visible empty slot, so the fill is unambiguous. */
.sc-pips { display: inline-flex; gap: 3px; padding: 2px; border-radius: var(--r-full); background: var(--line-soft, rgba(120, 120, 128, 0.12)); }
.sc-pip { width: 16px; height: 5px; border-radius: var(--r-full); background: var(--line); }
.sc-conf-lab { font-family: var(--mono); font-size: 11px; letter-spacing: 0.07em; text-transform: uppercase; font-weight: 700; }
.sc-capped { font-family: var(--mono); font-size: 11px; line-height: 1.5; color: var(--muted); margin-top: 7px; }

/* The one or two things driving the score — the sentence people remember. */
.sc-drivers { display: flex; flex-direction: column; gap: 2px; margin-top: 18px; padding-top: 14px; border-top: 1px solid var(--line); }
.sc-driver { display: flex; align-items: baseline; gap: 10px; padding: 7px 0; }
.sc-dir { font-size: 11px; flex: 0 0 auto; }
.sc-dlab { font-size: 15px; font-weight: 600; color: var(--text); line-height: 1.4; }
.sc-dmeta { margin-left: auto; font-family: var(--mono); font-size: 11px; color: var(--faint); white-space: nowrap; flex: 0 0 auto; }

/* Progressive disclosure: the backing is one tap away, not 700px down the page. */
.sc-work { margin-top: 16px; border-top: 1px solid var(--line); padding-top: 12px; }
.sc-work > summary {
  cursor: pointer; list-style: none; display: inline-flex; align-items: center; gap: 7px;
  font-family: var(--mono); font-size: 12px; letter-spacing: 0.05em; color: var(--accent);
  padding: 7px 0; min-height: 34px;
}
.sc-work > summary::-webkit-details-marker { display: none; }
.sc-work > summary::before { content: "▸"; font-size: 10px; transition: transform 0.2s var(--ease, ease); }
.sc-work[open] > summary::before { transform: rotate(90deg); }
.sc-work > summary:hover { color: var(--text); }
.sc-depth { font-family: var(--mono); font-size: 10px; color: var(--faint); white-space: nowrap; }
.sc-confwhy { margin-top: 18px; }
/* The checkable sentence: "Cheaper than 78% of Technology companies in the US" — the line
   that converts a sceptic, because they can go and check it. */
.sc-sentence { font-size: 12.5px; line-height: 1.5; color: var(--text); font-weight: 600; margin-bottom: 5px; }
/* The price stamp under the one price-sensitive pillar. Quiet by design: it is a caveat on a
   number, not a feature, and it must not compete with the pillar's own score. */
.sc-stamp { margin-top: 6px; font-size: var(--t-micro); color: var(--muted); line-height: 1.5; }
.sc-basis { font-family: var(--mono); font-size: 10.5px; line-height: 1.5; color: var(--faint); margin-bottom: 14px; }

.sc-havelab { display: block; font-family: var(--mono); font-size: 10px; letter-spacing: 0.13em; text-transform: uppercase; color: var(--faint); margin-bottom: 7px; }
.sc-supp-list { margin: 6px 0 0; padding-left: 18px; }
.sc-supp-list li { font-size: 12.5px; line-height: 1.6; color: var(--muted); margin-bottom: 4px; }

/* "We can't score this honestly" — a result, not an error. */
.sc-suppressed { display: flex; gap: 16px; align-items: flex-start; padding: 4px 0 2px; }
.sc-supp-ico { flex: 0 0 auto; font-size: 30px; line-height: 1; color: var(--muted); margin-top: 2px; }
.sc-supp-body { flex: 1; min-width: 0; }
.sc-supp-title { font-size: 17px; font-weight: 600; color: var(--text); margin-bottom: 6px; }
.sc-supp-why { font-size: 14px; line-height: 1.6; color: var(--muted); margin: 0; }
.sc-haves { margin-top: 16px; }
.sc-havechip { display: inline-block; font-family: var(--mono); font-size: 11px; color: var(--text); background: var(--surface-2); border: 1px solid var(--line-soft); border-radius: var(--r-full); padding: 4px 10px; margin: 0 6px 6px 0; }
.sc-havechip.muted { color: var(--muted); }

@media (max-width: 640px) {
  .sc-bandlab { font-size: 14.5px; }
  .sc-dlab { font-size: 14.5px; }
  .sc-dmeta { font-size: 10px; }
  .sc-suppressed { gap: 12px; }
  .sc-supp-title { font-size: 16px; }
}

/* ---------- factor scores (Pro) ---------- */
.factor-overall { font-family: var(--mono); font-size: 11px; font-weight: 700; color: var(--accent); text-transform: none; letter-spacing: 0.02em; margin-left: 8px; }
.factor-grid { display: flex; flex-direction: column; gap: 12px; }
.factor-row { display: grid; grid-template-columns: 120px minmax(0, 1fr) 88px; align-items: center; gap: 12px; }
.factor-lab { font-family: var(--mono); font-size: 12px; color: var(--text); }
.factor-bar { height: 8px; border-radius: var(--r-sm); background: var(--surface-2); overflow: hidden; }
.factor-bar span { display: block; height: 100%; border-radius: var(--r-sm); }
.factor-score { font-family: var(--mono); font-size: 15px; font-weight: 700; font-variant-numeric: tabular-nums; text-align: right; }
.factor-score small { font-weight: 400; font-size: 10px; color: var(--muted); margin-left: 5px; text-transform: uppercase; letter-spacing: 0.06em; }
.factor-inputs { display: flex; flex-wrap: wrap; gap: 7px; margin-top: 16px; }
.factor-chip { font-family: var(--mono); font-size: 10.5px; color: var(--muted); background: var(--surface-2); border: 1px solid var(--line-soft); border-radius: var(--r-full); padding: 3px 9px; }
.factor-disc { font-family: var(--mono); font-size: 10.5px; color: var(--faint); line-height: 1.5; margin-top: 14px; border-top: 1px solid var(--line-soft); padding-top: 10px; }
@media (max-width: 520px) { .factor-row { grid-template-columns: 92px minmax(0, 1fr) 72px; gap: 8px; } .factor-lab { font-size: 11px; } }

/* ---------- insider activity (SEC Form 4) ---------- */
.ins-cluster { display: flex; flex-wrap: wrap; align-items: baseline; gap: 6px 12px; padding: 9px 12px; margin-bottom: 12px; border: 1px solid var(--line-soft); border-radius: var(--r); background: var(--surface-2, rgba(0,0,0,0.02)); }
.ins-cluster.hot { border-color: var(--up); background: color-mix(in oklab, var(--up) 8%, transparent); }
.ins-cluster-tag { font-family: var(--mono); font-size: 11px; font-weight: 700; color: var(--muted); }
.ins-cluster.hot .ins-cluster-tag { color: var(--up); }
.ins-cluster-txt { font-size: 13px; color: var(--text); }
.ins-cluster-net { font-family: var(--mono); font-size: 11px; color: var(--faint); margin-left: auto; }
.ins-scroll { overflow-x: auto; }
.ins-table { width: 100%; border-collapse: collapse; font-size: 13px; }
.ins-table th { text-align: left; font-family: var(--mono); font-size: 10px; text-transform: uppercase; letter-spacing: 0.04em; color: var(--faint); font-weight: 500; padding: 0 10px 7px 0; white-space: nowrap; }
.ins-table td { padding: 8px 10px 8px 0; border-top: 1px solid var(--line-soft); vertical-align: top; }
.ins-date { font-family: var(--mono); font-size: 11.5px; color: var(--muted); white-space: nowrap; }
.ins-who { display: flex; flex-direction: column; gap: 1px; min-width: 130px; }
.ins-name { color: var(--text); }
.ins-role { font-size: 10.5px; color: var(--faint); }
.ins-act { font-family: var(--mono); font-size: 12px; white-space: nowrap; }
.ins-sh, .ins-px, .ins-val { font-family: var(--mono); font-size: 12px; text-align: right; white-space: nowrap; }
.ins-val a { color: var(--muted); text-decoration: none; border-bottom: 1px solid var(--line); }
.ins-val a:hover { color: var(--text); }
.ins-empty { text-align: center; color: var(--muted); font-size: var(--t-small); line-height: 1.6; padding: 16px 0; }
.ins-lock { font-family: var(--mono); font-size: 11px; color: var(--muted); margin-top: 10px; }
.ins-lock a { color: var(--accent); cursor: pointer; }

/* ---------- financial history (SEC XBRL) ---------- */
.fund-scroll { overflow-x: auto; }
.fund-table { border-collapse: collapse; font-family: var(--mono); font-size: 12.5px; min-width: 100%; }
.fund-table th, .fund-table td { padding: 7px 12px 7px 0; text-align: right; white-space: nowrap; border-top: 1px solid var(--line-soft); }
.fund-table thead th { border-top: none; border-bottom: 1px solid var(--line); font-size: 11px; color: var(--faint); font-weight: 500; padding-bottom: 8px; }
.fund-table .fund-lab { text-align: left; color: var(--text); position: sticky; left: 0; background: var(--surface); padding-right: 22px; font-family: var(--sans, inherit); }
.fund-table tbody .fund-lab { font-size: 13px; }
.fund-table td { color: var(--muted); }
.fund-dl { display: inline-block; margin-top: 12px; font-family: var(--mono); font-size: 12px; color: var(--accent); text-decoration: none; border: 1px solid var(--line); border-radius: var(--r-sm); padding: 7px 13px; }
.fund-dl:hover { border-color: var(--accent); }
.fund-lock { font-family: var(--mono); font-size: 11px; color: var(--muted); margin-top: 12px; line-height: 1.5; }
.fund-lock a { color: var(--accent); cursor: pointer; }

/* ---------- AI earnings digest ---------- */
.dig-summary { font-size: 15px; line-height: 1.6; color: var(--text); margin: 4px 0 14px; }
.dig-highlights { margin: 0 0 14px; padding-left: 20px; }
.dig-highlights li { font-size: 13.5px; line-height: 1.55; color: var(--muted); margin-bottom: 5px; }
.dig-link { font-family: var(--mono); font-size: 12px; color: var(--accent); text-decoration: none; }
.dig-link:hover { text-decoration: underline; }

/* ---------- saved screens ---------- */
.scr-save { font-family: var(--mono); font-size: 13px; padding: 9px 15px; border: 1px solid var(--line); border-radius: var(--r-full); background: transparent; color: var(--text); cursor: pointer; }
.scr-save:hover { border-color: var(--accent); }
.scr-saved { margin: 4px 0 18px; }
.scr-saved-row { display: flex; flex-wrap: wrap; align-items: center; gap: 8px; }
.scr-saved-lab { font-family: var(--mono); font-size: 10.5px; text-transform: uppercase; letter-spacing: 0.05em; color: var(--faint); margin-right: 2px; }
.scr-saved-hint { font-family: var(--mono); font-size: 11.5px; color: var(--faint); }
.scr-saved-cap { font-family: var(--mono); font-size: 11px; color: var(--muted); margin-left: auto; }
.scr-saved-cap a { color: var(--accent); cursor: pointer; }
.scr-chip { display: inline-flex; align-items: stretch; border: 1px solid var(--line); border-radius: var(--r-sm); overflow: hidden; }
.scr-chip-apply { font-family: var(--mono); font-size: 12px; padding: 6px 11px; background: transparent; border: none; color: var(--text); cursor: pointer; }
.scr-chip-apply:hover { background: color-mix(in oklab, var(--accent) 14%, transparent); }
.scr-chip-x { font-size: 11px; padding: 0 9px; background: transparent; border: none; border-left: 1px solid var(--line); color: var(--faint); cursor: pointer; }
.scr-chip-x:hover { color: var(--down); }

/* ---------- toast ---------- */
.vm-toast { position: fixed; left: 50%; bottom: 30px; transform: translateX(-50%) translateY(14px); z-index: 2000; padding: 11px 20px; border-radius: var(--r-sm); background: var(--text); color: var(--surface); font-family: var(--mono); font-size: 13px; box-shadow: 0 8px 30px rgba(0,0,0,0.22); opacity: 0; pointer-events: none;
  /* `ease`, not the house `--ease-out`, and a touch slower than the UI budget:
     a toast is the one surface tuned to feel elegant rather than instant (the
     Sonner cohesion rule). Transition — never keyframes — so a second toast
     retargets from the current position instead of restarting from zero. */
  transition: opacity var(--dur-3) ease, transform var(--dur-3) ease; }
.vm-toast.show { opacity: 1; transform: translateX(-50%) translateY(0); }
/* The message cross-fades when a second toast replaces the first; the pill itself keeps
   its position and its own --dur-3 transition. A transition rather than a keyframe on
   purpose — this is the most rapidly retriggered text in the app, and a transition
   retargets from what is on screen where a keyframe restarts from zero. `ease` matches the
   container's own documented curve rather than the house --ease-out, because the toast is
   the one surface tuned to feel elegant instead of instant. --dur-1 (120ms) must stay equal
   to TOAST_SWAP_MS in web/app.js: the fade has to finish before the words are replaced. */
.vm-toast-msg { display: block; transition: opacity var(--dur-1) ease; }
.vm-toast-msg.swapping { opacity: 0; }

/* The four kinds of feedback — status, completion, warning, error (apple-design §16).
   One toast used to render all four identically, so the reader had to READ the message
   to find out whether something had gone right, which defeats the point of a peripheral
   notification.

   A 3px rail, not a colour flood: this surface sits over dense financial content, and a
   solid green or red block at the bottom of a valuation page reads as a NUMBER changing.
   The rail is the smallest mark that is legible from the corner of the eye.

   The colours are the semantic tokens the app already owns; no new ones are introduced.
   `status` deliberately keeps the muted rail — a neutral edge IS the signal that nothing
   special happened, and colouring it would spend the reader's attention on "your link is
   in the clipboard".

   THE DARK-THEME DARKENING IS MEASURED, NOT TASTE. This toast inverts against the page —
   its background is `var(--text)` — so in dark mode the rail sits on a WHITE surface while
   the tokens get brighter, and the raw tokens measured 2.02 (success) and 1.78 (warning)
   against it: a 3px indicator nobody can see, which fails this plan's own "legible without
   reading the text" bar. Against the black light-mode toast the same tokens are 9.46 /
   4.04 / 5.92 and need nothing. So the dark block below mixes each token toward black by
   the smallest 5% step that clears 3:1 (WCAG 1.4.11, the bar for a non-text UI indicator):
   80% for success, 75% for warning, and error is left alone because 3.41 already clears.
   Derived from the tokens with color-mix, so retuning --up or --warn carries through. */
.vm-toast { --toast-accent: var(--muted); border-left: 3px solid var(--toast-accent); }
.vm-toast[data-kind="success"] { --toast-accent: var(--up); }
.vm-toast[data-kind="warning"] { --toast-accent: var(--warn); }
.vm-toast[data-kind="error"]   { --toast-accent: var(--down); }
/* Keyed on data-theme only, which is the file's single theming mechanism — the boot script
   always writes it on <html>, so a prefers-color-scheme copy would be a second source of
   truth for the same decision. */
:root[data-theme="dark"] .vm-toast[data-kind="success"] { --toast-accent: color-mix(in srgb, var(--up) 80%, black); }
:root[data-theme="dark"] .vm-toast[data-kind="warning"] { --toast-accent: color-mix(in srgb, var(--warn) 75%, black); }

/* ---------- portfolio tracker ---------- */
/* `.pf-cards` / `.pf-card` are declared where the grouped-card pass put them, further
   down this file. The originals stood here reading `--r-sm` long after they had stopped
   deciding anything, and every `.pf-` and `.pa-` rule written afterwards was written to
   match them — which is how one surface ended up with a shape vocabulary of its own.

   NOTE THE WORDING. This paragraph used to say `.pf-` star slash `.pa-` star, and the
   star-slash in the middle of that CLOSED THE COMMENT — everything after it became CSS,
   and the parser swallowed the rule that followed. A CSS syntax error is silent: nothing
   warns, the rule is simply not there. Never write a star followed by a slash inside a
   comment; tests/test_titan_sweep.py fails if one appears again. */
.pf-lab { font-family: var(--mono); font-size: var(--t-eyebrow); text-transform: uppercase; letter-spacing: var(--track-eyebrow); color: var(--faint); margin-bottom: 7px; }
.pf-val { font-size: 21px; font-weight: 700; }
.pf-val small { font-size: var(--t-meta); font-weight: 500; font-family: var(--mono); }
.pf-hint { cursor: help; color: var(--faint); font-size: var(--t-eyebrow); }
/* How much of the portfolio a card's number actually saw. Sits in the LABEL, not the
   tooltip, because the scope is part of what the number means — inherits the label's size
   so the card's rhythm is unchanged. */
.pf-scope { color: var(--muted); letter-spacing: 0; }
.pf-note { font-family: var(--mono); font-size: var(--t-micro); color: var(--muted); margin-bottom: 14px; }
.pf-add { display: flex; flex-wrap: wrap; gap: 10px; margin-bottom: 20px; }
.pf-add input { padding: 10px 13px; border: 1px solid var(--line); border-radius: var(--r-sm); background: transparent; color: var(--text); font-size: 14px; font-family: var(--mono); }
.pf-add input:focus { outline: none; border-color: var(--accent); }
/* wide enough for its own placeholder — "TICKER (E.G. AAPL)" is 18ch of 14px mono
   (~151px) plus padding, and at 160px the field clipped it mid-word ("…E.G. AAPL"),
   which is the header's "COM…" defect relocated to a form */
#pf-ticker { width: 186px; text-transform: uppercase; }
#pf-shares, #pf-cost { width: 140px; }
/* the units control sits with the money field it governs, not in a settings menu —
   a cost figure whose unit is a click away is a cost figure people get wrong */
.pf-cost-mode { flex: 0 0 auto; align-self: center; }
.pf-add-hint {
  font-family: var(--mono); font-size: var(--t-meta); color: var(--muted);
  margin: -12px 0 20px; line-height: 1.5;
}
/* our one-line read on a position — the fair-value gap said in words rather than left as a
   coloured percentage. Deliberately quiet: it is a measurement under a row, not a callout,
   and it must never look like the page is telling somebody to trade. */
.pf-take {
  padding: 9px 15px 11px; border-top: 1px solid var(--line-soft);
  font-size: var(--t-meta); color: var(--muted); line-height: 1.55;
}
/* a refusal is not a lesser claim, but it IS a different one — set back so a reader
   scanning for our verdict can see at a glance which rows we have a view on */
.pf-take-none { font-style: italic; opacity: 0.85; }
/* a model old enough that the line says so — the sentence carries the age, this only
   makes it visible while scanning */
.pf-take-stale { border-left: 2px solid var(--line-strong); padding-left: 13px; }

/* an entry whose units can't be right — stated, with the reading they probably meant,
   and a button that only ever acts when the user says so */
.pf-suspect {
  display: flex; flex-wrap: wrap; align-items: center; gap: 10px;
  padding: 11px 15px; border-top: 1px solid var(--line-soft);
  background: var(--surface-2);
}
.pf-suspect-txt { flex: 1 1 240px; min-width: 0; font-size: var(--t-meta); color: var(--muted); line-height: 1.55; }
.pf-suspect-txt b { color: var(--text); font-weight: 600; }
.pf-suspect-fix {
  flex: 0 0 auto; font-family: var(--mono); font-size: var(--t-micro);
  padding: 7px 12px; min-height: 32px; border: 1px solid var(--line-strong); border-radius: var(--r-sm);
  background: var(--surface); color: var(--text); cursor: pointer;
  transition: border-color 0.2s, color 0.2s;
}
.pf-suspect-fix:hover { border-color: var(--accent); color: var(--accent); }
.pf-acts { display: inline-flex; align-items: center; gap: 6px; flex: 0 0 auto; min-width: max-content; }
.pf-lock { font-family: var(--mono); font-size: var(--t-meta); color: var(--muted); margin-bottom: 20px; padding: 11px 14px; border: 1px solid var(--line); border-radius: var(--r-sm); }
.pf-lock a { color: var(--accent); cursor: pointer; }
.pf-scroll { overflow-x: auto; }
.pf-table { width: 100%; border-collapse: collapse; font-size: var(--t-small); }
.pf-table th { text-align: right; font-family: var(--mono); font-size: var(--t-eyebrow); text-transform: uppercase; letter-spacing: 0.03em; color: var(--faint); font-weight: 500; padding: 0 12px 8px 0; white-space: nowrap; }
.pf-table th:first-child { text-align: left; }
.pf-table td { text-align: right; padding: 10px 12px 10px 0; border-top: 1px solid var(--line-soft); white-space: nowrap; font-family: var(--mono); font-size: var(--t-meta); }
.pf-table td.pf-co { text-align: left; cursor: pointer; font-family: var(--sans, inherit); }
.pf-table td.pf-co:hover .pc-tk { color: var(--accent); }
.pf-table .scr-nm { font-size: var(--t-micro); color: var(--faint); }
.pf-unvalued { color: var(--accent); cursor: default; font-size: var(--t-micro); }
.pf-x { background: transparent; border: 1px solid var(--line); border-radius: var(--r-sm); color: var(--faint); cursor: pointer; padding: 3px 8px; font-size: var(--t-micro); white-space: nowrap; flex: 0 0 auto; }
.pf-x:hover { color: var(--down); border-color: var(--down); }
/* ---------- portfolio analysis ----------
   Deliberately monochrome. A risk is not red and an upside is not green: these are
   measurements of a portfolio, and colouring them as alarm or reassurance would turn a
   description into a recommendation. The only weight in the hierarchy is type size. */
.pa-read {
  font-size: 15.5px; line-height: 1.55; color: var(--text);
  border-left: 2px solid var(--line-strong); padding: 1px 0 1px 13px; margin: 4px 0 16px;
}
.pa-h {
  font-family: var(--mono); font-size: var(--t-eyebrow); text-transform: uppercase;
  letter-spacing: var(--track-eyebrow); color: var(--faint); margin-bottom: 10px;
}
.pa-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(240px, 1fr)); gap: 10px; }
.pa-panel { border: 1px solid var(--line-soft); border-radius: var(--r); padding: var(--panel-pad-y) var(--panel-pad-x); }
.pa-stat { display: flex; align-items: baseline; gap: 8px; flex-wrap: wrap; }
.pa-stat-v { font-size: 24px; font-weight: 700; font-variant-numeric: tabular-nums; }
.pa-stat-l { font-size: var(--t-meta); color: var(--muted); }
.pa-note { font-size: var(--t-meta); color: var(--faint); line-height: 1.5; margin-top: 6px; }
.pa-lock { font-size: var(--t-meta); color: var(--muted); line-height: 1.6; }
.pa-lock .lock-btn { margin-top: 11px; }

/* per-cell hairline — see the mosaic note on .metrics. Single column today, but the
   technique is uniform so a second column can never re-introduce the slab. */
.pa-list { display: grid; gap: 1px; background: var(--surface); border: 1px solid var(--line-soft); border-radius: var(--r); overflow: hidden; }
.pa-item { background: var(--surface); padding: var(--panel-pad-y) var(--panel-pad-x); box-shadow: 0 0 0 1px var(--line-soft); }
.pa-item-t { font-size: var(--t-small); font-weight: 600; color: var(--text); }
.pa-item-d { font-size: var(--t-meta); color: var(--muted); line-height: 1.5; margin-top: 4px; }

.pa-positions { display: grid; gap: 6px; }
.pa-pos { border: 1px solid var(--line-soft); border-radius: var(--r); background: var(--surface); }
/* padding shrinks; min-height does NOT — 44px is the thumb floor, not decoration */
.pa-pos > summary {
  display: flex; align-items: baseline; gap: 10px; cursor: pointer; list-style: none;
  padding: 10px var(--panel-pad-x); min-height: 44px;
}
.pa-pos > summary::-webkit-details-marker { display: none; }
.pa-pos > summary::after {
  content: "＋"; margin-left: auto; color: var(--faint); font-size: 13px; flex: 0 0 auto;
}
.pa-pos[open] > summary::after { content: "－"; }
.pa-pos-tk { font-weight: 700; font-size: var(--t-small); flex: 0 0 auto; }
.pa-pos-nm { color: var(--muted); font-size: var(--t-meta); min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.pa-pos-w { font-size: var(--t-meta); color: var(--faint); flex: 0 0 auto; }
.pa-pos-body { padding: 0 var(--panel-pad-x) 12px; border-top: 1px solid var(--line-soft); }
.pa-chips { display: flex; flex-wrap: wrap; gap: 6px; margin: 11px 0 2px; }
.pa-chip {
  font-family: var(--mono); font-size: var(--t-micro); color: var(--muted);
  border: 1px solid var(--line); border-radius: var(--r-full); padding: 3px 9px; white-space: nowrap;
}
.pa-sub { margin-top: 11px; }
.pa-sub-t { font-family: var(--mono); font-size: var(--t-micro); text-transform: uppercase; letter-spacing: 0.05em; color: var(--faint); }
.pa-ul { margin: 6px 0 0; padding-left: 17px; }
.pa-ul li { font-size: var(--t-meta); color: var(--muted); line-height: 1.5; margin-bottom: 3px; }
.pa-cov { font-family: var(--mono); font-size: var(--t-micro); color: var(--faint); line-height: 1.5; margin-top: 14px; }

/* ---------- what your money did ----------
   A table, because six numbers per holding IS tabular and pretending otherwise costs the
   reader the ability to scan a column. It scrolls inside its own box rather than widening
   the page; on a phone the two derived columns drop and the four that answer "what did I
   put in, what is it worth now" stay. */
.pf-perf { border: 1px solid var(--line-soft); border-radius: var(--r); overflow-x: auto; margin-top: 10px; }
.pf-perf-row {
  display: grid; grid-template-columns: 68px repeat(5, minmax(74px, 1fr));
  gap: 8px; align-items: baseline; padding: 9px 14px;
  border-top: 1px solid var(--line-soft); font-size: var(--t-meta);
  font-variant-numeric: tabular-nums; min-width: 480px;
}
.pf-perf-row > span:not(:first-child) { text-align: right; }
.pf-perf-row:first-child { border-top: none; }
.pf-perf-head {
  font-family: var(--mono); font-size: var(--t-eyebrow); text-transform: uppercase;
  letter-spacing: var(--track-eyebrow); color: var(--faint); background: var(--surface-2);
}
/* "GAIN/LOSS" has no natural break opportunity, so at 320px it ran 2px past its column.
   A slash is a fine place to wrap when the alternative is spilling. */
.pf-perf-head > span { overflow-wrap: anywhere; }
.pf-perf-tk { font-weight: 700; color: var(--text); }
.pf-perf-cost, .pf-perf-now { color: var(--muted); }
.pf-perf-c { color: var(--faint); }
@media (max-width: 560px) {
  .pf-perf-row { grid-template-columns: 56px repeat(3, minmax(62px, 1fr)); min-width: 0; padding: 9px 11px; }
  .pf-perf-p, .pf-perf-c, .pf-perf-head > span:nth-child(5), .pf-perf-head > span:nth-child(6) { display: none; }
}

/* ---------- scorecard ----------
   Monochrome bars. A coloured scorecard turns a measurement into a verdict, and the letter
   is the only thing here allowed to feel like a judgement — because it is one, and it is
   the one number on the block whose scale is defined elsewhere on the site. */
.pf-sc { border: 1px solid var(--line-soft); border-radius: var(--r); padding: var(--panel-pad-y) var(--panel-pad-x); }
.pf-sc-head { display: flex; align-items: center; gap: 14px; padding-bottom: 12px; border-bottom: 1px solid var(--line-soft); }
.pf-sc-grade {
  font-family: var(--mono); font-weight: 700; font-size: 30px; line-height: 1;
  width: 62px; height: 62px; display: grid; place-items: center; flex: 0 0 auto;
  border: 1px solid var(--line-strong); border-radius: var(--r-sm); color: var(--text);
}
.pf-sc-headnum { min-width: 0; }
.pf-sc-headnum b { font-size: 22px; }
.pf-sc-headnum span { color: var(--faint); font-size: var(--t-meta); }
.pf-sc-mean { color: var(--muted); font-size: var(--t-meta); line-height: 1.45; margin-top: 3px; }
.pf-sc-rows { display: grid; gap: 12px; margin: 13px 0 4px; }
.pf-sc-top { display: flex; align-items: baseline; justify-content: space-between; gap: 10px; }
.pf-sc-lab { font-size: var(--t-small); font-weight: 600; }
.pf-sc-val { font-size: var(--t-small); color: var(--muted); font-variant-numeric: tabular-nums; }
.pf-sc-bar { display: block; height: 5px; border-radius: var(--r-full); background: var(--surface-3); overflow: hidden; margin: 5px 0 5px; }
.pf-sc-bar i { display: block; height: 100%; background: var(--text); opacity: 0.55; }
.pf-sc-why { font-size: var(--t-meta); color: var(--muted); line-height: 1.5; }
.pf-sc-locked .pf-sc-val { color: var(--faint); }

/* ---------- option positions in the portfolio ----------
   Three exposure figures per line, given equal visual weight on purpose. Making one of them
   the headline would be picking the number for the reader, which is the whole thing this
   block refuses to do. */
.pf-opts { display: grid; gap: 8px; }
.pf-opt { border: 1px solid var(--line-soft); border-radius: var(--r); padding: var(--panel-pad-y) var(--panel-pad-x); }
.pf-opt-top { display: flex; align-items: baseline; gap: 9px; }
.pf-opt-tk { font-weight: 700; font-size: var(--t-small); flex: 0 0 auto; }
.pf-opt-desc { font-size: var(--t-meta); color: var(--muted); min-width: 0; }
.pf-opt-top .pf-x { margin-left: auto; }
.pf-opt-exp { display: grid; grid-template-columns: repeat(auto-fit, minmax(132px, 1fr));
  gap: 8px; margin: 10px 0 2px; }
.pf-opt-e { border: 1px solid var(--line-soft); border-radius: var(--r-sm); padding: 8px 10px; }
.pf-opt-e span { display: block; font-size: var(--t-micro); text-transform: uppercase;
  letter-spacing: 0.04em; color: var(--faint); }
.pf-opt-e b { display: block; font-size: var(--t-small); margin-top: 2px; font-variant-numeric: tabular-nums; }
.pf-opt-tot { font-size: var(--t-micro); color: var(--faint); margin-top: 9px; line-height: 1.5; }
.pf-opt-add select {
  padding: 10px 12px; border: 1px solid var(--line); border-radius: var(--r-sm);
  background: transparent; color: var(--text); font-size: 14px; font-family: var(--mono);
}
.pf-opt-add input[type="date"] { font-family: var(--mono); }

/* two scorecards, side by side at width, stacked on a phone */
/* 300px was wider than a 320px viewport once padding is taken, so the pair overflowed on
   the narrowest phone. `min()` lets the track shrink to whatever the column actually has. */
.pf-sc-pair { display: grid; grid-template-columns: repeat(auto-fit, minmax(min(280px, 100%), 1fr)); gap: 10px; }
.pf-sc-basis { font-size: var(--t-eyebrow); text-transform: uppercase; letter-spacing: var(--track-eyebrow);
  color: var(--faint); margin-bottom: 8px; }

/* ---------- bull / bear / threats ----------
   Three columns, one visual language. Green bull beside red bear would read as a
   recommendation with extra steps; these are exposures, and they look like it. */
.pf-cases { display: grid; grid-template-columns: repeat(auto-fit, minmax(260px, 1fr)); gap: 10px; }
.pf-case { border: 1px solid var(--line-soft); border-radius: var(--r); padding: var(--panel-pad-y) var(--panel-pad-x); }
.pf-case-h {
  font-family: var(--mono); font-size: var(--t-eyebrow); text-transform: uppercase;
  letter-spacing: var(--track-eyebrow); color: var(--faint); margin-bottom: 10px;
}
.pf-case-item { padding: 9px 0; border-top: 1px solid var(--line-soft); }
.pf-case-item:first-of-type { border-top: none; padding-top: 0; }
.pf-case-t { font-size: var(--t-small); font-weight: 600; line-height: 1.35; }
.pf-case-d { font-size: var(--t-meta); color: var(--muted); line-height: 1.5; margin-top: 4px; }

/* ---------- options ----------
   Monochrome, like the rest. The temptation on an options panel is a green "score" badge,
   and that is exactly the surface where a colour reads as permission. The only emphasis is
   type size, and the loudest thing on the block is the sentence about timing. */
.opt-frame { font-size: var(--t-meta); color: var(--muted); line-height: 1.55; margin-bottom: 12px; }
.opt-nogo { border: 1px solid var(--line-strong); border-radius: var(--r-sm); padding: 11px 13px; margin-top: 10px; }
.opt-nogo b { color: var(--text); }
.opt-lock { font-size: var(--t-meta); color: var(--muted); line-height: 1.6; }
.opt-lock .lock-btn { margin-top: 11px; }
.opt-controls { display: flex; flex-wrap: wrap; gap: 10px; align-items: center; margin-bottom: 12px; }
.opt-lab { display: flex; flex-direction: column; gap: 4px; font-size: var(--t-micro);
  text-transform: uppercase; letter-spacing: 0.05em; color: var(--faint); font-family: var(--mono); }
.opt-lab select {
  font-family: var(--mono); font-size: var(--t-meta); padding: 8px 10px; min-height: 38px;
  border: 1px solid var(--line); border-radius: var(--r-sm); background: var(--surface); color: var(--text);
}
.opt-meta { font-size: var(--t-meta); color: var(--faint); align-self: flex-end; padding-bottom: 9px; }
.opt-strikes-h, .opt-strike {
  display: grid; grid-template-columns: minmax(0, 1fr) minmax(0, 1fr) 68px; gap: 8px; align-items: baseline;
}
.opt-strikes-h {
  font-size: var(--t-micro); text-transform: uppercase; letter-spacing: 0.05em;
  color: var(--faint); padding: 0 12px 6px;
}
.opt-strikes-h > span:not(:first-child), .opt-strike > span:not(:first-child) { text-align: right; }
.opt-strikes { display: grid; gap: 4px; max-height: 268px; overflow-y: auto; margin-bottom: 14px; }
.opt-strike {
  width: 100%; text-align: left; cursor: pointer; padding: 9px 12px; min-height: 40px;
  border: 1px solid var(--line-soft); border-radius: var(--r-sm); background: var(--surface);
  color: var(--text); font-size: var(--t-meta); transition: border-color 0.15s;
}
.opt-strike:hover { border-color: var(--accent); }
.opt-strike-p { color: var(--muted); }
.opt-strike-iv { color: var(--faint); }

.opt-read { margin-top: 4px; }
/* per-cell hairline — see the mosaic note on .metrics */
.opt-facts { display: grid; grid-template-columns: repeat(auto-fit, minmax(148px, 1fr)); gap: 1px;
  background: var(--surface); border: 1px solid var(--line-soft); border-radius: var(--r-sm);
  overflow: hidden; margin-bottom: 12px; }
.opt-fact { background: var(--surface); padding: 10px 12px; display: flex;
  flex-direction: column; gap: 3px; box-shadow: 0 0 0 1px var(--line-soft); }
.opt-fact span { font-size: var(--t-micro); text-transform: uppercase; letter-spacing: 0.04em; color: var(--faint); }
.opt-fact b { font-size: var(--t-small); }

.opt-verdict { display: flex; align-items: center; gap: 13px; padding: 12px 0;
  border-top: 1px solid var(--line-soft); border-bottom: 1px solid var(--line-soft); margin-bottom: 13px; }
.opt-verdict-n { font-size: 26px; font-weight: 700; flex: 0 0 auto; }

/* the two numbers that must never be conflated, kept visually apart */
.opt-market { display: grid; grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
  gap: 10px; margin: 14px 0 4px; }
.opt-mkt-item { border: 1px solid var(--line-soft); border-radius: var(--r-sm); padding: 10px 12px; }
.opt-mkt-item span { display: block; font-size: var(--t-micro); text-transform: uppercase;
  letter-spacing: 0.04em; color: var(--faint); }
.opt-mkt-item b { display: block; font-size: 19px; margin: 3px 0 4px; }
.opt-mkt-item em { font-style: normal; font-size: var(--t-micro); color: var(--muted); line-height: 1.45; display: block; }
.opt-flags li { color: var(--muted); }
@media (max-width: 560px) {
  .opt-controls { gap: 8px; }
  .opt-lab { flex: 1 1 100%; }
  .opt-meta { padding-bottom: 0; }
}

/* ---------- ideas ----------
   Same monochrome discipline as the risk cards, and for a sharper reason: a suggestion
   tinted green would be a recommendation whatever the copy above it says. The only colour
   is the fair-value gap, which is a price-direction number and already carries that meaning
   everywhere else on the site. Single column on a phone, two at a comfortable width. */
.pa-ideas { display: grid; grid-template-columns: repeat(auto-fit, minmax(280px, 1fr)); gap: 8px; }
.pa-idea {
  border: 1px solid var(--line-soft); border-radius: var(--r); background: var(--surface);
  padding: var(--panel-pad-y) var(--panel-pad-x); cursor: pointer;
  transition: border-color 0.2s var(--ease-out), transform 0.2s var(--ease-out);
}
.pa-idea:hover { border-color: var(--accent); transform: translateY(-2px); }
.pa-idea:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }
.pa-idea-top { display: flex; align-items: baseline; gap: 8px; min-width: 0; }
.pa-idea-tk { font-weight: 700; font-size: var(--t-small); flex: 0 0 auto; }
.pa-idea-nm { color: var(--muted); font-size: var(--t-meta); min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.pa-idea-gap { margin-left: auto; flex: 0 0 auto; font-size: var(--t-meta); color: var(--up-text); font-variant-numeric: tabular-nums; }
.pa-idea .pa-chips { margin: 8px 0 2px; }
.pa-idea .pa-ul { margin-top: 7px; }
.pa-method { font-family: var(--mono); font-size: var(--t-micro); color: var(--faint); line-height: 1.5; margin-top: 10px; }
@media (prefers-reduced-motion: reduce) { .pa-idea, .pa-idea:hover { transition: none; transform: none; } }
/* the allocation row's 68px label column was sized for tickers; a sector or fund category
   ("Large Growth", "Communication Equipment") needs room or it wraps to two lines */

@media (max-width: 560px) {
  .pa-read { font-size: 14.5px; padding-left: 12px; }
  .pa-pos-nm { display: none; }   /* the ticker and the weight are what a phone row needs */
}

/* ---------- cost-basis units prompt ----------
   The one banner on this page that is allowed to be prominent: while it is showing, the
   cost basis and unrealized figures above it are a coin-flip between two readings, and a
   quiet note would let someone keep reading a wrong number as a right one. Still
   monochrome — this is a question, not an alarm. */
.pf-units {
  border: 1px solid var(--line-strong); border-radius: var(--r); background: var(--surface);
  padding: var(--panel-pad-y) var(--panel-pad-x); margin: 4px 0 18px;
}
.pf-units-head { font-size: var(--t-small); }
.pf-units-copy { font-size: var(--t-meta); color: var(--muted); line-height: 1.55; margin: 6px 0 12px; }
.pf-units-list { display: grid; gap: 8px; margin-bottom: 13px; }
.pf-units-row { display: flex; gap: 10px; align-items: baseline; }
.pf-units-tk { font-weight: 700; font-size: var(--t-meta); flex: 0 0 auto; min-width: 52px; }
.pf-units-txt { font-size: var(--t-meta); color: var(--muted); line-height: 1.55; min-width: 0; }
.pf-units-txt b { color: var(--text); font-weight: 600; }
.pf-units-acts { display: flex; flex-wrap: wrap; gap: 9px; align-items: center; }
.pf-units-acts .pf-x { min-height: 38px; padding: 8px 14px; }
@media (max-width: 560px) {
  .pf-units-row { flex-direction: column; gap: 2px; }
  .pf-units-acts .scr-run, .pf-units-acts .pf-x { flex: 1 1 100%; }
}

/* ---------- share panel ----------
   Deliberately quiet. This is the one control on the page whose effect leaves the account,
   so it reads as a settings block rather than a call to action: no accent fill, no urgency,
   and the copy says what a viewer would see BEFORE the link exists. */
.pf-share {
  border: 1px solid var(--line-soft); border-radius: var(--r);
  padding: var(--panel-pad-y) var(--panel-pad-x); margin-top: 22px;
}
.pf-share-head { display: flex; align-items: center; gap: 10px; font-size: var(--t-small); }
.pf-share-off, .pf-share-on {
  font-size: var(--t-micro); letter-spacing: 0.06em; text-transform: uppercase;
  border: 1px solid var(--line); border-radius: var(--r-full); padding: 2px 8px; color: var(--faint);
}
.pf-share-on { color: var(--text); border-color: var(--line-strong); }
.pf-share-copy { font-size: var(--t-meta); color: var(--muted); line-height: 1.55; margin: 8px 0 12px; }
.pf-share-url { display: flex; gap: 8px; align-items: center; margin-bottom: 10px; }
.pf-share-url input {
  flex: 1 1 auto; min-width: 0; font-family: var(--mono); font-size: var(--t-meta);
  padding: 9px 11px; border: 1px solid var(--line); border-radius: var(--r-sm);
  background: var(--surface-2); color: var(--text);
}
.pf-share-row {
  display: flex; align-items: flex-start; gap: 9px; margin-bottom: 10px;
  font-size: var(--t-meta); color: var(--muted); line-height: 1.5; cursor: pointer;
}
.pf-share-row input[type="checkbox"] { margin-top: 2px; width: 16px; height: 16px; flex: 0 0 auto; }
.pf-share-lab { flex: 0 0 auto; align-self: center; }
.pf-share-name {
  flex: 1 1 160px; min-width: 0; font-size: var(--t-meta); padding: 7px 10px;
  border: 1px solid var(--line); border-radius: var(--r-sm); background: transparent; color: var(--text);
}
.pf-share-acts { display: flex; flex-wrap: wrap; gap: 8px; margin-bottom: 10px; }
.pf-share-acts .pf-x { min-height: 34px; padding: 6px 12px; }
@media (max-width: 560px) {
  .pf-share-url { flex-wrap: wrap; }
  .pf-share-url input { flex: 1 1 100%; }
}

.pf-divs { margin-top: 26px; }
.pf-divs h3 { font-size: var(--t-small); }
.pf-div-row { display: flex; align-items: center; gap: 12px; padding: 8px 0; border-top: 1px solid var(--line-soft); font-family: var(--mono); font-size: var(--t-meta); }
.pf-div-tk { font-weight: 700; cursor: pointer; }
.pf-div-tk:hover { color: var(--accent); }
.pf-div-date { color: var(--muted); }
.pf-div-amt { margin-left: auto; color: var(--faint); }

/* ---------- programmatic SEO pages ---------- */
.seo-page {
  /* background-COLOR, not the shorthand: the shorthand was resetting the grain and
     gradient-mesh layers declared on body, so every SEO page shipped untextured. */
  background-color: var(--surface);
  background-image: var(--grain),
    radial-gradient(900px 520px at 88% -6%, var(--glow), transparent 60%);
  background-repeat: repeat, no-repeat;
  color: var(--text);
}
.seo-top { display: flex; justify-content: space-between; align-items: center; padding: 18px clamp(16px, 5vw, 60px); border-bottom: 1px solid var(--line-soft); }
/* The server-rendered pages get the same lockup WITHOUT A MARKUP CHANGE — their <head>
   and header are built in app/pages.py, which this rebuild must not touch. A ::before
   carrying the seal as a mask is a stylesheet-only way to say the same thing. */
.seo-brand {
  font-family: var(--sans); font-size: 20px; letter-spacing: -0.03em; font-weight: 400;
  text-decoration: none; color: var(--text);
}
/* NOT a flex container, and that is not a style preference. The markup here is
   `Ticker<b>Worth</b>` and it is frozen in app/pages.py — under `display: inline-flex`
   the bare text node and the <b> become two separate FLEX ITEMS, so the wordmark
   rendered as "Ticker  Worth" with a 10px hole in it. An inline-block ::before keeps
   the two halves one text run. */
.seo-brand::before {
  content: ""; display: inline-block; vertical-align: -0.28em;
  width: 24px; height: 24px; margin-right: 10px;
  background-color: currentColor;
  -webkit-mask: url("/assets/brand/tw-seal-micro.svg") center / contain no-repeat;
  mask: url("/assets/brand/tw-seal-micro.svg") center / contain no-repeat;
}
.seo-brand b { color: var(--accent); font-weight: 500; }
.seo-cta { font-family: var(--mono); font-size: 13px; color: var(--accent); text-decoration: none; border: 1px solid var(--line); border-radius: var(--r-full); padding: 8px 14px; }
.seo-cta:hover { border-color: var(--accent); }
.seo-main { max-width: 860px; margin: 0 auto; padding: 32px clamp(16px, 5vw, 40px) 60px; }
.seo-crumb { font-family: var(--mono); font-size: 12px; color: var(--faint); margin-bottom: 18px; }
.seo-crumb a { color: var(--muted); text-decoration: none; }
/* the crumb was the one link on /methodology with no hover at all — `text-decoration: none`
   at (0,2,0) beat the base `a:hover` underline, and nothing replaced it */
.seo-crumb a:hover { color: var(--text); text-decoration: underline; }
.seo-main h1 { font-family: var(--serif); font-size: clamp(26px, 4.5vw, 40px); line-height: var(--lead-display); margin: 0 0 8px; }
.seo-updated { font-family: var(--mono); font-size: 12px; color: var(--faint); margin: 0 0 26px; }
/* Degraded market data on a STATIC page — see .notice.degraded. A page that can't refresh
   itself has to carry the caveat in the markup, above the numbers it qualifies. */
.seo-degraded {
  border: 1px solid color-mix(in srgb, var(--warn) 45%, transparent);
  background: color-mix(in srgb, var(--warn) 11%, transparent);
  border-radius: var(--r-sm); padding: 11px 14px; font-size: 13.5px;
  color: var(--text); margin: -14px 0 26px; max-width: 76ch;
}
.seo-degraded b { color: var(--warn); }
.seo-headline { display: grid; grid-template-columns: repeat(auto-fit, minmax(150px, 1fr)); gap: 14px; margin-bottom: 32px; }
.sh-cell { border: 1px solid var(--line-soft); border-radius: var(--r-sm); padding: 15px 16px; }
.sh-lab { font-family: var(--mono); font-size: 10.5px; text-transform: uppercase; letter-spacing: 0.04em; color: var(--faint); margin-bottom: 8px; }
.sh-val { font-size: 23px; font-weight: 700; }
.sh-val.accent { color: var(--accent); }
.seo-slider { border: 1px solid var(--line-soft); border-radius: var(--r-sm); padding: 22px; margin-bottom: 34px; }
.seo-slider h2 { margin-top: 0; font-size: 18px; }
.slider-readout { display: flex; justify-content: space-between; font-family: var(--mono); font-size: 15px; margin: 16px 0 8px; }
#sv-val { color: var(--accent); font-weight: 700; }
#sv-range { width: 100%; accent-color: var(--accent); }
.slider-note { font-family: var(--mono); font-size: 11.5px; color: var(--faint); margin-top: 6px; }
.seo-prose p { font-size: 16px; line-height: 1.7; margin: 0 0 18px; }
.seo-assum { margin: 36px 0; }
.seo-assum h2, .seo-peers h2, .seo-hub h2 { font-size: 19px; margin-bottom: 14px; }
.seo-table { width: 100%; border-collapse: collapse; font-size: 13.5px; }
.seo-table th, .seo-table td { text-align: left; padding: 9px 14px 9px 0; border-top: 1px solid var(--line-soft); vertical-align: top; }
.seo-table thead th { border-top: none; font-family: var(--mono); font-size: 10.5px; text-transform: uppercase; letter-spacing: 0.03em; color: var(--faint); }
.seo-table tbody th { font-weight: 600; width: 200px; }
/* Source citations and the sourcing footnote are sentences, not figures — the company
   page's assumptions ledger already sets its `.src` in sans (line ~1092), and the SEO
   twin had drifted to mono. The accession numbers inside keep tabular digits in sans. */
.seo-table .src { color: var(--faint); font-size: 11.5px; font-family: var(--sans); font-variant-numeric: tabular-nums; }
.seo-autonote { font-family: var(--sans); font-size: 11.5px; color: var(--muted); margin-top: 12px; line-height: 1.5; }
.seo-peers { margin: 30px 0; }
.peer-links, .seo-hub-links { display: flex; flex-wrap: wrap; gap: 8px; }
.peer-links a, .seo-hub-links a { font-family: var(--mono); font-size: 13px; text-decoration: none; color: var(--text); border: 1px solid var(--line); border-radius: var(--r-sm); padding: 7px 12px; }
.peer-links a:hover, .seo-hub-links a:hover { border-color: var(--accent); color: var(--accent); }
.rel-chips { display: flex; flex-wrap: wrap; gap: 8px; margin-top: 4px; }
.rel-chip { display: inline-flex; flex-direction: column; align-items: flex-start; gap: 2px; background: none; cursor: pointer; text-align: left; border: 1px solid var(--line); border-radius: var(--r-sm); padding: 7px 12px; transition: border-color .15s, color .15s; }
.rel-chip:hover { border-color: var(--accent); }
.rel-chip-tk { font-family: var(--mono); font-size: 13px; color: var(--text); }
.rel-chip:hover .rel-chip-tk { color: var(--accent); }
.rel-chip-co { font-size: 11px; color: var(--muted); max-width: 160px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.rel-note { color: var(--faint); font-size: 12px; margin-top: 10px; }
.seo-hub { margin: 34px 0; font-size: 14px; line-height: 1.9; }
.seo-hub a { color: var(--accent); text-decoration: none; }
.seo-uv { display: grid; grid-template-columns: repeat(auto-fit, minmax(220px, 1fr)); gap: 12px; }
.uv-card { display: flex; flex-direction: column; gap: 3px; border: 1px solid var(--line-soft); border-radius: var(--r-sm); padding: 13px 15px; text-decoration: none; color: var(--text); }
.uv-card:hover { border-color: var(--accent); }
.uv-tk { font-weight: 700; }
.uv-co { font-size: 12px; color: var(--faint); }
.uv-up { font-family: var(--mono); font-size: 12px; color: var(--up-text); margin-top: 4px; }
/* A ranked hub can now be an OVERVALUED screen (app/screener.rank_overvalued), so the gap
   line's tone follows the direction rather than assuming one. `.uv-up` keeps its meaning as
   the base rule; these two override it where the row points the other way. Set by
   `pages._uv_gap`, which decides the words at the same time — a card whose colour and
   wording could disagree is the CSS half of the "-38.9% upside" defect. */
.uv-up.uv-down { color: var(--down-text); }
.uv-up.uv-flat { color: var(--faint); }
.seo-disc { font-family: var(--mono); font-size: 11px; color: var(--faint); line-height: 1.6; margin-top: 40px; border-top: 1px solid var(--line-soft); padding-top: 16px; }
.seo-foot { text-align: center; font-family: var(--mono); font-size: 12px; color: var(--faint); padding: 26px; border-top: 1px solid var(--line-soft); }
.seo-foot a { color: var(--muted); text-decoration: none; }

/* ---------- /stocks — the coverage index table ---------- */
/* The index is the widest SEO page we serve (six columns over ~1,300 rows), so it opts out
   of the 860px reading measure the prose pages use. The table itself scrolls inside its own
   container rather than letting the page scroll sideways. */
.seo-main.sx-main { max-width: 1120px; }
.sx-sr { position: absolute; width: 1px; height: 1px; overflow: hidden; clip: rect(0 0 0 0); white-space: nowrap; }
/* The index's intro is a coverage statement, not a one-line dateline like the other hubs' —
   it carries the filing-sourced/auto-modeled split and has to be readable prose. */
.sx-main .seo-updated { font-family: inherit; font-size: 15px; line-height: 1.65; color: var(--muted); max-width: 76ch; margin-bottom: 24px; }

.sx-search { display: flex; flex-wrap: wrap; gap: 8px; align-items: center; margin: 0 0 18px; }
.sx-search input {
  flex: 1 1 260px; min-width: 0; font: inherit; font-size: 14px; color: var(--text);
  background: var(--surface); border: 1px solid var(--line); border-radius: var(--r-sm); padding: 10px 13px;
}
.sx-search input:focus-visible { outline: 2px solid var(--accent); outline-offset: 1px; border-color: var(--accent); }
.sx-search button {
  font-family: var(--mono); font-size: 13px; color: var(--text); cursor: pointer;
  background: var(--surface); border: 1px solid var(--line); border-radius: var(--r-full); padding: 10px 16px;
  transition: border-color .15s, color .15s;
}
.sx-search button:hover { border-color: var(--accent); color: var(--accent); }
.sx-clear { font-family: var(--mono); font-size: 12.5px; color: var(--faint); text-decoration: none; padding: 0 4px; }
.sx-clear:hover { color: var(--accent); }
.sx-showing { font-family: var(--mono); font-size: 12px; color: var(--faint); margin: 0 0 12px; }
.sx-showing b { color: var(--text); }
.sx-empty { font-size: var(--t-small); color: var(--muted); line-height: 1.6; padding: 28px 0; }

.sx-wrap { overflow-x: auto; border: 1px solid var(--line-soft); border-radius: var(--r); }
.sx-table { width: 100%; border-collapse: collapse; font-size: 13.5px; }
/* Row separators are drawn ONCE, as a top border between rows, so the last row doesn't draw a
   line against the container's own rounded border. That means clearing the bare-element
   `th`/`td` bottom borders declared further down the sheet — and `th` carries `!important`
   there, which nothing but `!important` can override. Without this the ticker column's
   separators were a heavier colour (`--line`) than the rest of the row (`--line-soft`). */
.sx-table th, .sx-table td { text-align: left; padding: 10px 14px; white-space: nowrap; border-bottom: 0; }
.sx-table th { border-bottom: 0 !important; }
.sx-table thead th {
  position: sticky; top: 0; z-index: 2; background: var(--surface);
  font-family: var(--mono); font-size: 10.5px; text-transform: uppercase; letter-spacing: 0.04em;
  font-weight: 600; color: var(--faint); border-bottom: 1px solid var(--line);
}
.sx-table thead a { color: inherit; text-decoration: none; }
.sx-table thead a:hover { color: var(--accent); }
.sx-table thead th[aria-sort] a { color: var(--text); }
.sx-table tbody tr + tr th, .sx-table tbody tr + tr td { border-top: 1px solid var(--line-soft); }
.sx-table tbody tr:hover th, .sx-table tbody tr:hover td { background: var(--surface-2); }
.sx-tk { font-family: var(--mono); font-weight: 600; }
.sx-tk a, .sx-co a { color: var(--text); text-decoration: none; }
.sx-tk a:hover, .sx-co a:hover { color: var(--accent); }
.sx-co { white-space: normal; max-width: 320px; }
.sx-num { font-family: var(--mono); font-variant-numeric: tabular-nums; text-align: right; }
.sx-fv { font-weight: 600; }
.sx-none { color: var(--faint); }
.sx-nodcf, .sx-pending { font-size: 12px; color: var(--faint); border-bottom: 1px dotted var(--line); cursor: help; }
.sx-pending { border-bottom: none; cursor: default; }
.sx-under { color: var(--up-text); }
.sx-over { color: var(--down-text); }
.sx-level { color: var(--muted); }

/* Depth pills are deliberately MONOCHROME. Green and red mean "under/over fair value" in the
   gap column one cell to the left; a green pill here would read as a verdict on the stock
   rather than a statement about the evidence behind the number. Depth is conveyed by weight
   instead: filled → outlined → faint-hairline → ghost, deepest to thinnest. The third step
   was a DASHED outline until 2026-08-20; Titan draws hairlines and fills, and a dash reads
   as an unfinished element rather than as a fainter one. The weight of the hairline carries
   the same four steps without borrowing a wireframe convention. */
.dp {
  display: inline-block; font-family: var(--mono); font-size: 10px; font-weight: 700;
  text-transform: uppercase; letter-spacing: 0.06em; padding: 3px 9px; border-radius: var(--r-full);
  border: 1px solid var(--line); color: var(--muted); cursor: help;
}
.dp-hand { background: var(--accent-soft); color: var(--text); border-color: var(--line); }
.dp-filing { color: var(--text); border-color: var(--line); }
.dp-modeled { color: var(--faint); border-color: var(--line-soft); }
.dp-none { color: var(--faint); border-color: transparent; background: var(--surface-2); }

/* Sorting on narrow screens — hidden wherever the sortable header row is visible, so the two
   controls never coexist. */
.sx-sortbar { display: none; flex-wrap: wrap; gap: 6px; align-items: center; margin: 0 0 12px; }
.sb-lab { font-family: var(--mono); font-size: 10.5px; text-transform: uppercase; letter-spacing: 0.04em; color: var(--faint); margin-right: 2px; }
.sb-chip { font-family: var(--mono); font-size: 12px; text-decoration: none; color: var(--muted); border: 1px solid var(--line); border-radius: var(--r-full); padding: 5px 11px; }
.sb-chip.is-on { color: var(--accent); border-color: var(--accent); }

.sx-pager { display: flex; flex-wrap: wrap; align-items: center; gap: 6px; margin: 20px 0 8px; font-family: var(--mono); font-size: 13px; }
.sx-pager a, .sx-pager .pg-now {
  min-width: 34px; text-align: center; padding: 7px 10px; border-radius: var(--r-full);
  border: 1px solid var(--line); text-decoration: none; color: var(--text);
}
.sx-pager a:hover { border-color: var(--accent); color: var(--accent); }
.sx-pager .pg-now { border-color: var(--accent); color: var(--accent); font-weight: 700; }
.sx-pager .pg-gap { color: var(--faint); padding: 0 2px; }
.sx-pager .pg-step { font-size: 12.5px; }

.sx-legend { margin: 34px 0 0; border-top: 1px solid var(--line-soft); padding-top: 22px; }
.sx-legend h2 { font-size: 19px; margin-bottom: 14px; }
.lg-row { display: grid; grid-template-columns: 150px minmax(0, 1fr); gap: 14px; align-items: start; margin-bottom: 12px; }
.lg-row .dp { cursor: default; justify-self: start; }
.lg-row p { margin: 0; font-size: 13.5px; line-height: 1.6; color: var(--muted); }
.sx-legend-note { font-size: 13.5px; line-height: 1.65; color: var(--muted); margin-top: 18px; }

/* Narrow screens: the six columns can't fit, and a sideways-scrolling table hides the three
   that matter most (fair value, gap, coverage) off the right edge — a reader on a phone would
   see ticker, company, price and have no idea the rest existed. So each row RESTACKS into a
   card, labelled from the `data-label` the renderer already emits. The markup is untouched:
   still one <table>, still the same cells, still what a crawler sees. */
@media (max-width: 720px) {
  .sx-sortbar { display: flex; }
  .sx-wrap { border: 0; border-radius: 0; overflow-x: visible; }
  .sx-table, .sx-table tbody, .sx-table tr, .sx-table th, .sx-table td { display: block; }
  .sx-table thead { position: absolute; width: 1px; height: 1px; overflow: hidden; clip: rect(0 0 0 0); }
  .sx-table tr { border: 1px solid var(--line-soft); border-radius: var(--r); padding: 12px 14px; margin-bottom: 10px; }
  .sx-table th, .sx-table td { border: 0 !important; }   /* the card draws the only outline */
  .sx-table tbody tr:hover th, .sx-table tbody tr:hover td { background: none; }
  .sx-table th, .sx-table td { padding: 2px 0; white-space: normal; text-align: left; }
  .sx-tk { font-size: 15px; }
  .sx-co { max-width: none; margin-bottom: 8px; }
  .sx-co a { color: var(--muted); font-size: 13px; }
  .sx-table td[data-label]::before {
    content: attr(data-label); display: inline-block; min-width: 92px;
    font-family: var(--mono); font-size: 10px; font-weight: 600; text-transform: uppercase;
    letter-spacing: 0.04em; color: var(--faint);
  }
  .lg-row { grid-template-columns: minmax(0, 1fr); gap: 5px; }
}

/* ---------- reverse DCF (Pro) ---------- */
.rev-big { font-family: var(--mono); font-size: 34px; font-weight: 700; font-variant-numeric: tabular-nums; line-height: 1.1; }
.rev-big.accent { color: var(--accent); }
.rev-big .rev-sub { font-size: 14px; color: var(--muted); font-weight: 400; margin-left: 8px; }
/* [audit 2.5] Past the meaningful range the headline is a PHRASE ("more than 25%/yr"), not a
   figure — it needs room to breathe at a size a sentence can carry, and it deliberately drops
   the tabular-numeric treatment that makes a number look measured. */
.rev-big-phrase { font-size: 22px; letter-spacing: -0.01em; }
.rev-big-phrase .rev-sub { display: block; margin-left: 0; margin-top: 4px; }
.rev-caveat { border-left: 2px solid var(--accent-soft); padding-left: 11px; }
.rev-cmp { display: flex; align-items: flex-end; gap: 14px; margin: 16px 0 12px; }
.rev-cmp > div:not(.rev-arrow) { flex: 1; }
.rev-cmp .lab { font-family: var(--mono); font-size: 9.5px; letter-spacing: 0.14em; text-transform: uppercase; color: var(--faint); }
.rev-cmp .v { font-family: var(--mono); font-size: 22px; font-weight: 700; font-variant-numeric: tabular-nums; margin-top: 3px; }
.rev-cmp .v.accent { color: var(--accent); }
.rev-cmp .v small { font-size: 12px; color: var(--muted); font-weight: 400; }
.rev-arrow { font-family: var(--mono); color: var(--muted); font-size: 14px; padding-bottom: 4px; flex: 0 0 auto; }
.rev-verdict { color: var(--text); font-size: 13.5px; margin: 0 0 4px; }
.rev-note { color: var(--muted); font-size: 12.5px; line-height: 1.55; margin: 12px 0 0; }
.rev-note b { color: var(--text); }

/* ---------- implied growth (free reverse-DCF headline) ---------- */
.ig-head { margin: -4px 0 12px; }
.ig-chip {
  display: inline-flex; align-items: center; font-family: var(--mono);
  font-size: 10.5px; letter-spacing: 0.1em; text-transform: uppercase; font-weight: 700;
  padding: 4px 11px; border-radius: var(--r-full);
  background: var(--surface-2); color: var(--muted);
  border: 1px solid var(--line);
}
.ig-chip.warn { background: var(--down-soft); color: var(--down-text); border-color: transparent; }
.ig-chip.good { background: var(--up-soft); color: var(--up-text); border-color: transparent; }
.ig-sentence { font-size: 15px; line-height: 1.55; color: var(--text); margin: 0 0 16px; }
.ig-sentence b { font-weight: 700; }
.ig-bars { display: flex; flex-direction: column; gap: 9px; margin: 0 0 14px; }
.ig-row { display: flex; align-items: center; gap: 12px; }
.ig-lab {
  flex: 0 0 96px; font-family: var(--mono); font-size: 10px; letter-spacing: 0.1em;
  text-transform: uppercase; color: var(--faint); text-align: right;
}
.ig-track { flex: 1; height: 10px; background: var(--line); border-radius: var(--r-full); overflow: hidden; }
.ig-fill { height: 100%; border-radius: var(--r-full); background: var(--muted); }
.ig-fill.accent { background: var(--accent); }
/* [audit 2.5] The bar is CAPPED at the meaningful maximum, so it must not read as a measured
   length. The fade off the right edge says "this runs past the end of the scale" — the visual
   equivalent of refusing to quote the figure. */
.ig-fill-over {
  background: linear-gradient(90deg, var(--accent) 55%,
                              color-mix(in srgb, var(--accent) 25%, transparent) 100%);
}
.ig-val {
  flex: 0 0 auto; min-width: 62px; font-family: var(--mono); font-size: 14px; font-weight: 700;
  font-variant-numeric: tabular-nums; color: var(--text);
}
.ig-val.accent { color: var(--accent); }
.ig-why { color: var(--muted); font-size: 12.5px; line-height: 1.55; margin: 0; }
@media (max-width: 640px) {
  .ig-lab { flex-basis: 74px; font-size: 9px; }
  .ig-val { min-width: 54px; font-size: 13px; }
  .ig-sentence { font-size: 14px; }
}

/* ---------- market open outlook (Pro) ---------- */
.mo-panel .mo-tone { font-family: var(--mono); font-size: 15px; font-weight: 700; margin: -2px 0 8px; }
.mo-summary { font-size: 14.5px; line-height: 1.6; color: var(--text); margin: 0 0 16px; }
.mo-backdrop { display: flex; flex-direction: column; gap: 12px; margin-bottom: 16px; }
.mo-glab { font-family: var(--mono); font-size: 10px; letter-spacing: 0.12em; text-transform: uppercase; color: var(--faint); margin-bottom: 6px; }
.mo-chips { display: flex; flex-wrap: wrap; gap: 8px; }
.mo-chip { display: inline-flex; align-items: baseline; gap: 6px; padding: 5px 11px; border: 1px solid var(--line); border-radius: var(--r-full); }
.mo-clab { font-family: var(--mono); font-size: 11px; color: var(--muted); }
.mo-cval { font-family: var(--mono); font-size: 12.5px; font-weight: 700; font-variant-numeric: tabular-nums; }
.mo-sec { border-top: 1px solid var(--line); padding-top: 10px; margin-top: 10px; }
.mo-stitle { font-family: var(--mono); font-size: 10px; letter-spacing: 0.12em; text-transform: uppercase; color: var(--faint); margin-bottom: 6px; }
.mo-row { display: flex; align-items: baseline; gap: 10px; padding: 6px 0; cursor: pointer; }
.mo-row:hover .mo-tk { color: var(--accent); }
.mo-tk { font-family: var(--mono); font-weight: 700; font-size: 12.5px; color: var(--text); flex: 0 0 auto; }
.mo-nm { flex: 1; color: var(--muted); font-size: 12.5px; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.mo-v { font-family: var(--mono); font-size: 12.5px; font-weight: 700; font-variant-numeric: tabular-nums; }
.mo-dormant { font-size: 11.5px; line-height: 1.5; color: var(--muted); margin: 14px 0 0; padding: 10px 12px; background: var(--accent-soft); border-radius: var(--r-sm); }
.mo-why { color: var(--muted); font-size: 12px; line-height: 1.55; margin: 14px 0 0; }
.mo-disc { color: var(--faint); font-size: 11.5px; line-height: 1.5; margin: 10px 0 0; }
.mo-panel.locked { position: relative; overflow: hidden; }

/* ---------- today's move (free, leads the company page) ---------- */
.td-head { display: flex; align-items: baseline; flex-wrap: wrap; gap: 10px 16px; margin-bottom: 4px; }
.td-price { font-family: var(--mono); font-size: clamp(28px, 6vw, 38px); font-weight: 700; letter-spacing: -0.02em; font-variant-numeric: tabular-nums; }
.td-chg { display: inline-flex; align-items: baseline; gap: 6px; font-family: var(--mono); font-size: 16px; font-weight: 700; font-variant-numeric: tabular-nums; padding: 4px 10px; border-radius: var(--r-full); }
.td-chg.td-up { color: var(--up-text); background: var(--up-soft); }
.td-chg.td-down { color: var(--down-text); background: var(--down-soft); }
.td-chg.td-flat { color: var(--muted); background: var(--surface-2); }
.td-arrow { font-size: 11px; }
.td-pct { font-size: 13.5px; font-weight: 600; opacity: 0.85; }
.td-loading { color: var(--muted); font-family: var(--mono); font-size: 13px; }
/* the honest terminal state when the session feed cannot be reached — prose, so sans */
.td-unreachable { color: var(--muted); font-size: 13px; line-height: 1.55; }
.td-session { margin-left: auto; display: inline-flex; align-items: center; gap: 7px; }
.td-slab { font-family: var(--mono); font-size: 11.5px; letter-spacing: 0.04em; color: var(--muted); text-transform: uppercase; }
.td-live { font-family: var(--mono); font-size: 9.5px; font-weight: 700; letter-spacing: 0.1em; color: var(--up); background: var(--up-soft); padding: 3px 7px; border-radius: var(--r-sm); }

.td-range { margin: 14px 0 2px; }
.td-range-lab { display: flex; justify-content: space-between; font-family: var(--mono); font-size: 10.5px; color: var(--faint); margin-bottom: 5px; }
.td-range-track { position: relative; height: 6px; border-radius: var(--r-full); background: linear-gradient(90deg, var(--line), var(--surface-2), var(--line)); }
.td-range-dot { position: absolute; top: -3px; width: 12px; height: 12px; border-radius: 50%; transform: translateX(-6px); border: 2px solid var(--surface); background: var(--muted); }
.td-range-dot.td-up { background: var(--up); }
.td-range-dot.td-down { background: var(--down); }

.td-stats { display: grid; grid-template-columns: repeat(3, minmax(0, 1fr)); gap: 12px; margin-top: 16px; padding-top: 14px; border-top: 1px solid var(--line); }
.td-stat .lab { font-family: var(--mono); font-size: 10.5px; letter-spacing: 0.05em; text-transform: uppercase; color: var(--faint); }
.td-stat .val { font-family: var(--mono); font-size: 14px; font-weight: 700; font-variant-numeric: tabular-nums; margin-top: 3px; }
.td-stat .td-up { color: var(--up-text); }
.td-stat .td-down { color: var(--down); }
.td-vr { font-size: 11px; font-weight: 600; color: var(--accent); }
.td-brief { font-size: 14.5px; line-height: 1.65; margin: 16px 0 0; padding-top: 14px; border-top: 1px solid var(--line); }
.td-note { font-size: 12px; line-height: 1.5; color: var(--muted); margin: 10px 0 0; }
.td-disc { color: var(--faint); font-size: 11.5px; line-height: 1.5; margin: 10px 0 0; }
@media (max-width: 640px) {
  .td-session { margin-left: 0; width: 100%; }
  .td-stats { grid-template-columns: repeat(2, minmax(0, 1fr)); }
  .td-brief { font-size: 14px; }
}

/* ---------- catalyst / what's moving it now ---------- */
.cat-rows { display: flex; flex-direction: column; gap: 2px; }
.cat-row { display: flex; gap: 12px; padding: 11px 0; border-top: 1px solid var(--line); align-items: flex-start; }
.cat-row:first-child { border-top: none; }
.cat-ico { flex: 0 0 22px; text-align: center; font-size: 13px; line-height: 1.5; }
.cat-up .cat-ico { color: var(--up-text); }
.cat-down .cat-ico { color: var(--down-text); }
.cat-neutral .cat-ico { color: var(--accent); }
.cat-body { flex: 1; min-width: 0; }
.cat-lab { font-size: 14px; font-weight: 600; color: var(--text); }
.cat-val { font-family: var(--mono); font-size: 12.5px; font-weight: 700; color: var(--accent); margin-left: 4px; }
.cat-why { font-size: 12.5px; line-height: 1.5; color: var(--muted); margin-top: 2px; }
.cat-dormant { font-size: 11.5px; line-height: 1.5; color: var(--muted); margin: 14px 0 0; padding: 10px 12px; background: var(--accent-soft); border-radius: var(--r-sm); }
.cat-disc { color: var(--faint); font-size: 11.5px; line-height: 1.5; margin: 12px 0 0; }

/* ---------- recent activity & deals (SEC 8-K) — neutral, hairline rows, tag chip ---------- */
.deal-rows { display: flex; flex-direction: column; gap: 2px; }
.deal-row { display: flex; gap: 12px; padding: 11px 0; border-top: 1px solid var(--line); align-items: baseline; }
.deal-row:first-child { border-top: none; }
.deal-when { flex: 0 0 82px; font-size: 11.5px; color: var(--faint); white-space: nowrap; }
.deal-body { flex: 1 1 auto; min-width: 0; }
.deal-lab { font-size: 14px; font-weight: 600; color: var(--text); line-height: 1.45; }
.deal-lab:hover { color: var(--accent); }
.deal-also { font-size: 11.5px; color: var(--muted); margin-top: 2px; }
.deal-tag { flex: 0 0 auto; align-self: center; font-family: var(--mono); font-size: 9.5px;
  text-transform: uppercase; letter-spacing: 0.04em; color: var(--muted);
  border: 1px solid var(--line); border-radius: var(--r-sm); padding: 2px 6px; white-space: nowrap; }
@media (max-width: 560px) {
  .deal-row { flex-wrap: wrap; }
  .deal-when { flex-basis: 100%; order: -1; }
  .deal-tag { align-self: baseline; }
}

/* ---------- multi-model valuation (fair-value range) ---------- */
.vm-track { position: relative; height: 16px; margin: 8px 0 6px; background: var(--line); border-radius: var(--r-full); }
.vm-band { position: absolute; top: 0; height: 100%; background: var(--accent-soft); border: 1px solid var(--accent); border-radius: var(--r-full); }
.vm-median { position: absolute; top: -3px; width: 2px; height: 22px; background: var(--accent); transform: translateX(-1px); }
.vm-price { position: absolute; top: -5px; width: 0; height: 0; border-left: 6px solid transparent; border-right: 6px solid transparent; border-top: 9px solid var(--text); transform: translateX(-6px); }
.vm-scale { display: flex; justify-content: space-between; font-family: var(--mono); font-size: 11px; color: var(--muted); margin-bottom: 12px; }
.vm-scale .vm-mid { color: var(--accent); font-weight: 700; }
.vm-verdict { font-size: 14px; line-height: 1.55; color: var(--text); margin: 0 0 16px; }
.vm-rows { display: flex; flex-direction: column; gap: 2px; }
.vm-row { display: flex; align-items: center; gap: 12px; padding: 10px 0; border-top: 1px solid var(--line); }
.vm-row.vm-ref { opacity: 0.72; }
.vm-tag { flex: 0 0 66px; font-family: var(--mono); font-size: 9.5px; letter-spacing: 0.08em; text-transform: uppercase; font-weight: 700; text-align: center; padding: 4px 0; border-radius: var(--r-sm); background: var(--surface-2); color: var(--muted); }
.vm-dcf { background: var(--accent-soft); color: var(--accent); }
.vm-relative { background: var(--surface-2); color: var(--muted); }
.vm-body { flex: 1; min-width: 0; }
.vm-lab { font-size: 13.5px; font-weight: 600; color: var(--text); }
.vm-refnote { font-family: var(--mono); font-size: 10px; font-weight: 400; color: var(--faint); text-transform: none; letter-spacing: 0; }
.vm-why { font-size: 12px; line-height: 1.5; color: var(--muted); margin-top: 2px; }
.vm-val { flex: 0 0 auto; font-family: var(--mono); font-size: 15px; font-weight: 700; font-variant-numeric: tabular-nums; color: var(--text); }
.vm-disc { color: var(--faint); font-size: 11.5px; line-height: 1.5; margin: 16px 0 0; }
@media (max-width: 640px) {
  .vm-tag { flex-basis: 54px; font-size: 9px; }
  .vm-val { font-size: 14px; }
}

/* ---------- education tooltips ---------- */
.tip-ico {
  display: inline-flex; align-items: center; justify-content: center;
  width: 15px; height: 15px; padding: 0; margin-left: 4px; vertical-align: middle;
  border-radius: var(--r-full); border: 1px solid var(--line); background: transparent;
  color: var(--muted); font-family: var(--mono); font-size: 10px; font-weight: 700;
  line-height: 1; cursor: help; transition: color 0.15s, border-color 0.15s, background 0.15s;
}
.tip-ico:hover, .tip-ico:focus-visible { color: var(--accent); border-color: var(--accent); outline: none; }
.tip-pop {
  position: absolute; z-index: 1000; display: none; max-width: 300px;
  background: var(--surface); border: 1px solid var(--line); border-radius: var(--r-sm);
  padding: 12px 14px; box-shadow: 0 8px 30px rgba(0,0,0,0.16);
}
.tip-pop.show { display: block; }
.tip-term { font-family: var(--mono); font-size: 11px; letter-spacing: 0.06em; text-transform: uppercase; color: var(--accent); font-weight: 700; margin-bottom: 5px; }
.tip-def { font-size: 12.5px; line-height: 1.55; color: var(--text); }

/* ---------- plain-English narrative ---------- */
.nar-grid { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 16px; }
.nar-card { border-left: 2px solid var(--accent); padding: 2px 0 2px 16px; }
.nar-q { font-family: var(--mono); font-size: 11px; letter-spacing: 0.1em; text-transform: uppercase; color: var(--accent); font-weight: 700; margin-bottom: 7px; }
.nar-a { font-size: 14.5px; line-height: 1.62; color: var(--text); margin: 0; }
.nar-badge {
  font-family: var(--mono); font-size: 9.5px; letter-spacing: 0.08em; text-transform: uppercase;
  font-weight: 700; padding: 2px 8px; border-radius: var(--r-full); background: var(--accent-soft);
  color: var(--accent); vertical-align: middle; margin-left: 6px;
}
.nar-disc { color: var(--faint); font-size: 11.5px; line-height: 1.5; margin: 18px 0 0; }
@media (max-width: 640px) {
  .nar-grid { grid-template-columns: minmax(0, 1fr); gap: 18px; }
  .nar-a { font-size: 14px; }
}

/* ---------- health checks (free, snowflake radar) ---------- */
.chk-top { display: flex; gap: 26px; align-items: center; flex-wrap: wrap; margin-bottom: 8px; }
.chk-radar { flex: 0 0 264px; }
.rdr { width: 264px; max-width: 100%; height: auto; overflow: visible; }
.rdr-ring { fill: none; stroke: var(--line); stroke-width: 1; }
.rdr-axis { stroke: var(--line); stroke-width: 1; }
.rdr-fill { fill: var(--accent); fill-opacity: 0.22; stroke: var(--accent); stroke-width: 2; stroke-linejoin: round; }
.rdr-dot { fill: var(--accent); }
.rdr-lab { fill: var(--muted); font-family: var(--mono); font-size: 10px; letter-spacing: 0.06em; text-transform: uppercase; }
.rdr-val { fill: var(--text); font-family: var(--mono); font-size: 12px; font-weight: 700; }
.chk-summary { flex: 1; min-width: 200px; }
.chk-bignum { font-family: var(--mono); font-size: 46px; font-weight: 700; line-height: 1; color: var(--accent); font-variant-numeric: tabular-nums; }
.chk-bignum span { font-size: 24px; color: var(--muted); }
.chk-bignum-lab { font-family: var(--mono); font-size: 10px; letter-spacing: 0.14em; text-transform: uppercase; color: var(--faint); margin-top: 4px; }
.chk-intro { color: var(--muted); font-size: 12.5px; line-height: 1.55; margin: 14px 0 0; }
.chk-cats { display: grid; grid-template-columns: repeat(auto-fit, minmax(280px, 1fr)); gap: 18px; margin-top: 22px; }
/* FIVE CARDS MUST NOT SIT IN FOUR COLUMNS, which is the one count that strands a whole row.
   The health check renders five categories — Valuation, Growth, Financial health, Dividend
   (which emits even when there is no dividend, carrying "Pays no dividend") and Past
   performance. `auto-fit` with a 280px floor resolves to FOUR tracks between about 1080 and
   1700, and five cards in four columns is a full row followed by one card beside three empty
   cells. Measured at 1440: row one held four, row two held Past performance alone with 990px
   of nothing to its right — reported as the checks section having an "empty left half".

   Four is the only bad count here. Five leaves nothing empty, three leaves one cell, two
   leaves one cell, one leaves none — so this skips the four-column tier rather than pinning
   a width. Scoped with `:has()` to a grid that holds EXACTLY five cards, so a company that
   renders four categories keeps the four-across row that suits it. */
@media (min-width: 1080px) and (max-width: 1699px) {
  .chk-cats:has(> .chk-cat:nth-child(5):last-child) {
    grid-template-columns: repeat(3, minmax(0, 1fr));
  }
}
.chk-cat { border: 1px solid var(--line); border-radius: var(--r); padding: 4px 0 0; overflow: hidden; }
.chk-cat-head { display: flex; align-items: baseline; justify-content: space-between; gap: 10px; padding: 12px 16px 8px; }
.chk-cat-lab { font-family: var(--mono); font-size: 11px; letter-spacing: 0.12em; text-transform: uppercase; color: var(--text); font-weight: 700; }
.chk-cat-tally { font-family: var(--mono); font-size: 11px; color: var(--muted); }
.chk-row { display: flex; gap: 11px; padding: 11px 16px; border-top: 1px solid var(--line); }
.chk-ico {
  flex: 0 0 20px; height: 20px; border-radius: var(--r-full); display: inline-flex; align-items: center;
  justify-content: center; font-size: 12px; font-weight: 700; margin-top: 1px;
}
/* [F5] Dark glyph on the light systemGreen/systemRed discs (white measured 3.41:1); the
   accent disc keeps --bg, which is already the correct contrast for it in both themes. */
.chk-pass .chk-ico { background: var(--up); color: #111; }
.chk-watch .chk-ico { background: var(--accent); color: var(--bg); }
.chk-fail .chk-ico { background: var(--down); color: #111; }
.chk-na .chk-ico { color: var(--bg); }
.chk-na .chk-ico { background: var(--faint); }
.chk-body { flex: 1; min-width: 0; }
/* THE CLAIM AND THE EVIDENCE MUST NOT READ AS ONE SENTENCE. Measured on the live page:
   `.chk-lab` and `.chk-actual` resolved to the SAME family and the SAME colour — Geist,
   rgb(17,17,17) in light and rgb(255,255,255) in dark — separated only by 600 vs 700 and
   13.5px vs 12px. In running text that is no separation at all, and the markup joins them
   with a single space. What the reader got was:

       "Trades below our DCF fair value 57% above fair value"
       "Cash covers (most of) the debt net debt"

   The first of those reads as a contradiction of itself. This is a reading-correctness
   defect, not a styling one, which is why the fix is structural rather than another colour.

   The mono was removed from `.chk-actual` deliberately — it is a figure-plus-unit caption
   and the unit was pretending to be data (see the sans group later in this file). That
   decision stands. The value moves to the end of the row instead, which is exactly what
   `.chk-cat-head` already does with its label and tally one component up. */
.chk-lab {
  display: flex; flex-wrap: wrap; align-items: baseline; gap: 4px 12px;
  justify-content: space-between;
  font-size: 13.5px; color: var(--text); font-weight: 600; line-height: 1.4;
}
.chk-actual {
  font-size: 12px; font-weight: 600; color: var(--muted);
  font-variant-numeric: tabular-nums; min-width: 0;
}
.chk-meta { font-family: var(--mono); font-size: 10.5px; color: var(--faint); margin: 3px 0 2px; }
.chk-why { color: var(--muted); font-size: 12px; line-height: 1.5; }
.chk-disc { color: var(--faint); font-size: 11.5px; line-height: 1.5; margin: 20px 0 0; }
@media (max-width: 640px) {
  .chk-top { gap: 14px; justify-content: center; }
  .chk-radar { flex-basis: 240px; }
  .chk-cats { grid-template-columns: minmax(0, 1fr); }
  .chk-bignum { font-size: 38px; }
}

/* ---------- watchlist ☆/★ button ---------- */
.watch-btn {
  margin-left: auto; align-self: flex-start; margin-top: 6px;
  display: inline-flex; align-items: center; gap: 7px;
  font-family: var(--mono); font-size: 12px; letter-spacing: 0.04em;
  padding: 8px 14px; border-radius: var(--r-full); cursor: pointer;
  background: var(--surface); border: 1px solid var(--line); color: var(--muted);
  transition: border-color 0.2s, color 0.2s, background 0.2s, transform 0.15s;
}
.watch-btn:hover { color: var(--accent); border-color: var(--accent); transform: translateY(-1px); }
.watch-btn .star { font-size: 15px; line-height: 1; }
.watch-btn.on { color: var(--accent); border-color: color-mix(in srgb, var(--accent) 45%, var(--line)); background: var(--accent-soft); }
.share-btn {
  align-self: flex-start; margin-top: 6px;
  display: inline-flex; align-items: center; gap: 6px;
  font-family: var(--mono); font-size: 12px; letter-spacing: 0.04em;
  padding: 8px 14px; border-radius: var(--r-full); cursor: pointer;
  background: var(--surface); border: 1px solid var(--line); color: var(--muted);
  transition: border-color 0.2s, color 0.2s, transform 0.15s;
}
.share-btn:hover { color: var(--accent); border-color: var(--accent); transform: translateY(-1px); }

/* ---------- dashboard ---------- */
#dashboard { padding-bottom: 80px; }
.dash-head { display: flex; align-items: flex-start; justify-content: space-between; gap: 16px; flex-wrap: wrap; margin: 4px 0 26px; }
.dash-head h2 { font-family: var(--serif); font-weight: 400; font-size: clamp(1.7rem, 1.1rem + 2.4vw, 2.6rem); letter-spacing: -0.02em; margin: 0; display: flex; align-items: center; gap: 12px; }
.dash-sub { font-family: var(--mono); font-size: 12px; color: var(--muted); margin: 6px 0 0; }
.dash-section { margin: 30px 0; }
.brief-slot h3 { display: flex; align-items: center; gap: 10px; }
.brief-date { font-family: var(--mono); font-size: 10px; font-weight: 400; letter-spacing: 0.06em; color: var(--faint); text-transform: none; }
.brief-summary { color: var(--text); font-size: 14px; margin: 0 0 18px; line-height: 1.5; }
.brief-cols { display: grid; grid-template-columns: repeat(auto-fit, minmax(220px, 1fr)); gap: 22px; }
.brief-sec { min-width: 0; }
.brief-stitle { font-family: var(--mono); font-size: 10px; letter-spacing: 0.14em; text-transform: uppercase; color: var(--faint); margin-bottom: 8px; }
.brief-row { display: flex; align-items: baseline; gap: 8px; padding: 6px 0; border-top: 1px solid var(--line-soft); cursor: pointer; }
.brief-row:hover .brief-tk { color: var(--accent); }
.brief-tk { font-family: var(--mono); font-weight: 700; font-size: 12.5px; letter-spacing: 0.04em; flex: 0 0 auto; }
.brief-nm { color: var(--muted); font-size: 11.5px; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; flex: 1 1 auto; }
.brief-v { font-family: var(--mono); font-size: 12.5px; font-variant-numeric: tabular-nums; flex: 0 0 auto; }
/* The per-row caveat the server sends with a valuation kind — why a gap is a limit of the
   model rather than a verdict on the company. The morning EMAIL has rendered it since
   4.22.0; the board dropped it, so the same member read "MU -87.3% vs our mid-cycle value"
   in their inbox and "-87.3% above model" on the dashboard. It sits under its row, quiet:
   this is the qualifier on a number, not a second number. */
.brief-note { color: var(--faint); font-size: 11px; line-height: 1.45; padding: 0 0 8px; margin: -2px 0 0; }
.empty-state {
  font-family: var(--mono); font-size: 13px; color: var(--muted); line-height: 1.6;
  background: var(--surface-2); border: 1px solid var(--line); border-radius: var(--r-sm);
  padding: 22px 20px; text-align: center;
}
.empty-state b { color: var(--accent); }
.wl-remove {
  position: absolute; top: 12px; right: 12px; z-index: 2;
  width: 24px; height: 24px; border-radius: var(--r-full); cursor: pointer;
  display: inline-flex; align-items: center; justify-content: center;
  background: var(--surface-2); border: 1px solid var(--line); color: var(--muted);
  font-size: 12px; line-height: 1; transition: color 0.2s, border-color 0.2s, background 0.2s;
}
.wl-remove:hover { color: var(--down); border-color: var(--down); }

@media (prefers-reduced-motion: reduce) {
  html { scroll-behavior: auto; }
  *, *::before, *::after {
    animation-duration: 0.001ms !important;
    animation-iteration-count: 1 !important;
    transition-duration: 0.001ms !important;
  }
  .spin { animation: sp 0.8s linear infinite !important; } /* keep loader legible */
}

/* ---------------- light analysis (non-DCF fallback) ---------------- */
.lv-panel { }
.lv-framing b { color: var(--accent); }
.lv-sub { margin: -6px 0 14px; font-size: 12.5px; line-height: 1.5; }
.lv-foot { margin: 12px 0 0; font-size: 11.5px; line-height: 1.5; }
.lv-grid {
  display: grid; grid-template-columns: repeat(auto-fit, minmax(120px, 1fr));
  gap: 14px 18px;
}
.lv-kv .lab {
  font-family: var(--mono); font-size: 9.5px; letter-spacing: 0.14em;
  text-transform: uppercase; color: var(--faint);
}
.lv-kv .val {
  font-family: var(--mono); font-size: 17px; font-variant-numeric: tabular-nums;
  color: var(--text); margin-top: 3px;
}
/* 52-week range bar */
.lv-range { margin-top: 20px; }
.lv-range-lab {
  display: flex; justify-content: space-between; font-family: var(--mono);
  font-size: 10px; letter-spacing: 0.08em; text-transform: uppercase;
  color: var(--faint); margin-bottom: 7px;
}
.lv-range-lab span:nth-child(2) { color: var(--accent); }
.lv-range-track {
  position: relative; height: 6px; border-radius: var(--r-full);
  background: var(--surface-2); border: 1px solid var(--line-soft);
}
.lv-range-fill {
  position: absolute; left: 0; top: 0; bottom: 0; border-radius: var(--r-full);
  background: color-mix(in srgb, var(--accent) 45%, transparent);
}
.lv-range-dot {
  position: absolute; top: 50%; width: 12px; height: 12px; border-radius: 50%;
  background: var(--accent); border: 2px solid var(--surface);
  transform: translate(-50%, -50%);
}
/* multiples table */
.lv-tablewrap { overflow-x: auto; }
.lv-table { width: 100%; border-collapse: collapse; font-size: 13.5px; }
.lv-table th {
  font-family: var(--mono); font-size: 10px; letter-spacing: 0.12em;
  text-transform: uppercase; color: var(--faint); font-weight: 600;
  text-align: right; padding: 6px 10px; border-bottom: 1px solid var(--line);
}
.lv-table th:first-child { text-align: left; }
.lv-table td {
  padding: 9px 10px; text-align: right; font-variant-numeric: tabular-nums;
  border-bottom: 1px solid var(--line-soft); font-family: var(--mono);
}
.lv-table td:first-child { text-align: left; font-family: inherit; color: var(--muted); }
.lv-table tr:last-child td { border-bottom: none; }
.lv-table .mono { color: var(--muted); }
.lv-table .flag-cheap { color: var(--up); }
.lv-table .flag-rich { color: var(--down); }
.lv-flag {
  font-family: var(--mono); font-size: 10px; letter-spacing: 0.06em;
  padding: 2px 8px; border-radius: var(--r-full); text-transform: uppercase;
}
.lv-flag.flag-cheap { color: var(--up); background: var(--up-soft); }
.lv-flag.flag-rich { color: var(--down); background: var(--down-soft); }
.lv-flag.flag-inline { color: var(--muted); background: var(--surface-2); }
/* trend chip */
.lv-trend {
  font-family: var(--mono); font-size: 10.5px; letter-spacing: 0.04em;
  padding: 3px 9px; border-radius: var(--r-full); margin-left: 8px; vertical-align: middle;
}
.lv-trend.trend-up { color: var(--up); background: var(--up-soft); }
.lv-trend.trend-down { color: var(--down); background: var(--down-soft); }
.lv-trend.trend-mixed { color: var(--muted); background: var(--surface-2); }
/* completeness chips */
.lv-chips { display: flex; flex-wrap: wrap; align-items: center; gap: 7px; margin-top: 12px; }
.lv-chip-lab {
  font-family: var(--mono); font-size: 10px; letter-spacing: 0.12em;
  text-transform: uppercase; color: var(--faint); margin-right: 4px;
}
.lv-chip {
  font-family: var(--mono); font-size: 11px; padding: 3px 10px; border-radius: var(--r-full);
  border: 1px solid var(--line);
}
.lv-chip.have { color: var(--up); border-color: color-mix(in srgb, var(--up) 35%, var(--line)); }
.lv-chip.miss { color: var(--muted); background: var(--surface-2); }
.lv-cautions { margin: 14px 0 0; padding-left: 18px; font-size: 12.5px; color: var(--muted); line-height: 1.55; }
.lv-cautions li { margin-bottom: 6px; }

/* ---------------- personalized "For You" ---------------- */
.fy-note { font-size: 12.5px; margin: 0 0 14px; line-height: 1.5; }
.fy-card .fy-why {
  font-family: var(--mono); font-size: 10.5px; letter-spacing: 0.02em;
  color: var(--accent); background: var(--accent-soft); border-radius: var(--r-full);
  padding: 3px 9px; display: inline-block; margin: 2px 0 12px;
}

/* ============================================================================
   BEGINNER MOBILE SHELL (v1.65) — answer-bearing sections, sticky mini-header,
   bottom tab bar, bottom-sheet glossary.
   ========================================================================= */

/* ---------- answer-bearing accordion sections ---------- */
.sect {
  background: var(--surface); border: 1px solid var(--line);
  border-radius: var(--r); margin: 10px 0; overflow: hidden;
}
.sect[open] { border-color: color-mix(in srgb, var(--accent) 28%, var(--line)); }
.sect-sum {
  cursor: pointer; list-style: none; display: flex; align-items: center; gap: 12px;
  padding: var(--sect-pad-y) var(--sect-pad-x); min-height: var(--sect-min-h); user-select: none;
  transition: background 0.15s;
}
.sect-sum::-webkit-details-marker { display: none; }
/* fill removed — see the Phase 2 state matrix at the end of this file. */
.sect-sum:focus-visible { outline: 2px solid var(--accent); outline-offset: -2px; }
.sect-ico { flex: 0 0 auto; font-size: 18px; line-height: 1; }
.sect-txt { flex: 1; min-width: 0; display: flex; flex-direction: column; gap: 2px; }
.sect-title { font-size: 16px; font-weight: 600; color: var(--text); line-height: 1.3; }
/* The collapsed row must carry the ANSWER, not just a title — hidden content gets
   less attention than visible content, so this line does the work on its own. */
.sect-ans {
  font-size: 13px; line-height: 1.4; color: var(--muted);
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
.sect-tag {
  flex: 0 0 auto; font-family: var(--mono); font-size: 9px; font-weight: 700;
  letter-spacing: 0.12em; padding: 3px 8px; border-radius: var(--r-full);
  background: var(--accent-soft); color: var(--accent);
}
.sect-chev { flex: 0 0 auto; font-size: 12px; color: var(--faint); transition: transform 0.22s var(--ease, ease); }
.sect[open] .sect-chev { transform: rotate(180deg); }
.sect-inner { padding: 0 var(--sect-pad-x) var(--sect-pad-y); }
.sect-inner > .panel { margin: 0 0 10px; padding: 16px; }
.sect-inner > .panel:last-child { margin-bottom: 0; }
.sect-inner > .cols { margin-bottom: 10px; }

/* ---------- sticky mini-header ---------- */
.mini-head {
  position: fixed; top: 0; left: 0; right: 0; z-index: 90;
  display: flex; align-items: center; gap: 10px;
  /* Material comes from the Phase B bar group (search "FROSTED GLASS"), same tier as
     header.top — two bars in the same app must not be two materials. */
  padding: 10px 14px; border-bottom: 1px solid var(--line);
  transform: translateY(-100%); transition: transform 0.25s var(--ease, ease);
}
.mini-head.show { transform: translateY(0); }
.mini-back { background: none; border: none; cursor: pointer; color: var(--muted); font-size: 17px; padding: 4px 6px; line-height: 1; }
.mini-back:hover { color: var(--accent); }
.mini-tk { font-family: var(--mono); font-weight: 700; font-size: 14px; letter-spacing: 0.04em; flex: 0 0 auto; }
.mini-nm { flex: 1; min-width: 0; font-size: 13px; color: var(--muted); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.mini-score { flex: 0 0 auto; display: inline-flex; align-items: center; gap: 7px; }
.mini-num { font-family: var(--mono); font-size: 16px; font-weight: 700; font-variant-numeric: tabular-nums; }
.mini-grade { width: 22px; height: 22px; border-radius: var(--r-sm);
  border: 1px solid currentColor; background: var(--surface);
  font-family: var(--mono); font-weight: 700; font-size: 11px;
  display: grid; place-items: center; }

/* ---------- (bottom tab bar removed — Apple Stocks has none; nav lives in the header ••• menu) ---------- */
@media (max-width: 780px) {
  /* a little breathing room at the foot of scroll views */
  main, #home, #view, #dashboard, #screener, #calendar, #portfolio { padding-bottom: 32px; }
}

/* ---------- glossary as a bottom sheet (phones) ---------- */
.tip-sheet {
  position: fixed; inset: 0; z-index: 1200; display: flex; align-items: flex-end;
  background: rgba(0, 0, 0, 0.34); opacity: 0; pointer-events: none;
  transition: opacity 0.2s var(--ease, ease);
}
.tip-sheet.show { opacity: 1; pointer-events: auto; }
.tip-sheet-card {
  width: 100%; background: var(--surface); border-top-left-radius: 20px;
  border-top-right-radius: 20px; padding: 12px 22px calc(26px + env(safe-area-inset-bottom, 0));
  transform: translateY(14px); transition: transform 0.24s var(--ease, ease);
  box-shadow: 0 -8px 40px rgba(0, 0, 0, 0.22);
}
.tip-sheet.show .tip-sheet-card { transform: translateY(0); }
.tip-sheet-grip { width: 38px; height: 4px; border-radius: var(--r-full); background: var(--line); margin: 0 auto 16px; }
.tip-sheet .tip-term { font-family: var(--mono); font-size: 11px; letter-spacing: 0.1em; text-transform: uppercase; color: var(--accent); font-weight: 700; margin-bottom: 8px; }
.tip-sheet .tip-def { font-size: 15px; line-height: 1.65; color: var(--text); }
.tip-sheet-close {
  width: 100%; margin-top: 20px; padding: 13px; border-radius: var(--r-full); cursor: pointer;
  background: var(--surface-2); border: 1px solid var(--line); color: var(--text);
  font-family: var(--mono); font-size: 13px; letter-spacing: 0.05em;
}
.tip-sheet-close:hover { border-color: var(--accent); color: var(--accent); }

@media (max-width: 640px) {
  .sect-sum { padding: 14px 15px; gap: 12px; }
  .sect-title { font-size: 15px; }
  .sect-ans { font-size: 12.5px; }
  .sect-inner { padding: 0 10px 12px; }
  .sect-inner > .panel { padding-left: 14px; padding-right: 14px; }
}

/* ---------- score badge on discovery tiles ---------- */
.tile-score {
  display: inline-flex; align-items: center; gap: 5px; flex: 0 0 auto;
  font-family: var(--mono); font-size: 11px; font-weight: 700;
  padding: 3px 9px; border-radius: var(--r-full); border: 1px solid var(--line);
  font-variant-numeric: tabular-nums;
}
.tile-score .tg { font-size: 10px; opacity: 0.85; }

/* one-click "how is this calculated?" — the trust link, on every score card */
.sc-methlink {
  display: inline-block; margin-top: 14px; font-family: var(--mono); font-size: 12px;
  color: var(--accent); text-decoration: none; border-bottom: 1px solid transparent;
}
.sc-methlink:hover { border-bottom-color: var(--accent); }

/* ---------- /methodology page ---------- */
.meth h2 { font-family: var(--serif); font-weight: 400; font-size: 1.5rem; margin: 34px 0 10px; }
.meth p { line-height: 1.7; margin: 0 0 14px; }
.meth-tbl { width: 100%; border-collapse: collapse; margin: 12px 0 20px; font-size: 14.5px; }
.meth-tbl th { text-align: left; font-family: var(--mono); font-size: 10.5px; letter-spacing: 0.1em; text-transform: uppercase; color: var(--faint); padding: 8px 10px; border-bottom: 1px solid var(--line); }
.meth-tbl td { padding: 11px 10px; border-bottom: 1px solid var(--line-soft, var(--line)); vertical-align: top; line-height: 1.55; }
.meth-tbl td.w { font-family: var(--mono); font-weight: 700; white-space: nowrap; color: var(--accent); }
.meth-not { line-height: 1.7; padding-left: 20px; }
.meth-not li { margin-bottom: 8px; }

/* Jump list. The page is long by design — a sceptic should be able to land straight on
   "what this is bad at" without scrolling past the parts that flatter us. */
.meth-toc { display: flex; flex-wrap: wrap; gap: 6px 8px; margin: 4px 0 26px; }
.meth-toc a {
  font-family: var(--mono); font-size: 10.5px; letter-spacing: 0.06em; text-transform: uppercase;
  color: var(--muted); text-decoration: none; padding: 6px 10px;
  border: 1px solid var(--line); border-radius: var(--r-full); transition: color .18s, border-color .18s;
}
.meth-toc a:hover, .meth-toc a:focus-visible { color: var(--accent); border-color: var(--accent); }
.meth h2[id] { scroll-margin-top: 18px; }

.meth-steps { counter-reset: mstep; list-style: none; padding: 0; margin: 12px 0 20px; max-width: 68ch; }
.meth-steps li {
  counter-increment: mstep; position: relative; padding-left: 38px; margin-bottom: 14px;
  line-height: 1.7;
}
.meth-steps li::before {
  content: counter(mstep); position: absolute; left: 0; top: 1px;
  font-family: var(--mono); font-size: 11px; font-weight: 700; color: var(--accent);
  width: 24px; height: 24px; display: grid; place-items: center;
  border: 1px solid var(--line); border-radius: 50%;
}

.meth-tiers, .meth-bad { list-style: none; padding: 0; margin: 12px 0 20px; max-width: 68ch; }
.meth-tiers li, .meth-bad li {
  line-height: 1.65; margin-bottom: 12px; padding-left: 14px;
  border-left: 2px solid var(--line);
}
.meth-bad li { padding-bottom: 2px; }
.meth-bad li b { color: var(--text); }

.meth-rules { list-style: none; counter-reset: mrule; padding: 0; margin: 12px 0 20px; max-width: 68ch; }
.meth-rules li { counter-increment: mrule; margin-bottom: 14px; line-height: 1.65; }
.meth-rules li b { display: block; margin-bottom: 3px; }
.meth-rules li span { display: block; color: var(--muted); }

.meth-plan { line-height: 1.7; padding-left: 20px; max-width: 68ch; margin: 12px 0 20px; }

/* ── /changelog [audit 4.8] ────────────────────────────────────────────────────────────
   Reads as a RECORD, not a feed: the date is the structural anchor, entries hang off it,
   and a hairline rail down the left makes the chronology the shape of the page. Deliberately
   plain — an accountability page that looks marketed undermines what it is for. */
.seo-main.cl-main { max-width: 800px; }
.cl-day { margin: 0 0 34px; }
.cl-day > h2 {
  font-family: var(--mono); font-size: 12px; letter-spacing: 0.14em; text-transform: uppercase;
  color: var(--faint); font-weight: 600; margin: 0 0 14px; padding-bottom: 9px;
  border-bottom: 1px solid var(--accent-soft);
}
.cl-list { list-style: none; margin: 0; padding: 0 0 0 18px; border-left: 1px solid var(--accent-soft); }
.cl-item { position: relative; margin: 0 0 22px; }
.cl-item::before {
  content: ""; position: absolute; left: -22px; top: 8px; width: 7px; height: 7px;
  border-radius: 50%; background: var(--accent-soft); box-shadow: 0 0 0 3px var(--bg);
}
.cl-head { display: flex; align-items: baseline; gap: 10px; flex-wrap: wrap; margin-bottom: 5px; }
.cl-head h3 { font-size: 16.5px; line-height: 1.35; margin: 0; font-weight: 650; }
.cl-item p { color: var(--muted); font-size: 14.5px; line-height: 1.68; margin: 0; max-width: 70ch; }
.cl-kind {
  flex: 0 0 auto; font-family: var(--mono); font-size: 9.5px; letter-spacing: 0.12em;
  text-transform: uppercase; padding: 3px 7px; border-radius: var(--r-sm); font-weight: 600;
  background: var(--accent-soft); color: var(--muted);
}
/* A model revision can move a number a reader acted on — it earns the accent. Data and site
   entries are real but not that. */
.cl-kind.cl-model { background: color-mix(in srgb, var(--accent) 16%, transparent); color: var(--accent); }
.cl-foot {
  color: var(--faint); font-size: 13px; line-height: 1.65; max-width: 70ch;
  margin-top: 30px; padding-top: 18px; border-top: 1px solid var(--accent-soft);
}
.meth-plan li { margin-bottom: 9px; }

/* The no-backtest admission. Given weight on purpose — it is the single most load-bearing
   sentence on the page, and burying it would be the dishonest choice. */
.meth-callout {
  border: 1px solid var(--accent); border-radius: var(--r); padding: 14px 16px;
  margin: 14px 0 18px; line-height: 1.65; max-width: 68ch;
  background: color-mix(in srgb, var(--accent) 7%, transparent);
}

@media (max-width: 640px) {
  .meth-tbl { font-size: 13.5px; }
  .meth-tbl th, .meth-tbl td { padding: 8px 6px; }
  .meth-steps li { padding-left: 32px; }
  .meth-steps li::before { width: 21px; height: 21px; font-size: 10px; }
}

/* ---------- /pricing + /about ----------
   Both are server-rendered trust pages and both reuse the /methodology surface language
   (`.meth` sets the type scale, the rules and the tier lists) — a buyer who arrives from the
   methodology page should not feel handed to a different site. Only the things methodology
   has no equivalent for are defined here: the plan pair, the guarantees, the FAQ. */
.pr-plans { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 14px; margin: 8px 0 16px; }
.pr-plan {
  position: relative; border: 1px solid var(--line); border-radius: var(--r);
  padding: 20px 18px 18px; background: var(--surface);
}
/* The annual plan is the better deal, so it gets the emphasis rather than a "recommended"
   sticker doing the work. */
.pr-best { border-color: var(--accent); background: var(--surface-2); }
.pr-tag {
  position: absolute; top: -9px; left: 18px; padding: 2px 9px; border-radius: var(--r-full);
  background: var(--accent); color: var(--bg);
  font-family: var(--mono); font-size: 9.5px; letter-spacing: 0.1em; text-transform: uppercase;
}
.pr-nm { font-family: var(--mono); font-size: 11px; letter-spacing: 0.1em; text-transform: uppercase; color: var(--faint); }
.pr-amt { font-size: clamp(30px, 6vw, 40px); line-height: 1.1; margin: 6px 0 4px; letter-spacing: -0.02em; }
.pr-amt small { font-size: 14px; color: var(--muted); letter-spacing: 0; margin-left: 3px; }
.pr-sub { font-size: 13px; color: var(--muted); line-height: 1.5; }
.pr-saving { max-width: 68ch; }
/* THE STUDENT OFFER, between the saving arithmetic and the CTA. Deliberately built from the
   same parts as .pr-trust below it — a left rule, muted body, no fill — because it is a
   guarantee, not a promotion. A tinted card here would read as a banner ad on a page whose
   whole argument is that it does not do that, and the offer is more believable stated flatly.
   Each span is its own line so the promise sentence is never run together with the how. */
.pr-student {
  max-width: 68ch; margin: 18px 0 4px; padding-left: 14px;
  border-left: 2px solid var(--accent); line-height: 1.65; color: var(--muted);
}
.pr-student b { color: var(--text); display: block; margin-bottom: 4px; }
.pr-student span { display: block; margin-top: 4px; }
.pr-cta {
  display: inline-flex; align-items: center; justify-content: center; min-height: 46px;
  padding: 0 22px; border-radius: var(--r-full);
  background: var(--text); color: var(--bg); font-weight: 600; font-size: 14.5px;
  text-decoration: none;
}
.pr-cta:hover { text-decoration: none; filter: brightness(1.07); }
.pr-cta-sub { font-size: 13px; color: var(--muted); max-width: 62ch; margin: 10px 0 0; line-height: 1.6; }

/* The guarantees. A list, not a badge row: each one is a sentence you could hold us to. */
.pr-trust { list-style: none; padding: 0; margin: 20px 0 4px; max-width: 68ch; }
.pr-trust li {
  line-height: 1.65; margin-bottom: 12px; padding-left: 14px;
  border-left: 2px solid var(--accent); color: var(--muted);
}
.pr-trust li b { color: var(--text); }
.pr-list li b { color: var(--text); }
.pr-free li { color: var(--muted); }
.pr-inline { color: var(--accent); }

.pr-faq { margin: 12px 0 20px; max-width: 68ch; }
.pr-faq dt { font-weight: 600; margin-top: 16px; }
.pr-faq dd { margin: 5px 0 0; color: var(--muted); line-height: 1.65; }

.ab-two li b, .ab-links li b { color: var(--text); }
.ab-mail { font-size: clamp(17px, 3vw, 21px); margin-bottom: 8px; }
.ab-mail a { color: var(--accent); }
.ab-note { font-size: 13px; color: var(--faint); max-width: 62ch; }

@media (max-width: 560px) {
  .pr-plans { grid-template-columns: minmax(0, 1fr); }
  .pr-cta { width: 100%; }
}

/* ---------- /analytics (owner-only dashboard) ----------
   Same surface language as the rest of the server-rendered pages. Every bar is a div sized by
   a percentage — no chart library, no CDN, nothing the CSP would have to be widened for. */
.an h2 { font-family: var(--serif); font-weight: 400; font-size: 1.35rem; margin: 34px 0 10px; }
.an h3 { font-family: var(--mono); font-size: 10.5px; letter-spacing: 0.09em; text-transform: uppercase; color: var(--faint); margin: 0 0 10px; font-weight: 500; }
.an-bar { display: flex; flex-wrap: wrap; gap: 10px 16px; align-items: center; justify-content: space-between; margin-bottom: 20px; }
.an-ranges { display: flex; gap: 6px; }
.an-range {
  font-family: var(--mono); font-size: 11px; letter-spacing: 0.05em; text-transform: uppercase;
  color: var(--muted); background: none; border: 1px solid var(--line); border-radius: var(--r-full);
  padding: 7px 13px; cursor: pointer; transition: color .18s, border-color .18s, background .18s;
}
.an-range:hover { color: var(--text); border-color: var(--muted); }
.an-range.is-on { color: var(--bg); background: var(--text); border-color: var(--text); }
.an-meta { font-family: var(--mono); font-size: 11px; color: var(--faint); }

/* per-cell hairline — see the mosaic note on .metrics */
.an-kpis { display: grid; grid-template-columns: repeat(auto-fit, minmax(150px, 1fr)); gap: 1px; background: var(--bg); border: 1px solid var(--line); border-radius: var(--r); overflow: hidden; }
.an-kpi { background: var(--bg); padding: 15px 16px; box-shadow: 0 0 0 1px var(--line); }
.an-kpi-l { font-family: var(--mono); font-size: 10px; letter-spacing: 0.08em; text-transform: uppercase; color: var(--faint); }
.an-kpi-v { font-family: var(--mono); font-size: 24px; font-weight: 700; margin-top: 5px; font-variant-numeric: tabular-nums; }

/* one horizontal bar row: label · track · value */
.an-row { display: grid; grid-template-columns: minmax(0, 1fr) 40% auto; gap: 10px; align-items: center; padding: 6px 0; }
.an-k { font-size: 13px; color: var(--text); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.an-track { display: block; height: 7px; border-radius: var(--r-sm); background: var(--surface-2, var(--line)); overflow: hidden; }
.an-track > span { display: block; height: 100%; background: var(--accent); border-radius: var(--r-sm); }
.an-track-lg { height: 11px; margin: 7px 0 5px; }
.an-v { font-family: var(--mono); font-size: 12.5px; color: var(--muted); font-variant-numeric: tabular-nums; text-align: right; }
.an-empty { font-size: var(--t-small); color: var(--muted); line-height: 1.6; padding: 10px 0; }

/* the per-day column chart */
.an-chart { border: 1px solid var(--line); border-radius: var(--r); padding: 14px 14px 10px; }
.an-cols-chart { display: flex; align-items: flex-end; gap: 2px; height: 132px; }
/* `max-width` matters more than it looks: on a "today" range there is ONE column, and without
   a cap a single day fills the panel edge to edge and reads as a solid block rather than as a
   bar. Capped, one day is a bar and thirty days still fill the width. */
.an-cbar { flex: 1 1 0; min-width: 2px; max-width: 46px; height: 100%; display: flex; align-items: flex-end; }
.an-cbar > span { display: block; width: 100%; background: var(--accent); border-radius: var(--r-full) var(--r-full) 0 0; opacity: 0.85; }
.an-cbar:hover > span { opacity: 1; }
.an-xaxis { display: flex; justify-content: space-between; font-family: var(--mono); font-size: 10px; color: var(--faint); margin-top: 7px; }

.an-funnel { display: grid; gap: 14px; }
.an-step-h { display: flex; align-items: baseline; justify-content: space-between; gap: 12px; }
.an-step-n { font-family: var(--mono); font-size: 17px; font-weight: 700; font-variant-numeric: tabular-nums; }
.an-step-w { font-size: 12.5px; color: var(--muted); line-height: 1.5; }
.an-fr { font-family: var(--mono); font-size: 11px; color: var(--accent); white-space: nowrap; }
/* A rate over a tiny denominator is noise wearing a percent sign, so it is styled as the
   absence of a number rather than as a quieter number. */
.an-fr-thin { color: var(--faint); }
.an-note { font-size: 12.5px; color: var(--muted); line-height: 1.6; max-width: 68ch; margin: 0 0 14px; }
.an-foot { font-family: var(--mono); font-size: 11px; color: var(--faint); line-height: 1.7; margin-top: 26px; }

.an-cols { display: grid; grid-template-columns: repeat(auto-fit, minmax(260px, 1fr)); gap: 26px; }
.an-col { min-width: 0; }

@media (max-width: 640px) {
  .an-cols { grid-template-columns: minmax(0, 1fr); }      /* single column on a phone */
  .an-row { grid-template-columns: minmax(0, 1fr) 32% auto; }
  .an-kpi-v { font-size: 21px; }
  .an-bar { flex-direction: column; align-items: flex-start; }
}

/* ---------- learning path (/learn) ---------- */
.lpath { margin-bottom: 44px; }
.lpath-intro { line-height: 1.7; margin: 0 0 20px; }
.lpath-list { list-style: none; padding: 0; margin: 0; counter-reset: lp; }
.lpath-step { display: flex; gap: 16px; padding: 18px 0; border-top: 1px solid var(--line); }
.lpath-n {
  flex: 0 0 auto; width: 28px; height: 28px; border-radius: 50%;
  display: grid; place-items: center; background: var(--accent-soft); color: var(--accent);
  font-family: var(--mono); font-size: 13px; font-weight: 700;
}
.lpath-body { flex: 1; min-width: 0; }
.lpath-body h3 { margin: 2px 0 6px; font-size: 1.08rem; font-weight: 600; }
.lpath-body p { margin: 0 0 11px; line-height: 1.65; color: var(--muted); }
.lpath-chips { display: flex; flex-wrap: wrap; gap: 7px; }
.lpath-chip {
  font-family: var(--mono); font-size: 11.5px; padding: 4px 11px; border-radius: var(--r-full);
  border: 1px solid var(--line); color: var(--muted); text-decoration: none;
  transition: color 0.15s, border-color 0.15s;
}
.lpath-chip:hover { color: var(--accent); border-color: var(--accent); }

/* ============================================================================
   VISUAL + MOTION SYSTEM (v1.67) — the polish layer.
   Rules: animate transform/opacity/stroke/colour only; never layout. Nothing
   here runs before first paint. Everything degrades under reduced-motion.
   ========================================================================= */

/* ---------- the score hero: the product's signature moment ---------- */
.score-panel {
  position: relative; overflow: hidden;
  /* See the note on the later .score-panel rule: --el-hero is for detached overlays and
     this is an in-page card. Flat, with the hairline doing the edge. */
  box-shadow: var(--rim-strong);
  border-color: color-mix(in srgb, var(--accent) 22%, var(--line));
  padding: var(--s-6) var(--s-5);
}
/* a soft warm bloom behind the ring — depth without a heavy gradient */
.score-panel::before {
  content: ""; position: absolute; inset: -40% 40% 40% -30%;
  background: radial-gradient(closest-side, var(--glow), transparent 70%);
  pointer-events: none;
}
.score-head { position: relative; display: flex; align-items: center; gap: var(--s-6); flex-wrap: wrap; }

.score-ring { position: relative; width: 132px; height: 132px; flex: 0 0 auto; }
.score-ring svg { width: 100%; height: 100%; transform: rotate(-90deg); }
.ring-track { fill: none; stroke: var(--line); stroke-width: 8; }
.ring-arc {
  fill: none; stroke-width: 8; stroke-linecap: round;
  transition: stroke-dashoffset var(--dur-5) var(--ease-out);
}
.ring-mid {
  position: absolute; inset: 0; display: grid; place-content: center; text-align: center;
}
.ring-mid .score-num {
  font-family: var(--mono); font-size: 42px; font-weight: 700; line-height: 1;
  font-variant-numeric: tabular-nums; letter-spacing: var(--track-tight);
}
.ring-mid .score-num-band { font-size: 25px; }
.ring-mid .score-outof {
  font-family: var(--mono); font-size: var(--t-eyebrow); color: var(--faint);
  margin-top: 5px; letter-spacing: 0.08em;
}
.score-ring .score-grade {
  position: absolute; bottom: 2px; right: 2px; width: 34px; height: 34px;
  /* THE HERO GRADE, on the one-shape rule (see `.score-grade` above). Square at the control
     radius, hairline in the band colour, band colour on the glyph — the same badge the
     watchlist, the dashboard and the calendar draw, at the size this position earns.
     The 3px ring stays but becomes an OUTLINE: its job was never decoration, it is a
     KNOCKOUT that punches the badge out of the score ring it overlaps, and an outline does
     that without spending the badge's own border, which the band colour now needs.
     `color` is set inline by setMiniScore/renderScore and would win regardless; it is left
     out here rather than declared as something it isn't. */
  border-radius: var(--r-sm); font-family: var(--mono); font-weight: 700;
  font-size: 15px; display: grid; place-items: center;
  border: 1px solid currentColor; background: var(--surface);
  outline: 3px solid var(--surface); box-shadow: none;
  /* The most-read glyph in the product used to enter like a notification bubble in a
     game: scale(0.4) on --ease-spring over --dur-4. Measured, that was a badge starting
     at ~14px of its 34px, overshooting to 1.03x before settling, and not legible until
     ~810ms — a celebration gesture attached to a piece of financial data, arriving after
     the ring it belongs to.

     0.96 is the band every other entrance in this file uses (modalIn is the exemplar at
     0.98): close enough that the badge reads as ARRIVING rather than inflating. With a
     0.96 start there is nothing for an overshoot to do except wobble a letterform, so the
     curve is the house entrance ease. --dur-3 puts it fully legible at 260 + 320 = 580ms,
     comfortably inside the 900ms ring sweep it is meant to land with.

     The 260ms delay and `both` are untouched and load-bearing: the delay holds the badge
     until the ring has started drawing, and `both` is what stops it flashing at full size
     before the delay elapses. Reduced motion is handled by the MOTION OFF block, which
     names this selector — the badge appears flat, final and full size, no rule needed here. */
  animation: gradePop var(--dur-3) var(--ease-out) both;
  animation-delay: 260ms;
}
@keyframes gradePop { from { transform: scale(0.96); opacity: 0; } to { transform: scale(1); opacity: 1; } }

/* supporting lines cascade in behind the ring sweep */
.sc-reveal { animation: scReveal var(--dur-4) var(--ease-out) both; animation-delay: calc(90ms + var(--i, 0) * 70ms); }
@keyframes scReveal { from { opacity: 0; transform: translateY(10px); } to { opacity: 1; transform: none; } }

/* Hero intrinsic-value readout — the FREE fair-value teaser beside the ring. Tabular,
   monochrome; sits right of the score info on desktop, stacks below on mobile. */
.score-fv {
  flex: 0 0 auto; min-width: 176px; max-width: 240px; align-self: stretch;
  display: flex; flex-direction: column; justify-content: center; gap: 2px;
  padding-left: 22px; border-left: 1px solid var(--line);
  font-variant-numeric: tabular-nums; animation: scReveal var(--dur-4) var(--ease-out) both;
  animation-delay: 300ms;
}
.score-fv:empty { display: none; }
.fv-lab { font-family: var(--mono); font-size: 10.5px; letter-spacing: 0.08em; text-transform: uppercase; color: var(--muted); margin-bottom: 4px; }
.fv-val { font-family: var(--sans); font-size: 27px; font-weight: 700; letter-spacing: -0.01em; color: var(--text); line-height: 1.05; }
.fv-val.fv-range { font-size: 18px; letter-spacing: 0; }
.fv-sub { display: flex; justify-content: space-between; gap: 10px; font-size: 13px; color: var(--muted); margin-top: 5px; }
.fv-sub .fv-v { color: var(--text); font-weight: 600; }
/* The price beside the fair value is the same delayed number the chart header shows, and it
   says so — an honest "delayed" beats a figure that merely looks live. */
.fv-delayed { font-size: 9.5px; letter-spacing: 0.07em; text-transform: uppercase; color: var(--faint); margin-top: 2px; }
.fv-gap { font-size: 13.5px; font-weight: 600; color: var(--text); margin-top: 6px; }
.fv-gap-pct { color: var(--muted); font-weight: 500; }
.fv-caveat { font-size: 11px; color: var(--faint); margin-top: 6px; line-height: 1.4; }
@media (max-width: 640px) {
  .score-fv {
    flex: 1 1 100%; min-width: 0; max-width: none; align-self: auto;
    padding-left: 0; padding-top: 16px; margin-top: 6px;
    border-left: none; border-top: 1px solid var(--line);
  }
}
@media (prefers-reduced-motion: reduce) { .score-fv { animation: none; } }

/* Consistent, honest lock state — a frosted card with a lock glyph + the single unlock line +
   the upgrade CTA. Shown wherever content is gated (never fakes numbers). Native, monochrome. */
/* ---------- macro overlay + catalyst calendar (Pro) ----------
   Direction is stated in WORDS, never colour: a green "tailwind" pill would read as a signal,
   and these are classifications of the environment, not calls. Each factor shows the rule that
   produced it so a reader can disagree with the rule rather than trust a black box. */
ul.mc-macro, ul.mc-cal { list-style: none; margin: 0; padding: 0; }
ul.mc-macro li {
  padding: 12px 0; border-bottom: 1px solid var(--line-soft);
  display: grid; grid-template-columns: 84px minmax(0, 1fr); gap: 4px 12px; align-items: baseline;
}
ul.mc-macro li:last-child { border-bottom: none; }
ul.mc-macro .mm-dir {
  grid-row: span 3; font-size: 9.5px; letter-spacing: 0.09em; text-transform: uppercase;
  color: var(--muted); border: 1px solid var(--line); border-radius: var(--r-full);
  padding: 3px 0; text-align: center; align-self: start;
}
ul.mc-macro li[data-dir="tailwind"] .mm-dir,
ul.mc-macro li[data-dir="headwind"] .mm-dir { color: var(--text); border-color: var(--text); }
ul.mc-macro b { font-size: 13.5px; color: var(--text); font-weight: 600; }
ul.mc-macro span:not(.mm-dir):not(.mm-rule) {
  font-size: 12.5px; line-height: 1.55; color: var(--muted);
}
ul.mc-macro .mm-rule { font-size: 10.5px; color: var(--faint); }

.mcal-h {
  font-size: 10px; letter-spacing: 0.08em; text-transform: uppercase;
  color: var(--faint); margin: 12px 0 6px;
}
ul.mc-cal li {
  padding: 9px 0; border-bottom: 1px solid var(--line-soft);
  display: grid; grid-template-columns: 128px minmax(0, 1fr); gap: 2px 12px; align-items: baseline;
}
ul.mc-cal li:last-child { border-bottom: none; }
ul.mc-cal .mcal-d { font-size: 11px; color: var(--muted); grid-row: span 2; align-self: start; }
ul.mc-cal b { font-size: 13px; color: var(--text); font-weight: 600; }
ul.mc-cal b a { color: inherit; }
ul.mc-cal span:not(.mcal-d) { font-size: 12px; color: var(--muted); }
.mc-note { font-size: 11.5px; line-height: 1.5; color: var(--faint); margin-top: 10px; }

@media (max-width: 560px) {
  ul.mc-macro li, ul.mc-cal li { grid-template-columns: minmax(0, 1fr); }
  ul.mc-macro .mm-dir, ul.mc-cal .mcal-d { grid-row: auto; justify-self: start; padding: 3px 10px; }
}

/* ---------- delayed-quote badge + tick ----------
   The badge is small but never hidden: free market data is delayed, and the page says so
   beside the price rather than letting a stale number pass for a live one. The tick is a
   brief neutral flash — no green/red, since direction is already in the change figure. */
.quote-badge {
  font-size: 9.5px; letter-spacing: 0.08em; text-transform: uppercase;
  color: var(--faint); margin-top: 2px;
}
.quote-badge:empty { display: none; }
.chart-price.q-tick { animation: qtick 0.6s var(--ease-out, ease); }
@keyframes qtick {
  0% { background: color-mix(in srgb, var(--text) 14%, transparent); }
  100% { background: transparent; }
}
@media (prefers-reduced-motion: reduce) { .chart-price.q-tick { animation: none; } }

/* ---------- MOBILE LAUNCH PASS ----------
   Influencer traffic lands on a phone, so the phone is the primary target, not an adaptation.
   Two classes of defect found by measuring a 375px viewport rather than eyeballing it:
   wide tables blowing past the viewport, and conversion controls below the 44px tap minimum. */
@media (max-width: 560px) {
  /* Any wide table scrolls INSIDE its own container instead of stretching the page. The
     insider table measured 553px in a 375px viewport, which pushed 84 elements off-screen. */
  .ins-table, table.heat, table.mc-scen, .lens-table, .peer-table, .fund-table {
    display: block; width: 100%; overflow-x: auto; -webkit-overflow-scrolling: touch;
  }
  .ins-table > tbody, .ins-table > thead,
  table.heat > tbody, table.heat > thead { display: table; width: max-content; min-width: 100%; }

  /* 44px is the floor for anything a thumb has to hit. These are the CONVERSION controls —
     a mis-tap on the upgrade button is a lost sale, not a cosmetic issue. */
  .lock-card-cta, .lock-btn, .promo-cta,
  .mini-upgrade, .dl-cta, .signin-btn, .auth-submit, .upgrade {
    min-height: 44px; padding-top: 11px; padding-bottom: 11px;
  }
  .seg-opt { min-height: 40px; padding-left: 12px; padding-right: 12px; }
  .back, .share-btn { min-height: 40px; display: inline-flex; align-items: center; }
  .tip-ico { min-width: 26px; min-height: 26px; }      /* was 15px — unhittable */

  /* The header search is the primary action on a phone; 18px tall is not usable. */
  .search input, .hero .big-search input { min-height: 44px; }

  /* Lock cards stack rather than squeezing the CTA to a sliver. */
  .lock-card { flex-direction: column; align-items: stretch; text-align: left; }
  .lock-card-cta { width: 100%; }
  .promo-banner { flex-direction: column; align-items: stretch; }
  .promo-banner .promo-cta { width: 100%; }
  .promo-banner .promo-n { align-self: flex-start; }

  /* Long words (tickers, big numbers) must never force a horizontal scroll. */
  .memo-call .mc-verdict, .mc-comps p, ul.mc-risks span { overflow-wrap: anywhere; }
}

/* ---------- plan chooser ----------
   Stripe Checkout is one price per session, so the choice happens here. Annual is listed first
   and marked best value because it IS the better deal, but monthly sits right beneath it — a
   visitor who wants to pay monthly shouldn't have to guess the option exists. Monochrome: the
   offer is distinguished by a border and a count, not by colour. */
.plan-modal { max-width: 420px; }
.plan-head {
  font-family: var(--serif, var(--sans)); font-size: 21px; color: var(--text);
  margin: 0 0 16px;
}
.plan-opt {
  display: grid; grid-template-columns: minmax(0, 1fr) auto; gap: 2px 12px; width: 100%;
  text-align: left; cursor: pointer; background: var(--surface-2);
  border: 1px solid var(--line); border-radius: var(--r-sm);
  padding: 14px 16px; margin-bottom: 10px; min-height: 44px;
  transition: border-color 0.15s, transform 0.15s var(--ease-out, ease);
}
.plan-opt:hover { border-color: var(--text); }
.plan-opt:active { transform: scale(0.99); }
.plan-nm { font-size: 14px; font-weight: 600; color: var(--text); }
.plan-nm em {
  font-style: normal; font-size: 9.5px; letter-spacing: 0.08em; text-transform: uppercase;
  border: 1px solid var(--line-strong); border-radius: var(--r-full); padding: 2px 7px;
  margin-left: 8px; color: var(--muted); vertical-align: middle;
}
.plan-amt {
  grid-row: span 2; align-self: center; font-size: 20px; font-weight: 700;
  color: var(--text); white-space: nowrap;
}
.plan-amt small { font-size: 11px; font-weight: 400; color: var(--muted); }
.plan-sub { font-size: 12px; color: var(--muted); }
.plan-best { border-color: var(--text); }
.plan-offer { border-color: var(--text); background: var(--surface-2); }
.plan-foot { font-size: 11px; color: var(--faint); text-align: center; margin-top: 14px; }

@media (max-width: 560px) {
  .plan-modal { max-width: none; }
  .plan-opt { padding: 16px; }
}

/* ---------- landing: honesty wedge + why-pay ----------
   Mobile-first: single column by default, two only when there is room. Monochrome — the Pro
   column is distinguished by a border, not a colour, so nothing reads as a hype badge. */
.hero-wedge {
  list-style: none; margin: 18px 0 0; padding: 0;
  display: grid; gap: 10px;
}
.hero-wedge li {
  font-size: 14px; line-height: 1.55; color: var(--muted);
  padding-left: 16px; position: relative;
}
.hero-wedge li::before {
  content: ""; position: absolute; left: 0; top: 9px;
  width: 6px; height: 6px; border-radius: 50%; background: var(--text); opacity: 0.55;
}
.hero-wedge b { color: var(--text); font-weight: 600; }

.why-pay { margin: var(--space-section) 0; }
.wp-h {
  font-family: var(--serif, var(--sans)); font-weight: 400;
  font-size: clamp(21px, 3.4vw, 30px); line-height: 1.25; letter-spacing: -0.01em;
  color: var(--text); margin: 6px 0 22px;
}
.wp-h em { font-style: normal; font-weight: 500; }
.wp-grid { display: grid; grid-template-columns: minmax(0, 1fr); gap: 14px; }
.wp-col {
  border: 1px solid var(--line); border-radius: var(--r-sm);
  background: var(--surface); padding: 18px 20px;
}
.wp-pro { border-color: var(--text); }
.wp-t {
  font-size: 10.5px; letter-spacing: 0.1em; text-transform: uppercase;
  color: var(--faint); margin-bottom: 12px;
}
.wp-col ul { list-style: none; margin: 0; padding: 0; display: grid; gap: 9px; }
.wp-col li {
  font-size: 13.5px; line-height: 1.5; color: var(--muted);
  padding-left: 16px; position: relative;
}
.wp-col li::before {
  content: ""; position: absolute; left: 0; top: 8px;
  width: 5px; height: 5px; border-radius: 50%; background: var(--text); opacity: 0.45;
}
.wp-col li b { color: var(--text); font-weight: 600; }
.wp-cta {
  margin-top: 16px; width: 100%; min-height: 44px;
  background: var(--text); color: var(--bg); border: none; border-radius: var(--r-full);
  font-size: 14px; font-weight: 600; cursor: pointer;
}
.wp-cta:active { transform: scale(0.99); }
/* The crawlable twin of the CTA above: same block, quieter, for the reader who wants the
   price before they want an account. */
.wp-pricing {
  display: block; margin-top: 10px; text-align: center;
  font-family: var(--mono); font-size: 12px; color: var(--muted); text-decoration: none;
}
.wp-pricing:hover { color: var(--text); text-decoration: underline; }
.wp-trust {
  display: flex; flex-wrap: wrap; gap: 8px; justify-content: center;
  margin-top: 18px; font-size: 12px; color: var(--muted); text-align: center;
}
.wp-trust b { color: var(--text); font-weight: 600; }
.wp-legal {
  font-size: 11px; line-height: 1.5; color: var(--faint);
  text-align: center; margin-top: 10px;
}
/* `align-items: start` and not the grid default. Free lists four things and Pro lists five
   plus a button, so stretching left ~190px of empty card under the Free column at 1440 —
   and the two lists are not row-aligned anyway, so equal heights bought nothing to pay for
   it with. Miraaj flagged the class directly: "white spaces in the desktop". */
@media (min-width: 720px) {
  .wp-grid { grid-template-columns: repeat(2, minmax(0, 1fr)); align-items: start; }
}

/* ---------- portfolio allocation note + analytics lock (Pro) ----------
   THE BARS ARE GONE. "A pie chart instead of the bar charts we have to show their
   diversity" — the ring's key already carries every label and percentage the rows did, so
   keeping both was showing one answer twice. `.pf-alloc`, `.pf-al-row`, `.pf-al-tk`,
   `.pf-al-bar` and `.pf-al-w` went with the markup rather than being left behind: this
   stylesheet has been burned three times by inert CSS that reads as live, and a bar-chart
   rule sitting under a heading that says "allocation" is exactly the kind that gets wired
   back up by mistake. `.pf-al-note` stays — the note is still printed under the ring, and it
   is the sentence that states the denominator. */
.pf-al-note { font-size: var(--t-micro); line-height: 1.5; color: var(--faint); margin-top: 6px; }

.pf-card.pf-locked .pf-val { color: var(--muted); }
.pf-lockv {
  font-size: var(--t-micro); letter-spacing: 0.08em; text-transform: uppercase;
  border: 1px solid var(--line-strong); border-radius: var(--r-full); padding: 2px 9px;
  color: var(--muted);
}
.pf-analytics-lock {
  display: flex; align-items: center; gap: 12px; flex-wrap: wrap;
  border: 1px solid var(--line); border-radius: var(--r-sm); background: var(--surface-2);
  padding: 14px 16px; margin-top: 16px; font-size: var(--t-small); color: var(--muted);
}
.pf-analytics-lock .lock-btn { margin-left: auto; min-height: 44px; }
@media (max-width: 560px) {
  .pf-analytics-lock { flex-direction: column; align-items: stretch; }
  .pf-analytics-lock .lock-btn { margin-left: 0; width: 100%; }
}

/* A paying member must never see an acquisition surface — not the newcomer pitch, not the
   "first 10 lock $2/month" banner, not the FREE-vs-PRO upsell. Offering someone a cheaper plan
   than the one they already bought reads as broken. Applied by Access.applyPlanVisibility() on
   every auth/access change, since at boot we don't yet know the plan. */
.hidden-for-pro { display: none !important; }

/* ---------- launch-offer banner ----------
   Scarcity stated as a fact, not a countdown gimmick: the number comes from Stripe's own
   redemption count, and the banner disappears when the coupon is genuinely exhausted. */
.promo-banner {
  display: flex; align-items: center; gap: 12px; flex-wrap: wrap;
  padding: 11px 14px; margin: 0 0 16px;
  /* the CARD step. It has its own ground and holds a sentence, so it is a card, not a
     control — and it sat beside 20px panels at 6px on the landing and the watchlist. */
  border: 1px solid var(--line-strong); border-radius: var(--r);
  background: var(--surface-2);
}
/* The reserved slot, set before first paint by the cached hint in index.html. Values are the
   banner's MEASURED heights plus its own 16px bottom margin — 153/88/54 at ≤560/≤1023/≥1024
   on production — so a filled slot and an empty one displace the hero identically.
   `flow-root` is load-bearing, not tidiness: without it the banner's bottom margin collapses
   out of the wrapper, the filled state becomes min-height + 16px, and the reservation would
   introduce its own 16px shift instead of removing one. */
html.promo-slot #promo-wrap { display: flow-root; min-height: 70px; }
@media (max-width: 1023px) { html.promo-slot #promo-wrap { min-height: 104px; } }
@media (max-width: 560px)  { html.promo-slot #promo-wrap { min-height: 169px; } }
.promo-banner .promo-n {
  font-size: 10.5px; letter-spacing: 0.08em; text-transform: uppercase;
  color: var(--bg); background: var(--text); padding: 3px 8px; border-radius: var(--r-full);
}
/* `min-width` rather than a 240px flex-basis. The basis is measured along the flex axis, and
   the ≤560px rule turns this banner into a COLUMN — where a 240px basis stops being "wrap the
   copy below 240px wide" and becomes "reserve 240px of HEIGHT", giving a 354px card carrying
   57px of text and a 183px void above the button. `min-width` means the same thing in the
   desktop row (wrap once the copy can't have 240px) and nothing at all in the column. */
.promo-banner .promo-copy { font-size: 13px; color: var(--text); flex: 1 1 auto; min-width: 240px; }
.promo-banner .promo-cta {
  border: 1px solid var(--text); background: none; color: var(--text);
  border-radius: var(--r-full); padding: 7px 13px; font-size: 12.5px; cursor: pointer;
}
.promo-banner .promo-cta:hover { background: var(--text); color: var(--bg); }

/* ---------- Investment-committee memo (Pro) ----------
   Memo styling, not dashboard styling: a stated call, the conviction behind it, the figures
   that support it, then prose. Monochrome throughout — a verdict rendered in green would be a
   recommendation, and this is research for someone else's decision. */
.memo-call:empty, .memo-detail:empty { display: none; }
.memo-call .mc-head {
  font-size: var(--t-eyebrow); letter-spacing: var(--track-eyebrow); text-transform: uppercase;
  color: var(--faint); margin-bottom: 10px;
}
.memo-call .mc-verdict {
  font-family: var(--serif, var(--sans)); font-size: clamp(19px, 2.2vw, 25px);
  line-height: 1.28; color: var(--text); letter-spacing: -0.01em;
}
.memo-call .mc-novalue {
  font-size: 13px; line-height: 1.55; color: var(--muted); margin-top: 10px;
  border-left: 2px solid var(--line-strong); padding-left: 12px;
}
.memo-call .mc-conv {
  display: flex; align-items: center; gap: 10px; flex-wrap: wrap; margin: 14px 0 4px;
}
.memo-call .mc-meter { display: inline-flex; gap: 3px; }
.memo-call .mc-pip {
  width: 16px; height: 4px; border-radius: var(--r-sm); background: var(--line-strong); display: block;
}
.memo-call .mc-pip.on { background: var(--text); }
.memo-call .mc-lvl {
  font-size: 10.5px; letter-spacing: 0.09em; text-transform: uppercase; color: var(--text);
}
.memo-call .mc-convwhy { font-size: 12px; color: var(--muted); flex: 1 1 240px; }

.memo-call .mc-figs {
  display: flex; gap: 26px; flex-wrap: wrap;
  margin-top: 16px; padding-top: 14px; border-top: 1px solid var(--line-soft);
}
.memo-call .mc-fig { display: flex; flex-direction: column; gap: 2px; min-width: 0; }
.memo-call .mc-k {
  font-size: 10px; letter-spacing: 0.08em; text-transform: uppercase; color: var(--faint);
}
.memo-call .mc-v { font-size: 19px; font-weight: 700; color: var(--text); }
.memo-call .mc-sub { font-size: 11px; color: var(--muted); }

.memo-call .mc-thesis { margin-top: 18px; }
.mc-sect {
  font-size: 10.5px; letter-spacing: 0.1em; text-transform: uppercase;
  color: var(--faint); margin: 0 0 10px;
}
.memo-call .mc-thesis p {
  font-size: 14.5px; line-height: 1.6; color: var(--text); margin: 0 0 9px;
}
.mc-compliance {
  font-size: 11px; line-height: 1.5; color: var(--faint);
  margin-top: 16px; padding-top: 12px; border-top: 1px solid var(--line-soft);
}

.memo-detail .mc-sect { margin-top: 20px; }
.memo-detail .mc-sect:first-of-type { margin-top: 8px; }
table.mc-scen { width: 100%; border-collapse: collapse; font-size: 13px; }
table.mc-scen th {
  text-align: left; font-family: var(--mono); font-size: 10px; letter-spacing: 0.08em;
  text-transform: uppercase; color: var(--faint); font-weight: 400;
  padding: 0 10px 8px 0; border-bottom: 1px solid var(--line-soft);
}
table.mc-scen td { padding: 10px 10px 10px 0; border-bottom: 1px solid var(--line-soft);
  vertical-align: top; color: var(--muted); }
table.mc-scen tr:last-child td { border-bottom: none; }
table.mc-scen .mc-case { color: var(--text); font-weight: 600; white-space: nowrap; }
table.mc-scen .mc-val { color: var(--text); font-weight: 700; white-space: nowrap; }
table.mc-scen .mc-w { color: var(--text); white-space: nowrap; }
table.mc-scen .mc-as { line-height: 1.5; }

ul.mc-risks { list-style: none; margin: 0; padding: 0; }
ul.mc-risks li {
  padding: 11px 0; border-bottom: 1px solid var(--line-soft);
  display: flex; flex-direction: column; gap: 4px;
}
ul.mc-risks li:last-child { border-bottom: none; }
ul.mc-risks b { font-size: 13.5px; color: var(--text); font-weight: 600; }
ul.mc-risks span { font-size: 12.5px; line-height: 1.55; color: var(--muted); }

.mc-comps p { font-size: 14px; line-height: 1.55; color: var(--text); margin: 0 0 6px; }
.mc-comps .mc-compsub { font-size: 12px; color: var(--muted); }

/* (the memo's bespoke .mc-lock was retired in the polish pass — the locked memo
   now renders the shared lockCard(), so there is one lock design, not five) */

@media (max-width: 620px) {
  .memo-call .mc-figs { gap: 18px; }
  table.mc-scen .mc-as { display: none; }   /* the assumption prose is in the risks list too */
}

/* ---------- Horizon strip (Pro) — the four-horizon outlook beside the Score ----------
   Compact and monochrome. No red/green anywhere: tinting a forward range green would read
   as a recommendation, which is exactly what this is not. The daily/weekly cells carry an
   em-dash where a price would go, because we deliberately publish no point estimate there. */
.horizon-panel:empty { display: none; }
.horizon-panel .hz-head {
  display: flex; justify-content: space-between; align-items: baseline; gap: 12px;
  font-size: var(--t-eyebrow); letter-spacing: var(--track-eyebrow); text-transform: uppercase;
  color: var(--faint); margin-bottom: 12px;
}
.horizon-panel .hz-vol { text-transform: none; letter-spacing: 0.04em; }

/* per-cell hairline — see the mosaic note on .metrics; four fixed tracks, so a horizon
   with fewer than four points must not paint the missing ones as a slab */
.horizon-panel .hz-grid {
  display: grid; grid-template-columns: repeat(4, minmax(0, 1fr)); gap: 1px;
  background: var(--surface); border: 1px solid var(--line-soft);
  border-radius: var(--r-sm); overflow: hidden;
}
.horizon-panel .hz-cell {
  background: var(--surface); padding: 12px 10px; text-align: center; min-width: 0;
  box-shadow: 0 0 0 1px var(--line-soft);
}
.horizon-panel .hz-when {
  font-size: 10px; letter-spacing: 0.07em; text-transform: uppercase; color: var(--muted);
}
.horizon-panel .hz-point {
  font-size: 17px; font-weight: 700; color: var(--text); margin: 6px 0 4px;
  overflow: hidden; text-overflow: ellipsis;
}
.horizon-panel .hz-nopoint { color: var(--faint); font-weight: 400; }
.horizon-panel .hz-range { font-size: 11px; color: var(--muted); }
.horizon-panel .hz-conf {
  font-size: 9.5px; letter-spacing: 0.09em; text-transform: uppercase;
  color: var(--faint); margin-top: 6px;
}
.horizon-panel .hz-cell[data-conf="high"] .hz-conf { color: var(--text); }
.horizon-panel .hz-cell[data-conf="medium"] .hz-conf { color: var(--muted); }

.horizon-panel .hz-framing {
  font-size: 11.5px; line-height: 1.5; color: var(--muted);
  border-left: 2px solid var(--line-strong); padding: 8px 0 8px 12px; margin-top: 12px;
}
.horizon-panel .hz-more { margin-top: 10px; }
.horizon-panel .hz-more > summary {
  cursor: pointer; list-style: none; font-size: 10.5px; letter-spacing: 0.08em;
  text-transform: uppercase; color: var(--muted);
}
.horizon-panel .hz-more > summary::-webkit-details-marker { display: none; }
.horizon-panel .hz-more > summary::after { content: " ▾"; }
.horizon-panel .hz-more[open] > summary::after { content: " ▴"; }
.horizon-panel .hz-more ul { margin: 10px 0 0; padding-left: 18px; }
.horizon-panel .hz-more li {
  font-size: 12px; line-height: 1.55; color: var(--muted); margin-bottom: 8px;
}

/* (likewise .hz-lock — the horizon strip's locked state is a shared lockCard()) */
.horizon-panel .hz-absent { font-size: 13px; line-height: 1.55; color: var(--muted); }

@media (max-width: 620px) {
  .horizon-panel .hz-grid { grid-template-columns: repeat(2, minmax(0, 1fr)); }
}

/* ---------- Price Targets (Pro) — the forward estimate ----------
   Monochrome and memo-like on purpose. The band is a neutral rule, NOT a red/green
   gauge: colour-coding a forward estimate would read as a recommendation, which is
   exactly what this is not. The framing line is styled to be read, not skimmed past. */
.targets-panel .tgt-framing {
  font-family: var(--sans); font-size: var(--t-small); line-height: 1.5;
  color: var(--text);
  border-left: 2px solid var(--line-strong);
  padding: 10px 0 10px 14px; margin: 0 0 18px;
}
/* A target BELOW today's price is the one most likely to be misread as "they think it
   will fall", so its framing gets real weight instead of a quieter footnote. */
.targets-panel .tgt-framing-strong {
  border-left-color: var(--text);
  background: var(--surface-2);
  border-radius: 0 var(--r-sm) var(--r-sm) 0;
  padding: 12px 14px; font-size: var(--t-small);
}
.targets-panel .tgt-framing-lock { margin-top: 16px; margin-bottom: 0; font-size: var(--t-meta); }

.targets-panel .tgt-diverge {
  font-family: var(--sans); font-size: var(--t-small); line-height: 1.5; color: var(--text);
  background: var(--surface-2); border: 1px solid var(--line);
  border-radius: var(--r-sm); padding: 12px 14px; margin: 0 0 18px;
}

.targets-panel .tgt-now {
  display: flex; align-items: baseline; gap: 12px; flex-wrap: wrap;
  padding-bottom: 14px; border-bottom: 1px solid var(--line-soft); margin-bottom: 4px;
}
.targets-panel .tgt-now-k { font-size: var(--t-eyebrow); letter-spacing: 0.08em; text-transform: uppercase; color: var(--faint); }
.targets-panel .tgt-now-v { font-size: 26px; font-weight: 700; color: var(--text); }
.targets-panel .tgt-now-p { font-size: var(--t-meta); color: var(--muted); }

.targets-panel .tgt-row { padding: 16px 0; border-bottom: 1px solid var(--line); }
.targets-panel .tgt-row:last-child { border-bottom: none; }
.targets-panel .tgt-h { display: flex; align-items: baseline; justify-content: space-between; gap: 12px; }
.targets-panel .tgt-when {
  font-family: var(--mono); font-size: var(--t-micro); letter-spacing: 0.08em;
  text-transform: uppercase; color: var(--muted);
}
.targets-panel .tgt-exp { font-size: 21px; font-weight: 700; color: var(--text); }

.targets-panel .tgt-band {
  display: flex; align-items: center; gap: 10px; margin: 10px 0 8px;
  font-family: var(--mono); font-size: var(--t-meta); color: var(--muted);
}
.targets-panel .tgt-bar {
  position: relative; flex: 1 1 auto; height: 2px; border-radius: var(--r-sm);
  background: linear-gradient(90deg, var(--line-strong), var(--text), var(--line-strong));
  opacity: 0.65;
}
.targets-panel .tgt-basemark {
  position: absolute; left: 50%; top: 50%; width: 7px; height: 7px; margin: -3.5px 0 0 -3.5px;
  border-radius: 50%; background: var(--text);
}
.targets-panel .tgt-meta {
  display: flex; justify-content: space-between; gap: 12px; flex-wrap: wrap;
  font-size: var(--t-micro); color: var(--faint);
}
.targets-panel .tgt-vs { color: var(--muted); }

.targets-panel .tgt-conf {
  display: flex; align-items: baseline; gap: 10px; flex-wrap: wrap;
  margin-top: 16px; padding-top: 14px; border-top: 1px solid var(--line-soft);
  font-size: var(--t-micro); color: var(--faint);
}
.targets-panel .tgt-conf-tier { font-size: var(--t-eyebrow); letter-spacing: 0.09em; color: var(--text); font-weight: 700; }
.targets-panel .tgt-conf-low { color: var(--muted); }

.targets-panel .tgt-assump { margin-top: 14px; }
.targets-panel .tgt-assump > summary {
  cursor: pointer; font-size: var(--t-micro); letter-spacing: 0.08em; text-transform: uppercase;
  color: var(--muted); list-style: none;
}
.targets-panel .tgt-assump > summary::-webkit-details-marker { display: none; }
.targets-panel .tgt-assump > summary::after { content: " ▾"; }
.targets-panel .tgt-assump[open] > summary::after { content: " ▴"; }
.targets-panel .tgt-assump ul { margin: 12px 0 0; padding-left: 18px; }
.targets-panel .tgt-assump li {
  font-size: var(--t-meta); line-height: 1.55; color: var(--muted); margin-bottom: 8px;
}
.targets-panel .tgt-disc { font-size: var(--t-micro); line-height: 1.5; color: var(--faint); margin-top: 10px; }

.targets-panel .tgt-absent {
  border: 1px solid var(--line); border-radius: var(--r-sm);
  background: var(--surface-2); padding: 16px 18px;
}
.targets-panel .tgt-absent-why { font-size: var(--t-small); line-height: 1.55; color: var(--text); }
.targets-panel .tgt-absent-note {
  font-size: var(--t-meta); line-height: 1.5; color: var(--muted); margin-top: 10px;
}

@media (max-width: 560px) {
  .targets-panel .tgt-h { flex-wrap: wrap; }
  .targets-panel .tgt-meta { flex-direction: column; gap: 4px; }
}

.lock-card {
  display: flex; align-items: center; gap: 16px; flex-wrap: wrap;
  padding: 18px 20px; border-radius: var(--r); border: 1px solid var(--line);
  background: var(--surface-2);
  -webkit-backdrop-filter: var(--mat-blur); backdrop-filter: var(--mat-blur);
  box-shadow: var(--mat-hi);
}
.lock-card-ic {
  flex: 0 0 auto; width: 40px; height: 40px; border-radius: var(--r-sm); display: grid; place-items: center;
  color: var(--text); background: color-mix(in srgb, var(--text) 8%, transparent);
}
.lock-card-body { flex: 1 1 220px; min-width: 0; }
.lock-card-title { font-family: var(--sans); font-size: 16px; font-weight: 700; letter-spacing: -0.01em; color: var(--text); }
.lock-card-copy { font-size: 13.5px; color: var(--muted); margin-top: 3px; line-height: 1.4; }
.lock-card-sub { font-size: 12px; color: var(--faint); margin-top: 5px; line-height: 1.45; }
.lock-card-cta {
  flex: 0 0 auto; appearance: none; border: none; cursor: pointer;
  background: var(--accent); color: var(--bg); font-family: var(--mono); font-weight: 700;
  font-size: 13px; letter-spacing: 0.02em; padding: 11px 18px; border-radius: var(--r-full);
  transition: filter var(--dur-1), transform var(--dur-1);
}
.lock-card-cta:hover { filter: brightness(1.08); transform: translateY(-1px); }
@media (max-width: 560px) {
  .lock-card { gap: 12px; }
  .lock-card-cta { flex: 1 1 100%; }
}

.sc-pip { transition: background var(--dur-3) var(--ease-out); }
.sc-driver { transition: transform var(--dur-2) var(--ease-out); }
.sc-drivers { border-top-color: var(--line-soft); }

/* ---------- accordion: real eased expand/collapse ---------- */
/* grid-template-rows 0fr -> 1fr is the only way to ease to natural height without
   measuring in JS (a layout thrash per toggle) or clipping with a fixed max-height. */
.sect-anim {
  display: grid; grid-template-rows: 0fr;
  transition: grid-template-rows var(--dur-3) var(--ease-out);
}
.sect[open] .sect-anim { grid-template-rows: 1fr; }
.sect.closing .sect-anim { grid-template-rows: 0fr; }
.sect-body { overflow: hidden; min-height: 0; }

.sect {
  box-shadow: var(--el-1);
  transition: box-shadow var(--dur-3) var(--ease-out),
              border-color var(--dur-3) var(--ease-out),
              transform var(--dur-2) var(--ease-out);
}
/* `.sect:hover` is gone. It raised a shadow, and Titan has none — so after the token
   re-point it was a rule that ran and changed nothing. The CONTROL here is `.sect-sum`,
   the summary row you actually click, and that is what the state matrix lights. A
   container that highlights around a control is claiming to be one. */
.sect[open] { box-shadow: var(--el-2); }
.sect-sum { transition: background var(--dur-2) var(--ease-out); }
.sect-sum:active { background: var(--surface-2); }
.sect-ico { transition: transform var(--dur-3) var(--ease-spring); }
.sect[open] .sect-ico { transform: scale(1.12); }
.sect-chev { transition: transform var(--dur-3) var(--ease-out), color var(--dur-2); }
.sect-sum:hover .sect-chev { color: var(--accent); }
/* the answer line fades as it becomes redundant — the content itself is now visible */
.sect-ans { transition: opacity var(--dur-3) var(--ease-out); }
.sect[open] .sect-ans { opacity: 0.55; }

/* ---------- panels: soft elevation, designed press states ---------- */
.panel { box-shadow: var(--el-1); transition: box-shadow var(--dur-3) var(--ease-out); }
.tile-score { transition: transform var(--dur-2) var(--ease-spring); }

/* ---------- buttons: instant press feedback ---------- */
.dl-cta, .lock-btn, .auth-submit, .tip-sheet-close, .watch-btn, .share-btn, .back {
  transition: transform var(--dur-1) var(--ease-out), filter var(--dur-2),
              box-shadow var(--dur-2) var(--ease-out), color var(--dur-2), border-color var(--dur-2);
}
.dl-cta:active, .lock-btn:active, .auth-submit:active,
.tip-sheet-close:active, .watch-btn:active, .share-btn:active { transform: scale(0.97); }
:where(button, a, summary, input, [tabindex]):focus-visible {
  outline: none; box-shadow: var(--ring); border-radius: var(--r-sm);
}

/* A COMPOSED CONTROL TAKES THE RING ON ITS OWN EDGE, NOT ON ITS PARTS.
   ------------------------------------------------------------------------
   The rule above gives any focused `input` a ring AND `border-radius: var(--r-sm)`.
   That is right for a bare field and wrong for a search pill, where the input is one
   part of a control whose visible edge is the pill: focusing it drew a hard 6px-radius
   rectangle around the text area, INSIDE the rounded field, with the go button outside
   it. That is Miraaj's "the searchbar still has this weird inside box", on both the nav
   field and the hero field, and it is a REGRESSION — the same shape was removed once
   before, and came back when this shared ring was introduced, because the ring names
   `input` generically and cannot know which inputs are composed.

   So the ring moves to the control. `:focus-within` puts it on the pill, where the
   visible boundary actually is, and the input's own ring is suppressed. Keyboard focus
   is MORE visible after this, not less: the indicator now traces the whole control
   including the button, instead of a rectangle hidden inside it. */
/* `.hero .hero-ask` and not `.hero-ask`: a later rule, `.hero .hero-ask, .lp-cta-ask
   { box-shadow: none }`, carries the same (0,2,0) weight and would win on order alone.
   Matched at its weight rather than escalated with !important — the third time today a
   later declaration at equal specificity has silently beaten an earlier one on this
   stylesheet, so it is worth naming rather than working around. */
/* THE HALO, WITHOUT THE SECOND BORDER.
   `--ring` is two shadows: a 3px accent-20% halo AND a 1px accent-55% line. On a control
   that has no border of its own that inner line IS the ring, which is why the token carries
   it. These four all have a 1px border already, so the inner line landed directly inside it
   and read as a doubled stroke — very visible on a fully-round pill, and reported as
   "a doubled border, an inner outline inside the outer pill" on the header field.
   So the pills take the halo and move their OWN border to the accent instead of growing a
   second one. Same signal, one stroke. `--ring` is untouched for the five other places that
   use it on borderless controls. */
.search:focus-within,
.hero .hero-ask:focus-within,
.lp-cta-ask:focus-within,
.big-search:focus-within {
  border-color: color-mix(in srgb, var(--accent) 55%, transparent);
  box-shadow: 0 0 0 3px color-mix(in srgb, var(--accent) 20%, transparent);
}
.search input:focus-visible,
.hero-ask input:focus-visible,
.lp-cta-ask input:focus-visible,
.big-search input:focus-visible {
  box-shadow: none;
  border-radius: inherit;
  background: transparent;
  border: 0;
}

/* ---------- route transition: sections fade-slide in ----------
   This is the most-seen animation in the product — it plays on all eight routes
   (Router._enter), so a reader comparing three companies triggers it six times a
   minute. It used to borrow --dur-4 (520ms), the token whose own comment reads
   "the hero reveal": a once-per-session duration on a many-times-per-minute
   surface, and 1.7x the UI budget. --dur-3 is the token for "panels, sheets,
   accordions", which is exactly what a route view is. Travel is a pair with
   duration: 12px reads as a rise over 520ms, but as a jump over 320ms, so it
   drops to the 8px @keyframes fade already uses. Reduced motion is handled in
   two places on purpose — _enter() never adds the class, and MOTION OFF below
   neutralises it anyway. */
/* `backwards`, not `both`, and the difference is not cosmetic. `both` keeps the animation
   in effect in its AFTER phase, so the pane's computed transform settles at the identity
   matrix rather than the keyword `none` — and an element with a transform is the containing
   block for its `position: fixed` descendants. Measured: `.mini-head` reported
   `offsetParent: #view` and its viewport top tracked the scroll exactly (72 -> -328 ->
   -826 -> -1451), i.e. the compact header never pinned; it scrolled away with the page.

   `backwards` keeps the half that matters — the `from` keyframe applies before the
   animation's first tick, which is what stops the pane flashing at full opacity in the gap
   between the class landing and the animation starting — and drops the half that was
   creating the containing block. Nothing about the entrance changes: the `to` keyframe is
   `opacity: 1; transform: none`, which is exactly the element's natural resting state, so
   there is nothing for a forwards fill to hold. Sampled frame by frame, the entrance is
   identical either way. */
.view-enter { animation: viewEnter var(--dur-3) var(--ease-out) backwards; }
@keyframes viewEnter { from { opacity: 0; transform: translateY(8px); } to { opacity: 1; transform: none; } }

/* Direction, so navigation has a sense of place (apple-design §7: "if something
   disappears one way, we expect it to emerge from where it came", and §8: the
   in-between frames should point at the outcome). Going DEEPER — opening a company —
   arrives from the right; coming BACK to the list arrives from the left, the exact
   reverse. Lateral moves between the primary tabs keep the neutral rise above,
   because a tab switch is not a push or a pop.

   The travel is 8px, not the 16px plan 006 proposed. This is the animation every
   reader sees on every navigation, and emil's frequency table puts that tier at
   "remove or drastically reduce"; plan 009 already settled 8px as the one entrance
   distance in this file, and having forward/back travel twice as far as the neutral
   entrance would read as animation rather than as direction. The plan's own bar:
   "if a reader consciously notices it, it is too big."

   Reduced motion needs no rule of its own here: Router._enter returns before adding
   the class, and the MOTION OFF block neutralises `.view-enter` whatever its
   animation-name. Both were re-measured under an emulated reduce. */
.view-enter[data-dir="forward"] { animation-name: viewFwd; }
.view-enter[data-dir="back"]    { animation-name: viewBack; }
@keyframes viewFwd  { from { opacity: 0; transform: translateX(8px); }  to { opacity: 1; transform: none; } }
@keyframes viewBack { from { opacity: 0; transform: translateX(-8px); } to { opacity: 1; transform: none; } }


/* ---------- bottom sheet: spring in, ease out, and draggable ----------
   The grip is the universal "drag me down to dismiss" affordance, and bindSheetDrag()
   now delivers one — so the vertical axis belongs to the handler, and the grip is
   allowed to look grabbable. The reduced-motion variant is in the MOTION OFF block
   below: it kills the slide, and the JS drops the spring settle, but the drag itself
   still tracks the finger, because that is direct manipulation rather than motion the
   reader did not ask for. */
.tip-sheet-card {
  transform: translateY(100%); transition: transform var(--dur-3) var(--ease-out);
  touch-action: pan-x;
}
.tip-sheet.show .tip-sheet-card { transform: translateY(0); }
.tip-sheet { transition: opacity var(--dur-3) var(--ease-out); }
.tip-sheet-grip { cursor: grab; }
.tip-sheet-card:active .tip-sheet-grip { cursor: grabbing; }

/* ---------- mini header ---------- */
.mini-head { box-shadow: var(--el-2); transition: transform var(--dur-3) var(--ease-out); }
.mini-num, .mini-grade { transition: color var(--dur-3), background var(--dur-3); }

/* ---------- typography rhythm ---------- */
.sect-title { letter-spacing: -0.005em; }
.sc-bandlab { font-size: var(--t-title); line-height: 1.3; letter-spacing: var(--track-tight); font-weight: 600; }
.panel h3 { letter-spacing: var(--track-eyebrow); }
.sect-inner > .panel { box-shadow: none; border-color: var(--line-soft); }

/* ============ MOTION OFF ==================================================
   A real reduced-motion path, not just "make it fast": the ring, the count-up
   and the cascades are disabled outright, and every element lands in its FINAL
   state (never mid-animation, never invisible). */
@media (prefers-reduced-motion: reduce) {
  .ring-arc { transition: none !important; }
  .sc-reveal, .score-ring .score-grade, .view-enter, .fade {
    animation: none !important; opacity: 1 !important; transform: none !important;
  }
  .sect-anim { transition: none !important; }
  .sect-ico { transition: none !important; transform: none !important; }
  .sect:hover { transform: none !important; }
  .tip-sheet-card { transition: none !important; }
}

/* ---------- Pro upsell: an invitation, not a wall ---------- */
/* The hover used to LIFT the blur — 6px to 5px, 0.5 to 0.6 — as if leaning in would let
   you read a little more of what you have not paid for. With the blur gone the gesture has
   nothing to do, so it says the honest version instead: the ghost brightens slightly to
   acknowledge the pointer, and the CTA underneath is what responds. */
.locked-blur { transition: opacity var(--dur-4) var(--ease-out); }
.locked:hover .locked-blur { opacity: 0.22; }
.locked-cta {
  background: radial-gradient(closest-side,
    color-mix(in srgb, var(--surface) 92%, transparent), transparent 85%);
  border-radius: var(--r);
}
/* The lock used to bob forever: a 4.5s loop, on three to five icons at once on a locked
   company page, beside dense financial data the reader is trying to read. Two rules make
   that the wrong call, and they point the same way:

   - apple-design §14 names the exact failure mode — avoid "slow looping oscillations
     (near 0.2 Hz / one cycle per 5s)". 4.5s per cycle is 0.222 Hz, dead centre.
   - The purpose test has no entry a never-ending loop satisfies. Once the reader has seen
     the lock, the motion carries no remaining information; it is just movement next to a
     price, on the surface where they are being asked to decide something.

   So it keeps the legitimate half — drawing the eye once, as state indication — and stops.
   The -4px travel is carried over from the float on purpose: the icon settles down through
   the distance it used to bob through, so nothing about the resting layout changes.
   The reduced-motion block below still names .lock-ico and still should: this is an
   animation, just a finite one. */
.lock-ico {
  display: inline-block;
  animation: lockRise var(--dur-3) var(--ease-out) both;
}
@keyframes lockRise { from { opacity: 0; transform: translateY(-4px); } to { opacity: 1; transform: none; } }
.lock-title { letter-spacing: var(--track-tight); }
.lock-btn { box-shadow: var(--el-2); }
/* No lift on hover: --el-3 is the OVERLAY shadow (see the token block), and a lock button
   sits in the page like everything else. The hover state is carried by colour. */
.lock-btn:hover { border-color: var(--accent); }
.sect-tag { transition: background var(--dur-2), color var(--dur-2); }
.sect[open] .sect-tag { background: var(--accent); color: var(--bg); }

/* ---------- home / search: give the hero real presence ---------- */
/* (the headline's tracking is set once, further down, next to its line-height) */
.hero .big-search input {
  transition: border-color var(--dur-2) var(--ease-out), box-shadow var(--dur-3) var(--ease-out),
              transform var(--dur-2) var(--ease-out);
}
.hero .big-search input:focus {
  outline: none; border-color: var(--accent); box-shadow: var(--ring), var(--el-2);
}
.ac-dropdown { box-shadow: var(--el-3); }
.ac-row { transition: background var(--dur-1), padding-left var(--dur-2) var(--ease-out); }
.ac-row:hover, .ac-row.on { padding-left: 18px; }
/* .eyebrow's tracking is on the rule itself now — it used to be set here, 3,500 lines
   from the declaration, so every component that copied the primitive copied the 0.22em
   that never rendered. */

/* Skeletons should breathe rather than blink — a calmer wait. */
.sk, .skeleton { animation: skPulse 1.6s var(--ease-in-out) infinite; }
@keyframes skPulse { 0%, 100% { opacity: 0.55; } 50% { opacity: 0.9; } }

@media (prefers-reduced-motion: reduce) {
  .lock-ico, .sk, .skeleton { animation: none !important; }
  .ac-row:hover, .ac-row.on { padding-left: inherit; }
}

/* ============================================================================
   PREMIUM LAYER (v1.68) — research-grade polish.
   Techniques borrowed, and why:
   · No pure neutrals — every surface/border/muted tone carries the warm brand
     hue; flat grey is what makes a UI read "assembled". (Stripe/Linear/Vercel)
   · Layered shadows on the doubling rule (1→2→4→8→16 blur, low uniform alpha)
     instead of one heavy blur, plus a 1px inset rim. (Tobias Ahlin)
   · Hairline borders + lit top edge do the elevation work on light; the rim does
     it on dark, where drop shadows barely read.
   · Tabular LINING figures everywhere a number appears — the fintech gold
     standard, so digits never shift as values change.
   · 5% grain over gradients to kill banding and add a printed, tactile quality.
   · One accent, used for meaning only. Semantics tuned deep, not garish.
   ========================================================================= */

/* ---------- grain + mesh: background LAYERS, not a blended overlay ----------
   Deliberately NOT a fixed pseudo-element with mix-blend-mode. That pattern is
   everywhere, but a full-viewport blended layer forces the browser to composite
   the entire page on every scroll — measurable jank on a mid-range phone. As an
   extra background-image on an element that already paints a background, the
   grain is a cached static tile and costs nothing. */
body {
  background:
    var(--grain),
    radial-gradient(1200px 700px at 78% -10%, var(--glow), transparent 62%),
    radial-gradient(900px 600px at 8% 4%, color-mix(in srgb, var(--accent) 4%, transparent), transparent 60%),
    var(--bg);
  background-repeat: repeat, no-repeat, no-repeat, no-repeat;
  /* NOT background-attachment: fixed — it forces a full repaint on every scroll frame
     and is the classic source of mobile scroll jank (iOS Safari handles it especially
     badly). A scrolling background costs nothing and looks identical here. */
  font-optical-sizing: auto;
  text-rendering: optimizeLegibility;
}

/* ---------- numbers: tabular lining figures, globally ---------- */
/* Anything that can show a digit gets uniform-width lining figures so columns
   align and live values never jitter as they update. */
.mono, .tnum, input, table, .score-num, .sc-cval, .sc-dmeta, .td-price, .td-chg,
.td-stat .val, .vm-val, .factor-score, .chk-actual, .chk-bignum, .mini-num,
.tile-score, .meth-tbl td, .lpath-n, .sc-depth, .sh-read .v, .duel-val,
.metric b, .pill, .brief-v, .mo-cval, .ig-val, .cat-val, .sc-havechip {
  font-variant-numeric: lining-nums tabular-nums;
  font-feature-settings: "lnum" 1, "tnum" 1;   /* fallback for older engines */
}

/* A CARD'S OWN PADDING DEFINES ITS TOP AND BOTTOM SPACE. Nothing inside it gets to add
   more at the ends. `.eyebrow` carries `margin-top: 40px`, which is right when it separates
   one section from the next in a flow and dead space when it is the first thing in a padded
   box: measured on News, 62px above the content against 22px below it, on five of six
   panels. That asymmetry is Miraaj's "deep top padding, tight bottom" — it was never the
   padding, which is a uniform 22px all round.

   Seven pairs across the app: five News eyebrows at 40px top, and `.opt-panel` and
   `.related-panel` on the company page with a 12px bottom margin under their h3. */
.panel > :first-child { margin-top: 0; }
.panel > :last-child { margin-bottom: 0; }

/* ---------- surfaces: hairline + lit rim + layered elevation ---------- */
.panel {
  border-color: var(--line);
  box-shadow: var(--el-1), var(--rim);
  background: linear-gradient(180deg,
    color-mix(in srgb, var(--surface) 100%, transparent),
    color-mix(in srgb, var(--surface-2) 26%, var(--surface)));
}
.sect { box-shadow: var(--el-1), var(--rim); }
.sect[open] { box-shadow: none; }   /* see the note above: open-ness is shown by the
                                       chevron and the disclosed content, not elevation */
.notice { box-shadow: var(--rim); border-color: var(--accent-rim); }

/* ---------- typography: editorial display, restrained UI ---------- */
/* `.hero h1` is deliberately NOT in this list: it is an `h1`, so it already inherits every
   declaration here from the bare selector — listing it again only raised the specificity of a
   shared letter-spacing above the hero's own rule, which is how the headline's tracking ended
   up being decided in the display-type block instead of next to the headline. */
h1, h2, .co-head h2, .dash-head h2, .hw-pagetitle {
  font-optical-sizing: auto;
  letter-spacing: var(--track-display);   /* large type needs tightening; small type needs air */
  font-feature-settings: "kern" 1, "liga" 1;
}
/* Leading, at the same altitude as the tracking above — because the two are one
   decision and splitting them is how they drifted. Every heading below used to
   INHERIT `body { line-height: 1.5 }` (verified via CSSOM, not by reading
   declarations: no rule declared leading for `.legal h1`, `.legal h2`, `.meth h2`
   or the /stocks and /news heads at all). A 44.8px serif headline set at body
   leading is the single most "unstyled document" thing on the site.

   The two rules are split by BAND, not by tag, because `h2` spans 19px to 47px in
   this product: the serif page heads (.co-head/.dash-head) sit in the display band
   with the h1s, and the bare `h2` remainder is the 19-24px title band. Bare `h2`
   comes second so it wins the title tracking over the shared rule above at equal
   specificity, while the two class-qualified selectors keep the display value. */
h1, .co-head h2, .dash-head h2 { line-height: var(--lead-display); }
h2 { letter-spacing: var(--track-title); line-height: var(--lead-title); }
/* The hero headline's tracking, in the one place that actually WINS the cascade — it was
   declared three times (the size rule, a `var(--track-tight)` rule above, and the group
   directly overhead), which is how a property that decides whether the line fits ended up
   somewhere nobody would look when the line didn't. Tighter than the shared display value;
   the headroom arithmetic in tests/test_hero_headline.py assumes NONE of it, so this can only
   ever add clearance, never remove it. */
.hero h1 { line-height: var(--lead-hero); letter-spacing: var(--track-hero); }
.hero p.lede { max-width: 34em; letter-spacing: -0.003em; }
/* eyebrow labels: the mono uppercase micro-type that signals "research tool" */
.panel h3, .eyebrow, .sect-tag, .sc-havelab, .meth-tbl th, .lpath-chip,
.sc-conf-lab, .score-checked, .tab-lab {
  font-feature-settings: "kern" 1;
  -webkit-font-smoothing: antialiased;
}
.panel h3 {
  display: flex; align-items: center; gap: 8px;
  color: var(--faint);
}
/* a hairline rule trailing every panel title — a small editorial signal */
.panel h3::after {
  content: ""; flex: 1; height: 1px; min-width: 12px;
  background: linear-gradient(90deg, var(--line), transparent);
}

/* ---------- the score hero: the most expensive-looking thing we own ---------- */
.score-panel {
  background:
    radial-gradient(120% 90% at 12% 0%, color-mix(in srgb, var(--accent) 7%, transparent), transparent 58%),
    linear-gradient(180deg, var(--surface), color-mix(in srgb, var(--surface-2) 55%, var(--surface)));
  border-color: var(--accent-rim);
  /* THE SHADOW IS GONE, and the reason is this file's own rule rather than taste. The
     `--el-3`/`--el-hero` exception is written down at the token block: they "are only ever
     worn by DETACHED overlays — dropdowns, menus, popovers, modals, the tip sheet. Titan has
     no overlays at all, so it has no rule to break here." The score panel is not an overlay.
     It is an in-page card, and it was the ONLY thing in the product wearing --el-hero, which
     made the exception's justification and its single use case disagree.

     What replaces it is the kit's own elevation, which the grade-badge note already states:
     a fill plus a hairline. `--rim-strong` is that hairline and it stays. */
  box-shadow: var(--rim-strong);
}
.ring-track { stroke: color-mix(in srgb, var(--line) 88%, transparent); }
/* `filter: drop-shadow` is still a shadow — it simply wasn't caught by a rule written about
   `box-shadow`, which is how the flattest surface in the product kept a soft coloured glow
   under its signature arc. The arc reads on its own: it is 8px of solid stroke against a
   hairline track. */
.ring-arc { filter: none; }
.ring-mid .score-num { letter-spacing: -0.045em; font-weight: 700; }
.ring-mid .score-outof { opacity: 0.8; }
/* The elevation pass gave this badge a drop shadow and an inset highlight — the glass
   treatment that suited a filled disc. On the hairline form it is neither needed nor
   allowed: the kit's elevation is a fill plus a hairline, and this was one of only two
   real drop shadows left in the product. The knockout outline does the separating. */
.score-ring .score-grade { box-shadow: none; }
.sc-bandlab { font-family: var(--serif); font-weight: 400; font-size: clamp(1.28rem, 1.05rem + 1vw, 1.62rem); letter-spacing: -0.022em; line-height: 1.24; }
.sc-drivers { border-top: 1px solid var(--line-soft); }
.sc-driver + .sc-driver { border-top: 1px solid var(--line-soft); }
.sc-dlab { letter-spacing: -0.008em; }
.sc-pip { height: 4px; width: 18px; }
.sc-methlink { font-weight: 600; }

/* ---------- data surfaces: thin rules, muted axes, quiet zebra ---------- */
table { border-collapse: collapse; }
th {
  color: var(--faint); font-weight: 600;
  border-bottom: 1px solid var(--line) !important;
}
td { border-bottom: 1px solid var(--line-soft); }
tbody tr:hover { background: color-mix(in srgb, var(--surface-2) 60%, transparent); }
.rdr-ring, .rdr-axis { stroke: color-mix(in srgb, var(--line) 70%, transparent); }
.chart-box canvas { border-radius: var(--r-sm); }

/* ---------- controls ---------- */
/* ============ THE TWO FAMILIES REACH THE CONTROLS TOO ================================
   Titan ships Geist and Geist Mono and nothing else, and `--serif` was collapsed onto Geist
   for exactly that reason. But form controls do not inherit `font-family` — the UA sheet
   sets `font: 400 13.333px Arial` on button/input/select/textarea — so the brand font was
   something each control had to OPT IN to, one rule at a time (`.dt-row`, `.sx-search input`
   and a handful of others say `font: inherit` by hand). Everything that never opted in was
   rendering in Arial.

   Measured on a signed-in company page before this rule: the chart range buttons (1D · 1W ·
   1M · 1Y · 2Y) at 12px Arial, the related-company chip's "Apple Inc." at 11px Arial, the
   theme toggle and the back arrow likewise — 37 nodes on that page, 13 on the dashboard,
   112 on the newsroom. A third family, shipped by default, on the surfaces a signed-in
   member spends the most time on.

   `font-family` alone, NOT the `font` shorthand: every one of these controls already sets
   its own size and weight, and `font: inherit` would blow those away and reflow the lot.
   This changes the face and nothing else. */
input, select, button, textarea, optgroup { font-family: inherit; }
input, select, button { font-feature-settings: "kern" 1; }
/* NO BLANKET INSET WELL. This set `box-shadow: inset 0 1px 2px` on every input in the
   product — an inset-field idiom from a different design language. Titan defines a field
   with a hairline, not with a shadow inside it, and on a field that has no ground of its
   own the shadow is not subtle, it is a PHANTOM RECTANGLE: the hero's search input is
   transparent and borderless inside a translucent pill, so the inset drew a visible box
   inscribed in the pill, stopping dead where the arrow button begins.

   That is Miraaj's "stray inner rectangle in the search field", and it was never the field
   — it was one selector applying to every input on the site. Removed rather than scoped
   away from the hero: every other field here already carries its own hairline, so the well
   was adding nothing anywhere and a phantom in the one place it had nothing to sit on. */
.dl-cta, .lock-btn {
  background: linear-gradient(180deg, var(--accent-bright), var(--accent));
  box-shadow: var(--el-2), inset 0 1px 0 rgba(255,255,255,0.3);
  letter-spacing: 0.01em;
}
.tk-big, .badge, .tier-tag {
  box-shadow: var(--rim);
}

/* ---------- chrome ---------- */
header.top { border-bottom: 1px solid var(--line-soft); box-shadow: 0 1px 0 rgba(var(--sh-tint), 0.02); }
.mini-head { box-shadow: var(--el-2), var(--rim); border-bottom-color: var(--line); }
.tip-sheet-card { box-shadow: 0 -2px 8px rgba(var(--sh-tint), 0.08), 0 -24px 60px -20px rgba(var(--sh-tint), 0.3), var(--rim-strong); }
.ac-dropdown { box-shadow: var(--el-3), var(--rim); border-color: var(--line); }

/* ---------- Pro upsell: quiet luxury, not a hard wall ---------- */
.locked-cta {
  background:
    radial-gradient(closest-side, color-mix(in srgb, var(--surface) 94%, transparent), transparent 88%);
}
.lock-title { font-family: var(--serif); letter-spacing: -0.022em; }
.sect-tag {
  border: 1px solid var(--accent-rim);
  background: color-mix(in srgb, var(--accent-soft) 60%, transparent);
}

/* ---------- /methodology + /learn: editorial long-form ---------- */
/* Was -0.03em — the hero's value, on a heading two size-steps down. It belongs to the
   display band with the other page h1s. */
.seo-main h1 { letter-spacing: var(--track-display); }
/* Serif, so it takes the serif variant of the title band rather than the sans one — see
   --track-title-serif. Plan 005 moved this to --track-title and that wrapped the /learn
   "Start here" head onto a second line at 375px; the rest of 005 (the leading bands, the
   rem rhythm, the display-band corrections) is unaffected and stays. */
.meth h2, .gl-cat {
  font-family: var(--serif); letter-spacing: var(--track-title-serif);
  padding-bottom: var(--s-3); border-bottom: 1px solid var(--line);
}
.meth p, .lpath-body p, .gl-list dd { max-width: 68ch; }
.meth-tbl th { text-transform: uppercase; }
.meth-tbl tbody tr:last-child td { border-bottom: none; }
.lpath-step { border-top-color: var(--line-soft); }
.lpath-n { box-shadow: var(--rim); border: 1px solid var(--accent-rim); }
.lpath-chip:hover { background: color-mix(in srgb, var(--accent-soft) 45%, transparent); }

/* ---------- mobile: keep the density premium, not cramped ---------- */
@media (max-width: 640px) {
  .score-panel { padding: var(--s-6) var(--s-4); }
  .panel { padding: 18px 16px; }
  .panel h3::after { min-width: 8px; }
}

/* the hero card gets its own texture so the signature surface feels printed too */
.score-panel {
  background-image:
    var(--grain),
    radial-gradient(120% 90% at 12% 0%, color-mix(in srgb, var(--accent) 7%, transparent), transparent 58%),
    linear-gradient(180deg, var(--surface), color-mix(in srgb, var(--surface-2) 55%, var(--surface)));
  background-repeat: repeat, no-repeat, no-repeat;
}

/* ============================================================================
   NATIVE ORNAMENT SYSTEM (v1.69) — texture crafted in SVG/CSS, no raster art.
   For a research product this reads MORE credible than photographic imagery:
   it's crisp at every DPI, themeable (the colour comes from a CSS variable via
   mask, which no image can do), ~743 bytes gzipped, and it never competes with
   the data for attention.
   ========================================================================= */

/* ---------- guilloché: an engine-turned rosette watermark. Masked so it takes a
     colour; retinted to a neutral hairline (no gold, no accent). ---------- */
.orn {
  position: absolute; pointer-events: none; z-index: 0;
  background-color: var(--muted);
  -webkit-mask: url("/assets/guilloche.svg") center / contain no-repeat;
  mask: url("/assets/guilloche.svg") center / contain no-repeat;
}

/* Score hero — a rosette bleeding off the top-right corner, barely there.
   It sits behind the ring and reads as watermark, not decoration. */
.score-panel::after {
  content: ""; position: absolute; top: -34%; right: -22%;
  width: 430px; height: 430px; pointer-events: none; z-index: 0;
  background-color: var(--muted);
  -webkit-mask: url("/assets/guilloche.svg") center / contain no-repeat;
  mask: url("/assets/guilloche.svg") center / contain no-repeat;
  /* Tuned by eye against the real card: below ~0.12 it vanishes on a phone, above
     ~0.22 it starts competing with the ring. A watermark should be findable, not
     noticeable. */
  opacity: 0.16;
}
:root[data-theme="dark"] .score-panel::after { opacity: 0.2; }
.score-head, .sc-drivers, .sc-work, .score-panel .factor-disc { position: relative; z-index: 1; }

/* ---------- engine-turn: hairline diagonal weave, pure gradients ---------- */
/* The second half of the security-print look — a fine regular ruling under the
   rosette. Two passes at different angles give the woven moiré. */
.engine-turn {
  background-image:
    repeating-linear-gradient(115deg,
      transparent 0 4px, color-mix(in srgb, var(--muted) 22%, transparent) 4px 4.6px),
    repeating-linear-gradient(-115deg,
      transparent 0 6px, color-mix(in srgb, var(--muted) 14%, transparent) 6px 6.5px);
}

/* ---------- Pro block: quiet certificate treatment ---------- */
.sect-tag { position: relative; }
.locked-cta::before {
  content: ""; position: absolute; inset: 0; pointer-events: none; z-index: 0;
  border-radius: var(--r);
  background-image:
    repeating-linear-gradient(115deg,
      transparent 0 5px, color-mix(in srgb, var(--muted) 26%, transparent) 5px 5.7px);
  opacity: 0.12;
  -webkit-mask: radial-gradient(closest-side, #000 30%, transparent 82%);
  mask: radial-gradient(closest-side, #000 30%, transparent 82%);
}
.locked-cta > * { position: relative; z-index: 1; }

/* ---------- editorial divider: hairline rule + centred lozenge ---------- */
.rule-orn {
  display: flex; align-items: center; gap: 14px;
  margin: var(--s-7) 0; color: var(--accent);
}
.rule-orn::before, .rule-orn::after {
  content: ""; flex: 1; height: 1px;
  background: linear-gradient(90deg, transparent, var(--line-strong), transparent);
}
.rule-orn i {
  width: 7px; height: 7px; transform: rotate(45deg); flex: 0 0 auto;
  border: 1px solid var(--accent); opacity: 0.75;
}

/* ---------- methodology / learn: a guilloché band behind the page title ---------- */
.seo-main > h1 { position: relative; z-index: 1; }
.seo-main { position: relative; }
.seo-main::before {
  content: ""; position: absolute; top: -120px; right: -140px;
  width: 480px; height: 480px; pointer-events: none; z-index: 0;
  background-color: var(--muted);
  -webkit-mask: url("/assets/guilloche.svg") center / contain no-repeat;
  mask: url("/assets/guilloche.svg") center / contain no-repeat;
  opacity: 0.13;
}
:root[data-theme="dark"] .seo-main::before { opacity: 0.17; }
.seo-main > * { position: relative; z-index: 1; }

/* ---------- thin-line chart motif: decorative sparkline for empty states ---------- */
.motif-chart { display: block; width: 100%; height: 46px; opacity: 0.5; }
.motif-chart .mc-grid { stroke: var(--line); stroke-width: 0.6; }
.motif-chart .mc-line { stroke: var(--accent); stroke-width: 1.4; fill: none; stroke-linecap: round; }
.motif-chart .mc-fill { fill: var(--accent); opacity: 0.07; }
.empty-state { position: relative; overflow: hidden; }

/* Older engines without mask support simply get no ornament — the layout is
   identical, so this degrades to exactly the v1.68 design. */
@supports not ((mask: url("")) or (-webkit-mask: url(""))) {
  .orn, .score-panel::after, .seo-main::before { display: none; }
}

@media (max-width: 640px) {
  .score-panel::after { width: 320px; height: 320px; top: -28%; right: -30%; }
  .seo-main::before { width: 320px; height: 320px; top: -80px; right: -110px; }
}

/* ============================================================================
   APPLE-STOCKS REDESIGN — core components (v1.73)
   Calm, dark-first, hairline-separated. Consumed by the company view (Phase 2)
   and the watchlist home (Phase 3). Tabular figures throughout; motion stays on
   transform/opacity/colour only, and the reduced-motion guard at the end kills it.
   ========================================================================= */

/* ---- iOS-style segmented control ------------------------------------------ */
.seg {
  display: inline-flex; align-items: stretch; gap: 2px;
  background: var(--surface-2); border: 1px solid var(--line-soft);
  border-radius: var(--r-sm); padding: 3px; position: relative; max-width: 100%;
  font-family: var(--mono);
}
.seg-opt {
  appearance: none; border: none; background: none; cursor: pointer;
  color: var(--muted); font-size: 12px; letter-spacing: 0.03em;
  padding: 6px 12px; border-radius: var(--r-full); white-space: nowrap;
  font-variant-numeric: tabular-nums;
  transition: color var(--dur-2) var(--ease-out), background var(--dur-2) var(--ease-out);
}
.seg-opt:hover { color: var(--text); }
.seg-opt.on { color: var(--text); background: var(--surface); box-shadow: var(--el-1); }
.seg-opt.locked { color: var(--faint); }
.seg-opt:focus-visible { outline: none; box-shadow: var(--ring); }
/* the 9-range chart control scrolls horizontally on a phone instead of wrapping */
.seg.seg-scroll { display: flex; overflow-x: auto; -webkit-overflow-scrolling: touch; scrollbar-width: none; }
.seg.seg-scroll::-webkit-scrollbar { display: none; }

/* ---- filled change pill (Apple green/red rounded rect) -------------------- */
.chg {
  font-family: var(--mono); font-variant-numeric: tabular-nums;
  font-size: 13px; font-weight: 600; letter-spacing: 0.01em;
  padding: 3px 8px; border-radius: var(--r-sm); display: inline-block;
  min-width: 60px; text-align: center; line-height: 1.35;
  /* the base colour is only ever seen by a pill with no direction class; it was a
     hardcoded #fff, which is invisible on Titan's light ground. */
  color: var(--text);
}
/* THE INK ON A FILLED PILL IS THE GROUND, NOT A FIXED NEAR-BLACK.
   The [F5] pass hardcoded `#111` here, and it was right for the palette it was written
   for: Apple's systemGreen/systemRed are LIGHT saturated fills, so white measured
   2.02:1 and near-black cleared AA. Titan's direction inks are the other way round —
   the LIGHT theme's fills are dark (#2e6648 ledger green, #b3241a brick) and the DARK
   theme's are light. Measured with `#111` still in place: 2.90:1 on the green pill and
   2.97:1 on the red, across 173 pills on one page. That was the single worst contrast
   defect the token re-point introduced, and only a screenshot found it.

   `var(--bg)` is the fix in BOTH themes and needs no per-theme rule, because the ground
   is by construction the far end of the ramp from the ink: cream on the dark fills in
   light (5.90 / 5.77), near-black on the light fills in dark (6.77 / 6.86). */
.chg.up { background: var(--up); color: var(--bg); }
.chg.down { background: var(--down); color: var(--bg); }
/* A GAP IS NOT A DAY CHANGE, so it does not wear a day change's chip.
   Same colour semantics — a fair value below the price still reads "down" — but OUTLINED
   rather than filled, wider (it carries "vs price"), and in the row's normal ink. The two
   were visually identical, so the dashboard's "Recently valued" tile read as "Nokia is
   trading at $3.69, down 63.6% today" when it meant "our fair value is $3.69, which is
   63.6% below the price". */
.chg.chg-gap {
  background: none; border: 1px solid currentColor;
  min-width: 0; padding: 2px 7px; font-weight: 500; font-size: 12px;
}
.chg.chg-gap.up { color: var(--up-text, var(--up)); }
.chg.chg-gap.down { color: var(--down-text, var(--down)); }
.chg.chg-gap.flat { color: var(--muted); }
/* The caption over a figure that is not the obvious one. Deliberately quiet — it is a
   clarification, not a heading, and it must not out-weigh the number it labels. */
.wl-plab {
  font-size: var(--t-micro); letter-spacing: 0.06em; text-transform: uppercase;
  color: var(--muted); line-height: 1.2;
}
.chg.flat { background: var(--surface-3); color: var(--muted); }
/* tinted (not filled) variant for dense rows / secondary use */
.chg.soft { color: var(--text); }
.chg.soft.up { background: var(--up-soft); color: var(--up-text); }
.chg.soft.down { background: var(--down-soft); color: var(--down-text); }
.chg.soft.flat { background: var(--surface-2); color: var(--muted); }
/* high-contrast filled variant for the text-heavy grouped lists (screener / portfolio /
   calendar). It used to also swap the ink to a hardcoded near-black; under Titan the base
   pill already carries the correct inverse ink, so what remains — and all that was ever
   load-bearing — is the weight step. */
.chg.hc { color: var(--bg); font-weight: 700; }
.chg.hc.flat { color: var(--muted); }

/* ---- sparkline ------------------------------------------------------------ */
.spark { display: block; overflow: visible; }
.spark-line { fill: none; stroke-width: 1.6; stroke-linecap: round; stroke-linejoin: round; }
.spark-line.up { stroke: var(--up); }
.spark-line.down { stroke: var(--down); }
.spark-line.flat { stroke: var(--muted); }
.spark-fill.up { fill: var(--up); opacity: 0.12; }
.spark-fill.down { fill: var(--down); opacity: 0.12; }
.spark-fill.flat { fill: var(--muted); opacity: 0.10; }
.spark-base { stroke: var(--line); stroke-width: 0.75; stroke-dasharray: 2 3; }

/* ---- watchlist row (Apple Stocks home row) -------------------------------- */
.wl-list { display: flex; flex-direction: column; }
.wl-row {
  display: grid;
  /* superseded by the density-pass rule near the end of the file (fix #6): the sparkline
     column collapses, the quote column holds a max-content floor. Kept in the same shape
     so a reader of THIS rule is not misled about which template actually applies. */
  grid-template-columns: minmax(92px, 1fr) minmax(0, 88px) max-content auto;
  align-items: center; gap: 12px;
  padding: var(--row-pad-y) 6px; cursor: pointer;
  border-bottom: 1px solid var(--line-soft);
  transition: background var(--dur-1) var(--ease-out);
}
/* fill removed — see the Phase 2 state matrix at the end of this file. */
.wl-row:last-child { border-bottom: none; }
.wl-id { min-width: 0; }
/* A LONG TICKER SPILLED ITS COLUMN. Measured at 390px on the Discover list: "RELIANCE.NS"
   is one unbreakable 103px token in a 98px identity column, so it painted 5px into the
   sparkline beside it (the tier tag was already crushed to 0 and was not the cause).
   Pre-dates this density pass; fixed here because a ticker breaking its own row is exactly
   what "tidy" is supposed to mean. Two rules, in priority order: the phone gives the
   identity column room by narrowing the DECORATIVE column (the same call `.wl-row` already
   makes — the sparkline is the one allowed to give), and an ellipsis backstops any ticker
   long enough to beat that. */
.wl-sym {
  font-family: var(--mono); font-weight: 700; font-size: 15px; letter-spacing: 0.02em;
  color: var(--text); min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
.wl-nm { color: var(--muted); font-size: 12px; margin-top: 1px; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.wl-spark { display: grid; place-items: center; min-width: 0; }
.wl-quote { display: flex; flex-direction: column; align-items: flex-end; gap: 4px; }
.wl-price { font-family: var(--mono); font-variant-numeric: tabular-nums; font-size: 15px; color: var(--text); text-align: right; }
.wl-score {
  font-family: var(--mono); font-weight: 700; font-size: 12px;
  width: 30px; height: 30px; border-radius: var(--r-sm); display: grid; place-items: center;
  border: 1px solid var(--line); background: var(--surface-2); letter-spacing: 0;
}
.wl-nm .wl-tag { margin-left: 6px; }
.wl-meta { color: var(--faint); font-size: 11.5px; margin-top: 2px; font-variant-numeric: tabular-nums; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.wl-row-static { cursor: default; }
.wl-row-static:hover { background: none; }
.wl-empty { color: var(--muted); font-size: var(--t-small); line-height: 1.6; padding: 26px 8px; text-align: center; }
/* grouped inset card wrapping a hairline-separated ticker list (Apple grouped list) */
.grp .wl-list { margin: -2px 0; }
.grp-list { padding: 6px 4px; }
.grp-list .wl-row:first-child { border-top: none; }
@media (max-width: 430px) {
  .wl-row { grid-template-columns: minmax(84px, 1fr) minmax(0, 64px) max-content auto; gap: 9px; }
  .wl-price { font-size: 14px; }
  .chg { min-width: 54px; font-size: 12px; }
}
@media (max-width: 340px) { .wl-spark { display: none; } }

/* ---- Apple grouped card + key-stats grid ---------------------------------- */
.grp {
  background: var(--surface); border: 1px solid var(--line); border-radius: var(--r);
  padding: 16px 16px 14px; box-shadow: var(--el-1);
}
.grp + .grp { margin-top: 14px; }
.grp-title {
  font-family: var(--mono); font-size: var(--t-eyebrow); letter-spacing: var(--track-eyebrow); text-transform: uppercase;
  color: var(--faint); margin: 0 0 12px; display: flex; align-items: center; gap: 10px;
}
.kstat-grid { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); column-gap: 0; }
.kstat {
  display: flex; justify-content: space-between; align-items: baseline; gap: 12px;
  padding: 11px 2px; border-bottom: 1px solid var(--line-soft); min-width: 0;
}
.kstat-grid .kstat:nth-child(odd) { padding-right: 18px; border-right: 1px solid var(--line-soft); }
.kstat-grid .kstat:nth-child(even) { padding-left: 18px; }
.kstat .k { color: var(--muted); font-size: 13px; white-space: nowrap; }
.kstat .v { font-family: var(--mono); font-variant-numeric: tabular-nums; font-size: 14px; color: var(--text); text-align: right; white-space: nowrap; }
@media (max-width: 380px) {
  .kstat-grid { grid-template-columns: minmax(0, 1fr); }
  .kstat-grid .kstat:nth-child(odd) { padding-right: 2px; border-right: none; }
  .kstat-grid .kstat:nth-child(even) { padding-left: 2px; }
}

/* ---- interactive price chart (Apple Stocks detail view) ------------------- */
.stx-chart .chart-head {
  display: flex; align-items: flex-start; justify-content: space-between; gap: 12px;
  margin: 2px 0 8px; min-height: 46px;
}
.stx-chart .chart-price { font-size: 26px; font-weight: 700; color: var(--text); letter-spacing: -0.01em; line-height: 1.1; }
.stx-chart .chart-delta { font-size: 14px; margin-top: 3px; }
.stx-chart .chart-delta .chart-rangelab { color: var(--faint); }
.stx-chart .chart-session { font-size: var(--t-meta); color: var(--muted); margin-top: 3px; letter-spacing: 0.01em; }
.stx-chart .chart-session:empty { display: none; }
.stx-chart .chart-session.live { color: var(--text); }
.stx-chart .chart-session.live::before { content: ""; display: inline-block; width: 6px; height: 6px; border-radius: 50%; background: var(--text); margin-right: 5px; vertical-align: 1px; }
.stx-chart .chart-readout {
  text-align: right; color: var(--muted); font-size: var(--t-meta); min-width: 96px;
  align-self: center; font-variant-numeric: tabular-nums;
}
.stx-chart .price-lock { display: inline-flex; flex-wrap: wrap; align-items: center; gap: 8px; font-size: var(--t-meta); color: var(--muted); }
.stx-chart .chart-box { margin: 6px 0 12px; }
.stx-chart #price-canvas { height: 250px; touch-action: pan-y; cursor: crosshair; outline: none; }
.stx-chart #price-canvas:focus-visible { box-shadow: var(--ring); border-radius: var(--r-sm); }
.seg.chart-ranges { width: 100%; justify-content: space-between; }
.seg.chart-ranges .seg-opt { flex: 1 1 auto; text-align: center; padding: 6px 8px; }
@media (max-width: 480px) {
  .stx-chart .chart-price { font-size: 22px; }
  .stx-chart #price-canvas { height: 220px; }
  .seg.chart-ranges .seg-opt { padding: 7px 10px; }   /* comfortable tap targets while scrolling */
}

/* ---- real usage counter (home hero) --------------------------------------- */
.usage-stat {
  margin-top: 14px; font-family: var(--mono); font-size: 12.5px; letter-spacing: 0.02em;
  color: var(--faint); font-variant-numeric: tabular-nums;
}
.usage-stat .us-num { color: var(--accent); font-weight: 700; }
.usage-stat .us-dot { opacity: 0.5; margin: 0 2px; }

/* ============================================================================
   APPLE iOS STOCKS — home fidelity (Phase 11)
   Flat full-width watchlist rows on black, hairline-separated. Large title + date,
   a search/more capsule, a horizontal index strip, and a bottom Top-Stories sheet.
   ========================================================================= */
/* full-bleed to the viewport edges regardless of .wrap's (breakpoint-varying) padding:
   calc(50% - 50vw) == -(parent padding) exactly, so the rows never overshoot the viewport */
#mylist { margin-left: calc(50% - 50vw); margin-right: calc(50% - 50vw); }
.hw-nav {
  position: relative; display: flex; align-items: flex-start; justify-content: space-between;
  gap: 12px; padding: 18px 20px 10px;
}
/* `.hw-title` / `.hw-date` were the pre-rebuild header and no element has carried either
   class since. Removed rather than left in place: this file has been burned three times by
   inert CSS that reads as live, and `.hw-title` in particular still had a responsive
   override below that looked like the page's type scale while governing nothing. */
.hw-cap {
  display: inline-flex; align-items: center; gap: 2px; flex: 0 0 auto; margin-top: 4px;
  background: var(--surface); border-radius: var(--r-full); padding: 4px;
}
.hw-capbtn {
  appearance: none; border: none; background: none; color: var(--accent); cursor: pointer;
  width: 34px; height: 34px; border-radius: var(--r-full); display: grid; place-items: center;
  transition: background var(--dur-1);
}
.hw-capbtn:hover { background: var(--surface-2); }
.hw-capdiv { width: 1px; height: 18px; background: var(--line); }
/* ••• popover menu */
.hw-menu {
  /* max-width is a CONTAINMENT rule, not a cosmetic one: a menu is a list of short labels,
     and anything that makes one wide is a bug somewhere else — which is exactly how the
     `.back` collision below escaped notice until the panel had crossed the whole viewport.
     Capped against the viewport too, so it can never be clipped off the edge of a phone. */
  position: absolute; top: 54px; right: 20px; z-index: 60;
  min-width: 254px; max-width: min(340px, calc(100vw - 32px));
  background: var(--surface-2); border: 1px solid var(--line-soft); border-radius: var(--r-sm);
  box-shadow: var(--el-3); padding: 5px; overflow: hidden;   /* blur removed: measured inert over an opaque fill */
  /* motion: see the F5 popover block. `.drill-in` below keeps its own animation —
     it out-specifies the shared rule, and it is a submenu swap, not an entrance. */
}
/* drill-in submenu slide (root ↔ sub); disabled under reduced-motion below */
.hw-menu.drill-in { animation: hwDrill 0.2s var(--ease-out); }
@keyframes hwDrill { from { opacity: 0.4; transform: translateX(7px); } to { opacity: 1; transform: none; } }
.hw-mi {
  display: flex; align-items: center; gap: 9px; width: 100%; text-align: left;
  appearance: none; border: none; background: none; color: var(--text); cursor: pointer;
  font-size: 16px; padding: 10px 11px; border-radius: var(--r-sm); line-height: 1.2;
}
.hw-mi > span:not([class]) { flex: 1 1 auto; }             /* the label (a class-less span) takes the slack; trailing bits push right */
.hw-mi:hover, .hw-mi:focus-visible { outline: none; }
.hw-mi:active { background: color-mix(in srgb, var(--text) 14%, transparent); }
.hw-mi-chk { width: 18px; flex: 0 0 auto; color: var(--text); font-weight: 700; text-align: center; }
.hw-mi-ic { flex: 0 0 auto; color: var(--muted); display: inline-grid; place-items: center; }
.hw-mi-val { flex: 0 0 auto; color: var(--muted); font-size: 14px; }
.hw-mi-more { flex: 0 0 auto; color: var(--faint); font-size: 18px; font-weight: 600; line-height: 1; }
/* RENAMED off the bare `.back`, which is a PAGE CONTROL class in this codebase — the
   "← Home" button, nineteen of them in app.js plus two static pages. A layout rule near the
   end of this file gives `.back` the page shell's width and a negative margin-inline so the
   button lines up with the content column. That rule was also matching this chevron: a "‹"
   glyph inside the popover was being handed `width: 1224px`, which blew the Sort menu to
   713px wide and dragged it left across the page, heading clipped, overlapping the rows
   under it. Sorting was unusable, and it read as a positioning bug in the popover — the
   popover's own anchoring was correct the whole time. */
.hw-mi-more-back { color: var(--muted); margin-right: 2px; }
.hw-mi-back { font-weight: 600; }
.hw-mi-sep { height: 1px; background: var(--line-soft); margin: 5px 4px; }
.hw-mi-lab { font-size: 11.5px; letter-spacing: 0.04em; text-transform: uppercase; color: var(--faint); padding: 7px 11px 3px; }
/* Edit Watchlist mode — red − remove handles, Done affordance */
/* edit mode adds a leading auto column for the − handle; the rest of the grid is unchanged */
#mylist.editing .hw-row { cursor: default; grid-template-columns: auto minmax(96px, 1fr) 140px auto auto; }
.hw-del {
  flex: 0 0 auto; width: 24px; height: 24px; border-radius: 50%;
  border: none; cursor: pointer; display: grid; place-items: center;
  background: var(--down); color: #111; animation: fade 0.2s var(--ease-out);
}
.hw-del-glyph { font-size: 19px; font-weight: 700; line-height: 1; margin-top: -2px; }
.hw-row-removing { opacity: 0; transform: translateX(-14px); transition: opacity 0.28s var(--ease), transform 0.28s var(--ease); }
.hw-done {
  appearance: none; border: none; background: none; cursor: pointer; color: var(--accent);
  font-family: var(--mono); font-weight: 700; font-size: 14px; padding: 7px 13px; letter-spacing: 0.02em;
}
.hw-cap.hw-cap-done { padding: 0 4px; }

/* ---- the watchlist page speaks the dashboard's language -------------------
   Mono uppercase eyebrow with a right-aligned qualifier, exactly like the dashboard's
   own cards (TODAY'S MOVERS · US market, WATCHLIST · 2 names). The 34px/800 page title
   that used to sit here belonged to no other surface. */
/* THE PAGE TITLE, in the register Dashboard, Portfolio and the screener already use
   (`.dash-head h2`). It was a mono uppercase eyebrow — the dashboard's TILE header borrowed
   one level too far up: an eyebrow labels a card inside a page, it does not name the page.
   Beside 34px row tickers it read as a caption on its own content.
   Deliberately NOT `.dash-head h2` itself: `.hw-nav` is a flex row that owns the Sort and
   Edit buttons, and `.dash-head` brings its own flex layout and margins that would fight it.
   Same type, same clamp, same optical sizing — see the `h1, h2, .co-head h2, .dash-head h2`
   rule, which this is added to. */
.hw-pagetitle {
  font-family: var(--serif); font-weight: 400;
  font-size: clamp(1.7rem, 1.1rem + 2.4vw, 2.6rem);
  letter-spacing: -0.02em; margin: 0; line-height: 1.04; color: var(--text);
}
.hw-qual { font-family: var(--mono); font-size: 12px; color: var(--muted); margin-top: 6px; }
/* The price stamp. Quieter than the qualifier above it — it is a caveat, not a headline —
   and it collapses entirely when there is nothing to say, so the header does not carry an
   empty line on a list we hold no timestamps for. */
.hw-stamp { font-family: var(--mono); font-size: 11px; color: var(--faint); margin-top: 3px; }
.hw-stamp:empty { display: none; }
.hw-actions { display: flex; align-items: center; gap: 8px; flex: 0 0 auto; margin-top: 2px; }
.hw-act {
  font-family: var(--mono); font-size: var(--t-meta); letter-spacing: 0.04em;
  color: var(--muted); background: none; border: 1px solid var(--line);
  border-radius: var(--r-full); padding: 5px 12px; cursor: pointer;
  transition: color var(--dur-2) var(--ease-out), border-color var(--dur-2) var(--ease-out);
}
.hw-act:hover { color: var(--text); border-color: var(--text); }
@media (pointer: coarse) { .hw-act { min-height: var(--tap); } }
@media (prefers-reduced-motion: reduce) {
  .hw-menu.drill-in { animation: none; }
  .hw-row-removing { transition: none; }
}

/* MARKET STATUS PILL — the sentence is the server's; this is only its dress.

   Anchored on this comment line rather than on a selector because a `.hw-…` prefix appears
   inside compound selectors elsewhere in this file, and an insertion that lands mid-selector
   splits it silently: the rules still parse, they just scope to something nobody intended.
   That has happened twice in this stylesheet. */
.hw-pill-row { padding: 2px 20px 8px; }
.hw-pill {
  display: inline-flex; align-items: center; gap: 7px;
  font-size: 12px; font-weight: 600; letter-spacing: 0.01em; color: var(--muted);
  background: var(--surface); border-radius: var(--r-pill); padding: 5px 11px 5px 9px;
}
.hw-pill-dot { width: 6px; height: 6px; border-radius: 50%; background: currentColor; flex: 0 0 auto; }
/* Colour carries the same claim the words do, so it hedges when they hedge. `open` is the only
   state that earns the affirmative green, and only while the server is certain — outside the
   years its holiday rules were checked, an ad-hoc closure would be invisible to it, and a
   confident green dot beside "scheduled open" would be the client re-asserting what the server
   just declined to. */
.hw-pill.open { color: var(--up); }
.hw-pill.extended { color: var(--text); }
.hw-pill.closed { color: var(--muted); }
.hw-pill.unsure { color: var(--muted); }

/* The sparkline sits UNDER the number and is absent when there is no series — never an empty
   frame. See stripSpark() in app.js and app/glance.py: a line drawn across an empty box says
   the index did not move, which is a claim we never made. */
.hw-tick-spark { display: block; width: 92px; height: 22px; margin-top: 6px; }

/* horizontal index strip */
.hw-strip { overflow-x: auto; -webkit-overflow-scrolling: touch; scrollbar-width: none; padding: 4px 0 10px; }
.hw-strip::-webkit-scrollbar { display: none; }
.hw-strip-inner { display: flex; gap: 10px; padding: 0 20px; }
.hw-tick {
  flex: 0 0 auto; min-width: 118px; background: var(--surface); border-radius: var(--r); padding: 11px 13px;
}
.hw-tick-nm { font-size: 12px; font-weight: 600; color: var(--muted); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.hw-tick-val { font-family: var(--mono); font-variant-numeric: tabular-nums; font-size: 17px; font-weight: 700; color: var(--text); margin: 3px 0 2px; }
.hw-tick-chg { font-family: var(--mono); font-variant-numeric: tabular-nums; font-size: 12.5px; font-weight: 600; }
.hw-tick-chg.big { font-size: 19px; font-weight: 700; margin-top: 3px; }
.hw-tick-chg.up { color: var(--up); } .hw-tick-chg.down { color: var(--down); } .hw-tick-chg.flat { color: var(--muted); }

/* the redundant global-header search is hidden on the home (the nav capsule handles it) */
body:has(#home:not(.hidden)) header.top .search { display: none; }
body.search-reveal header.top .search { display: flex !important; }

/* flat watchlist rows */
.hw-list { display: flex; flex-direction: column; }
.hw-row {
  display: grid; grid-template-columns: minmax(96px, 1fr) 140px auto auto;
  align-items: center; gap: 12px; padding: 0 20px; min-height: 68px; cursor: pointer;
  border-bottom: 1px solid var(--line); transition: background var(--dur-1);
}
.hw-row:first-child { border-top: 1px solid var(--line); }
.hw-row:active { background: var(--surface); }
.hw-id { min-width: 0; }
.hw-sym { font-family: var(--sans); font-size: 22px; font-weight: 700; letter-spacing: -0.01em; color: var(--text); line-height: 1.1; }
.hw-nm { font-size: 15px; color: var(--muted); margin-top: 1px; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; max-width: 100%; }
.hw-spark { display: grid; place-items: center; }
.hw-spark .spark { width: 140px; height: 44px; }
.hw-grade {
  font-family: var(--mono); font-weight: 700; font-size: 12px; width: 26px; height: 26px;
  border-radius: var(--r-sm); display: grid; place-items: center; border: 1px solid var(--line); background: var(--surface);
}
.hw-grade-empty { border: none; background: none; }
.hw-quote { display: flex; flex-direction: column; align-items: flex-end; gap: 5px; min-width: 84px; }
.hw-price { font-family: var(--sans); font-variant-numeric: tabular-nums; font-size: 21px; font-weight: 600; color: var(--text); }
.hw-pill { min-width: 74px; text-align: center; }
.hw-empty, .hw-loading { color: var(--muted); font-size: var(--t-small); line-height: 1.6; padding: 40px 20px; text-align: center; }
/* An empty state names an action, so it offers it. Sized off the band rhythm rather than
   a full-height void: the old state put one centred sentence in ~1500px of nothing. */
.hw-empty-cta { display: grid; justify-items: center; gap: 10px; padding: 56px 24px 64px; max-width: 34rem; margin-inline: auto; }
.hw-empty-t { font-size: 17px; font-weight: 600; color: var(--text); }
.hw-empty-d { color: var(--muted); font-size: var(--t-small); line-height: 1.6; }
/* `.btn` is not defined anywhere in this stylesheet, so the first version of this button
   rendered as a browser-default rectangle — the exact failure `test_a_cta_is_a_pill`
   exists to stop, walked past because that test enumerates class names. Same register as
   .hero-cta: mono, uppercase, filled with the ink, pill. */
.hw-empty-b {
  margin-top: 6px; display: inline-block;
  font-family: var(--mono); font-size: var(--t-meta); letter-spacing: 0.06em;
  text-transform: uppercase; text-decoration: none; cursor: pointer;
  padding: 12px 20px; border: 0; border-radius: var(--r-full);
  background: var(--text); color: var(--bg);
  transition: transform var(--dur-2) var(--ease-out), opacity var(--dur-2) var(--ease-out);
}
.hw-empty-b:hover { opacity: 0.9; }
.hw-empty-b:active { transform: scale(0.98); }
/* one control, so no divider gap to balance */
.hw-cap.hw-cap-solo { gap: 0; }

.hw-back { appearance: none; border: none; background: none; color: var(--accent); font-size: 15px; cursor: pointer; padding: 14px 0 6px; }

/* sparkline gradient fill + dashed baseline */
.spark-grad { stroke: none; }
.sg-top.up { stop-color: var(--up); stop-opacity: 0.28; }
.sg-bot.up { stop-color: var(--up); stop-opacity: 0; }
.sg-top.down { stop-color: var(--down); stop-opacity: 0.28; }
.sg-bot.down { stop-color: var(--down); stop-opacity: 0; }
.sg-top.flat { stop-color: var(--muted); stop-opacity: 0.18; }
.sg-bot.flat { stop-color: var(--muted); stop-opacity: 0; }
.spark-base { stroke-dasharray: 3 3; stroke-width: 1; }
.spark-base.up { stroke: var(--up); opacity: 0.3; }
.spark-base.down { stroke: var(--down); opacity: 0.3; }
.spark-base.flat { stroke: var(--muted); opacity: 0.3; }

@media (max-width: 430px) {
  .hw-nav, .hw-strip-inner, .hw-pill-row, .hw-row { padding-left: 16px; padding-right: 16px; }
  /* THE SPARKLINE YIELDS FIRST — the same principle the 360px tier below already states,
     which this tier was not applying. Measured at 402px with the old template: a wide
     price (₩1,645,000 is 116px at this size) took its width from `auto`, the identity
     column absorbed the loss and fell to 89px, and MACRONIX truncated. A truncated
     TICKER is not acceptable at any width — it is the row's identity.

     Two changes carry that. The identity column gets a real 120px floor instead of 80px,
     so it can no longer be squeezed by its neighbours; the sparkline column becomes
     `minmax(0, 88px)`, so it is the track that gives way when the row runs short. The
     price column gets a 112px floor with `max-content` above it: 112px is wider than
     every price in the roster, so ordinary rows all resolve to the SAME width and the
     column finally lines up down the list — each `.hw-row` is its own grid, so before
     this the sparkline started at 146, 124, 136, 131 and 114 on consecutive rows — while
     an unusually long price can still expand its own row rather than being cut. */
  /* 140px, not 120: the floor has to clear the LONGEST ticker, not a typical one.
     `RELIANCE.NS` is eleven characters and needs ~132px at this size, so a 120px floor
     truncated the one thing this row must never truncate. The sparkline yields the
     difference, which is what it is for. */
  .hw-row { grid-template-columns: minmax(140px, 1fr) minmax(0, 88px) 26px minmax(112px, max-content); gap: 9px; }
  #mylist.editing .hw-row { grid-template-columns: auto minmax(96px, 1fr) minmax(0, 72px) 26px minmax(112px, max-content); }
  .hw-spark { min-width: 0; overflow: hidden; }
  .hw-spark .spark { width: 100%; max-width: 88px; height: auto; }
  .hw-sym { font-size: 20px; } .hw-price { font-size: 20px; }   /* Apple 20–22 semibold */
  .hw-pill { font-size: 13px; }                   /* keep the home pill at 13, not the dense-list 12 */
}
/* 320px phones. The ≤430px row's own minimums add up to more viewport than there is:
   16 + 80 identity + 9 + 112 spark + 9 + 26 grade + 9 + 84 quote + 16 = 361px, so the price
   column ran 25px off the right edge and took the document with it. Same principle as the
   watchlist row — the sparkline is the only decorative column, so it yields first and the
   ticker and the price both keep their floor. New total: 283px. */
@media (max-width: 360px) {
  .hw-nav, .hw-strip-inner, .hw-pill-row, .hw-row { padding-left: 12px; padding-right: 12px; }
  /* AT 320 THE SPARKLINE GOES ENTIRELY, which is this tier's own principle taken to its
     end: it is the only decorative column, so it yields first — and here there is nothing
     left for it to yield. The arithmetic: 320 - 24 padding - gaps leaves 275px; the price
     needs 112 and the grade 26, so the identity column gets 137 — and `RELIANCE.NS` needs
     132 of that. A ticker is the row's identity and is never the thing that truncates. */
  .hw-row { grid-template-columns: minmax(132px, 1fr) 26px minmax(100px, max-content); gap: 7px; }
  .hw-row .hw-spark { display: none; }
  #mylist.editing .hw-row { grid-template-columns: auto minmax(64px, 1fr) 72px auto auto; }
  .hw-spark .spark { width: 72px; }
  .hw-quote { min-width: 76px; }
  .hw-pill { min-width: 68px; }
}
@media (min-width: 760px) {
  #mylist { margin: 0; }
  .hw-nav, .hw-strip-inner, .hw-pill-row, .hw-row { padding-left: 4px; padding-right: 4px; }
}

/* ---- watchlist home (Phase 3) --------------------------------------------- */
/* `.home-seg` / `.home-seg-ctl` removed 2026-08-19 with renderHomeSeg(): the host element
   #home-seg was never in the markup, so neither rule ever matched anything. */
.mylist { margin: 6px 0 20px; }
.mylist-head { display: flex; flex-wrap: wrap; align-items: baseline; gap: 6px 12px; margin: 6px 2px 4px; }
.mylist-lab { font-family: var(--mono); font-size: 11px; letter-spacing: 0.16em; text-transform: uppercase; color: var(--faint); }
.mylist-sub { font-size: 12.5px; color: var(--muted); }
.mylist-link { cursor: pointer; color: var(--accent); }
.mylist .wl-list { border-top: 1px solid var(--line-soft); margin-top: 6px; }

/* ---- secondary pages: shared grouped-card list + screener (Phase 5) -------- */
.scr-panel { margin: 14px 0 18px; }
.scr-panel .scr-controls { margin: 0 0 10px; }
.scr-panel .scr-adv-head { margin-top: 8px; }
/* the results card: hairline rows, no zebra table */
#scr-results .grp-list { padding: 4px 6px; }
#scr-results .wl-row { grid-template-columns: minmax(150px, 1.4fr) minmax(0, 84px) max-content auto; }
.scr-count { font-family: var(--mono); font-size: 12px; color: var(--muted); }
@media (max-width: 560px) {
  #scr-results .wl-row { grid-template-columns: minmax(120px, 1fr) max-content auto; }
  #scr-results .wl-spark { display: none; }   /* protect the intrinsic + upside pill on a phone */
}

/* ---- portfolio + calendar (Phase 6) --------------------------------------- */
/* the quote column's max-content floor — see fix #6's note; this compound selector beats
   the density-pass base rule, so it must carry the same floor or the Watch-button rows
   regress to the starved track that painted the pill over the sparkline */
.wl-row.wl-has-extra { grid-template-columns: minmax(92px, 1fr) minmax(0, 88px) max-content auto auto; }
.wl-extra { display: flex; align-items: center; justify-content: flex-end; }
.pf-x {
  background: none; border: 1px solid var(--line); color: var(--faint);
  width: 26px; height: 26px; border-radius: var(--r-sm); cursor: pointer; font-size: var(--t-micro); line-height: 1;
  transition: color var(--dur-1), border-color var(--dur-1);
}
.pf-x:hover { color: var(--down); border-color: var(--down); }
/* `.pf-x` above is the 26px SQUARE icon button used in a holding row (✕ / ✎). The same
   class is reused for text buttons in the units, share and copy panels, where a fixed
   26px box crushed "They were per share" to 28px total — 115px of text in a 0px content
   box. Those opt back out of the square. */
.pf-units-acts .pf-x, .pf-share-acts .pf-x, .pf-share-url .pf-x {
  width: auto; height: auto; line-height: 1.2;
}
/* portfolio summary stat tiles → Apple grouped stat cards */
.pf-cards { display: grid; grid-template-columns: repeat(auto-fit, minmax(150px, 1fr)); gap: 12px; margin: 16px 0 8px; }
.pf-card { background: var(--surface); border: 1px solid var(--line); border-radius: var(--r); padding: 13px 15px; box-shadow: var(--el-1); }
.pf-lab { font-family: var(--mono); font-size: var(--t-eyebrow); letter-spacing: var(--track-eyebrow); text-transform: uppercase; color: var(--faint); }
.pf-val { font-family: var(--mono); font-variant-numeric: tabular-nums; font-size: 20px; margin-top: 6px; color: var(--text); }
.pf-val small { font-size: var(--t-small); }
#pf-holdings { margin-top: 6px; }
/* calendar: date cards + macro grouped card */
.cal-cols { display: grid; grid-template-columns: minmax(0, 1.3fr) minmax(0, 1fr); gap: 20px; align-items: start; }
.cal-daycard { margin-bottom: 12px; }
.cal-daycard > .grp-title { color: var(--accent); }   /* colour only — the size is the eyebrow's */
.cal-macro-list { display: flex; flex-direction: column; }
.cal-macro { display: flex; gap: 12px; align-items: baseline; padding: 10px 2px; border-bottom: 1px solid var(--line); }
.cal-macro:last-child { border-bottom: none; }
.cal-mdate { font-family: var(--mono); color: var(--muted); font-size: 12px; min-width: 54px; font-variant-numeric: tabular-nums; }
.cal-mname { font-size: 14px; color: var(--text); }
.cal-mcat { font-size: 11.5px; color: var(--faint); margin-top: 2px; }
.cal-macro-note { font-size: 11px; color: var(--faint); margin-top: 10px; line-height: 1.5; }
.cal-watch { color: var(--accent); }

/* ---------------- forward market calendar (#news) ----------------
   IMPACT IS WEIGHT, NOT HUE. This palette reserves green and red strictly for price
   direction (see the :root note), so ranking an event High/Medium/Low by colour would
   collide with the one meaning colour already carries here. The three badges are the same
   ink at three weights — solid, outlined, ghost — and every one of them also spells the
   word, so the ranking survives greyscale, colour-blindness and a screen reader alike.

   The one place hue IS used is a released figure against what was expected, which is
   genuinely directional — and even there it ships a glyph and an aria-label, never colour
   alone. */
.mc-imp {
  font-family: var(--mono); font-size: 10px; letter-spacing: 0.04em; text-transform: uppercase;
  padding: 3px 7px; border-radius: var(--r-full); white-space: nowrap; line-height: 1;
}
.mc-imp-high { background: var(--accent); color: var(--surface); }
.mc-imp-medium { border: 1px solid var(--line-strong); color: var(--muted); }
.mc-imp-low { color: var(--faint); }

/* digest */
.mc-blurbs { list-style: none; margin: 0; padding: 0; display: flex; flex-direction: column; gap: 16px; }
.mc-blurb { padding-bottom: 16px; border-bottom: 1px solid var(--line-soft); }
.mc-blurb:last-child { padding-bottom: 0; border-bottom: none; }
.mc-blurb-head { display: flex; align-items: center; gap: 9px; flex-wrap: wrap; }
/* `.panel h3` (further up this file) is the eyebrow treatment — mono, uppercase, tracked —
   and it is MORE specific than a bare class, so a plain `.mc-blurb-t` lost to it and the
   digest headline rendered as a label with a trailing rule instead of as a sentence. Scoped
   under `.mc-blurb` to win, and the inherited eyebrow properties are reset explicitly rather
   than left to chance. */
.mc-blurb .mc-blurb-t {
  font-family: inherit; font-size: 15px; font-weight: 600; color: var(--text);
  letter-spacing: normal; text-transform: none; line-height: 1.4; margin: 0;
  display: block;
}
/* `.panel h3::after` trails a hairline rule off every panel title — right for a section
   label, wrong for a sentence, where it reads as a stray dash after the full stop. The
   section eyebrows above these still get theirs. */
.mc-blurb .mc-blurb-t::after { content: none; }
.mc-blurb-why { margin: 8px 0 0; font-size: 13.5px; line-height: 1.6; color: var(--muted); }
.mc-tag-noise {
  font-family: var(--mono); font-size: 10px; text-transform: uppercase; letter-spacing: 0.04em;
  color: var(--faint); border: 1px solid var(--line); border-radius: var(--r-full); padding: 3px 7px;
}
.mc-noise .mc-blurb-t, .mc-noise .mc-ev-t { color: var(--muted); }

/* this-week strip */
.mc-head { display: flex; align-items: baseline; justify-content: space-between; gap: 12px; }
.mc-count { font-family: var(--mono); font-size: 11px; color: var(--faint); }
.mc-days { display: grid; grid-template-columns: repeat(auto-fit, minmax(150px, 1fr)); gap: 14px; margin-top: 14px; }
/* Same specificity trap as `.mc-blurb-t` above — this one WANTS the eyebrow look, but it
   still has to win to set its own size and colour. */
.mc-day .mc-day-h { margin: 0 0 8px; font-family: var(--mono); font-size: 11px;
  color: var(--accent); text-transform: uppercase; letter-spacing: 0.05em; font-weight: 600; }
.mc-day-n { color: var(--faint); margin-left: 5px; }
.mc-evs { list-style: none; margin: 0; padding: 0; display: flex; flex-direction: column; gap: 10px; }
.mc-ev { background: var(--surface-2); border: 1px solid var(--line-soft); border-radius: var(--r-sm); padding: 10px 11px; }
.mc-ev-head { display: flex; align-items: flex-start; gap: 8px; justify-content: space-between; }
.mc-ev-t { font-size: 13px; line-height: 1.35; color: var(--text); }
.mc-ev-when { font-family: var(--mono); font-size: 10.5px; color: var(--faint); margin-top: 4px; }
.mc-approx { border-bottom: 1px dotted var(--line-strong); cursor: help; }

/* consensus / prior / actual */
.mc-figs { display: flex; flex-wrap: wrap; gap: 12px; margin-top: 8px; }
.mc-fig { display: flex; flex-direction: column; gap: 1px; }
.mc-fig-l { font-family: var(--mono); font-size: 9.5px; text-transform: uppercase;
  letter-spacing: 0.05em; color: var(--faint); }
.mc-fig b { font-size: 13px; color: var(--text); font-variant-numeric: tabular-nums; }
.mc-fig-actual.mc-above b { color: var(--up-text); }
.mc-fig-actual.mc-below b { color: var(--down-text); }

/* the expandable lesson — native <details>, so aria-expanded and keys come free */
.mc-why { margin-top: 8px; }
.mc-why > summary {
  font-family: var(--mono); font-size: 10.5px; letter-spacing: 0.03em; color: var(--muted);
  cursor: pointer; list-style: none; padding: 3px 0; min-height: 24px; display: flex; align-items: center;
}
.mc-why > summary::-webkit-details-marker { display: none; }
.mc-why > summary::before { content: "＋"; margin-right: 6px; color: var(--faint); }
.mc-why[open] > summary::before { content: "－"; }
.mc-why > summary:hover { color: var(--accent); }
.mc-why > summary:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; border-radius: var(--r-sm); }
.mc-why p { margin: 6px 0 0; font-size: 12.5px; line-height: 1.6; color: var(--muted); }

/* on the horizon */
.mc-hz { list-style: none; margin: 12px 0 0; padding: 0; }
.mc-hz-row { display: flex; align-items: baseline; justify-content: space-between; gap: 14px;
  padding: 11px 2px; border-bottom: 1px solid var(--line-soft); }
.mc-hz-row:last-child { border-bottom: none; }
.mc-hz-t { font-size: 14px; color: var(--text); }
.mc-sep { font-family: var(--mono); font-size: 10px; color: var(--faint); margin-left: 7px;
  border-bottom: 1px dotted var(--line-strong); cursor: help; }
.mc-hz-when { display: flex; gap: 9px; align-items: baseline; font-family: var(--mono); font-size: 12px; }
.mc-hz-when time { color: var(--muted); font-variant-numeric: tabular-nums; }
.mc-hz-away { color: var(--faint); }

/* earnings coming up */
.mc-erows { list-style: none; margin: 12px 0 0; padding: 0; display: flex; flex-direction: column; gap: 8px; }
.mc-ebtn {
  width: 100%; text-align: left; background: var(--surface-2); border: 1px solid var(--line-soft);
  border-radius: var(--r-sm); padding: 12px 13px; cursor: pointer; color: inherit;
  transition: border-color var(--dur-2) var(--ease-out), background var(--dur-2) var(--ease-out);
}
/* fill + hairline removed — both are what the Phase 2 state matrix now sets. */
.mc-ebtn:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }
.mc-etop { display: flex; align-items: baseline; gap: 9px; }
.mc-etk { font-family: var(--mono); font-size: 13px; font-weight: 600; color: var(--accent); }
.mc-eco { font-size: 12.5px; color: var(--muted); overflow: hidden; text-overflow: ellipsis;
  white-space: nowrap; flex: 1; }
.mc-edate { font-family: var(--mono); font-size: 11.5px; color: var(--faint); font-variant-numeric: tabular-nums; }
.mc-evals { display: flex; flex-wrap: wrap; gap: 14px; margin-top: 7px; font-size: 12px; color: var(--muted); }
.mc-evals b { color: var(--text); font-variant-numeric: tabular-nums; }
.mc-evals-none { color: var(--faint); font-style: normal; }
.mc-gap { font-family: var(--mono); font-variant-numeric: tabular-nums; }
/* The session and the street's estimates. Deliberately quieter than our own fair value —
   they are context for the date, not the number the reader came for. */
.mc-ewhen { margin-top: 5px; }
.mc-esess { font-family: var(--mono); font-size: 10.5px; letter-spacing: 0.03em;
  color: var(--muted); border: 1px solid var(--line-soft); border-radius: var(--r-full);
  padding: 2px 7px; }
.mc-eest { display: flex; flex-wrap: wrap; align-items: baseline; gap: 7px; margin-top: 6px;
  font-size: 11.5px; color: var(--faint); }
.mc-eest b { color: var(--muted); font-variant-numeric: tabular-nums; font-weight: 600; }
.mc-edot { color: var(--line-strong); }

/* status + empty */
.mc-note { font-size: 11.5px; line-height: 1.55; color: var(--faint); margin: 10px 0 0; }
.mc-empty { font-size: var(--t-small); line-height: 1.6; color: var(--muted); margin: 10px 0 0; }
.mc-stale {
  margin-top: 16px; padding: 11px 13px; border-radius: var(--r-sm); font-size: 12.5px;
  line-height: 1.55; color: var(--text);
  border: 1px solid color-mix(in srgb, var(--warn) 45%, transparent);
  background: color-mix(in srgb, var(--warn) 11%, transparent);
}
@media (max-width: 640px) {
  .mc-days { grid-template-columns: minmax(0, 1fr); }
  /* Touch targets: the disclosure and the earnings row are the two things a thumb aims at. */
  .mc-why > summary { min-height: 44px; }
  .mc-ebtn { padding: 14px 13px; }
}

@media (max-width: 720px) { .cal-cols { grid-template-columns: minmax(0, 1fr); } }
@media (max-width: 480px) { #pf-holdings .wl-spark { display: none; } }
/* A holding's meta carries both cost readings ("0.7249 sh · $689.75/sh · $500.00 basis"),
   which is the whole point — ellipsising it on a phone hides the number the user came to
   check. Portfolio rows alone let it wrap; every other list keeps its single tidy line. */
@media (max-width: 620px) {
  #pf-holdings .wl-meta { white-space: normal; overflow: visible; text-overflow: clip; line-height: 1.45; }
  .pf-meta-px { display: none; }
  /* …but NOT when the price is the thing that's missing. The px reading is hidden on a phone
     because the row's own market value sits right beside it — that reasoning inverts the
     moment there is no market value: a blank row with no explanation is exactly the state
     that got reported as "the portfolio is broken". These two stay. */
  .pf-meta-px.pf-meta-pending, .pf-meta-px.pf-meta-cold { display: inline; }
}

/* A holding still being fetched, and one the feed could not price. Muted, never alarming —
   an unpriced holding is a gap in our data, not a problem with the reader's position. */
.pf-meta-pending { color: var(--muted); font-style: italic; }
.pf-meta-cold { color: var(--muted); }

/* ---- server-rendered SEO + compare pages → Apple grouped inset cards (Phase 8) ----
   Both the per-ticker SEO pages and the X-vs-Y compare pages share the .seo-* classes,
   so filling the structured sections with a surface card + hairline + soft elevation
   (and tabular figures) brings them into the same calm dark language as the SPA. Pure
   CSS — the server HTML, JSON-LD and inlined perf are untouched. */
.sh-cell { background: var(--surface); border: 1px solid var(--line); box-shadow: var(--el-1); border-radius: var(--r); }
.sh-val { font-variant-numeric: tabular-nums; }
.seo-slider, .seo-assum, .seo-peers, .seo-hub {
  background: var(--surface); border: 1px solid var(--line); border-radius: var(--r);
  padding: 20px 22px; box-shadow: var(--el-1);
}
.seo-assum, .seo-peers, .seo-hub { margin: 22px 0; }
.seo-assum h2, .seo-peers h2, .seo-hub h2 { margin-top: 0; }
/* a wide comparison/assumptions table scrolls inside its own card — the page body
   must never scroll horizontally (esp. the 2-ticker compare table at 390px). */
.seo-assum { overflow-x: auto; }
.seo-assum .seo-table { min-width: 440px; }
/* A COMPARISON PAGE WITH THE SECOND COLUMN OFF-SCREEN IS NOT COMPARING ANYTHING.
   The head-to-head table is 440px of label(200) + A(120) + B(120) inside a 362px phone
   column. `overflow-x: auto` above makes that REACHABLE — the document does not overflow
   and the reader can scroll it sideways — which is why no probe ever flagged it, and it is
   not good enough here: on first paint the whole MSFT column sat past the right edge and
   every one of its figures was cut mid-value. "$36" where the fair value is $360.24, and
   "25% … valu" for the verdict. A truncated money figure that still reads as a number is
   the one thing this product must never print.

   `table-layout: fixed` makes the table exactly its container and lets the LABEL column
   wrap instead — labels are prose and wrap harmlessly, figures are not and must not be
   cut. Both tickers are visible without scrolling. CSS only; the server-rendered markup,
   JSON-LD and canonical are untouched.

   Two rules had to give: `.seo-assum .seo-table { min-width: 440px }` is the floor that
   forces the overflow, and `.seo-table tbody th { width: 200px }` is what makes the label
   column half the table. Both are matched at their own weight rather than escalated. */
@media (max-width: 560px) {
  .seo-assum .seo-table { min-width: 0; table-layout: fixed; width: 100%; }
  .seo-assum .seo-table tbody th { width: 42%; }
  .seo-assum .seo-table th, .seo-assum .seo-table td {
    padding-right: 8px; overflow-wrap: anywhere;
  }
}

@media (max-width: 480px) { .seo-slider, .seo-assum, .seo-peers, .seo-hub { padding: 16px 15px; } }
.seo-table td, .seo-table th { border-top-color: var(--line-soft); }
.seo-table td { font-variant-numeric: tabular-nums; }
.uv-card { background: var(--surface); border-color: var(--line); box-shadow: var(--el-1); border-radius: var(--r); }
.uv-up { font-variant-numeric: tabular-nums; }
/* the decorative guilloché watermark bleeds off the top-right corner; clip its horizontal
   overrun at its offset parent so a phone never scrolls sideways (vertical bleed unaffected,
   and `clip` — unlike `hidden` — establishes no scroll container). */
.seo-main { overflow-x: clip; }
/* the compare table reads as a clean two-column grouped card */
.seo-prose + .seo-assum { margin-top: 26px; }

/* ---- company-view Phase-2 pieces ------------------------------------------ */
.sc-workhint { color: var(--faint); font-size: 12px; margin-top: 12px; }
.keystats-panel .kstat-grid { margin-top: 2px; }
.pro-strip { background: linear-gradient(180deg, var(--surface-2), var(--surface)); border-color: var(--accent-rim); }
.pro-strip-in { display: flex; align-items: center; gap: 16px; flex-wrap: wrap; }
.pro-strip-txt { flex: 1 1 240px; font-size: 13.5px; color: var(--muted); }
.pro-strip-txt b { color: var(--text); }
.pro-strip .lock-btn { flex: 0 0 auto; }

/* new components animate on transform/opacity/colour only; reduced-motion stills them */
@media (prefers-reduced-motion: reduce) {
  .seg-opt, .wl-row, .spark-line { transition: none !important; }
}


/* ============================================================================
   FROSTED GLASS / VIBRANCY (Phase B) — Apple "materials"
   Translucent, blurred, saturated surfaces over content on every bar / capsule /
   menu / sheet / popover / modal, with a hairline + 1px inner top highlight so they
   read as glass. Composited (backdrop-filter promotes the layer) so there's no
   per-frame cost when static. Solid fallbacks for reduced-transparency and for
   engines without backdrop-filter.
   ========================================================================= */
  :root { --header-h: 74px; }
/* TITAN RE-POINT. Titan documents no translucent material — it has no bars,
   menus, popovers or sheets to put one on, so there was nothing to copy and
   nothing to violate. The RAMP is kept intact (it was measured and audited in
   plan 004, and every glass surface has both degradation paths); only the FILLS
   move onto Titan's grounds, so a bar over cream reads as warm paper rather
   than as cool grey glass. The blur radii are unchanged. */
:root {
  /* the dark bar material, addressable from a light-theme document — the hero
     needs it regardless of which theme is active. */
  --mat-regular-dark: rgba(27, 25, 22, 0.68);
  --mat-thin: rgba(255, 255, 255, 0.62);
  --mat-regular: rgba(243, 239, 235, 0.74);     /* Titan cream, translucent */
  --mat-thick: rgba(255, 255, 255, 0.9);
  --mat-screen: rgba(255, 255, 255, 0.84);      /* full-screen takeover: the step past thick */
  --mat-blur: saturate(180%) blur(20px);
  --mat-blur-soft: saturate(180%) blur(12px);   /* chips: popovers, menus, capsules */
  --mat-blur-strong: saturate(180%) blur(30px);
  --mat-hairline: rgba(17, 17, 17, 0.09);
  /* Titan has no lit top edge; the hairline IS the edge. Neutralised rather
     than removed so every `box-shadow: ..., var(--mat-hi)` stays valid. */
  --mat-hi: inset 0 0 0 0 transparent;
}
:root[data-theme="dark"] {
  --mat-thin: rgba(33, 31, 27, 0.64);
  --mat-regular: rgba(27, 25, 22, 0.68);
  --mat-thick: rgba(16, 15, 13, 0.86);
  /* more opaque than light on purpose: --bg is near-black in dark, so a translucent
     takeover over it needs more body to keep the search field legible. */
  --mat-screen: rgba(16, 15, 13, 0.88);
  --mat-hairline: rgba(255, 255, 255, 0.09);
  --mat-hi: inset 0 0 0 0 transparent;
}

/* The blur ramp is ordered by surface size, not by z-order (apple-design §12: "bigger
   surfaces should read as thicker"). chip 12px < bar 20px < sheet/screen 30px. A 254px
   menu is a small chip next to a full-width bar and a full-viewport sheet, so it sits
   below both — even though it floats above them. */
/* nav bars: material + hairline + inner highlight, NO drop shadow over glass */
header.top, .mini-head {
  background: var(--mat-regular);
  -webkit-backdrop-filter: var(--mat-blur); backdrop-filter: var(--mat-blur);
  border-bottom: 1px solid var(--mat-hairline); box-shadow: var(--mat-hi);
}
/* the search + ••• capsule: thin material, chip tier */
.hw-cap {
  background: var(--mat-thin);
  -webkit-backdrop-filter: var(--mat-blur-soft); backdrop-filter: var(--mat-blur-soft);
  box-shadow: var(--mat-hi), 0 0 0 0.5px var(--mat-hairline);
}
/* floating popovers/menus: thick material + a whisper of ambient shadow (allowed off-content).
   Thick stays thick: these carry text over scrolling content, and §12's "vibrancy keeps text
   legible" outranks its "lighter materials draw attention" for a surface with copy on it. */
/* THESE ONES ARE OPAQUE, and the group's own comment above is the argument for it:
   "vibrancy keeps text legible" outranks "lighter materials draw attention" for a surface
   with copy on it. `--mat-thick` is 0.9 in light and 0.86 in dark, and 0.86 of anything is
   not opaque. Measured with a maximum-contrast striped backdrop injected behind the open
   search dropdown and compared against the same pixels with the stripes removed: the
   backdrop moved the popover's interior by a mean of 9 levels in light and 12.9 in dark,
   peaking at 34. That is Miraaj reading the page through the alerts popover.

   `--surface` is the token the no-backdrop-filter and reduced-transparency fallbacks below
   already use for exactly these selectors, so this is the appearance the design has always
   accepted for them — it is now the appearance everyone gets. The blur goes with it: over
   an opaque fill it samples a backdrop nobody can see and still promotes a layer. Bars and
   the capsule keep their glass; they carry no copy over scrolling content. */
.hw-menu, .ac-dropdown, .notif-dropdown, .acct-menu, .tip-pop {
  background: var(--surface) !important;
  border: 1px solid var(--mat-hairline);
  box-shadow: var(--mat-hi), 0 12px 34px -12px rgba(0, 0, 0, 0.5);
}
/* The bar yields to a popover that reaches it — see syncBarUnderPopover() in app.js for
   why the popover cannot simply be raised above it. Transform only, so no layout shift. */
.pnav-bar { transition: transform var(--dur-2, 0.22s) var(--ease-out, ease); }
body.pop-over-bar .pnav-bar { transform: translateY(100%); pointer-events: none; }
/* Any other floating control simply gets out of the way — it has nowhere to slide to, and
   fading it is honest: it is not reachable while something is open over it anyway. */
.yields-to-overlay:not(.pnav-bar) { opacity: 0; pointer-events: none; }
.hw-cap { transition: opacity var(--dur-2, .2s) var(--ease-out, ease); }
@media (prefers-reduced-motion: reduce) { .pnav-bar { transition: none; } }
/* modal (auth) + glossary bottom sheet card */
.modal, .tip-sheet-card {
  background: var(--mat-thick);
  -webkit-backdrop-filter: var(--mat-blur-strong); backdrop-filter: var(--mat-blur-strong);
  box-shadow: var(--mat-hi), 0 24px 60px -20px rgba(0, 0, 0, 0.55);
}
/* --- fallbacks: no backdrop-filter → opaque; reduced-transparency → opaque, no blur --- */
@supports not ((backdrop-filter: blur(1px)) or (-webkit-backdrop-filter: blur(1px))) {
  header.top, .mini-head, .hw-cap { background: var(--surface); }
  .modal, .tip-sheet-card { background: var(--surface-2) !important; }   /* the popovers are opaque by default now */
  /* The marketing cards: without a backdrop-filter the 72% mix reads as a washed-out panel
     over the page background rather than as glass, so they go solid like the chips do. */
  .hero-wedge li, .wp-col { background: var(--surface) !important; }
  /* .modal-backdrop needs nothing here: an unsupported backdrop-filter is simply ignored
     and the scrim alone still reads as a modal. */
}
@media (prefers-reduced-transparency: reduce) {
  header.top, .mini-head, .hw-cap,
  .modal, .tip-sheet-card, .modal-backdrop, .hero-wedge li, .wp-col {
    -webkit-backdrop-filter: none !important; backdrop-filter: none !important;
  }
  header.top, .mini-head, .hw-cap { background: var(--surface) !important; }
  .modal, .tip-sheet-card { background: var(--surface-2) !important; }   /* the popovers are opaque by default now */
  .hero-wedge li, .wp-col { background: var(--surface) !important; }
}
/* High contrast: near-solid backgrounds and a defined, contrasting border (apple-design
   §14 treats it as its own signal, independent of transparency and motion). This TIGHTENS
   the borders these surfaces already have; it does not add a box around the sticky header. */
@media (prefers-contrast: more) {
  header.top, .mini-head, .hw-cap, .hw-menu, .ac-dropdown, .notif-dropdown, .acct-menu,
  .tip-pop, .modal, .tip-sheet-card, .search-screen, .modal-backdrop {
    -webkit-backdrop-filter: none !important; backdrop-filter: none !important;
  }
  header.top, .mini-head, .hw-cap, .search-screen { background: var(--surface) !important; }
  .hw-menu, .ac-dropdown, .notif-dropdown, .acct-menu, .tip-pop, .modal, .tip-sheet-card {
    background: var(--surface-2) !important;
  }
  header.top, .mini-head, .hw-cap, .hw-menu, .ac-dropdown, .notif-dropdown, .acct-menu,
  .tip-pop, .modal, .tip-sheet-card { border-color: var(--text) !important; }
}

/* ============================================================================
   APPLE SEARCH SCREEN (Phase C4) — full frosted overlay: search field + Cancel,
   a Recents section when empty, live result rows with a trailing + add button.
   ========================================================================= */
.search-screen {
  position: fixed; inset: 0; z-index: 120; display: flex; flex-direction: column;
  /* full-screen takeover, above the tab bar (z-95). Translucent on purpose: a takeover
     differs from a navigation precisely in that where you came from stays faintly present.
     scale(1.01) → rest so the glass settles rather than slides (§12 "materialize, don't just
     fade"); the blur never animates — it is not compositor-friendly. */
  background: var(--mat-screen);
  -webkit-backdrop-filter: var(--mat-blur-strong); backdrop-filter: var(--mat-blur-strong);
  opacity: 0; transform: scale(1.01);
  transition: opacity 0.22s var(--ease-out), transform 0.22s var(--ease-out);
}
.search-screen.ss-in { opacity: 1; transform: none; }
body.ss-locked { overflow: hidden; }
.ss-bar {
  display: flex; align-items: center; gap: 12px; padding: 14px 16px 10px;
  padding-top: max(14px, env(safe-area-inset-top)); border-bottom: 1px solid var(--mat-hairline);
}
.ss-field {
  flex: 1 1 auto; display: flex; align-items: center; gap: 8px; min-width: 0;
  background: var(--surface-2); border-radius: var(--r-sm); padding: 9px 12px;
  /* The field is a filled box with no border, so its visual boundary IS the difference
     between its fill and the takeover behind it — and measured, that difference was
     1.12:1 in light and 1.34:1 in dark, against the 3:1 WCAG 1.4.11 asks for the boundary
     of a UI component. On the one surface whose entire job is "type here".

     An INSET shadow rather than a border: a border would grow the box by 2px and reflow
     the row, and this fix must not move anything. --faint is the weakest token already in
     the palette that clears the bar in both themes (measured 4.72 light / 4.80 dark,
     composited over the field's own fill); --line, --line-soft and --line-strong all come
     in under 2.2. No new colour, and the takeover's own material is untouched. */
  box-shadow: inset 0 0 0 1px var(--faint);
}
.ss-mag { flex: 0 0 auto; color: var(--muted); display: inline-grid; place-items: center; }
.ss-mag svg { width: 16px; height: 16px; }
.ss-input {
  flex: 1 1 auto; min-width: 0; appearance: none; border: none; background: none; outline: none;
  color: var(--text); font-family: var(--sans); font-size: 17px; letter-spacing: -0.01em;
}
.ss-input::placeholder { color: var(--faint); }
.ss-input::-webkit-search-cancel-button { display: none; }
.ss-clear { flex: 0 0 auto; appearance: none; border: none; background: none; cursor: pointer; color: var(--muted); display: inline-grid; place-items: center; padding: 0; }
.ss-cancel {
  flex: 0 0 auto; appearance: none; border: none; background: none; cursor: pointer;
  color: var(--accent); font-size: 17px; padding: 4px 2px;
}
.ss-body { flex: 1 1 auto; overflow-y: auto; -webkit-overflow-scrolling: touch; padding-bottom: 40px; }
.ss-lab { font-size: 12px; letter-spacing: 0.05em; text-transform: uppercase; color: var(--muted); padding: 16px 20px 6px; font-family: var(--mono); }
.ss-hint, .ss-empty { color: var(--muted); font-size: 15px; padding: 28px 22px; text-align: center; line-height: 1.5; }
.ss-list { display: flex; flex-direction: column; }
.ss-row {
  display: grid; grid-template-columns: minmax(0, 1fr) auto auto; align-items: center; gap: 12px;
  padding: 12px 20px; cursor: pointer; border-bottom: 1px solid var(--line);
  transition: background var(--dur-1);
}
.ss-row:active { background: var(--surface); }
.ss-id { min-width: 0; }
.ss-sym { font-family: var(--sans); font-size: 18px; font-weight: 700; letter-spacing: -0.01em; color: var(--text); line-height: 1.15; }
.ss-nm { font-size: 13.5px; color: var(--muted); margin-top: 1px; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; max-width: 100%; }
.ss-ex { font-family: var(--mono); font-size: 11.5px; color: var(--faint); white-space: nowrap; }
.ss-add {
  flex: 0 0 auto; width: 30px; height: 30px; border-radius: 50%; cursor: pointer;
  border: 1px solid var(--line-strong); background: none; color: var(--text);
  display: grid; place-items: center; transition: background var(--dur-1), border-color var(--dur-1);
}
.ss-add:hover { background: color-mix(in srgb, var(--text) 8%, transparent); }
.ss-add.on { border-color: transparent; background: var(--text); color: var(--bg); }
@media (prefers-reduced-motion: reduce) {
  .search-screen { transition: none; }
}
@supports not ((backdrop-filter: blur(1px)) or (-webkit-backdrop-filter: blur(1px))) {
  .search-screen { background: var(--bg); }
}
@media (prefers-reduced-transparency: reduce) {
  .search-screen { -webkit-backdrop-filter: none !important; backdrop-filter: none !important; background: var(--bg) !important; }
}

/* ---------------- advanced chart (candles + volume + studies) ----------------
   The panel sits inside a collapsed <details>, so it must lay out correctly the
   first time it is measured — the chart library is told its width by a
   ResizeObserver on .adv-plot rather than by a window resize listener. */
.adv-panel { padding: 14px 14px 10px; }
.adv-head {
  display: flex; align-items: baseline; justify-content: space-between; gap: 12px;
  margin-bottom: 10px;
}
.adv-title { font-family: var(--sans); font-size: var(--t-body); font-weight: 650; color: var(--text); }
.adv-state { font-size: var(--t-micro); color: var(--faint); white-space: nowrap; }

.adv-chips { display: flex; flex-wrap: wrap; gap: 6px; margin: 10px 0 8px; }
.adv-chip {
  font-family: var(--mono); font-size: var(--t-micro); letter-spacing: 0.01em;
  padding: 5px 10px; border-radius: var(--r-full); cursor: pointer;
  border: 1px solid var(--line); background: none; color: var(--muted);
  display: inline-flex; align-items: center; gap: 5px;
  transition: background var(--dur-1), border-color var(--dur-1), color var(--dur-1);
}
.adv-chip:hover { border-color: var(--line-strong); color: var(--text); }
.adv-chip:focus-visible { outline: 2px solid var(--text); outline-offset: 2px; }
.adv-chip.on { background: var(--text); border-color: transparent; color: var(--bg); }
/* Locked and unavailable read differently ON PURPOSE: one is a paywall, the other is
   honestly not enough data. Same greyed weight would conflate them. */
.adv-chip.locked { color: var(--faint); border-color: var(--line-strong); }
.adv-chip.unavailable { color: var(--faint); opacity: 0.55; }
.adv-chip-pro {
  font-size: 8.5px; font-weight: 700; letter-spacing: 0.08em;
  padding: 1px 4px; border-radius: var(--r-sm); background: var(--line); color: var(--muted);
}

.adv-say { font-size: var(--t-micro); color: var(--faint); margin: 0 0 8px; min-height: 14px; }

.adv-plot { width: 100%; height: 320px; }
.adv-osc { width: 100%; height: 120px; margin-top: 6px; }
.adv-empty {
  display: grid; place-items: center; height: 100%; min-height: 160px; text-align: center;
  font-size: var(--t-small); line-height: 1.6; color: var(--muted); padding: 0 16px;
}
.adv-lock { margin-top: 12px; }
.adv-note {
  margin: 12px 0 0; font-size: var(--t-micro); line-height: 1.45; color: var(--faint);
  border-top: 1px solid var(--line-soft); padding-top: 10px;
}
.seg.adv-ranges { margin-bottom: 10px; }

@media (max-width: 560px) {
  .adv-panel { padding: 12px 10px 8px; }
  .adv-plot { height: 260px; }
  .adv-osc { height: 100px; }
  .adv-head { flex-direction: column; align-items: flex-start; gap: 2px; }
}

/* ============================================================================
   POLISH PASS 1 — THE PRO-LOCK FAMILY
   ---------------------------------------------------------------------------
   The gate grew organically: five surfaces each invented their own lock, so a
   phone scrolling one company view met five different "this is Pro" designs —
   a frosted card, a blurred overlay, a bare strip, a chip-sized button and a
   full-width bar — with four different button shapes between them. Nothing was
   broken; it just read as five products stitched together.

   This section does not add a sixth. It states the ONE lock vocabulary and
   forces the existing variants onto it. Two idioms survive, because they say
   different things:

     (a) THE LOCK CARD (.lock-card) — replaces content outright. Used where we
         have nothing honest to show.
     (b) THE BLURRED TEASER (.locked / .locked-blur / .locked-cta) — shows the
         SHAPE of what is withheld behind a blur. Used where the layout itself
         is informative.

   What is unified across both: the CTA (one pill), the title (one size and
   family), the supporting copy (one size and colour), and the press feedback.
   Nothing here changes what any control DOES — every CTA still calls the same
   Billing.upgrade(); this is shape, type and rhythm only.
   ========================================================================== */
:root {
  /* the thumb floor — Apple's HIG minimum, and the reason a mis-tap on an
     upgrade button is a lost sale rather than a cosmetic complaint */
  --tap: 44px;
  --tap-desk: 38px;
  /* the Pro-lock scale, so a title is the same size on every surface */
  --fs-lock-title: 16px;
  --fs-lock-copy: 13.5px;
  --fs-lock-sub: 12px;
  --pad-lock: 18px 20px;
}

/* ---- ONE Pro CTA: an inline pill, identical on every surface --------------
   .lock-btn used to be a soft rectangle carrying a drop shadow (--el-2), which
   made it float above the very card it belonged to; .mini-upgrade was already a
   pill but two sizes smaller; .promo-cta was an outline rectangle. They now all
   render as the same pill and differ only in emphasis. */
.lock-card-cta,
.lock-btn,
.mini-upgrade,
.pf-analytics-lock .lock-btn {
  appearance: none; border: none; cursor: pointer;
  display: inline-flex; align-items: center; justify-content: center;
  background: var(--accent); color: var(--bg);
  font-family: var(--mono); font-weight: 700; letter-spacing: 0.02em;
  border-radius: var(--r-full);
  min-height: var(--tap-desk);
  box-shadow: none;
  transition: filter var(--dur-1) var(--ease-out),
              transform var(--dur-1) var(--ease-out),
              box-shadow var(--dur-2) var(--ease-out);
}
.lock-card-cta, .lock-btn { font-size: 13px; padding: 11px 18px; }
.mini-upgrade { font-size: 12px; padding: 7px 14px; min-height: 32px; }
.lock-card-cta:hover, .lock-btn:hover, .mini-upgrade:hover {
  filter: brightness(1.08); transform: translateY(-1px);
}
.lock-card-cta:active, .lock-btn:active, .mini-upgrade:active {
  transform: scale(0.97); filter: brightness(0.94);
}
/* the .lock-btn inside a blurred teaser sat 10px under a centred stack; the
   rhythm now matches the card's, so the two idioms feel like one family */
.locked-cta .lock-btn { margin-top: 14px; }
.upgrade-card .lock-btn { margin-top: 18px; }

/* ---- ONE lock voice: same title, same copy, on both idioms ---------------- */
.lock-card-title,
.locked-cta .lock-title {
  font-family: var(--sans); font-size: var(--fs-lock-title); font-weight: 700;
  letter-spacing: var(--track-tight); line-height: 1.25; color: var(--text);
}
.lock-card-copy,
.locked-cta .lock-sub {
  font-size: var(--fs-lock-copy); line-height: 1.45; color: var(--muted);
  margin-top: 4px; max-width: 44ch;
}
.lock-card-sub { font-size: var(--fs-lock-sub); line-height: 1.45; color: var(--faint); margin-top: 6px; }
/* the blurred idiom kept an empty .lock-ico div reserving 26px of dead space
   where an emoji used to be (emoji were removed in v1.83). Give it the same
   rounded lock glyph the card uses, rather than a hole. */
.locked-cta .lock-ico {
  width: 40px; height: 40px; border-radius: var(--r-sm);
  display: grid; place-items: center; margin-bottom: 2px;
  background: color-mix(in srgb, var(--text) 8%, transparent);
  font-size: 0;
}
/* the same padlock lockIcon() draws in JS, as a mask so it inherits --text */
.locked-cta .lock-ico::before {
  content: ""; display: block; width: 20px; height: 20px; background: var(--text);
  --lock-glyph: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24'%3E%3Crect x='5' y='10.5' width='14' height='9.5' rx='2.2' fill='none' stroke='%23000' stroke-width='1.6'/%3E%3Cpath d='M8 10.5V8a4 4 0 0 1 8 0v2.5' fill='none' stroke='%23000' stroke-width='1.6'/%3E%3C/svg%3E");
  -webkit-mask: var(--lock-glyph) center / contain no-repeat;
  mask: var(--lock-glyph) center / contain no-repeat;
}

/* ---- the bespoke memo / horizon locks are gone (they now call lockCard()),
        and the strip-style locks adopt the card's own surface ---------------- */
.pf-analytics-lock {
  padding: var(--pad-lock);
  background: var(--surface-2);
  -webkit-backdrop-filter: var(--mat-blur); backdrop-filter: var(--mat-blur);
  box-shadow: var(--mat-hi);
  font-size: var(--fs-lock-copy); color: var(--muted);
}
.pro-strip .lock-btn { flex: 0 0 auto; }
.scr-upsell { font-size: var(--fs-lock-sub); }

/* ---- mobile: the CTA is a full-width bar, and it clears the thumb floor ---- */
@media (max-width: 560px) {
  .lock-card-cta, .lock-btn, .pf-analytics-lock .lock-btn {
    min-height: var(--tap); width: 100%;
  }
  .mini-upgrade { min-height: 36px; }
  /* THE 356px LOCK CARD. Two rules disagreed and the later one won by accident:
     the mobile block asked for `flex-direction: column; align-items: stretch`,
     but the base `.lock-card` rule sits ~430 lines further down and re-asserted
     `align-items: center`. The card therefore stacked in a column while centring
     its children, and `.lock-card-body { flex: 1 1 220px }` — written for the ROW
     layout, where 220px is a width — became a 220px MINIMUM HEIGHT once the main
     axis turned vertical. Result: a 315×356px card holding four lines of text,
     its lock glyph floating in the middle, repeated seven times down the highest-
     traffic page on a phone. Measured, not eyeballed. */
  .lock-card {
    flex-direction: column; align-items: flex-start; text-align: left;
    padding: 16px; gap: 12px;
  }
  .lock-card-body { flex: 0 1 auto; width: 100%; }
  .lock-card-ic { align-self: flex-start; width: 34px; height: 34px; border-radius: var(--r-sm); }
  .locked-cta { padding: 18px 16px; }
  .locked-cta .lock-ico { width: 34px; height: 34px; border-radius: var(--r-sm); }
}

/* ============================================================================
   POLISH PASS 1 — LANDING RHYTHM (the screen influencer traffic lands on)
   ---------------------------------------------------------------------------
   Measured at 375px, the pitch had three rhythm faults that made an otherwise
   careful page feel unfinished:

   1. The wedge list ended with margin-bottom: 0, so "It admits doubt…" collided
      with the search field — the single most important control on the page had
      less air above it than the bullets had between them.
   2. The free-quota strip was an inline-FLEX pill, so its sentence broke into
      anonymous flex items and centred itself into a ragged two-line block with
      "Go Pro" stranded on the right. It reads as a sentence, so it should lay
      out as one.
   3. The two utility links sat inside the hint sentence separated by &nbsp;·&nbsp;,
      which gave a tappable target the same weight as body copy and no hit area.

   Rhythm here follows the existing --s-* scale rather than new numbers.
   ========================================================================== */

/* 1 — the wedge breathes into the search, and the search is the loudest thing
       on the page rather than the thing squeezed under a list */
.hero-wedge { margin: var(--s-5) 0 var(--s-6); }
.hero .big-search { margin-top: 0; }
.hero .big-search input,
.hero .big-search button { min-height: var(--tap); }

/* 2 — the quota strip lays out as the sentence it is */
.free-meter {
  display: inline-block; line-height: 1.6; padding: 7px 14px;
  text-align: left; max-width: 100%;
}
.free-meter .pro-badge, .free-meter b { vertical-align: baseline; }
.free-meter a { text-decoration: underline; text-underline-offset: 2px; }
.free-meter a:hover { text-decoration-thickness: 2px; }

/* 3 — the utility links become real targets on their own line */
/* Sits OUTSIDE .hero (see index.html) so it survives the Pro pitch-hiding, which means it
   also has to carry its own spacing on both sides — for a Pro member it is the FIRST visible
   thing in the Discover pane, with no hero above it. */
.hint-links {
  display: flex; flex-wrap: wrap; gap: var(--s-2);
  margin-top: var(--s-3); margin-bottom: var(--s-3);
}
.hero.hidden-for-pro + .hint-links { margin-top: var(--s-5); }
.hint-links .screener-link {
  font-family: var(--mono); font-size: 12px; font-weight: 600;
  display: inline-flex; align-items: center; min-height: 34px;
  padding: 0 13px; border-radius: var(--r-full);
  border: 1px solid var(--line); background: var(--surface);
  /* Some of these are real hrefs now (the worked example, pricing) and some are Router
     calls. `color` is stated so a chip looks identical either way — without it the linked
     ones picked up the global blue and the row read as two different kinds of control. */
  color: var(--text); text-decoration: none;
  transition: border-color var(--dur-2) var(--ease-out),
              background var(--dur-2) var(--ease-out),
              transform var(--dur-1) var(--ease-out);
}
.hint-links .screener-link:hover {
  /* fill removed — the state matrix owns it. The `background` shorthand here was the
     second instance of the defect that cost the desktop nav its hover: it resets
     background-image, so it did not sit alongside the wash, it deleted it. */
  text-decoration: none; border-color: var(--line-strong);
}
.hint-links .screener-link:active { transform: scale(0.97); }

/* the two stat lines are one thought, so they read as one block rather than as
   two unrelated mono sentences with a full line of dead space between them */
.hero { padding-bottom: var(--s-2); }        /* usage-stat is the hero's last child */
.usage-stat { margin-top: var(--s-4); margin-bottom: 0; }
.analysed-stat { margin: 2px 0 var(--s-4); }

@media (max-width: 560px) {
  .hero { padding-top: var(--s-6); }
  .hero-wedge { margin-bottom: var(--s-5); }
  .hint-links .screener-link { min-height: var(--tap); flex: 1 1 auto; justify-content: center; }
  .free-meter-wrap { margin: var(--s-2) 0 var(--s-6); }
}

/* ---- no card inside a card ------------------------------------------------
   Three panels on the company view hold nothing but a lock card (the memo, the
   horizon strip, key statistics). That rendered a bordered, frosted card inside
   a bordered, frosted panel — two rounded rectangles 16px apart saying the same
   thing. Where the lock IS the whole panel, the panel steps aside and the lock
   card becomes the surface. Where the panel also carries a heading or real
   content, both keep their chrome, because there the nesting means something.
   Falls back to the (harmless) double border wherever :has() is unsupported. */
.panel:has(> .lock-card:only-child) {
  background: none; border: none; box-shadow: none; padding: 0;
  -webkit-backdrop-filter: none; backdrop-filter: none;
}
.panel:has(> .lock-card:only-child) > .lock-card { border-radius: var(--r); }

/* ============================================================================
   POLISH PASS 2 — WAITING, EMPTINESS, AND THE HEADER
   ---------------------------------------------------------------------------
   Every async panel on the company view mounted an empty bordered box and then
   jumped to full height when its request landed — a dozen small reflows on a
   phone, on the screen that matters most. Skeletons make the page settle once.

   Three separate "nothing to show" designs also existed (.err-card.upgrade-card,
   a bare .empty-state, and a centred spinner), so signing out looked like a
   different product on the dashboard than on the portfolio. emptyState() now
   shares the lock card's geometry: same tile, same title, same CTA pill. A page
   that shows one of each still reads as one product.
   ========================================================================== */

/* ---- skeletons ------------------------------------------------------------
   .sk already carries the reduced-motion opt-out (see MOTION OFF), so a visitor
   who has asked for stillness gets a static grey block rather than a pulse. */
.sk-wrap { display: grid; gap: 9px; }
.sk {
  background: color-mix(in srgb, var(--text) 7%, transparent);
  border-radius: var(--r-sm);
}
.sk-line { height: 12px; }
.sk-line.sk-head { height: 15px; border-radius: var(--r-sm); margin-bottom: 3px; }
.sk-block { border-radius: var(--r-sm); margin: 3px 0 6px; }
/* the pulse is deliberately slow and shallow: a skeleton that strobes reads as
   an error state, and this one may sit on screen for a second on a cold cache.
   Defined ONCE, next to `.sk, .skeleton` — a second @keyframes of the same name
   used to live here and silently overrode it with a 55% swing, so the "shallow"
   this comment promised was never what shipped. */

/* ---- empty states: the lock card's twin ----------------------------------- */
.empty-card { background: var(--surface); }
.empty-card .empty-ic { color: var(--muted); }
/* an empty state is an invitation, not a warning — it never borrows the lock's
   frosted material, which on this product means "you cannot have this" */
.empty-card {
  -webkit-backdrop-filter: none; backdrop-filter: none;
  border: 1px solid var(--line-soft); box-shadow: none;
}

/* ---- header: the primary action must never break in half ------------------
   Was: `.search button { white-space: nowrap; flex: 0 0 auto }`, because "Value it"
   wrapped onto two lines at 375px. A glyph cannot wrap, and `.search-go` already
   declares the same `flex: 0 0 auto` itself — so both halves were describing a
   label that no longer exists. See the note at `.search-go`'s old skin above. */
.search input { text-overflow: ellipsis; }
@media (max-width: 620px) {
  .search { padding: 6px 6px 6px 14px; }
  /* `.search button { min-height: var(--tap); padding: 0 16px }` lived here and was the
     same misalignment as the desktop one, worse: 16px of side padding on a round disc,
     at the width where there is least room to absorb it. The touch floor it was also
     setting is not lost — `.search-go` takes the full var(--tap) square under
     `@media (pointer: coarse)`, which is the more accurate gate anyway: a small viewport
     is not the same thing as a finger. */
  .search input { min-height: var(--tap); }
}

/* ---- short pages: the footer sits at the bottom, not mid-screen ------------
   The signed-out portfolio and dashboard are a heading and one card, so the
   footer landed two-thirds up with a field of empty ground beneath it, which
   reads as a page that failed to finish loading. */
body { min-height: 100svh; display: flex; flex-direction: column; }
/* `.wrap` centres itself with `margin: 0 auto`. Inside a flex COLUMN that auto margin
   applies to the cross axis, which makes the box shrink-to-fit and centre — main
   collapsed to 146px on a loading page. An explicit width restores the block behaviour
   the auto margins were written for. */
body > main, body > .footer { width: 100%; flex: 0 0 auto; }
body > main { flex: 1 0 auto; }
body > .footer { margin-top: auto; }

/* ---- the route loader reserves a screen -----------------------------------
   Every route rendered a 28px spinner into an otherwise empty page, so the
   footer sat mid-viewport and then got shoved a full screen down the moment
   content arrived. Measured on the company view: ONE shift worth 0.92 of a
   1.13 CLS — the spinner, not the content, was the layout-shift budget.
   The loader now reserves the fold; the mount grows the page downward instead
   of moving anything already on screen. */
.loader { min-height: 58svh; display: grid; align-content: center; }
.view-skel { display: grid; gap: 12px; padding: 26px 0 0; min-height: 60svh; }
.view-skel .panel { padding: 18px 16px; }
.view-skel .sk-line { border-radius: var(--r-sm); }
/* visually-hidden text for screen readers — the skeleton is aria-hidden decoration,
   so the "Valuing AAPL…" status still has to be announced somewhere */
.sr-only {
  position: absolute; width: 1px; height: 1px; padding: 0; margin: -1px;
  overflow: hidden; clip: rect(0 0 0 0); clip-path: inset(50%); white-space: nowrap; border: 0;
}

/* ============================================================================
   POLISH PASS 3 — PRIMARY NAVIGATION
   ---------------------------------------------------------------------------
   Six routable areas had one entrance between them: a ••• popover that only
   existed on the home screen. A visitor who landed on the pitch could not tell
   that a watchlist or a portfolio existed, and once they navigated away the
   popover left with them. This is the wayfinding layer.

   ONE definition, two hosts (see renderPrimaryNav): a fixed frosted tab bar on
   the bottom edge of a phone, a slim inline row in the sticky header on desktop.
   Exactly one is visible at a time.

   MONOCHROME ACTIVE STATE. Apple tints the active tab; we have no brand hue to
   tint with, so the active tab is carried by three cues at once — label colour
   steps from tertiary to label, the icon gains weight via a filled backing, and
   a hairline rides above it. Colour is never the only signal, which is also what
   keeps this legible for anyone who cannot separate the two greys.
   ========================================================================== */
.pnav { display: flex; }
/* INACTIVE LABELS ARE `--muted`, NOT `--faint`. Measured against the real hero footage:
   over the clip's brightest frame the bar composites to a mid grey, and `--faint` labels
   land at 4.23:1 — under AA. `--muted` gives 5.42:1 over the same frame, and 6.49:1 over
   the poster.

   Chosen over the two alternatives because it is the only one that helps AWAY from the
   hero. A scrim behind the bar, or moving `header.top` up a material tier, would each exist
   to serve one page and leave chrome that has to be remembered and suppressed on every
   other. This makes the nav more legible everywhere there is a nav — including over the
   ordinary page background, where it goes 5.54:1 -> higher.

   The active-tab treatment is unchanged and still carried by three cues (colour step, icon
   backing, hairline), so lifting the inactive colour narrows the gap without removing it. */
/* Four of the five are now <a href> rather than <button>, so this resets both element types:
   `text-decoration` and the visited colour are the two an anchor brings that a button did not,
   and either would show as a change in a bar nobody asked to change. */
.pnav-item {
  appearance: none; background: none; border: none; cursor: pointer;
  text-decoration: none;
  display: flex; align-items: center; justify-content: center;
  color: var(--muted); font-family: var(--sans);
  transition: color var(--dur-2) var(--ease-out),
              background var(--dur-2) var(--ease-out),
              transform var(--dur-1) var(--ease-out);
}
.pnav-item:visited { color: var(--muted); }
.pnav-item.is-on:visited { color: var(--text); }
.pnav-item:active { transform: scale(0.94); }
.pnav-item.is-on { color: var(--text); }
.pnav-ic { display: grid; place-items: center; }
.pnav-lab { letter-spacing: 0.01em; }

/* ---- phone: the bottom tab bar -------------------------------------------- */
.pnav-bar {
  position: fixed; left: 0; right: 0; bottom: 0; z-index: 60;
  justify-content: space-around; align-items: stretch;
  padding-bottom: env(safe-area-inset-bottom, 0px);
  background: var(--mat-thick);
  -webkit-backdrop-filter: var(--mat-blur-strong); backdrop-filter: var(--mat-blur-strong);
  border-top: 1px solid var(--mat-hairline);
}
.pnav-bar .pnav-item {
  flex: 1 1 0; flex-direction: column; gap: 3px;
  /* 52px + the label's line box clears the 44px thumb floor with room to spare,
     and the button fills its whole column so the tap target is the full cell */
  min-height: 52px; padding: 7px 2px 8px; position: relative;
}
.pnav-bar .pnav-lab { font-size: 10.5px; font-weight: 600; }
.pnav-bar .pnav-ic { width: 24px; height: 24px; }
.pnav-bar .pnav-item.is-on .pnav-ic {
  /* the third cue: a soft backing behind the active glyph, not a colour change.
     A DISC, not a rounded square, and that is a measured correction rather than taste. The
     box-shadow here is not a shadow — it is the same colour as the fill, spread 5px to grow a
     24px icon box into a 34px backing without touching layout. But a spread ring paints at
     ELEMENT RADIUS + SPREAD, so `--r-sm` (6px) was painting an 11px corner: a value the CSS
     never names and that is not one of Titan's steps (0 / 6 / 20 / 32 / 140 / 160 / full).
     The declared radius was legal and the painted one was not.
     `--r-full` is immune to the arithmetic — a circle plus a spread is still a circle — so it
     lands on a kit step by construction, keeps the 34px backing and the layout identical, and
     is the treatment Titan already gives discs and avatars. */
  background: color-mix(in srgb, var(--text) 12%, transparent);
  border-radius: var(--r-full); box-shadow: 0 0 0 5px color-mix(in srgb, var(--text) 12%, transparent);
}
.pnav-bar .pnav-item.is-on::before {
  content: ""; position: absolute; top: 0; left: 50%; transform: translateX(-50%);
  width: 26px; height: 2px; border-radius: 0 0 var(--r-full) var(--r-full); background: var(--text);
}
/* content must clear the bar — including the footer, which is the last thing on a page */
/* The header row costs ~330px of horizontal space once it carries three destinations
   plus a usable search field. Below 900px there isn't room for all of it without
   squeezing the search into uselessness, so the bottom bar keeps going — which is the
   better control on a tablet held in two hands anyway. */
@media (max-width: 899px) {
  body { padding-bottom: calc(60px + env(safe-area-inset-bottom, 0px)); }
  .pnav-desk { display: none; }
  /* signed OUT, the header's only content is a Sign in pill that the Account tab now
     covers exactly. Signed IN it still carries the alert bell and the account menu, so
     it stays. `:has()` makes the distinction; without it the header simply keeps both. */
  header.top .account:not(:has(.acct-wrap)) { display: none; }
}

/* ---- desktop: a slim row in the header ------------------------------------ */
@media (min-width: 900px) {
  .pnav-bar { display: none; }
  .pnav-desk { gap: 2px; margin-left: 22px; }
  .pnav-desk .pnav-item {
    gap: 8px; min-height: var(--tap-desk); padding: 0 13px; border-radius: var(--r-full);
    font-size: 13.5px; font-weight: 550;
  }
  .pnav-desk .pnav-ic { width: 17px; height: 17px; }
  .pnav-desk .pnav-ic svg { width: 17px; height: 17px; }
  /* REMOVED, and this one mattered: at (0,3,0) it out-specified the state matrix, and
     its `background` SHORTHAND reset background-image to none — so it did not merely
     duplicate the wash, it deleted it, on four of the five desktop nav items. The
     matrix covers every width and the coarse-pointer block handles touch. */
  .pnav-desk .pnav-item.is-on { background: color-mix(in srgb, var(--text) 9%, transparent); }
  /* the account chip already lives at the far right of the header — a second one in
     the nav row would be the clutter this pass exists to remove */
  .pnav-desk .pnav-account { display: none; }
  /* Search is a primary destination, so on desktop it is always in the header — the
     Apple-home rule that hides it belongs to the phone layout, where the home screen
     has its own search capsule and the Discover tab is one thumb away. */
  body:has(#home:not(.hidden)) header.top .search { display: flex; }
  .top-inner { gap: 12px; }
}
/* below the header-nav breakpoint the bar is in charge, and the header keeps its
   original Apple-home behaviour (search revealed by the home's own capsule) */
@media (min-width: 900px) {
  .pnav-desk { display: flex; }
}

/* the mobile home's own search/••• capsule is now one of two ways in, not the only
   one; nothing about it changes, but the bar must not sit under the stories sheet
   or the edit-mode footer if either returns */
.hw-cap, .hw-menu { z-index: 61; }

/* ---- header fit: the nav row costs horizontal space, so nothing may wrap ----
   Adding four destinations to the header squeezed two labels into two lines each
   ("DCF · INTRINSIC / VALUE" and "Sign / in"). Both are single-line by nature;
   the tagline is the one piece of pure decoration up there, so it is the first
   thing to go when the row gets tight rather than the first thing to break. */
@media (min-width: 900px) {
  .brand .tag, .signin-btn, .acct-btn { white-space: nowrap; }
  /* the field may shrink; 260px of unshrinkable minimum was what pushed the account
     button off the right edge at the narrow end of the desktop range */
  .search { flex: 1 1 220px; min-width: 0; }
}
/* MEASURED 2026-08-19, signed in, at seven widths. The row overflowed at 1440, 1200, 1024,
   1000 and 900 — everywhere except 1100 and 768 — for TWO different reasons, which is why
   one breakpoint could never have fixed it:

     wide (>=1200)  content 1248px against `.top-inner`'s 1120px cap. The viewport is not
                    the constraint; the site's own content width is. The tagline is 183px of
                    that, so it has to go before the cap bites, not at 1150.
     900-1150       content 1016-1028px against the viewport itself. The nav's five LABELS
                    are ~315px of it, and the desktop nav appears at exactly 900px — so its
                    worst case is its own breakpoint.

   The tagline is pure decoration and goes first, as this block already argued. The labels
   are the next cheapest thing: the icons carry the same five destinations, `aria-label`
   keeps the accessible name, and a row that fits beats a row that reads slightly faster. */
/* At every width where the desktop nav is present. There is no width at which the tagline
   fits: the row needs 1248px with it and `.top-inner` is capped at 1120px to match the
   site's content width, so a wider viewport buys nothing. Below 900px the phone bar takes
   the destinations and the tagline comes back. */
@media (min-width: 900px) {
  .brand .tag { display: none; }
}
@media (min-width: 900px) and (max-width: 1319px) {
  .pnav-desk { margin-left: 14px; }
  .pnav-desk .pnav-item { padding: 0 10px; }
}
@media (min-width: 900px) and (max-width: 1149px) {
  /* icons only — the labels are what does not fit here */
  .pnav-desk .pnav-lab { display: none; }
  .pnav-desk .pnav-item { padding: 0 9px; }
}

/* On a COARSE pointer the DESKTOP nav row is what an iPad gets — six 38px targets under
   the 44px floor. The phone bar was already over it, which is why every sweep at 390
   reported the nav clean; this is only visible with hasTouch at a tablet width, not at
   any viewport width on its own. `.pnav-desk .pnav-item` sets --tap-desk at (0,2,0), so
   the override has to match that specificity — a bare `.pnav-item` loses regardless of
   where in the file it sits.
   HEIGHT ONLY. The icon-only row at 1024 is 37px WIDE, and widening six items to 44 adds
   42px to a header that is already the tightest thing in the layout at that width. That
   is a layout decision, not an accessibility floor: 37x44 clears WCAG 2.5.8 AA (24x24)
   and misses only this product's own AAA-level 44px target. */
@media (pointer: coarse) {
  .pnav-item, .pnav-desk .pnav-item { min-height: var(--tap); }
}

/* ---- de-TradingView: belt and braces --------------------------------------
   `layout.attributionLogo: false` removes the badge at the source. This is the
   backstop in case a future version of the library reintroduces it under a new
   default — a chart in this product must never carry another company's mark.
   The Apache-2.0 obligation is the licence + NOTICE text, which is kept verbatim
   in web/assets/vendor/ alongside the script; a visual badge was never required. */
.adv-panel a[href*="tradingview" i],
.adv-panel #tv-attr-logo,
.chart-panel a[href*="tradingview" i] { display: none !important; }
/* the library's own tooltip/legend chrome adopts our type + surface */
.adv-panel table.tv-lightweight-charts { font-family: var(--mono) !important; }

/* ============================================================================
   POLISH PASS 5 — GLITCH SWEEP
   ---------------------------------------------------------------------------
   Found by walking every visible node at four viewport/theme combinations and
   flagging: content wider than the box holding it, elements left mid-transform,
   bordered boxes with nothing in them, and interactive targets under the thumb
   floor. Only genuine defects are fixed here — the rotated accordion chevron and
   the parked mini-header both showed up as "mid-transform" and are meant to be.
   ========================================================================== */

/* 1 — THE SPARKLINE SPILLED OUT OF ITS CELL. `.wl-spark` is a grid cell of
       minmax(48px, 88px) but the sparkline is drawn at a fixed 76-140px, and the
       cell has no overflow rule — so on a narrow row the chart drew straight over
       the price beside it. Measured 76px of art in a 64px cell on a phone, and
       284px of row in a 225px column in the Discover grid on desktop: two numbers
       overlapping is exactly what "glitchy" looks like. */
.wl-spark { overflow: hidden; }
.wl-spark svg, .wl-spark .spark { width: 100%; height: auto; max-width: 100%; display: block; }

/* 2 — THE LAST RANGE OPTION SAT OFF-SCREEN. `.seg` capped itself at 100% width
       but never said what to do when its options did not fit, so "All" was clipped
       past the right edge with no way to reach it. It scrolls now, without a
       scrollbar drawn across the control. */
.seg { overflow-x: auto; overflow-y: hidden; scrollbar-width: none; -webkit-overflow-scrolling: touch; }
.seg::-webkit-scrollbar { display: none; }
.seg-opt { flex-shrink: 0; }
.seg.chart-ranges .seg-opt, .seg.adv-ranges .seg-opt { flex: 1 0 auto; }

/* 3 — EMPTY BORDERED SLIVERS. A section whose panels all removed themselves left
       a 12px bordered sliver behind, which reads as a rendering fault rather than
       as nothing-to-show. */
.sect-inner:empty { display: none; }
.sect:has(> .sect-body > .sect-inner:empty) { display: none; }
.panel:empty, .grp:empty, .wl-list:empty { display: none; }

/* 4 — TAP TARGETS UNDER THE FLOOR. The glossary "?" is a 15px circle by design —
       it must not shout next to a heading — so it keeps its size and gains an
       invisible 32px hit area instead. Sector chips were 26px tall. */
.tip-ico { position: relative; }
.tip-ico::after {
  content: ""; position: absolute; top: 50%; left: 50%; transform: translate(-50%, -50%);
  width: 32px; height: 32px; border-radius: 50%;
}
.sector-chip { min-height: 32px; display: inline-flex; align-items: center; }
@media (max-width: 560px) {
  .tip-ico::after { width: 44px; height: 44px; }
  .sector-chip { min-height: 36px; }
}

/* 5 — the factor rows overflowed by a few pixels on a narrow phone because the
       label column was a hard 120px against a fixed 88px score column */
@media (max-width: 420px) {
  .factor-row { grid-template-columns: minmax(0, 96px) minmax(0, 1fr) minmax(0, 72px); gap: 8px; }
}

/* 6 — THE ROW STILL SPILLED IN A NARROW CARD. `.wl-row`'s sparkline column had a
       48px hard minimum and its two trailing columns were `auto`, so inside the
       Discover grid's 225px card the row laid out at 284px and its price and grade
       painted outside the card entirely. Every column may now collapse; the
       sparkline is the first thing to give, since it is the only decorative one.

       AND ONLY THE SPARKLINE. The first version of this fix said "every column may
       now collapse" and put `min-width: 0` on the quote cell too — so in the
       dashboard's 419px tile the browser starved the QUOTE track down to the width
       of a bare "—" price and the +198.94% pill escaped its own cell, painting over
       the sparkline beside it (measured: an 81px pill in a 34px track, 35px of
       painted overlap). The quote column is the two most checkable figures in the
       row; it takes a `max-content` floor — the row will widen before a price or a
       pill ever collides — and the decorative column is the ONLY one that gives.
       Same principle as `.duel`: spilling is ugly and complete, colliding numbers
       silently change what the page says. */
.wl-row { grid-template-columns: minmax(88px, 1fr) minmax(0, 88px) max-content auto; }
/* on a phone the sparkline gives up a third of its cap so a long ticker fits whole */
@media (max-width: 480px) { .wl-row { grid-template-columns: minmax(88px, 1fr) minmax(0, 56px) max-content auto; } }
/* and below a usable width the sparkline is dropped outright rather than rendered as
   a 7px sliver — a container query, because the constraint is the CARD's width (the
   Discover grid makes 225px cards at every viewport), not the viewport's */
.wl-list { container-type: inline-size; }
@container (max-width: 320px) { .wl-spark { display: none; } }
.wl-price, .wl-meta { max-width: 100%; overflow: hidden; text-overflow: ellipsis; }

/* 7 — THE HEADER SEARCH COLLAPSED BEHIND A LONG EMAIL. A signed-in Pro account chip
       renders the full address, which squeezed the search field to 111px — narrower
       than its own placeholder. The field holds a floor; the address truncates. */
@media (min-width: 900px) {
  header.top .search { min-width: 150px; }
  /* the field may go narrower than its own placeholder before the form overflows —
     the placeholder ellipses, which is preferable to a control that spills */
  header.top .search input { min-width: 40px; }
}

/* 8 — the back affordance was a 16px-tall click target on desktop.
       SCOPED to desktop on purpose: the mobile rule at the (max-width: 560px) block
       already gives it a 40px floor, and an unscoped `.back { min-height: 32px }` this
       far down the file has equal specificity and simply wins on source order — which
       SHRANK the mobile target from 40px to 32px. Measured 40 -> 32 before this was
       scoped; a "bigger tap target" fix that makes the phone target smaller is worse
       than no fix. */
@media (min-width: 561px) {
  .back { min-height: 32px; display: inline-flex; align-items: center; }
}

/* 9 — THE SIGNED-IN PHONE BAR WAS STILL TWO ROWS. The one-row fix was measured
       signed OUT; a Pro session adds the bell, the plan chip and the avatar, and
       at 390px the actions row wrapped under the brand again — 116px of sticky
       header where the signed-out bar keeps 69. Two causes, both measured:
       · the header rework's later `.top-inner { gap: var(--s-4) }` silently beat
         the old (max-width: 620px) tightening on source order, so phones were
         laying the bar out with a 20px desktop gap — these rules carry
         `header.top` so the cascade cannot re-run that race;
       · the signed-in cluster itself was ~44px too wide. Same shedding order
         Miraaj set for the desktop squeeze: LABELS GO FIRST. The plan chip is a
         label on the account button (the dashboard heading and the account menu
         both still say Pro, and the button's aria-label carries the plan), so
         below 600px it is the first thing to give; below 400px the decorative
         caret follows and the paddings tighten one step. Nothing loses its
         entrance. Measured after: one row, 69px, at 360 / 390 / 600 signed in. */
@media (max-width: 620px) {
  header.top .top-inner { gap: 10px; padding: 12px 16px; }
  header.top .top-actions { gap: 8px; }
}
@media (max-width: 600px) {
  .acct-btn .pro-badge { display: none; }
  .account { gap: 7px; }
}
@media (max-width: 400px) {
  header.top .top-inner { padding: 12px 14px; }
  .account { gap: 6px; }
  .acct-btn { padding: 6px 8px; }
  .acct-btn .acct-caret { display: none; }
}
/* at the very smallest widths the wordmark yields and the SEAL carries the brand —
   the kit's own reduction rule (favicons and avatars are the seal alone), and the
   only way a signed-in bar holds one row at 320 without shrinking a tap target */
/* The coarse bound is 359 and not 340 for the same reason as the padding rung below: the
   touch bar is ~18px wider, so the width at which tightening runs out and the wordmark has
   to go arrives sooner. Measured with the fullest signed-in bar: at 341 on a coarse pointer
   the bar still wrapped AND the document overflowed by 15px with the wordmark in place. */
@media (max-width: 340px), (pointer: coarse) and (max-width: 359px) {
  .brand .mark { display: none; }
}
/* AND THE LADDER HAS TO REACH ONE RUNG FURTHER ON A COARSE POINTER, because that bar is
   wider than the one the ladder above was measured against. Every control in it takes
   `var(--tap)` under `@media (pointer: coarse)` — 44px against ~32px — so the actions
   group is 198px on a touch device where a mouse gets 180px. At 360px, which is a very
   common Android width and always a coarse pointer, the signed-in bar needed 367px of a
   360px viewport and WRAPPED TO TWO ROWS: measured, brand at y=12 and the actions at y=49.

   Eight pixels of padding is the whole difference, so the paddings tighten one more step
   rather than the wordmark yielding at 360 — hiding the brand on the commonest Android
   width to buy 7px would be the more destructive of the two fixes. The ladder's own rule
   still holds underneath it: at 340 and below the wordmark still goes, because by then
   tightening is exhausted and a tap target is the only thing left to take. */
@media (pointer: coarse) and (max-width: 380px) {
  header.top .top-inner { padding: 12px 10px; }
  .account { gap: 5px; }
}

/* ============================================================================
   POLISH PASS 12 — THE NEWER SURFACES JOIN THE TYPE RAMP
   ---------------------------------------------------------------------------
   Portfolio, dividends (pf-div-*), the advanced chart and price targets were
   sized by hand: 13.5 / 12.5 / 11.5 / 10.5px half-steps that belong to no scale
   and drift a little further with every surface added. 45 declarations across
   those four surfaces now read from the ramp.

   The ramp itself was missing a step. It ran 15 / 13 / 11 / 10 while 12px was
   the single most-used size in this entire file (75 uses) — so a data-dense row
   had nowhere to land and rounded away from the scale instead of onto it.
   `--t-meta: 12px` is that step, and it is why this pass could bind the surfaces
   without redesigning them.

   Every substitution rounds DOWN or is exact, so no text could grow into an
   overflow — verified anyway at 375/1440/1920 with a real Pro session: no
   horizontal overflow, no clipped text, no console errors.

   The display end (20px+) is deliberately left as fixed figures. Swapping a
   stat number for the --t-title clamp would change its size across breakpoints,
   which is a design change rather than a unification.

   NOTE this pass covers the surfaces named in the polish brief, not the file.
   565 hand-set px sizes remain elsewhere against a ramp used twice — a
   whole-stylesheet unification is a separate, much larger job that would touch
   the frozen landing and the frozen mobile layout.
   ========================================================================== */

/* ============================================================================
   POLISH PASS 6 — DESKTOP TILE GRID (company / analysis view)
   ---------------------------------------------------------------------------
   Desktop was the mobile column stretched: 23 stacked full-width blocks inside a
   1120px cap, which at 1440px meant ~180px of dead margin either side and a
   3300px scroll to reach the bottom of one company. This turns the view into a
   12-column widget grid that uses the width it has.

   EVERYTHING HERE IS SCOPED TO >= 1024px. Mobile and tablet keep the single
   column exactly as they are — the phone layout is not being redesigned, and no
   rule in this block can reach it.

   It is also purely presentational: no markup, no data, no gating. Every tile is
   an element the view already rendered, so a locked Pro widget stays locked and
   the server still withholds what it withheld before — the paywall does not know
   this file exists.
   ========================================================================== */

@media (min-width: 1024px) {
  /* The wrap's 1120px cap is right for a reading column and wrong for a grid.
     Widened only when it is actually holding the company view. The dashboard and
     the screener each opt in from their own block (passes 7 and 10); the landing
     never does. */
  /* SUPERSEDED — the four per-page opt-ins are now one rule; see
     "ONE SHELL, TWO REGISTERS" in the token block and `.shell-tiles` below. */

  #view {
    display: grid;
    grid-template-columns: repeat(12, minmax(0, 1fr));
    gap: 16px;
    align-items: start;      /* tiles keep their own height; no stretching to the row */
  }

  /* Default EVERY child to the full row. The view also renders a loader, an error
     card and whatever gets added next — an unknown child must fall back to the
     safe full-width form rather than land in a half-column it was never designed
     for. Named tiles opt into a narrower span below. */
  #view > * { grid-column: 1 / -1; min-width: 0; }

  /* the grid owns vertical rhythm now; stacked margins would double the gap */
  #view > *, #view > .panel, #view > .sect, #view > .notice { margin-top: 0; margin-bottom: 0; }

  /* --- NO PANEL IS NARROWER THAN THE ROW ------------------------------------
     Every span rule that used to live here is gone, and the reason is measured
     rather than aesthetic. A grid row is as tall as its tallest member, so any
     short panel placed beside a tall one leaves dead column — and the heights
     here are content-dependent, so no assignment of spans can predict them. The
     claim this block used to carry, that two ~450px columns "genuinely do pack",
     does not survive measurement:

         width   two columns              one column
         1024    29.5% dead, 1161px void  9.0% dead, 0px  (and 311px SHORTER)
         1200    29.1% dead, 1094px void  9.1% dead, 0px
         1440    28.4% dead, 1071px void  9.3% dead, 0px
         1920    30.7% dead,  895px void  9.5% dead, 0px

     Dead space falls by two thirds at every width and the void goes to zero,
     because with one child per row there is no sibling to be short beside. See
     the block near the end of this file for the full note. */

  /* A section carries its own bottom margin in the column layout; inside the grid
     the gap does that job, and the border-radius should not be interrupted. */
  #view > details.sect { margin: 0; }

  /* --- full-width by intent ----------------------------------------------- */
  /* Banners, upsells and the closing rail read as rails, not as cards. */
  #view > .memo-call,
  #view > .pro-strip,
  #view > .related-panel,
  #view > .notice,
  #view > .disclaimer,
  #view > .back,
  #view > .co-head,
  #view > .badges { grid-column: 1 / -1; }

  /* the header block should not inherit the column-layout margins that were
     tuned for a stacked page */
  #view > .back { margin: 18px 0 0; justify-self: start; }
  #view > .co-head { margin-bottom: 0; }
}

/* The three-across tier and its ragged-row workaround lived here. Both were
   attempts at the same problem — a row is as tall as its tallest member — and both
   moved the void rather than removing it. Superseded by the one-column rule near
   the end of this file, which removes the row instead of trying to balance it. */

/* ============================================================================
   POLISH PASS 7 — DESKTOP TILE GRID, PART 2: DASHBOARD + SIGNED-IN HOME
   ---------------------------------------------------------------------------
   Same idea as the company view (pass 6), but these two surfaces carry their
   height differently and so need different treatment:

     #dashboard  is a stack of sibling panels — tiled the same way as #view.
     the signed-in home is ONE 1768px list — no amount of sibling tiling helps,
                 so the list itself becomes multi-column.

   NOT TOUCHED HERE: the signed-out home. That is the landing pitch and part of
   the funnel; `.mylist` carries `hidden` for a logged-out visitor, and every
   selector below requires `.mylist:not(.hidden)`, so the landing keeps its
   reading measure and its layout exactly as shipped.

   As in pass 6, everything is behind a min-width query — mobile is untouched —
   and it is presentation only: no markup, no data, no gating.
   ========================================================================== */

@media (min-width: 1024px) {
  /* Widen only these two, and only when they are actually on screen. The landing,
     the screener and the company view keep whatever measure they already had. */
  /* SUPERSEDED — see the one shell rule. */

  /* --- the dashboard: sibling panels become tiles ------------------------- */
  #dashboard {
    display: grid;
    grid-template-columns: repeat(12, minmax(0, 1fr));
    gap: 16px;
    align-items: start;
  }
  /* unknown children fall back to the full row, same contract as #view */
  #dashboard > * { grid-column: 1 / -1; min-width: 0; margin-top: 0; margin-bottom: 0; }
  #dashboard > .back { justify-self: start; margin: 18px 0 0; }
  /* the morning brief and the two dashboard rails pair up two-across; the market-open
     panel keeps the full row because it is the widest read on the page */
  #dashboard > .brief-slot { grid-column: span 6; }
  #dashboard > .dash-section { grid-column: span 6; }

  /* --- the signed-in home: the LIST goes multi-column --------------------- */
  /* This is where the home's scroll actually lives — one 1768px column of rows
     inside a 1076px page. Two columns of rows halves it without touching a single
     row's internals, so the sparkline/price/grade layout inside each row is byte
     for byte what it was. */
  /* ONE NAME PER ROW, AT EVERY WIDTH. This laid the watchlist out two-across here and
     three-across below, and a watchlist row is not a card — it is a line with a ticker, a
     name, a sparkline, a grade and a price in five columns of their own. Cutting the page
     into three 344px tracks left each of those five ~60px, so prices printed on top of the
     next column's ticker: "$973.4MSFT", "$495.4RELIANCE.NS". The dashboard's own WATCHLIST
     card has always rendered this correctly, one name per full-width row, and that is the
     model — the page is that card at page scale. */
}

@media (min-width: 1600px) {
  /* three columns of rows once there is room for three readable ones */
  /* (the three-across tier is gone with the two-across one above) */
  #dashboard > .brief-slot { grid-column: span 4; }
  #dashboard > .dash-section { grid-column: span 4; }
}

/* ============================================================================
   POLISH PASS 9 — TAP TARGETS: THE 44px FLOOR ON TOUCH
   ---------------------------------------------------------------------------
   Measured at 375px on prod: 17 controls under 44px on the home screen and 37
   on a company page (52 with every section expanded). Some were badly under —
   the parked mini-header's back arrow was 29x25, and the SEC filing links in
   the insider table were as narrow as 7px.

   Scoped to `(max-width: 899px), (pointer: coarse)`: the phone breakpoint the
   nav bar already uses, OR any touch pointer regardless of width, so a touch
   laptop gets the floor too. A mouse on a desktop viewport matches neither and
   sees none of this — the tile grid's tuning is untouched.

   Everything here is a MINIMUM (`min-height`/`min-width`) or a hit area, never
   a fixed size. The last time this file tried to grow a target it used a bare
   `min-height` far down the file, which won on source order and SHRANK the
   phone's back button from 40px to 32px; a minimum can only ever grow.

   Two techniques, chosen per control:
     CONTROLS grow. A button is a target, so it gets the floor outright.
     INLINE LINKS keep their size and gain a hit area over the row they sit in,
       via an absolutely-positioned `::after`. Inflating a link inside a
       sentence or a table cell would wreck the text it lives in — and WCAG
       2.5.8 exempts inline links for exactly that reason. Extending the target
       to the whole row is what a reader is aiming at anyway.
   ========================================================================== */

@media (max-width: 899px), (pointer: coarse) {

  /* --- controls: grow to the floor -------------------------------------- */
  .theme-btn { width: 44px; height: 44px; }
  .back, .share-btn { min-height: 44px; display: inline-flex; align-items: center; }
  .sector-chip { min-height: 44px; }
  .hw-back { min-height: 44px; }
  .skip-link { min-height: 44px; display: inline-flex; align-items: center; }

  /* the chart range strip: 35x40 options. It already scrolls horizontally
     (pass 5), so widening them costs scroll, not reachability. */
  .seg-opt { min-height: 44px; min-width: 44px; }

  /* the advanced chart's study chips were 26px tall — the shortest real
     control on the page */
  .adv-chip { min-height: 44px; display: inline-flex; align-items: center; }

  /* the parked sticky mini-header's back arrow: 29x25, the worst on the page */
  .mini-back {
    min-width: 44px; min-height: 44px;
    display: inline-flex; align-items: center; justify-content: center;
  }

  /* --- inline links: extend the target, keep the type ------------------- */
  /* A headline link is one or two lines of 14px text inside a padded row. The
     row is what a thumb aims at, so the row is what it gets — including the
     source/time meta line under it. No visual change at all. */
  .hl-item { position: relative; }
  .hl-item .hl-link::after { content: ""; position: absolute; inset: 0; }

  /* same idea for a filing/deal row */
  .deal-row { position: relative; }
  .deal-row .deal-lab::after { content: ""; position: absolute; inset: 0; }

  /* The insider table's EDGAR links are right-aligned dollar figures — one was
     7px wide. The cell becomes the target, and the rows get the height to make
     that cell a legal one. */
  .ins-table td { padding-top: 15px; padding-bottom: 15px; }
  .ins-val { position: relative; }
  .ins-val a::after { content: ""; position: absolute; inset: 0; }

  /* The footer's legal links are a 13px-tall row of five. Padding gives each a
     44px target; the negative margin hands the space back to the layout, so the
     footer does not grow — the targets simply overlap the empty band around
     the row, where there is nothing else to hit.
     6px of side padding rather than 4px because "Terms" is only 35.6px of text
     and 4px each side left it at 43.6 — a floor you miss by a third of a pixel
     is a floor you miss. The separators between the links are ~12px, so two
     neighbouring targets meet in the middle and never overlap. */
  .footer-links a {
    display: inline-block;
    padding: 16px 6px;
    margin: -16px -6px;
  }
  /* The negative INLINE margin is what gives each link its 44px target without moving the
     sentence — and it is also why the first and last link on every wrapped line hung 6px
     past the container, which is a horizontal scrollbar on the whole document. Measured at
     4px on a 390px viewport and 5-6px at 768, across three phases of reports. Giving the
     row 6px of its own inline padding is exactly the room those margins need, so the
     targets are unchanged and the overhang has somewhere to be. */
  .footer-links { padding-inline: 6px; }
}

/* ============================================================================
   POLISH PASS 10 — THE SCREENER JOINS THE TILE GRID
   ---------------------------------------------------------------------------
   The screener was skipped in passes 6 and 7 because its results landed in an
   unclassed `<div id="scr-results">` with nothing stable to hold on to. It has
   a class now (`.scr-results`, set in renderScreener) and the same shape as the
   signed-in home did: measured as Pro at 1440 AND at 1920, an identical 1076px
   page carrying ONE 3772px column of 43 rows. The wider viewport bought nothing
   at all, because the wrap was never widened for this surface.

   So it gets the pass-7 treatment exactly: widen the wrap, and put the LIST
   into columns rather than tiling its siblings — the scroll lives inside that
   one list, and a row's internals (sparkline, price, grade, tier tag) are
   untouched, so a screener row and a watchlist row still read identically.

   The filter panel deliberately stays a full-width bar. It is a row of short
   fields, not a sidebar, and turning it into one would move the Pro upsell and
   the export/save controls that sit inside it.

   Free and anonymous visitors are unaffected in substance: the screener is a
   Pro tool, so `#scr-results` holds a lock card for them, no `.wl-list` exists,
   and the column rule below simply never matches. Presentation only — no
   gating, no data, no markup beyond the one class name.
   ========================================================================== */

@media (min-width: 1024px) {
  /* SUPERSEDED — see the one shell rule. */

  /* Scoped through #screener as well as .scr-results: `.wl-list` is the shared
     row-list used by the watchlist, portfolio, dashboard and calendar too, and
     none of those are being changed here. */
  #screener .scr-results .wl-list {
    display: grid;
    grid-template-columns: repeat(2, minmax(0, 1fr));
    column-gap: 22px;
    align-items: start;
  }
  #screener .scr-results .wl-list > .wl-row { min-width: 0; }
}

@media (min-width: 1600px) {
  #screener .scr-results .wl-list { grid-template-columns: repeat(3, minmax(0, 1fr)); }
}

/* ============================================================================
   POLISH PASS 11 — MOTION: THE TWO PLACES THAT STILL CUT
   ---------------------------------------------------------------------------
   1. THE ACCORDION NEVER ACTUALLY EASED. `.sect-anim` has declared a
      `grid-template-rows: 0fr -> 1fr` transition for a long time, and it has
      never run: measured, a section opens 76px -> 791px across THREE frames.
      The reason is that the transition was written for the wrong element. A
      <details> element's content is hidden by the user agent, not by us — in
      current browsers via `content-visibility` on `::details-content`. A
      skipped element has no rendered "from" value, so there is nothing to
      interpolate and the browser jumps straight to the end.

      The fix animates the element the UA is actually toggling. `block-size:
      0 -> auto` needs `interpolate-size: allow-keywords` to be interpolable,
      and `content-visibility` needs `allow-discrete` so it flips at the right
      end of the transition rather than instantly.

      Gated behind @supports for both features together. Where either is
      missing the browser keeps exactly today's behaviour — an instant open,
      which is not a regression, just the absence of the improvement.

      `interpolate-size` is set on `.sect` rather than `:root` on purpose: it
      inherits, and switching every `auto` on the page to interpolable would
      quietly animate things nobody asked to animate.

   2. TWO ROUTES ARRIVED WITH A HARD CUT. Discover, Watchlist, the dashboard,
      the screener and a company view all fade-slide in through `Router._enter`.
      Portfolio and the calendar did not — and Portfolio is one of the four
      primary nav destinations, so a quarter of the tab bar felt different from
      the rest. Fixed in app.js by calling the same `_enter`; no new motion
      idiom, just the existing one applied where it was missing.

   Reduced motion is handled where it already was: `_enter` returns early under
   `prefersReducedMotion()` so no class is ever added, and the global MOTION OFF
   block collapses every transition-duration. Both were re-measured under an
   emulated `prefers-reduced-motion: reduce` — panes report animation-name
   `none` and never receive `.view-enter`.
   ========================================================================== */

@supports selector(details::details-content) and (interpolate-size: allow-keywords) {
  .sect { interpolate-size: allow-keywords; }

  .sect::details-content {
    block-size: 0;
    overflow: hidden;
    transition: block-size var(--dur-3) var(--ease-out),
                content-visibility var(--dur-3) allow-discrete;
  }
  .sect[open]::details-content { block-size: auto; }

  /* The inner 0fr/1fr grid is now doing the same job one level down, and two
     transitions racing on one open produces a visible double-step. The outer
     element owns the motion; the inner one just stops being a grid. */
  .sect .sect-anim { display: block; transition: none; }
}

/* The global MOTION OFF block collapses every transition on `*, *::before,
   *::after` — and `::details-content` is none of the three, so the accordion
   above kept easing for a reader who asked for no motion. Measured: 17 distinct
   heights under an emulated `prefers-reduced-motion: reduce`. It opens flat now.
   Any future motion hung on a pseudo-element outside ::before/::after needs the
   same explicit opt-out; the wildcard will not catch it. */
@media (prefers-reduced-motion: reduce) {
  .sect::details-content {
    transition-duration: 0.001ms !important;
    transition-behavior: normal !important;
  }
}

/* ============================================================================
   POLISH PASS 13 — DESKTOP TILES CARRY CONTENT
   ---------------------------------------------------------------------------
   Support for autoOpenDesktopSections(): see app.js for why this exists at all
   (short version — the grid was already ~80% covered and still read sparse,
   because eleven of its tiles were closed 76px headers).

   `.measuring` lifts the user agent's content-visibility so a CLOSED section can
   be measured. It is added and removed inside one task, so no paint happens
   while it is on and it can never be seen. `!important` because it has to beat
   both the pass-11 ::details-content rule and the older grid-track idiom.

   `.no-sect-anim` suppresses the open transition for that one pass. Eleven
   sections easing open while the page assembles is jank plus a layout-shift
   bill, and CLS on this view was fought from 1.13 down to 0.10.
   ========================================================================== */

#view.measuring .sect::details-content {
  content-visibility: visible !important;
  block-size: auto !important;
}
#view.measuring .sect .sect-anim { grid-template-rows: 1fr !important; display: block !important; }
#view.no-sect-anim .sect::details-content { transition: none !important; }

/* ---------------- Discover tiles ----------------
   The Discover strips are a `.grid`, but each was handed ONE grouped list card, so the
   whole list stacked into the first 232px column while the rest of the row stood empty.
   On a wide screen every ticker now gets a tile of its own; below 720px the compact list
   is still rendered, so the phone layout is untouched.

   Same radius, same 1px hairline, same lift on hover as the rest of the product's
   surfaces, so Discover reads as part of it rather than a second visual system. (This
   used to say it mirrored `.card`; those rules were deleted 2026-08-10 once measurement
   showed nothing rendered them.) Monochrome throughout: the only colour on a tile
   is the change pill and the score badge, both of which already carry meaning. */
.dtile-grid { grid-template-columns: repeat(auto-fit, minmax(248px, 1fr)); }

.dtile {
  background: var(--surface); border: 1px solid var(--line); border-radius: var(--r);
  padding: var(--tile-pad-y) var(--tile-pad-x) calc(var(--tile-pad-y) - 2px);
  cursor: pointer; position: relative;
  display: flex; flex-direction: column; min-height: var(--tile-min-h);
  transition: transform 0.25s var(--ease), border-color 0.25s, box-shadow 0.25s;
}
.dtile:hover { transform: translateY(-3px); border-color: var(--accent); box-shadow: var(--shadow); }
.dtile:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }

.dtile-head { display: flex; align-items: center; justify-content: space-between; gap: 8px; }
.dtile-sym {
  font-family: var(--mono); font-weight: 700; font-size: 15px; letter-spacing: 0.05em;
  white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
}
/* the score badge keeps the list-row treatment; only its placement changes */
.dtile-score { flex: 0 0 auto; }
/* no grade came back (the batch score endpoint is bounded on purpose) — show nothing
   rather than an empty bordered box, which on a tile this size reads as a broken badge */
.dtile-score-none { display: none; }
/* ONE line, not two. A wrapped company name was the tile's tallest secondary block and
   the least load-bearing thing on it — the ticker above it already identifies the row,
   and the tile opens to the full name. Ellipsis, so nothing reads as truncated data. */
.dtile-nm {
  color: var(--muted); font-size: 12.5px; line-height: 1.35; margin: 2px 0 0;
  white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
}

/* the value block sits on the BASELINE of the tile so a row of tiles aligns
   regardless of how many lines the company name took */
.dtile-body {
  margin-top: auto; padding-top: 9px;
  display: flex; align-items: flex-end; justify-content: space-between; gap: var(--tile-gap);
}
/* THE VALUE COLUMN OWNS THE LINE; THE SPARKLINE IS THE THING THAT GIVES.
   `.dtile-spark` was `flex: 0 0 auto` — it could not shrink — so whenever the tile's
   content line ran short of room the *value* column was the one squeezed, and its number
   and change badge (neither of which can reflow) overflowed to the right and painted
   under the chart. On tickerworth.com that read as a green line drawn straight through
   "$193.00" and "+99.28%". Three rules make the overlap structurally impossible:
     1. the value column may not be squeezed below its own content (min-width: min-content)
        and never breaks a number across lines,
     2. the sparkline may shrink to nothing before that happens (flex: 0 1 76px, min-width: 0),
     3. and it paints below the value column, so even a rounding pixel can't obscure a digit. */
.dtile-val {
  display: flex; flex-direction: column; align-items: flex-start; gap: 4px;
  flex: 1 1 auto; min-width: min-content; max-width: 100%;
  position: relative; z-index: 1;
}
.dtile-cap {
  font-family: var(--mono); font-size: 9.5px; letter-spacing: var(--track-eyebrow);
  text-transform: uppercase; color: var(--faint);
  max-width: 100%; white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
}
.dtile-px {
  font-family: var(--mono); font-size: 19px; font-variant-numeric: tabular-nums;
  color: var(--accent); line-height: 1.1;
  max-width: 100%; white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
}
.dtile-px-none { color: var(--faint); }
.dtile-spark {
  flex: 0 1 76px; min-width: 0; max-width: 76px;
  opacity: 0.85; min-height: 30px; display: flex; align-items: flex-end;
  position: relative; z-index: 0; pointer-events: none;
}
/* Below a usable width the chart is dropped outright rather than drawn as a few-pixel
   sliver — the same call `.wl-spark` already makes inside a narrow list card. The
   constraint is the TILE's width, not the viewport's, so it's a container query. */
.dtile { container-type: inline-size; }
@container (max-width: 216px) { .dtile-spark { display: none; } }
/* backstop: nothing a tile holds may paint outside the tile, whatever the font metrics */
.dtile { overflow: hidden; }

/* The footer keeps its NUMBER and loses its BOX. "Trades at $96.85" is the price the
   upside is measured from — the tile states both halves of the comparison rather than a
   percentage with nothing to stand on, and that stays. What goes is the rule above it and
   the 20px of padding it sat in, which is what made it read as a whole extra band. */
.dtile-foot {
  margin-top: 7px;
  font-family: var(--mono); font-size: 10px; letter-spacing: 0.08em;
  text-transform: uppercase; color: var(--faint);
  white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
}

@media (prefers-reduced-motion: reduce) {
  .dtile { transition: none; }
  .dtile:hover { transform: none; }
}

/* ============================================================================
   DESKTOP PASS 12 — DISCOVER USES THE WIDTH IT HAS
   ---------------------------------------------------------------------------
   Passes 6, 7 and 10 widened the company view, the dashboard/watchlist and the
   screener out of `.wrap`'s 1120px reading cap, each opting in from its own
   block. Discover was deliberately left behind — and it is the page with the
   most to lay out. Six data sections (world markets, gainers/losers, most
   undervalued, rising, trending, researched models) sat in a 1076px column with
   ~180px of dead margin down each side at 1440, and ~420px at 1920, while the
   company page beside it ran to 1516px.

   The subtlety that kept it narrow is real: Discover is the only route that
   holds MARKETING COPY and DATA GRIDS in one scroll. Widening the wrap alone
   would drag the hero headline and the paywall panel to a 1516px measure, which
   is not a wider page, it is a worse one. So the wrap widens and the prose is
   pinned back to exactly the measure it had — the masthead is unchanged to the
   pixel, and only the things that read better wide get wider.

   Everything below the masthead then fills on its own, because it was already
   written to: `.movers-grid` is `1fr 1fr`, `.overnight-strip` is a wrap-flex,
   and the tile grids are `auto-fill` — the same declarations, finally given the
   room they were asking for. The one addition is the researched-models list,
   which would otherwise be the single narrow column left on a wide page.

   SCOPED TO >= 1024px, so the phone and tablet layouts cannot be reached by any
   rule here. Presentation only: no markup, no data, no gating — the sections
   render exactly what they rendered before, in the same order.
   ========================================================================== */

@media (min-width: 1024px) {
  /* SUPERSEDED — see the one shell rule. */

  /* The masthead keeps its reading measure inside the wider wrap: 1076px is what
     `.wrap` (1120px less its 22px gutters) has always given it, so the hero, the
     promo bar, the counts, the free-valuations meter and the free-vs-Pro panel
     are pixel-identical to before.

     An explicit left offset rather than `margin: 0 auto`, because `.hw-back` is a
     BUTTON: auto margins don't centre an intrinsically-sized element, so it alone
     would have stayed pinned to the wide edge, 140px left of the headline it sits
     above. `max(0px, …)` keeps the offset from going negative — and the whole
     block from overflowing — on a 1024px screen, where the wrap is narrower than
     the measure it is protecting. Only the horizontal margins are set;
     `.why-pay`'s vertical rhythm is its own. */
  #discover > .hw-back,
  #discover > #promo-wrap,
  #discover > .hero,
  #discover > #analysed-stat,
  #discover > #free-meter,
  #discover > .why-pay {
    max-width: 1076px;
    margin-left: max(0px, calc(50% - 538px));
    margin-right: auto;
  }

  /* Six world-market pills packed against the left edge of a 1516px row read as
     a leftover, not a strip. Sharing the row makes the section look measured
     rather than half-filled — and `auto-fit` means four indices or eight both
     land evenly instead of one width being tuned for today's list. */
  .overnight-strip {
    display: grid;
    grid-template-columns: repeat(auto-fit, minmax(168px, 1fr));
  }
  .ov-item { justify-content: center; }

  /* The researched-models grid is a stack of per-sector grouped cards, each a
     single hairline list. Left alone it would be the one narrow column on a wide
     page, and the tallest thing on it. Two-up, exactly as the screener's results
     do it (pass 10) — same idiom, so the two pages read as one product. */
  #company-grid .sector-group .wl-list {
    display: grid;
    grid-template-columns: repeat(2, minmax(0, 1fr));
    column-gap: 22px;
    align-items: start;
  }
  #company-grid .sector-group .wl-list > .wl-row { min-width: 0; }
  /* `:first-child` drops the top hairline so a list doesn't double its card's
     border. In two columns the SECOND column's first row is also at the top of
     the card and needs the same treatment. */
  #company-grid .sector-group .wl-list > .wl-row:nth-child(2) { border-top: none; }
}

@media (min-width: 1600px) {
  #company-grid .sector-group .wl-list { grid-template-columns: repeat(3, minmax(0, 1fr)); }
  #company-grid .sector-group .wl-list > .wl-row:nth-child(3) { border-top: none; }
}

/* ---------- Wall Street consensus — a second opinion, visibly not ours ----------
   Deliberately quieter than our own valuation blocks: no accent on the street's numbers,
   no coloured verdict, no lift on hover. The one accent in the section is OUR fair value
   in the comparison row, because the section exists to show where the two disagree and the
   reader should never be in doubt about which number the product stands behind. */
.street-head {
  display: grid; grid-template-columns: repeat(auto-fit, minmax(150px, 1fr));
  gap: 14px; margin: 4px 0 14px;
}
.street-k { min-width: 0; }
.street-cap {
  font-family: var(--mono); font-size: 9.5px; letter-spacing: var(--track-eyebrow);
  text-transform: uppercase; color: var(--faint); margin-bottom: 5px;
}
.street-v {
  font-family: var(--mono); font-size: 20px; font-variant-numeric: tabular-nums;
  color: var(--text); line-height: 1.15;
}
.street-rate, .street-range { font-size: 16px; }
.street-sub {
  font-family: var(--mono); font-size: 11px; color: var(--muted); margin-top: 5px;
  display: flex; align-items: center; gap: 6px; flex-wrap: wrap;
}

.street-vs {
  border: 1px solid var(--line); border-radius: var(--r); background: var(--surface-2);
  padding: 13px 15px; margin: 4px 0 14px;
}
.street-vs-row {
  display: flex; justify-content: space-between; align-items: baseline; gap: 12px;
  font-family: var(--mono); font-size: 12.5px; color: var(--muted); padding: 3px 0;
}
.street-vs-n { font-size: 15px; font-variant-numeric: tabular-nums; color: var(--text); }
.street-vs-n.accent { color: var(--accent); }
.street-vs-note {
  font-size: 12.5px; color: var(--muted); line-height: 1.55; margin-top: 9px;
  padding-top: 9px; border-top: 1px solid var(--line-soft);
}

.street-trend { margin: 0 0 14px; }
.street-tr { display: flex; align-items: center; gap: 10px; padding: 4px 0; }
.street-tr-p {
  font-family: var(--mono); font-size: 11px; color: var(--faint);
  flex: 0 0 auto; min-width: 54px;
}
.street-tr-bar {
  flex: 1 1 auto; display: flex; height: 7px; border-radius: var(--r-full); overflow: hidden;
  background: var(--surface-2); min-width: 0;
}
.stb { display: block; height: 100%; }
.stb-b { background: var(--up-text); }
.stb-h { background: var(--faint); }
.stb-s { background: var(--down-text); }
.street-tr-n {
  font-family: var(--mono); font-size: 10.5px; color: var(--muted);
  flex: 0 0 auto; white-space: nowrap;
}

.street-est { margin: 0 0 14px; }
.street-est-row {
  display: flex; justify-content: space-between; gap: 12px; padding: 6px 0;
  border-top: 1px solid var(--line-soft); font-size: 13px; color: var(--muted);
}
.street-est-row:first-of-type { border-top: none; }
.street-est-v { font-family: var(--mono); font-variant-numeric: tabular-nums; color: var(--text); }

.street-absent { font-size: 13.5px; color: var(--muted); line-height: 1.6; margin-bottom: 12px; }
.street-disc {
  font-family: var(--mono); font-size: 10.5px; color: var(--faint); line-height: 1.55;
  border-top: 1px solid var(--line-soft); padding-top: 10px; margin-top: 4px;
}

/* ---------- headline tone: a keyword read, dressed as one ----------
   Deliberately understated. A big coloured "SENTIMENT: POSITIVE" badge would claim an
   assessment we haven't made — this is a word count, so it looks like a footnote. The tag
   says "positive WORDING", never "positive news", and an unclear headline carries no tag
   at all rather than a neutral chip that would read as a considered verdict. */
.hl-tone {
  border: 1px solid var(--line); border-left: 2px solid var(--faint);
  border-radius: var(--r); background: var(--surface-2);
  padding: 11px 14px; margin: 2px 0 14px;
}
.hl-tone-positive { border-left-color: var(--up-text); }
.hl-tone-negative { border-left-color: var(--down-text); }
.hl-tone-cap {
  font-family: var(--mono); font-size: 9.5px; letter-spacing: var(--track-eyebrow);
  text-transform: uppercase; color: var(--faint); margin-bottom: 5px;
}
.hl-tone-txt { font-size: 13px; color: var(--text); line-height: 1.55; }
.hl-tone-method {
  font-family: var(--mono); font-size: 10.5px; color: var(--faint); line-height: 1.5;
  margin-top: 7px;
}
.hl-sent {
  font-family: var(--mono); font-size: 9.5px; letter-spacing: 0.06em; text-transform: uppercase;
  border: 1px solid var(--line); border-radius: var(--r-full); padding: 1px 7px; margin-left: 8px;
  color: var(--muted); white-space: nowrap;
}
.hl-sent-positive { color: var(--up-text); }
.hl-sent-negative { color: var(--down-text); }

/* ── The free worked example (app/access.WORKED_EXAMPLE_TICKERS) ────────────────────────
   One ticker whose page is fully un-gated, and the written argument for the number on it.
   Styled as a quiet editorial block rather than a promo banner: it is a statement about how
   the product works, and a page that shouts about being free reads like a page that is
   selling, which is the opposite of the point. Single column at every width. */
.we-banner {
  display: flex; gap: 14px; align-items: flex-start; flex-wrap: wrap;
  /* The rail is 2px everywhere else it appears — .news-item, .nw-item.mine, .sc-comp2,
     .rev-caveat, .nar-card and seven more. This banner was the only one at 3px, sitting
     on the same page as .nar-card, which draws the SAME accent rail one pixel thinner.
     Two weights for one primitive, a screen apart. (.vm-toast keeps 3px: a floating
     status stripe is a different register and its weight was tuned for WCAG 1.4.11.) */
  border: 1px solid var(--line); border-left: 2px solid var(--accent);
  border-radius: var(--r); background: var(--surface);
  padding: 14px 16px; margin: 4px 0 16px;
}
.we-flag {
  font-family: var(--mono); font-size: 9.5px; letter-spacing: var(--track-eyebrow);
  text-transform: uppercase; color: var(--bg); background: var(--accent);
  border-radius: var(--r-full); padding: 3px 9px; white-space: nowrap; flex: 0 0 auto;
}
.we-banner-body { flex: 1 1 260px; min-width: 0; }
.we-banner-title { font-weight: 700; font-size: 15px; margin-bottom: 4px; }
.we-banner-copy { font-size: 13px; line-height: 1.6; color: var(--muted); }

.we-panel .we-nums {
  display: flex; flex-wrap: wrap; gap: 10px; margin: 10px 0 14px;
}
.we-num {
  flex: 1 1 150px; min-width: 0;
  border: 1px solid var(--line-soft); border-radius: var(--r-sm);
  padding: 9px 11px; background: var(--surface);
}
.we-num-lab {
  display: block; font-family: var(--mono); font-size: 9.5px;
  letter-spacing: var(--track-eyebrow); text-transform: uppercase; color: var(--faint);
  margin-bottom: 3px;
}
.we-num-val { display: block; font-size: 19px; font-weight: 700; }
.we-prose p { font-size: 14px; line-height: 1.68; margin: 0 0 11px; }
.we-sub {
  font-family: var(--mono); font-size: 10px; letter-spacing: var(--track-eyebrow);
  text-transform: uppercase; color: var(--faint); margin: 16px 0 8px;
}
.we-drivers, .we-mind { list-style: none; padding: 0; margin: 0; }
.we-driver {
  border-top: 1px solid var(--line-soft); padding: 10px 0;
}
.we-driver:first-child { border-top: 0; padding-top: 0; }
.we-driver-lab { font-weight: 600; font-size: 13.5px; }
.we-driver-val { font-size: 12.5px; color: var(--accent); margin: 2px 0 4px; }
.we-driver-why { font-size: 13px; line-height: 1.6; color: var(--muted); }
.we-mind li {
  font-size: 13px; line-height: 1.6; color: var(--muted);
  padding: 6px 0 6px 16px; position: relative;
}
.we-mind li::before {
  content: "—"; position: absolute; left: 0; color: var(--faint);
}
.we-disc {
  font-family: var(--mono); font-size: 10.5px; line-height: 1.55; color: var(--faint);
  margin-top: 14px; padding-top: 10px; border-top: 1px solid var(--line-soft);
}
@media (max-width: 620px) {
  .we-banner { flex-direction: column; gap: 9px; }
  /* In a COLUMN flex container `flex-basis` sizes the HEIGHT, so the 260px basis that keeps
     the copy beside the flag on a wide screen padded ~90px of dead space under it on a
     phone. Basis belongs to the row layout only. */
  .we-banner-body { flex: 1 1 auto; }
  .we-num { flex: 1 1 100%; }
  .we-num-val { font-size: 17px; }
}

/* The homepage pointer at the free worked example. Deliberately plain — it is an answer to a
   fair question ("how do I check this before paying?"), not a promotion. */
.wp-sample {
  border: 1px solid var(--line-soft); border-radius: var(--r);
  background: var(--surface); padding: 12px 14px; margin-top: 14px;
  font-size: 13px; line-height: 1.62; color: var(--muted);
}
.wp-sample a { color: var(--accent); cursor: pointer; text-decoration: underline; }
.hint-links .we-link { font-weight: 600; }

/* ── The reverse trial's strip (app/trial.py) ───────────────────────────────────────────
   One honest line: how many days are left, and the promise that nothing can charge you.
   Deliberately NOT alarm-coloured until the last two days, and never animated — a ticking
   countdown is a pressure device, and this trial's whole design is the absence of one. */
.trial-strip-wrap { max-width: 1180px; margin: 0 auto; padding: 0 20px; }
.trial-strip {
  display: flex; align-items: center; gap: 10px; flex-wrap: wrap;
  border: 1px solid var(--line-soft); border-radius: var(--r-sm);
  background: var(--surface); padding: 9px 13px; margin: 12px 0 0;
  font-size: 13px; line-height: 1.5;
}
.trial-badge {
  font-family: var(--mono); font-size: 9.5px; letter-spacing: var(--track-eyebrow);
  text-transform: uppercase; color: var(--bg); background: var(--accent);
  border-radius: var(--r-full); padding: 3px 9px; white-space: nowrap; flex: 0 0 auto;
}
.trial-msg { flex: 1 1 240px; min-width: 0; color: var(--muted); }
.trial-msg b { color: var(--text); }
.trial-last { border-color: var(--line-strong); }
.trial-over .trial-badge { background: var(--surface-3); color: var(--text); }
.trial-extend, .trial-cta {
  font-family: var(--mono); font-size: 11.5px; border-radius: var(--r-full);
  padding: 6px 13px; cursor: pointer; white-space: nowrap; flex: 0 0 auto;
  transition: background var(--dur-2) var(--ease), opacity var(--dur-2) var(--ease);
}
.trial-extend { background: transparent; border: 1px solid var(--line); color: var(--text); }
.trial-extend:hover { background: var(--accent-soft); }
.trial-cta { background: var(--accent); border: 1px solid var(--accent); color: var(--bg); }
.trial-cta:hover { opacity: 0.86; }
.acct-note {
  font-size: 12px; line-height: 1.5; color: var(--text);
  padding: 8px 12px; border-bottom: 1px solid var(--line-soft);
}
.acct-note span { color: var(--faint); font-size: 11.5px; }
.plan-lede {
  font-size: 13px; line-height: 1.6; color: var(--muted); margin: 0 0 12px;
}
/* The card-trial terms. Deliberately at BODY size and full contrast, not the shrunken grey
   the industry reserves for the sentence about being charged — these five lines are the most
   important text in the modal, so they are styled like it. */
.trial-terms {
  list-style: none; margin: 0 0 16px; padding: 12px 14px;
  background: var(--surface-2, rgba(127, 127, 127, 0.06));
  border: 1px solid var(--line-soft); border-radius: var(--r-sm);
}
.trial-terms li {
  font-size: 13px; line-height: 1.6; color: var(--text);
  padding-left: 18px; position: relative;
}
.trial-terms li + li { margin-top: 7px; }
.trial-terms li::before {
  content: "—"; position: absolute; left: 0; color: var(--faint);
}
@media (max-width: 620px) {
  .trial-strip-wrap { padding: 0 14px; }
  .trial-strip { gap: 8px; }
  .trial-msg { flex: 1 1 100%; }
  .trial-extend, .trial-cta { flex: 1 1 auto; text-align: center; }
}

/* S&P 500 coverage, printed under the usage counters. Its own line because it carries two
   numbers that have to be read together — what we cover, and what we refuse to value. */
.us-sp {
  display: block; margin-top: 6px; font-size: 12.5px; line-height: 1.6; color: var(--faint);
}
/* The coverage sentence's own clock. Quieter than the counts it dates — a caveat on them,
   not another statistic. */
.us-asof { opacity: .75; font-variant-numeric: tabular-nums; }

/* ============================================================================
   MOTION PASS — craft audit against the Emil Kowalski / Apple design-engineering
   bar (.claude/skills/emil-design-eng, review-animations). Findings and the
   reasoning behind each fix live in docs/MOTION-AUDIT.md.
   ========================================================================= */

/* ---- F1. Hover motion is pointer-gated ------------------------------------
   22 rules moved an element on :hover with no pointer query. On a touch device
   a tap fires :hover and the transform STICKS until the reader taps elsewhere —
   a card left hanging 3px above the grid, a back-arrow shunted left. TickerWorth
   is majority-mobile (full-screen search takeover, tab bar), so this was the
   most-seen motion defect in the app.

   Gated negatively rather than by rewriting all 22 rules in place: one block,
   no risk of dropping a declaration in a 5.7k-line production sheet. Colour and
   brightness hovers are deliberately left alone — they read as a highlight, not
   as movement, and don't strand the layout. */
@media (hover: none), (pointer: coarse) {
  .set-opt:hover,
  /* the three landing worked-example cards */
  .lp-work:hover,

  /* The hero's CTA and the hero search's submit. Added with the Origin-shaped stack; the
     motion guard caught them ungated, which is the whole reason it exists — a transform on
     :hover fires on a touch tap and then STICKS until the next tap elsewhere.

     The HEADER's `.search-go` was gated here too, until the stale "Value it" skin that
     gave it a `translateY(-1px)` was deleted. It has no transform left to guard — only a
     background step, which this block deliberately does not touch. */
  .hero-cta:hover,
  .hero-ask-go:hover,
  .hero-wedge li:hover,
  .wp-col:hover,
  .hero .big-search button:hover,
  .mini-upgrade:hover,
  .dl-cta:hover,
  .dl-btn:hover,
  .lock-btn:hover,
  .lock-card-cta:hover,
  .signin-btn:hover,
  .auth-submit:hover,
  .auth-google:hover,
  .back:hover,
  .scr-run:hover,
  .pa-idea:hover,
  .watch-btn:hover,
  .share-btn:hover,
  .mr-go:hover,
  .dtile:hover {
    transform: none;
  }
  /* PHASE 2 STATE MATRIX. A hover FILL is not a transform, but it fails the same way on
     touch: it fires on tap and then sticks until the next tap elsewhere, so a phone reader
     is left with one permanently highlighted row. Neutralised here rather than gated with
     `@media (hover: hover)`, because this file has one mechanism for this and following it
     beats adding a second that half-agrees. The :active wash is deliberately NOT
     neutralised — that one is real press feedback and it ends when the finger lifts. */
  .wl-row:not(.wl-row-static):hover,
  .hw-row:hover,
  .sect > .sect-sum:hover,
  .dt-row:hover,
  .ac-item:hover,
  .ss-row:hover,
  .hw-mi:hover,
  .meth-toc a:hover,
  .pf-idea:hover,
  .pa-idea:hover,
  .sector-chip:not(.active):hover,
  .adv-chip:hover,
  .seg-opt:not(.on):hover,
  .theme-btn:hover,
  .tip-ico:hover,
  .reset-w:hover,
  .mc-ebtn:hover,
  .dt-watch:not(:disabled):hover,
  .set-opt:not(.on):hover,
  .dt-more:hover,
  .hint-links .screener-link:hover,
  .pnav-item:hover,
  .mover:hover,
  .nw-mv-row:hover,
  .mc-etop:hover,
  summary.mono:hover,
  .rel-chip:hover,
  .scr-export:hover,
  .pc-peer:hover > td {
    background-image: none;
  }
}

/* ---- F5. Popovers scale out of their trigger ------------------------------
   Every menu in the app entered with the generic `fade` keyframe — opacity plus
   translateY(8px), rising from below with a centred origin. That reads as "a
   panel arrived from somewhere", not "this came out of the thing I just
   pressed". The fix is the origin, not the duration: each menu now scales from
   the corner it is anchored to, so the spatial relationship between trigger and
   panel is legible.

   Never scale(0) — nothing in the real world appears from nothing. 0.96 plus
   opacity keeps a visible shape at frame one. Durations stay inside the 125–250ms
   popover budget.

   006 replaced the popIn KEYFRAME with a transition out of @starting-style. Two
   reasons, both from the same page of the rulebook:

   1. There was no return path. These four animated in and then ceased to exist —
      `.hidden` is `display: none !important`, so the exit was a cut. apple-design §7:
      "if something disappears one way, we expect it to emerge from where it came."
      A transition gives the same scale+fade backwards, out of the same origin.
   2. They are the most rapidly retriggered surfaces in the app — the autocomplete
      re-renders on every keystroke — and a keyframe restarts from zero on every
      retrigger while a transition retargets from the value currently on screen
      (emil, "keyframes on a rapidly-triggered element → use transitions").

   The state is `[data-open]`, not `.hidden`, because `display: none !important`
   cannot be transitioned and the global `.hidden` utility is used all over the app
   and must not change. `display` rides along via `allow-discrete` so the panel stays
   painted while it retracts; closed is still `display: none`, so a shut panel is out
   of the accessibility tree and out of the tab order exactly as it was before.
   `pointer-events: none` stops a retracting panel from swallowing the click that
   dismissed it. A browser without @starting-style/allow-discrete degrades to the
   instant show/hide this used to have. */
.ac-dropdown, .acct-menu, .notif-dropdown, .hw-menu {
  display: none;
  opacity: 0; transform: scale(0.96); pointer-events: none;
  /* Exit is the FAST half: the system responding, not the reader deciding
     (emil, asymmetric enter/exit). --dur-1 out, --dur-2 in. */
  transition: opacity var(--dur-1) var(--ease-out),
              transform var(--dur-1) var(--ease-out),
              display var(--dur-1) allow-discrete;
}
.ac-dropdown[data-open], .acct-menu[data-open],
.notif-dropdown[data-open], .hw-menu[data-open] {
  display: block; opacity: 1; transform: none; pointer-events: auto;
  transition-duration: var(--dur-2);
}
@starting-style {
  .ac-dropdown[data-open], .acct-menu[data-open],
  .notif-dropdown[data-open], .hw-menu[data-open] { opacity: 0; transform: scale(0.96); }
}

.ac-dropdown  { transform-origin: top center; }
.acct-menu    { transform-origin: top right; }
.notif-dropdown { transform-origin: top right; }
.hw-menu      { transform-origin: top right; }

/* The modal is the documented exception: it is not anchored to a trigger, it
   appears centred in the viewport, so it keeps a centred origin. */
.modal { transform-origin: center; }

/* Gentler, not zero (apple-design §14): the panel still fades both ways so a reader
   can see it arrive and leave, it just does not travel or scale. The `!important` is
   needed to outrank the blanket `*` reduced-motion floor above, which would otherwise
   collapse the fade to 0.001ms and turn the exit back into a cut. */
@media (prefers-reduced-motion: reduce) {
  .ac-dropdown, .acct-menu, .notif-dropdown, .hw-menu {
    transform: none !important;
    transition: opacity var(--dur-1) linear,
                display var(--dur-1) allow-discrete !important;
  }
}

/* ---- F7. Reduced motion is gentler, not zero ------------------------------
   The blanket `*` rule above collapses every duration to 0.001ms, which also
   kills the opacity fades that help a reader track a surface arriving. Reduced
   motion means fewer and gentler animations, not none — drop the movement, keep
   the comprehension cues. Restored only where something appears unannounced or
   takes over the screen; everything else stays instant. */
@media (prefers-reduced-motion: reduce) {
  .vm-toast { transition-duration: 140ms !important; transform: translateX(-50%) !important; }
  .vm-toast.show { transform: translateX(-50%) !important; }
  /* The text still dips — an opacity cross-fade is exactly the form apple-design §14 asks
     for in place of movement, and it is the cue that tells a reader the words CHANGED
     rather than that they misread them. Shortened, not removed: gentler, not zero. The
     120ms JS timer is unchanged, so the swap still lands after the fade completes. */
  .vm-toast-msg { transition-duration: 100ms !important; }
  .search-screen { transition: opacity 140ms linear !important; transform: none !important; }
  .search-screen.ss-in { transform: none !important; }
}

/* ---- F8. Every pressable element answers the press ------------------------
   A press that produces no visual change reads as an interface that didn't
   hear you. These are the button-shaped controls that had colour feedback but
   no physical one. 0.97 is subtle enough to survive being tapped all day, and
   `scale()` takes the label and icon with it, which is what sells the press.

   Row-shaped controls (list rows, table rows, notification items) are
   deliberately excluded — they already respond with a background change, and
   scaling a full-width row looks like a glitch rather than a button.

   `.dl-btn`, `.signin-btn`, `.scr-run`, `.pa-idea` and `.dtile` already list
   `transform` in their own transition — only the `:active` state was missing, so
   their shorthands are deliberately left untouched here. Redeclaring
   `transition` on them would silently drop their colour and border easing. */
.scr-export, .seg-opt, .rel-chip, .auth-tab, .modal-close {
  transition: transform var(--dur-1) var(--ease-out),
              color 0.2s, border-color 0.2s, background 0.2s;
}
.dl-btn:active, .signin-btn:active, .scr-run:active, .scr-export:active,
.seg-opt:active, .rel-chip:active, .pa-idea:active, .dtile:active,
.auth-tab:active, .modal-close:active {
  transform: scale(0.97);
}
@media (prefers-reduced-motion: reduce) {
  .dl-btn:active, .signin-btn:active, .scr-run:active, .scr-export:active,
  .seg-opt:active, .rel-chip:active, .pa-idea:active, .dtile:active,
  .auth-tab:active, .modal-close:active { transform: none; }
}

/* ============================================================================
   Plan 013 — the tiled dashboard + the tiled news page
   One ordered list, drawn as a grid. Two tile widths (regular / wide). Edit mode adds a
   handle bar per tile; drag reorders with a transform-only FLIP; nothing here animates a
   layout property, nothing runs longer than --dur-3, hover motion is pointer-gated, and
   reduced motion falls back to the blanket floor already declared at the top of this file.
   ============================================================================ */
.dash-actions { display: flex; gap: 8px; align-items: center; flex: 0 0 auto; }
.dash-btn {
  min-height: 40px; padding: 8px 16px; border-radius: var(--r-full); cursor: pointer;
  background: var(--surface-2); border: 1px solid var(--line); color: var(--text);
  font-family: var(--mono); font-size: 12.5px; letter-spacing: 0.04em;
  transition: color var(--dur-2) var(--ease-out), border-color var(--dur-2) var(--ease-out),
              background-color var(--dur-2) var(--ease-out);
}
.dash-btn:hover { border-color: var(--accent); color: var(--accent); }
.dash-btn-primary { background: var(--accent); color: var(--bg); border-color: var(--accent); }
.dash-btn-primary:hover { color: var(--bg); filter: brightness(1.05); }

.dash-grid { display: block; }
.dash-tile { margin: 0; min-width: 0; padding: 18px 18px 16px; position: relative; }
/* `.dash-tile.size-2 { grid-column: span 2 }` WAS HERE AND WAS DEAD. `.dash-grid` is
   `display: block` + `columns:` (see `.tile-flow`), and multicol has no per-item width —
   a `grid-column` on a multicol child is inert. Measured on production: a `size-2` tile
   renders at 455px, the same as `size-1`, so the size the server declares in
   `/api/dashboard`.tiles[].size has not reached the layout since News and the dashboard
   moved onto the shared flow.
   REMOVED RATHER THAN LEFT IN PLACE, per the policy stated on the News block: inert CSS
   that reads as live is what let this sit unnoticed. The dashboard keeps multicol — it is
   the layout that suits it, measured at 1470 the three columns end 1169 / 1158 / 1287, a
   129px spread against News' 702px.
   THE PRODUCT CALL THIS COMMENT DEFERRED WAS MADE (Miraaj, 2026-08-30): drop the width
   entirely rather than teach multicol to honour it. So `size` is gone from the layout, from
   `/api/dashboard`.tiles[] and from the catalog, and the `⇔ Make wide` control that wrote
   it is gone from the Arrange bar — a button whose whole effect was this rule outlived the
   rule by pointing at a class no stylesheet has. `test_a_control_is_never_drawn_for_a_class
   _that_matches_nothing` in tests/test_dashboard_tiles.py is what now fails if a tile is
   ever given a class this file does not define. */
.dash-tile.is-dragging { z-index: 5; box-shadow: var(--el-2, var(--el-1)); cursor: grabbing; will-change: transform; }
.dash-grid.is-editing .dash-tile { transition: transform var(--dur-3) var(--ease-out); }
.dash-grid.is-editing .dash-tile.is-dragging { transition: none; }
.dt-head { display: flex; align-items: baseline; justify-content: space-between; gap: 10px; margin: 0 0 12px; }
.dt-title { margin: 0; font-family: var(--mono); font-size: var(--t-eyebrow); letter-spacing: var(--track-eyebrow); text-transform: uppercase; color: var(--faint); }
.dt-meta { font-size: 11px; color: var(--faint); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.dt-body { min-width: 0; }
.dt-loading, .dt-empty { color: var(--muted); font-size: var(--t-small); line-height: 1.6; padding: 10px 0; }
.dt-note { color: var(--faint); font-size: 11.5px; margin-top: 10px; line-height: 1.5; }
.dt-more { display: inline-block; margin-top: 12px; font-family: var(--mono); font-size: 12px; color: var(--accent); cursor: pointer; }
.dt-sub { font-size: 10px; letter-spacing: 0.14em; text-transform: uppercase; color: var(--faint); margin: 12px 0 6px; }
.dt-sub:first-child { margin-top: 0; }

/* the three-number strip (portfolio) */
.dt-strip, .dt-teach-strip { display: grid; grid-template-columns: repeat(3, minmax(0, 1fr)); gap: 12px; margin-bottom: 14px; }
.dt-kv { display: flex; flex-direction: column; gap: 3px; min-width: 0; }
.dt-k { font-family: var(--mono); font-size: 10px; letter-spacing: 0.12em; text-transform: uppercase; color: var(--faint); }
.dt-v { font-size: 20px; font-weight: 600; letter-spacing: -0.01em; font-variant-numeric: tabular-nums; }
.dt-teach-strip .dt-v { color: var(--faint); }
.dt-teach-copy { color: var(--muted); font-size: 13.5px; line-height: 1.55; margin: 0 0 14px; }
.dt-teach .lock-card-cta { margin-top: 2px; }
/* `.pf-x` is a 26px square icon button; the suggested-watch pill needs its own width */
.dt-watch { color: var(--accent); border-color: var(--accent); white-space: nowrap; display: inline-flex; align-items: center; gap: 4px; padding: 0 9px; width: auto; height: 28px; font-size: var(--t-micro); }
.dt-watch .ic { width: 13px; height: 13px; }
/* opacity + cursor: see the state matrix at the end of this file */

/* compact rows (movers) and lists (calendar/alerts) */
.dt-cols { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 14px; }
.dt-row { display: flex; align-items: baseline; gap: 8px; width: 100%; padding: 6px 0; background: none; border: 0; border-top: 1px solid var(--line-soft); color: var(--text); text-align: left; cursor: pointer; font: inherit; }
.dt-row:first-of-type { border-top: 0; }
.dt-row-t { font-family: var(--mono); font-weight: 700; font-size: 12.5px; flex: 0 0 auto; }
.dt-row-n { color: var(--muted); font-size: 11.5px; flex: 1 1 auto; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.dt-row-c { font-family: var(--mono); font-size: 12.5px; font-variant-numeric: tabular-nums; flex: 0 0 auto; }
.dt-row-c.up { color: var(--up-text); } .dt-row-c.down { color: var(--down-text); }
.dt-list { list-style: none; margin: 0; padding: 0; }
.dt-li { display: flex; align-items: baseline; justify-content: space-between; gap: 10px; padding: 7px 0; border-top: 1px solid var(--line-soft); font-size: 13px; line-height: 1.4; }
.dt-li:first-child { border-top: 0; }
.dt-li-t { min-width: 0; }
.dt-li-r { font-family: var(--mono); font-size: 11.5px; color: var(--faint); flex: 0 0 auto; white-space: nowrap; }
.dt-news { display: grid; gap: 10px; }
.dt-nw { padding: 8px 0; border-top: 1px solid var(--line-soft); }
.dt-nw:first-child { border-top: 0; padding-top: 0; }
.dt-nw-meta { display: flex; gap: 8px; align-items: center; font-size: 10.5px; color: var(--faint); margin-bottom: 3px; }
.dt-nw-hl { color: var(--text); font-size: 13.5px; line-height: 1.4; text-decoration: none; }
.dt-nw-hl:hover { color: var(--accent); }
.dt-grades { display: flex; flex-wrap: wrap; gap: 8px; }
.dt-grade { display: flex; flex-direction: column; gap: 2px; min-width: 76px; padding: 8px 10px; border-radius: var(--r-sm); background: var(--surface-2); border: 1px solid var(--line-soft); cursor: pointer; font: inherit; color: var(--text); text-align: left; }
.dt-grade-t { font-family: var(--mono); font-weight: 700; font-size: 12px; }
.dt-grade-g { font-size: 12.5px; color: var(--muted); }
.dt-locked-blurb { color: var(--muted); font-size: 13.5px; margin: 0 0 12px; }

/* edit mode: the handle bar */
.dt-bar {
  display: flex; align-items: center; gap: 6px; margin: -6px -6px 10px; padding: 4px 6px;
  border-radius: var(--r-sm); background: var(--surface-2); touch-action: none; cursor: grab;
}
.dt-grip { font-family: var(--mono); color: var(--faint); letter-spacing: -0.2em; padding: 0 4px; user-select: none; }
.dt-ctl {
  min-width: 44px; min-height: 40px; border-radius: var(--r-sm); cursor: pointer;
  background: transparent; border: 1px solid transparent; color: var(--text); font-size: 13px;
  transition: background-color var(--dur-1) var(--ease-out), border-color var(--dur-1) var(--ease-out);
}
.dt-ctl:hover { border-color: var(--line); }
.dt-remove { margin-left: auto; color: var(--faint); }
.dt-remove:hover { color: var(--down); border-color: var(--down); }
.dash-empty { margin-top: 8px; }

/* the add-tile sheet rows */
.dash-add-list { display: grid; gap: 8px; max-height: 52vh; overflow: auto; }
.dash-add-row { display: flex; flex-direction: column; gap: 3px; align-items: flex-start; width: 100%; padding: 12px 14px; border-radius: var(--r-sm); background: var(--surface-2); border: 1px solid var(--line-soft); color: var(--text); text-align: left; cursor: pointer; font: inherit; }
.dash-add-row:hover { border-color: var(--accent); }
.dash-add-lab { font-weight: 600; font-size: 14px; display: flex; gap: 8px; align-items: center; }
.dash-add-blurb { color: var(--muted); font-size: 12.5px; }

/* the news page grid: forward-first DOM order; on a wide screen the digest leads and spans
   two columns, the brief sits beside it, the headline feed runs down the right */
/* News and the dashboard tiles are FLOWS now, not span grids — see the DYNAMIC TILING
   block at the end of this file for the measurement that chose it. The span rules below
   are kept because they still apply at the single-column width, where `.tile-flow` is
   `display: block` and `columns` is unset. */
/* NEWS IS A MULTI-COLUMN FLOW, NOT A GRID, AND THESE RULES WERE DEAD.
   `.nw-grid` also carries `.tile-flow`, which sets `columns: var(--tile-col)`, and this
   rule sets `display: block`. Seven `grid-column` / `grid-row` / `order` declarations and
   two responsive `grid-template-columns` sat underneath describing a deliberate layout —
   digest spanning two tracks, the feed spanning two rows, an explicit order 0 through 6 —
   that has not run since News moved onto the shared flow. Removed rather than left in
   place: inert CSS that reads as live is the trap that produced the `.hw-menu` blur, the
   undefined `.btn`, and the 720-899px header hole, all in the last two days.

   THE INTENT IS KEPT HERE IN WORDS, because it is worth reviving deliberately one day.
   Measured on production at 1920 the six panels are 193 / 398 / 261 / 1042 / 406 / 930px.
   Multicol preserves source order and cannot split a panel, so it partitions them
   811 / 1042 / 1316 — a 505px hole under the first column, which is what Miraaj is seeing.
   Sorting tallest-first closes it to 266 (and to 516 from 675 at 1440), but that reorders
   the page: "What matters today" would stop being first, and on a news page reading order
   outranks packing. A GRID can have both, because grid placement is independent of source
   order — 1042 / 1123 / 1065 is reachable, an 81px hole, with the editorial order intact.
   That is a layout decision for Miraaj rather than a 3am one, and it is the only remaining
   lever: capping the tall panels was tried and measured, and moved the hole between
   columns without shrinking it. */
/* NEWS IS A GRID AGAIN — the decision the block above deferred to Miraaj.
   ---------------------------------------------------------------------------
   The note above measured the two options and left the choice open: multicol
   preserves source order but cannot place, so it partitioned the panels
   811 / 1042 / 1316 at 1920 — the hole Miraaj kept seeing. Measured again on
   production at 1470 before this change, it was worse than that note recorded,
   because `.mc-grid` has since been given `tile-span-all`:

       lead band    174 / 339 / 496      a 322px hole under "What matters today"
       lower band   col bottoms 2413 / 2197 / 1711   a 702px ragged edge

   `column-span: all` chops a flow into segments that each balance alone — the
   failure mode the comment on `.tile-flow` itself warns about — so the earnings
   calendar sitting mid-page turned one mediocre partition into two bad ones.

   A grid fixes both without touching DOM order, which is what that note said was
   the only remaining lever:

     · THE LEAD BAND stretches to a common bottom edge. Three panels, three
       tracks, no placement needed. Equal-height cards are what Miraaj asked for
       ("the tiles are not equally sized"), and a card carrying its own trailing
       space reads as composed where a gap between cards reads as broken.

     · THE LOWER BAND is placed. The two tall panels (earnings detail ~977,
       session brief ~1002) take a track each and span both rows; the two short
       ones (horizon ~221, headline feed ~516) stack in the first. That is the
       optimal 3-way partition of those four heights, and it lands the bottom
       edge flush instead of 702px ragged.

   PLACEMENT IS BY CLASS, NEVER BY ROW INDEX, because these blocks are
   conditional — `mcStatusBlock` and friends return "" on a quiet day. A rule
   keyed to `grid-row: 3` would silently mis-place the page the first time a
   panel did not render; a rule keyed to `.mc-earn` cannot.
   The count is fixed at three rather than `auto-fit`, because explicit column
   assignments and a variable track count cannot both be true. `--shell-tiles`
   already caps the width, so three tracks stay a workbench and never a billboard. */
.nw-grid {
  display: grid;
  grid-template-columns: repeat(3, minmax(0, 1fr));
  gap: var(--s-4);
  align-items: stretch;
}
.nw-grid > * { margin-top: 0 !important; margin-bottom: 0 !important; min-width: 0; }
/* the earnings calendar is a seven-day week — it runs the full width or not at all */
.nw-grid > .tile-span-all { grid-column: 1 / -1; }
/* the lower band, placed tallest-into-its-own-track (see the partition above) */
.nw-grid > .mc-horizon { grid-column: 1; }
.nw-grid > .mc-earn    { grid-column: 2; grid-row: span 2; }
.nw-grid > .nw-brief   { grid-column: 3; grid-row: span 2; }
.nw-grid > .nw-feed    { grid-column: 1; }
/* THE RECAP RUNS FULL WIDTH, and that is a placement decision rather than a default.
   The partition above was measured for four panels; dropping a fifth into auto-placement
   would have it fill whichever cell happened to be free and silently re-rag the edge the
   block above spent two attempts closing. A full-width row cannot do that — it takes a row
   of its own and leaves the measured three-track band exactly as it was.
   It also suits the content: six stories each carrying a sentence is a wide, short band,
   and squeezed into a third of the width every mechanism sentence would wrap to five lines
   directly under the headline it belongs to, which is where it stops reading as a caption.
   Unlike `column-span: all` in the multicol fallback below, a full-width GRID row does not
   chop the layout into separately-balanced segments — that failure mode is multicol's. */
.nw-grid > .nw-recap   { grid-column: 1 / -1; }
/* The stories sit side by side when there is room and stack when there is not — `auto-fit`
   with a floor, never `auto-fill`, and never a fixed count.
   THE PANEL IS ROUTINELY SHORT. Its cap is six but zero, one and two are all correct
   outcomes: most headlines are not one of the eleven kinds we have written a sentence for,
   and the feature's whole posture is that a thin answer is a decision. `auto-fill` reserves
   the tracks that WOULD fit and leaves the surplus empty, so a two-story panel in a
   full-width band would sit in the left third with two empty tracks beside it — the exact
   failure `test_a_grid_never_reserves_a_track_it_has_no_child_for` was written for after it
   stranded 619px on Discover. `auto-fit` collapses the empty tracks and the stories stretch.
   The two are identical when the panel is full, which is how this went unnoticed there. */
.nw-recap .mr-stories {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(min(100%, 22rem), 1fr));
  gap: var(--s-4);
}

/* TWO TRACKS IS WHERE PLACEMENT STOPS PAYING, so below 1100 News goes back to the
   flow the dashboard uses. Four panels into two tracks is two rows exactly, and a grid
   row stretches to its tallest member: every pairing of 221 / 977 / 1013 / 516 puts a
   short panel beside a tall one, so a stretched grid here means a card with 790px of
   nothing in it. Measured with the three-track placement still applied at 1000px, the
   session brief stretched from 1013 to 1747.
   Multicol has the opposite trade — it cannot place, but it also never stretches, and
   with only two tracks to fill its packing is close to optimal (1218 / 1549). Declared
   here rather than by putting `.tile-flow` back on the element, so that one element has
   one layout owner and the three-track placement above cannot half-apply. */
@media (max-width: 1099px) {
  .nw-grid {
    display: block;
    columns: var(--tile-col);
    column-gap: var(--s-4);
  }
  .nw-grid > * {
    break-inside: avoid;
    -webkit-column-break-inside: avoid;
    display: block;
    width: 100%;
    grid-column: auto;
    grid-row: auto;
    margin: 0 0 var(--s-4) !important;
  }
  .nw-grid > .tile-span-all { column-span: all; }
}
@media (max-width: 719px) {
  /* One column — `--tile-col` bottoms out at 340px, which no phone fits twice — and the
     SAME order as everywhere else; "wide" means nothing on a phone. The gap is set through
     the token because the rhythm here comes from the margin above, not from `gap`; that is
     the bug that left the declared 12px rendering at 20px on every phone. */
  .nw-grid { --s-4: 12px; }
  .nw-grid > * { order: initial !important; }
  /* THE SAME BUG ON THE DASHBOARD, AND THE SAME FIX. `.dash-grid` had
     `gap: 12px` here and it never applied: gap does nothing on a block box, and the
     vertical rhythm in a `.tile-flow` comes from `.tile-flow > * { margin-bottom }`,
     which is `!important` and 20px. Measured at 720px wide before this change: declared
     12px, actual 20px. Overriding the token the margin is built from moves both the
     column gap and the row gap together, which is what the 12px was asking for. */
  .dash-grid { --s-4: 12px; }
  .dt-cols { grid-template-columns: minmax(0, 1fr); }
  .dt-v { font-size: 18px; }
  .dash-head { margin-bottom: 18px; }
}
@media (prefers-reduced-motion: reduce) {
  .dash-grid.is-editing .dash-tile { transition: none; }
}
/* long lists stay inside their tile; the whole list lives one tap away on its own page */
.dash-tile #dt-wl-list, .dash-tile #dt-pf-list { max-height: 440px; overflow: auto; }

/* ============================================================================
   THE MARKETING SURFACE — density, bento, glass, reveal
   Built entirely from tokens that already exist (--mat-blur-soft, --el-*, --dur-*,
   --ease-out, --space-section). No new palette, no new type, no new material scale — the
   product's wedge is credibility about money, and chrome that reads as a hype product is
   the one thing this page cannot afford.

   Everything here animates transform and opacity only, stays inside --dur-3, gates hover
   behind a fine pointer, and collapses under the blanket reduced-motion floor.
   ============================================================================ */

/* ---- density. The single biggest gap versus the references: blocks sat 10-16px apart on
   a page whose job is to feel considered. The hero keeps its own rhythm; what changes is
   the air BETWEEN the blocks, which is what reads as confidence. ---- */
.hero { padding-bottom: 40px; }
.hero-wedge { margin-top: 30px; gap: 14px; }
.hint-links { margin-top: 30px; }
#analysed-stat, #free-meter { margin-top: 22px; }
.why-pay { margin: calc(var(--space-section) * 1.6) 0 var(--space-section); }
.wp-h { margin-bottom: 30px; }
.wp-grid { gap: 18px; }
.wp-sample, .wp-trust { margin-top: 26px; }

/* ---- bento. Three claims, unequal weight: the filings claim is the load-bearing one, so
   it takes the full width and the two supporting claims sit beside each other under it.
   An even 3-up grid says all three matter the same amount, which is not what we mean. ---- */
@media (min-width: 760px) {
  .hero-wedge { grid-template-columns: repeat(2, minmax(0, 1fr)); }
  .hero-wedge li:first-child { grid-column: 1 / -1; }
}

/* ---- glass with real depth. `--mat-blur-soft` is the CHIP tier and these are chip-sized
   cards, so this extends the existing ramp rather than forking it. Both degradation paths
   are declared with the other glass surfaces further up this file. ---- */
.hero-wedge li, .wp-col {
  background: color-mix(in srgb, var(--surface) 72%, transparent);
  -webkit-backdrop-filter: var(--mat-blur-soft); backdrop-filter: var(--mat-blur-soft);
  border: 1px solid var(--line-soft);
  border-radius: var(--r-lg);
  box-shadow: var(--el-1);
}
.hero-wedge li {
  padding: 18px 20px 18px 34px;   /* left pad clears the existing bullet, which stays */
}
.hero-wedge li::before { left: 18px; top: 27px; }
.wp-col { padding: 22px; }
.wp-pro { box-shadow: var(--el-2); }

/* ---- hover. Declared plainly and neutralised in the `(hover: none), (pointer: coarse)`
   block further down, which is how the other seventeen hover transforms in this file are
   gated. Wrapping these in `(hover: hover)` instead would work equally well and would make
   the entry in that block unreachable — one mechanism, followed, beats two that half-agree.
   The lift is 2px: enough to read as a response, not enough to be a bounce. ---- */
.hero-wedge li, .wp-col {
  transition: transform var(--dur-2) var(--ease-out),
              border-color var(--dur-2) var(--ease-out),
              box-shadow var(--dur-2) var(--ease-out);
}
.hero-wedge li:hover, .wp-col:hover {
  transform: translateY(-2px);
  border-color: var(--line-strong);
  box-shadow: var(--el-2);
}
/* Same rule: the Pro column is an in-page card, not a popover. */
.wp-pro:hover { border-color: var(--accent); }

/* ---- reveal on scroll. PROGRESSIVE ENHANCEMENT, and that is the load-bearing detail:
   nothing is hidden until JS adds `.rv-on` to the container. A reveal system that hides
   content in the stylesheet and waits for script to un-hide it turns a failed script into
   a blank marketing page — the same defect class as the deep-link guard two commits ago,
   and the reason that one carries a fallback.

   Transitions rather than keyframes so a fast scroll can retarget mid-flight instead of
   restarting from zero, and so the reduced-motion floor can collapse them by duration. ---- */
.rv-on .rv {
  opacity: 0;
  transform: translateY(10px);
  transition: opacity var(--dur-3) var(--ease-out), transform var(--dur-3) var(--ease-out);
  transition-delay: calc(var(--i, 0) * 60ms);
}
.rv-on .rv.rv-in { opacity: 1; transform: none; }

/* Reduced motion: the floor already flattens the DURATION to 0.001ms, but a 10px jump is
   still a jump. Neutralising the offset means the content cross-fades in place — reveals
   on scroll are the exact category that triggers vestibular symptoms, so this removes the
   movement rather than merely shortening it. */
@media (prefers-reduced-motion: reduce) {
  .rv-on .rv { transform: none; }
}

/* The WACC dial is a DCF SENSITIVITY CURVE, not the published fair value. It used to repaint
   the headline duel with its raw per-share figure the moment the reader dragged, so the page
   changed its mind about what a company was worth in response to an interaction. The repaint
   is gone (see app.js `update()`); this is the other half — the panel says which lens it is
   speaking for, so a number that legitimately differs from the hero reads as a different
   question rather than a contradiction. */
.lens-tag {
  margin-left: 10px; font-family: var(--mono); font-size: 10px; letter-spacing: 0.1em;
  text-transform: uppercase; color: var(--faint);
  border: 1px solid var(--line-soft); border-radius: var(--r-full); padding: 3px 8px;
  vertical-align: middle; white-space: nowrap;
}

/* ---- "you already hold this" ------------------------------------------------
   The collision prompt (Portfolio.askExisting). Re-adding a ticker used to overwrite the
   position silently, destroying a cost basis with no undo; the server now refuses a collision
   it was not told how to resolve, and this is where the reader answers. It shows BOTH
   positions and the arithmetic of the blend, so neither button is a leap. */
.pf-ask { max-width: 460px; }
.pf-ask-row {
  display: flex; justify-content: space-between; align-items: baseline; gap: 14px;
  padding: 10px 0; border-bottom: 1px solid var(--line-soft);
}
.pf-ask-lab { color: var(--muted); font-size: var(--t-small); }
.pf-ask-val { font-size: var(--t-body); color: var(--text); font-variant-numeric: tabular-nums; }
.pf-ask-blend {
  margin: 16px 0 4px; padding: 12px 14px; border-radius: var(--r-sm);
  background: var(--surface-2); font-size: var(--t-small); line-height: 1.5; color: var(--muted);
}
.pf-ask-blend b { color: var(--text); font-variant-numeric: tabular-nums; }
.pf-ask-btns { display: flex; gap: 10px; margin-top: 18px; }
.pf-ask-btns > * { flex: 1 1 0; justify-content: center; }
/* The destructive option states its cost in words rather than being styled as a warning —
   the reader is choosing between two legitimate intentions, not being talked out of one. */
.pf-ask-note { margin-top: 12px; font-size: var(--t-micro); line-height: 1.5; color: var(--faint); }
@media (max-width: 460px) { .pf-ask-btns { flex-direction: column; } }

/* ---- the hero's atmosphere -------------------------------------------------
   The poster carries this section on its own. The video is enhancement, and the
   rules below are ordered so it is NEVER FETCHED where it isn't wanted: `display:none`
   on a <video> with preload="none" means no request at all, which is the difference
   between "hidden" and "free". */
.hero-bg { position: absolute; inset: 0; overflow: hidden; z-index: 0; border-radius: inherit; }
/* `> *` used to be enough because the backdrop's only child WAS the <img>. With a
   <picture> the img is a GRANDCHILD, so the cover rule stopped reaching it and the plate
   rendered at its intrinsic 1920x1080 — at a 390px viewport that is the top-left fifth of
   the crop, not the crop. Both levels are named now. */
.hero-bg > *, .hero-bg > picture > * {
  position: absolute; inset: 0; width: 100%; height: 100%; object-fit: cover;
}
.hero { position: relative; isolation: isolate; }
.hero > *:not(.hero-bg) { position: relative; z-index: 1; }
/* …AND THAT IS WHY THE AUTOCOMPLETE LOOKED TRANSPARENT.
   The rule above lifts every hero child clear of the background image, which is right — but
   `position: relative` + `z-index: 1` makes each of them its own STACKING CONTEXT, all at the
   same level. Among equals, DOM order decides, and the search form is written before the
   tagline, the three cards, the hint and the stats line. So `.ac-dropdown`'s `z-index: 40` was
   never competing with them: it is scoped inside the form's own context and cannot rise past
   the form's 1.

   The dropdown was fully opaque the whole time (`background: var(--surface) !important`). It
   was being painted, and then four later siblings were painted on top of it — which reads
   exactly like a transparent panel, and got reported as "the fonts are overlapping".

   Raising the form is the fix rather than raising the dropdown: a child cannot escape its
   parent's stacking context, so the number that has to change is the parent's. Unconditional
   because the form is a single row in normal flow with nothing beneath it to occlude — and
   because a `:has()`-conditional version would put the whole defect behind one selector's
   support. `.lp-cta-ask` carries the identical trap on the landing CTA (its `.lp-cta-sub`
   paragraph follows it), so it is fixed in the same breath rather than waiting to be
   rediscovered. */
.hero > .hero-ask,
.lp-cta > .lp-cta-ask { z-index: 2; }
/* The video sits ON TOP of the still and simply is not there until it can play, so there is
   no swap to see — the first frame of the clip IS the poster (measured: frame 0). */

/* PHONES: still only. A hero video on a phone spends someone's data on decoration, and the
   344px slot shows so little of the frame that the drift reads as noise. */
@media (max-width: 899px) {
}
/* REDUCED MOTION: the offset is REMOVED, not shortened — a slow drift is still drift. */
@media (prefers-reduced-motion: reduce) {
}

/* ---- chrome over the hero --------------------------------------------------
   INSIDE THE HERO THE BAR STOPS TRACKING THE THEME, because the backdrop there is the
   image, not the page. Measured against the alpine footage, which is 2.7x brighter than
   the fog it replaced (nav-band peak 0.5350 vs 0.1996 — its DARKEST frame is brighter
   than the fog's brightest):

       dark bar + white labels   9.50:1 over the poster, 7.39:1 over the brightest frame
       --muted labels            3.88:1 over the brightest frame — under AA
       white on the LIGHT bar    1.23:1 — effectively invisible

   That last number is why "white labels over the hero" is not on its own the fix: in light
   theme the bar is light, and white on light fails catastrophically. The bar has to go dark
   too, in both themes.

   Scoped deliberately. `--faint` -> `--muted` was moved globally earlier tonight and paid
   for itself on ordinary pages; ratcheting a global token a second time to fit one image
   would be spending the whole site's palette on the top 800px of one page.

   SCOPED ON THE HERO BEING VISIBLE, not merely present. `.hero-bg` lives inside `#home`,
   which the SPA HIDES rather than removes when you open a company — so `body:has(.hero-bg)`
   matched on every route and applied dark chrome sitewide, which is the exact ratchet this
   block exists to avoid. Caught by checking a company page rather than only the home page.
   `#home:not(.hidden)` is the idiom the rest of this file already uses for the same reason. */
/* SUPERSEDED 2026-08-20 — the rules that stood here are deleted, not moved, and the reason
   is worth keeping because the comment above argues for them.

   They keyed the reversed bar off the hero BEING IN THE DOCUMENT (`#home:not(.hidden)
   .hero-bg`). That is presence, not ground. The landing is ~27,000px tall, the hero is the
   top 800 of it, and `#home` stays un-hidden the whole way down — so from y=800 onward the
   bar kept a 0.68-alpha DARK film and white labels while sitting over cream bands. Measured
   at 1440 scrolled to y=1500, light theme: brand 3.15:1, theme toggle 1.00:1, search field
   3.46:1, and the dark `.lp-range` band scrolling underneath was legible straight through
   the bar — it read as a rendering fault rather than as a material.

   Miraaj asked for exactly the thing that fixes it: "make the choice follow the ground
   rather than the theme." `body.over-hero` does — an IntersectionObserver on `.hero-bg`
   watching the header band, so the ink reverses while the plate is actually behind the bar
   and returns to `--mat-regular` the moment it is not. One mechanism, in the ground-following
   ink block further down this file. Two would disagree, and did. */

/* ---- the hero stack --------------------------------------------------------
   Studied from useorigin.com: the ORDER (badge, headline, subhead, CTA, search, tagline),
   the ~6:1 jump from headline to body, the 0.9 line-height, and a ~24-29px rhythm between
   every element. Expressed in our own tokens — their palette, typefaces and words are not
   ours to take, and every string here is written for this product.
   ========================================================================== */
/* FULL BLEED, and behind the bar. `.hero` sits inside `.wrap` (max-width 1120), so left to
   itself the image was a boxed panel with page background around it and the header sitting
   ABOVE it rather than over it — which is most of what makes the reference read as a hero.
   The background breaks the container with the standard 50%/-50vw escape, and the section
   pulls up under the sticky header by its height so the video runs behind the chrome the
   scoped white-label treatment was written for. */
.hero {
  text-align: center;
  padding: clamp(72px, 7vw, 130px) 0 clamp(56px, 5vw, 84px);
}
/* The backdrop is EXTENDED UPWARD rather than the section being pulled up. A negative
   margin-top on `.hero` collapsed against its wrapper — only 37px of a declared -74px
   applied, leaving a band where the bar sat over the page rather than the picture. Growing
   the absolutely-positioned backdrop has no margin to collapse and no effect on the flow of
   anything inside it. */
.hero-bg {
  left: 50%; right: auto; width: 100vw; margin-left: -50vw;
  top: calc(-1 * var(--header-h, 74px));
  height: calc(100% + var(--header-h, 74px));
  border-radius: 0;
}
/* The "< Watchlist" back button sits ABOVE the hero in the discover pane and pushed the
   whole section 37px down, leaving a band where the bar was over the page rather than the
   picture. Hidden while the hero is showing, because the hero IS the top of this page and a
   back-link above a full-bleed image reads as a mistake.
   NOT a stranding: the watchlist is reachable from the account menu ("My watchlist", added
   2026-08-19) and `homeActivePane()` sends anyone who owns a watched ticker straight to it
   on arrival. Both verified in a browser when that menu entry was built. */
body:has(#home:not(.hidden) .hero-bg) .hw-back { display: none; }

/* Anything that renders dark-on-light by default is unreadable over the picture. These are
   the two strips that sit inside .hero and were never designed to. */
body:has(#home:not(.hidden) .hero-bg) .hero .hint,
body:has(#home:not(.hidden) .hero-bg) .hero .usage-stat { color: var(--on-plate-2); }
body:has(#home:not(.hidden) .hero-bg) .hero .hint b { color: var(--on-plate); }
/* The counters and their separators render at --text / --faint, which are dark in light
   theme and near-black inside the usage strip. Over the picture they disappeared entirely.
   Listed explicitly rather than blanket-whitening the hero: `.hero-wedge li` keeps its own
   light card and its dark text is correct ON that card. */
body:has(#home:not(.hidden) .hero-bg) .hero .us-num,
body:has(#home:not(.hidden) .hero-bg) .hero .us-sp { color: var(--on-plate); }
body:has(#home:not(.hidden) .hero-bg) .hero .us-dot,
body:has(#home:not(.hidden) .hero-bg) .hero .usage-stat { color: var(--on-plate-2); }
/* The stack's measure. `.hero-ask` sets its own narrower one and must win, so this does not
   use a selector more specific than a single class. */
.hero > *:not(.hero-bg) { max-width: 820px; margin-left: auto; margin-right: auto; }
.hero > .hero-ask { max-width: 640px; }

.hero-badge {
  display: inline-block; font-family: var(--mono); font-size: var(--t-meta);
  letter-spacing: 0.08em; text-transform: uppercase;
  padding: 6px 14px; border-radius: var(--r-full);
  background: var(--accent-soft); color: var(--accent);
  margin-bottom: 26px;
}
/* The headline. Weight 300 and a line-height under 1 are the whole character: the SIZE
   carries it, not the weight — a heavy 88px serif would read as shouting rather than
   confident. The italic first word is a serif's own emphasis, not an added colour. */
.hero-h1 {
  font-family: var(--serif); font-size: var(--t-hero); font-weight: 300;
  line-height: 0.9; letter-spacing: -0.015em; color: var(--text);
  margin: 0 0 26px;
}
.hero-h1 em { font-style: normal; font-weight: 500; }

.hero-sub {
  font-size: var(--t-body); line-height: 1.5; color: var(--text);
  margin: 0 auto; max-width: 46ch;
}
.hero-sub-2 { color: var(--muted); margin-top: 6px; }

.hero-cta {
  display: inline-block; margin-top: 26px;
  font-family: var(--mono); font-size: var(--t-meta); letter-spacing: 0.06em;
  text-transform: uppercase; text-decoration: none;
  padding: 13px 20px; border-radius: var(--r-full);
  background: var(--text); color: var(--bg);
  transition: transform var(--dur-2) var(--ease-out), opacity var(--dur-2) var(--ease-out);
}
.hero-cta:hover { transform: translateY(-1px); opacity: 0.9; }

/* The search field. Origin's is a large rounded "ask" input with a circular arrow; the FORM
   is borrowed and the promise is not — ours searches companies and says so. */
.hero-ask {
  position: relative; display: flex; align-items: center; gap: 8px;
  margin: 40px auto 0; max-width: 640px;
  padding: 8px 8px 8px 24px; border-radius: var(--r-full);
  background: color-mix(in srgb, var(--surface) 62%, transparent);
  -webkit-backdrop-filter: var(--mat-blur-soft); backdrop-filter: var(--mat-blur-soft);
  border: 1px solid var(--mat-hairline);
}
.hero-ask input {
  flex: 1 1 auto; min-width: 0; background: none; border: none; outline: none;
  font-family: var(--sans); font-size: var(--t-body); color: var(--text);
  padding: 14px 0;
}
.hero-ask input::placeholder { color: var(--muted); }
.hero-ask-go {
  /* The base rule, for any `.hero-ask` outside the two skinned surfaces below. Same change:
     the button is as wide as its word. */
  flex: 0 0 auto; width: auto; min-width: 44px; height: 44px; padding: 0 18px;
  border-radius: var(--r-full); border: none;
  display: grid; place-items: center; cursor: pointer;
  background: var(--text); color: var(--bg);
  font-family: var(--sans); font-size: 14px; font-weight: 600; white-space: nowrap;
  transition: transform var(--dur-2) var(--ease-out);
}
.hero-ask-go:hover { transform: scale(1.06); }
.hero-ask-go:active { transform: scale(0.97); }

.hero-tag { margin-top: 22px; font-size: var(--t-small); color: var(--muted); }

/* Over the hero image the stack is light-on-dark in BOTH themes, for the same reason the
   bar is: the backdrop is the picture. */
body:has(#home:not(.hidden) .hero-bg) .hero-h1,
body:has(#home:not(.hidden) .hero-bg) .hero-sub { color: var(--on-plate); }
body:has(#home:not(.hidden) .hero-bg) .hero-sub-2,
body:has(#home:not(.hidden) .hero-bg) .hero-tag { color: var(--on-plate-2); }
/* SCOPED TO `.hero`, and it has to be. These rules invert the CTA and the field so they
   read OVER THE PICTURE — but the selector only asked whether the page HAS a hero, so it
   reached every `.hero-cta` and `.hero-ask` on the document. Phase 4 then reused both in
   the how-it-works band and the closing CTA, hundreds of pixels below the photograph,
   where they inherited an inversion meant for a backdrop that is not behind them: a white
   pill on a white band, and a white arrow button on cream. Invisible, and measured as such.

   `#0f1011` went with it. It is a hex from a retired palette that is not a Titan token —
   the ink is #111111. */
body:has(#home:not(.hidden) .hero-bg) .hero > .hero-cta,
body:has(#home:not(.hidden) .hero-bg) .hero .hero-cta {
  background: var(--on-plate); color: rgb(var(--plate-wash));
}
body:has(#home:not(.hidden) .hero-bg) .hero .hero-ask {
  background: var(--on-plate-fill); border-color: var(--on-plate-rim-2);
}
body:has(#home:not(.hidden) .hero-bg) .hero .hero-ask input { color: var(--on-plate); }
body:has(#home:not(.hidden) .hero-bg) .hero .hero-ask input::placeholder { color: var(--on-plate-3); }
body:has(#home:not(.hidden) .hero-bg) .hero .hero-ask-go { background: var(--on-plate); color: rgb(var(--plate-wash)); }

@media (max-width: 620px) {
  .hero-ask { padding-left: 18px; margin-top: 30px; }
  /* On a phone the field is the scarce thing, not the button. Tighter padding keeps the word
     and still leaves the placeholder its measure; the 44px minimum keeps the tap target. */
  .hero .hero-ask-go, .lp-cta-ask .hero-ask-go { padding: 0 var(--s-3); font-size: 13px; }
  .hero-ask input { font-size: var(--t-small); }
}

@media (max-width: 699px) {
  /* 8em = 0.90x the worst-case single line. Under the crossing, never at it. */
  .hero h1 { max-width: 8em; }
}

/* The score panel's "couldn't load" state. Deliberately plain: it is an admission, not a
   feature, and it must not look like a designed empty state that the reader mistakes for
   "this company has no score". */
.sc-unavail { color: var(--muted); font-size: var(--t-small); line-height: 1.6; padding: 18px 0; }
/* A BUTTON THAT READS AS A LINK, unscoped. This was written as `.sc-unavail .linkish` and the
   class was then used in two other places — the reload prompt on a failed panel, and the
   "did you mean" suggestions on an unresolved ticker — where it fell back to a default browser
   button: grey chrome, boxed, in the middle of a sentence. A class named for how it should
   look belongs at the level it is used. */
.linkish {
  background: none; border: none; padding: 0; cursor: pointer;
  color: var(--accent); font: inherit; text-decoration: underline;
}
.linkish:hover { text-decoration-thickness: 2px; }
.linkish:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; border-radius: var(--r-sm); }

/* The allocation illustration on the portfolio page. Deliberately quiet for something that
   talks about money: it is a worked example, and the moment it looks like a product
   recommendation the copy is fighting the design. */
.pf-plan { margin-top: 22px; padding: 18px; border: 1px solid var(--line-soft); border-radius: var(--r); }
.pf-plan-head { display: flex; align-items: center; gap: 10px; margin-bottom: 8px; }
.pf-plan-tag {
  font-size: var(--t-micro); letter-spacing: .08em; color: var(--muted);
  border: 1px solid var(--line); border-radius: var(--r-sm); padding: 1px 6px;
}
.pf-plan-copy { color: var(--muted); font-size: var(--t-small); line-height: 1.6; margin-bottom: 12px; }
.pf-plan-bar { display: flex; height: 10px; border-radius: var(--r-pill); overflow: hidden; margin: 14px 0 18px; }
.pf-plan-seg { display: block; height: 100%; }
.pf-plan-dot { width: 9px; height: 9px; border-radius: 50%; display: inline-block; }
/* Four sleeves, four steps of one hue rather than four colours — they are parts of one
   allocation, not four competing things. */
.pf-plan-us_equity { background: var(--accent); }
.pf-plan-intl_equity { background: color-mix(in srgb, var(--accent) 62%, var(--surface)); }
.pf-plan-bonds { background: color-mix(in srgb, var(--accent) 34%, var(--surface)); }
.pf-plan-short_term { background: var(--line); }
.pf-plan-row { padding: 12px 0; border-top: 1px solid var(--line-soft); }
.pf-plan-rhead { display: flex; align-items: baseline; gap: 8px; }
.pf-plan-lab { font-weight: 600; }
.pf-plan-pct { margin-left: auto; font-variant-numeric: tabular-nums; color: var(--text); }
.pf-plan-what { color: var(--text); font-size: var(--t-small); margin-top: 4px; }
.pf-plan-why { color: var(--muted); font-size: var(--t-small); line-height: 1.6; margin-top: 3px; }
.pf-plan-funds { color: var(--muted); font-size: var(--t-micro); margin-top: 6px; }
.pf-plan-any { opacity: .8; }
/* The locked teaser's sleeve names — what the allocation holds, with the weights withheld
   server-side. Deliberately quiet: it is a list of what is there, not a second headline. */
.pf-plan-sleeves {
  color: var(--muted); font-size: var(--t-micro); line-height: 1.7; margin: 4px 0 14px;
}
.pf-plan-because, .pf-plan-note {
  color: var(--muted); font-size: var(--t-small); line-height: 1.6; margin-top: 10px;
}
.pf-plan-disc {
  color: var(--muted); font-size: var(--t-micro); line-height: 1.6;
  margin-top: 14px; padding-top: 12px; border-top: 1px solid var(--line-soft);
}
/* The panel's "change your answers" control. `.linkish` is scoped to .sc-unavail on main, so
   this states its own — #106 promotes the class properly and this rule becomes redundant
   then, harmlessly. */
.pf-plan .linkish {
  background: none; border: none; padding: 0; cursor: pointer;
  color: var(--accent); font: inherit; text-decoration: underline;
}

/* ---- account settings ------------------------------------------------------ */
.set-modal { max-width: 520px; text-align: left; max-height: 86vh; overflow-y: auto; }
.set-sec { padding: 20px 0; border-top: 1px solid var(--line-soft); }
.set-sec:first-of-type { border-top: none; padding-top: 8px; }
.set-sec h3 { font-size: var(--t-title); margin: 0 0 6px; }
.set-note { font-size: var(--t-small); color: var(--muted); margin: 0 0 14px; line-height: 1.5; }
.set-sec input {
  display: block; width: 100%; margin-bottom: 10px; padding: 11px 13px;
  border: 1px solid var(--line); border-radius: var(--r-sm);
  background: var(--surface); color: var(--text); font: inherit; font-size: var(--t-body);
}
.set-sec input:focus { outline: none; border-color: var(--accent); box-shadow: 0 0 0 3px var(--accent-soft); }
.set-q { margin-bottom: 16px; }
.set-q-label { font-size: var(--t-small); color: var(--muted); margin-bottom: 8px; }
.set-q-opts { display: flex; flex-wrap: wrap; gap: 8px; }
.set-opt {
  padding: 9px 13px; border-radius: var(--r-full); cursor: pointer;
  border: 1px solid var(--line); background: var(--surface);
  color: var(--text); font: inherit; font-size: var(--t-small);
  transition: border-color var(--dur-2) var(--ease-out), background var(--dur-2) var(--ease-out);
}
.set-opt.on { border-color: var(--accent); background: var(--accent-soft); color: var(--accent); }
/* The message line is the only feedback these forms give, so it must never be mistaken for
   decoration: it says what happened, and failure reads differently from success. */
.set-msg { font-size: var(--t-small); margin-top: 8px; min-height: 1.2em; }
.set-msg.ok { color: var(--up-text, var(--accent)); }
.set-msg.bad { color: var(--down-text, #d64545); }
/* The suggestion the form produces, drawn inside the modal the moment it is saved. It reuses
   .pf-plan wholesale — same markup, same rules — so the answer cannot look like one thing in
   the modal and another on the portfolio page. Only the top margin is its own. */
.set-plan:empty { display: none; }
.set-plan .pf-plan { margin-top: 16px; }

/* ============================================================================
   TITAN PHASE 2 — THE STATE MATRIX, APPLIED
   ---------------------------------------------------------------------------
   Phase 1 declared the state tokens. This is where every interactive primitive
   in the app actually reads them, so no surface improvises its own.

   WHY THIS BLOCK EXISTS AT ALL — measured, not assumed. Forcing :hover, :active
   and :focus-visible through CDP and diffing the COMPUTED style across six
   surfaces found EIGHTEEN interactive primitives with no hover response
   whatsoever. Seventeen of them were already dead before Titan; the token
   re-point killed exactly one (`.sect`, whose hover was `box-shadow:
   var(--el-2)`, and --el-2 is now `none` because Titan has no drop shadows).

   So this is not a Titan regression being patched — it is a gap the state
   matrix was declared to close. Several of the eighteen HAD a hover rule that
   simply lost the cascade to a later, more specific one; that is the
   "declared twice, decided somewhere nobody would look" defect this file has
   hit repeatedly. Rather than re-derive the specificity of each one, the rules
   live here, at the end, where the cascade is not in question. Re-measured
   after: 0 of 18.

   THE IDIOM IS TITAN'S, NOT APPLE'S. With no shadow to raise and no accent to
   tint with, a state change is a change of FILL WEIGHT or HAIRLINE WEIGHT.
   Nothing in this block moves: no transform, no travel. That is a design
   choice AND it means this block adds nothing for the reduced-motion floor to
   catch — the only animated properties are colours.

   THE WASH IS A GRADIENT, NOT A BACKGROUND-COLOUR, and that is load-bearing.
   These primitives sit on four different grounds (transparent rows on a card,
   canvas chips on cream, cream chips on canvas, ink buttons). A
   `background-color` would REPLACE each one's own fill and would have to be
   written four times; a single-stop `linear-gradient` composites OVER whatever
   background-color is already there, so one declaration is correct on every
   ground. None of the targets below uses background-image for anything else.

   WHAT DELIBERATELY GETS NO HOVER: `.panel`, `.grp`, `.chg`, `.badge`, `.tag`,
   `.sc-bandlab` and the chart canvas. They are surfaces and readouts, not
   controls, and a thing that highlights under the cursor is claiming to be
   clickable. The audit reports them as unresponsive and they should stay that
   way — that entry in the report is a PASS, not a finding.
   ========================================================================= */

/* one wash, two weights */
:root {
  --wash-hover: linear-gradient(var(--state-hover-fill), var(--state-hover-fill));
  --wash-active: linear-gradient(var(--state-active-fill), var(--state-active-fill));
}

/* ---- row-like: clickable list rows and disclosure summaries ---------------
   The whole app is lists. These are the most-hovered elements in the product
   and eight of them had nothing. */
.wl-row:not(.wl-row-static),
.hw-row,
.sect > .sect-sum,
.dt-row,
.ac-item,
.ss-row,
.hw-mi,
.meth-toc a,
.pf-idea,
.pa-idea {
  transition: background-color var(--dur-2) var(--ease-out),
              background-image var(--dur-2) var(--ease-out),
              border-color var(--dur-2) var(--ease-out),
              color var(--dur-2) var(--ease-out);
}
.wl-row:not(.wl-row-static):hover,
.hw-row:hover,
.sect > .sect-sum:hover,
.dt-row:hover,
.ac-item:hover,
.ss-row:hover,
.hw-mi:hover,
.meth-toc a:hover,
.pf-idea:hover,
.pa-idea:hover {
  background-image: var(--wash-hover);
}
.wl-row:not(.wl-row-static):active,
.hw-row:active,
.sect > .sect-sum:active,
.dt-row:active,
.ac-item:active,
.ss-row:active,
.hw-mi:active,
.pf-idea:active,
.pa-idea:active {
  background-image: var(--wash-active);
}

/* ---- the ones a hand-written list missed ---------------------------------
   Found by asking the DOM instead: every element that ADVERTISES itself as
   clickable (a real control, an onclick, role=button, or `cursor: pointer`) and
   does not respond. Run over nine surfaces it returned 69 — but 54 of those were
   descendants, because `cursor: pointer` inherits and every span inside a
   clickable row reports as clickable. Counting only the OUTERMOST control left
   15, of which these are the real ones; the rest are selected states that are
   excluded on purpose, and the WACC slider, which answers to focus and to its
   own thumb.

   The lesson worth keeping: a hand-written selector list will always miss a
   surface. The probe is `tools/titan/clickable.py`; run it before shipping a
   phase, not after. */
.mover,
.nw-mv-row,
.pc-peer > td,
.mc-etop,
summary.mono,
.rel-chip,
.scr-export {
  transition: background-color var(--dur-2) var(--ease-out),
              background-image var(--dur-2) var(--ease-out),
              border-color var(--dur-2) var(--ease-out);
}
.mover:hover,
.nw-mv-row:hover,
.mc-etop:hover,
summary.mono:hover,
.rel-chip:hover,
.scr-export:hover {
  background-image: var(--wash-hover);
}
/* The wash goes on the CELLS, not on the <tr>. A row's background paints behind its
   cells, and these cells set their own opaque fill (the first column is a sticky
   `background: var(--surface)`), so a wash on the row was painted over and read as a
   dead hover in the probe. */
.pc-peer:hover > td { background-image: var(--wash-hover); }
.mover:active,
.nw-mv-row:active,
.mc-etop:active,
summary.mono:active,
.rel-chip:active {
  background-image: var(--wash-active);
}
.pc-peer:active > td { background-image: var(--wash-active); }

/* The wordmark is a link, not a control, and Titan gives links no colour of
   their own — so it says so the way text does. Same treatment `.seo-brand`
   already had; the SPA's copy of it simply never got one. */
/* A BRAND LOCKUP IS NOT A TEXT LINK, so it does not take the text-link hover.
   An underline under the mark reads as body copy, and it sits awkwardly against the
   guilloche seal beside it. Stated as the rule rather than as a one-off override, so the
   next "give every link a hover" pass cannot quietly put it back.

   AND IT CANNOT REFLOW. Miraaj suggested bold; a weight change on a wordmark grows the
   box and nudges whatever sits beside it — a layout shift on the most-seen element on the
   site. Opacity changes no metric at all, and it dims the SEAL AND THE WORDMARK TOGETHER,
   because they are one mark — which a text-decoration on `.mark` alone never did.
   Verified: the header's geometry is byte-identical hovered and unhovered, both themes. */
.brand:hover, .brand:hover .mark, a.brand:hover { text-decoration: none; }
.brand { transition: opacity var(--dur-2) var(--ease-out); }
.brand:hover { opacity: 0.62; }
.brand:active { opacity: 0.5; }

/* Two CTAs that were built as buttons and styled as neither. */
.wp-cta:hover,
.pr-cta:hover { background: color-mix(in srgb, var(--accent) 88%, var(--bg)); }

/* ---- chip-like: small bordered controls ----------------------------------
   Two cues, not one. A 4.5% wash on a 26px chip is easy to miss on its own, so
   the hairline steps up with it — which is also the only "elevation" Titan
   has. */
.sector-chip,
.adv-chip,
.seg-opt,
.theme-btn,
.tip-ico,
.reset-w,
.mc-ebtn,
.dt-watch,
.set-opt,
.dt-more,
.hint-links .screener-link,
.pnav-item {
  transition: background-color var(--dur-2) var(--ease-out),
              background-image var(--dur-2) var(--ease-out),
              border-color var(--dur-2) var(--ease-out),
              color var(--dur-2) var(--ease-out);
}
.sector-chip:not(.active):hover,
.adv-chip:hover,
.seg-opt:not(.on):hover,
.theme-btn:hover,
.tip-ico:hover,
.reset-w:hover,
.mc-ebtn:hover,
.dt-watch:not(:disabled):hover,
.set-opt:not(.on):hover,
.dt-more:hover,
.hint-links .screener-link:hover,
.pnav-item:hover {
  background-image: var(--wash-hover);
  border-color: var(--state-hover-line);
  color: var(--text);
}
.sector-chip:not(.active):active,
.adv-chip:active,
.seg-opt:not(.on):active,
.theme-btn:active,
.tip-ico:active,
.reset-w:active,
.mc-ebtn:active,
.set-opt:not(.on):active,
.pnav-item:active {
  background-image: var(--wash-active);
  border-color: var(--state-active-line);
}
/* An ALREADY-SELECTED chip does not get a hover wash. Titan's one loud state is
   the inverted ink fill, and washing over it makes the selection look like it is
   about to change. `.sector-chip.active` / `.seg-opt.on` / `.set-opt.on` are
   excluded above for exactly that reason, not by oversight. */

/* ---- solid buttons -------------------------------------------------------
   These already carry a press transform, which stays — a button is the one
   thing that SHOULD move. What they lacked was a hover that reads on an ink
   fill: `filter: brightness()` on near-black moves almost nothing, which is
   why it looked designed and did nothing. The fill steps toward the ground
   instead, which is visible in both themes. */
.dl-cta:hover,
.lock-btn:hover,
.lock-card-cta:hover,
.auth-submit:hover,
.hero .big-search button:hover,
.scr-run:hover,
.dash-btn-primary:hover {
  background: color-mix(in srgb, var(--accent) 88%, var(--bg));
}

/* ---- disabled: ONE opacity, from the token --------------------------------
   It was improvised in two places at two different values (0.6 and 0.7), which
   is how a "disabled" control ends up looking more available than another
   disabled control beside it. `cursor: not-allowed` rather than `default`: the
   control is unavailable, not decorative. */
button:disabled,
input:disabled,
select:disabled,
textarea:disabled,
[aria-disabled="true"] {
  opacity: var(--state-disabled-opacity);
  cursor: not-allowed;
  pointer-events: none;
}
/* A disabled control must not also try to respond. */
button:disabled:hover,
[aria-disabled="true"]:hover {
  background-image: none;
  border-color: var(--line);
}

/* The last off-palette literal in this file. `--down-text` is always defined, so
   the fallback was dead as well as off-palette. */
.set-msg.bad { color: var(--down-text); }

/* ============================================================================
   TITAN PHASE 3 — THE BRAND KIT
   ---------------------------------------------------------------------------
   Helpers for the assets in web/assets/{brand,engraved,photo}, built to the
   TickerWorth-Titan-Kit README. Two of its rules are easy to get wrong and are
   the reason these are tokens rather than per-surface values.
   ========================================================================= */

/* ---- PHOTOGRAPHY: corners in CSS, never baked in --------------------------
   Titan frames photography at rounded.md (32px = --r-lg). The kit ships the
   crops with square corners ON PURPOSE, so the AVIF and JPEG stay usable
   anywhere — a rounded PNG is a picture with a hole in it. */
.image-frame {
  border-radius: var(--r-lg);
  overflow: hidden;
  display: block;
  background: var(--bg);
}
.image-frame > img,
.image-frame > picture > img { display: block; width: 100%; height: auto; }

/* ---- ENGRAVINGS: unframed, in negative space, never cropped ---------------
   "Engravings float unframed in whitespace; photography sits in 32px frames."
   The opacity comes from the per-ground register in the token block — one
   shared number does not work, which is the whole finding.

   `pointer-events: none` because these sit UNDER headlines and a reader
   selecting a sentence must not select the ornament instead. */
/* MASKED, not <img>, and the reason is dark mode. The kit ships each plate twice, `-ink`
   for light grounds and `-white` reversed — and an <img> cannot choose between them when
   the theme is a `data-theme` attribute rather than the OS preference, so the ink plate
   was invisible the moment a reader toggled to dark. As a mask the line art's ALPHA is the
   shape and `currentColor` is the colour, so one file is correct on every ground. Same
   mechanism the seal already uses; the `-white` files are not shipped. */
.engrave {
  position: absolute;
  pointer-events: none;
  user-select: none;
  z-index: 0;
  opacity: var(--engrave-on-cream);
  background-color: currentColor;
  color: var(--text);
  -webkit-mask-repeat: no-repeat; mask-repeat: no-repeat;
  -webkit-mask-size: contain; mask-size: contain;
  -webkit-mask-position: center; mask-position: center;
}
.engrave[data-plate="wave-field"] {
  -webkit-mask-image: url("/assets/engraved/wave-field-ink-1920.webp");
  mask-image: url("/assets/engraved/wave-field-ink-1920.webp");
}
.engrave[data-plate="value-curves"] {
  -webkit-mask-image: url("/assets/engraved/value-curves-ink-1920.webp");
  mask-image: url("/assets/engraved/value-curves-ink-1920.webp");
}
.engrave[data-plate="rosette-bloom"] {
  -webkit-mask-image: url("/assets/engraved/rosette-bloom-ink-1920.webp");
  mask-image: url("/assets/engraved/rosette-bloom-ink-1920.webp");
}
/* the plates are 16:9 except the rosette, which is square */
.lp-steps-orn, .lp-range-orn { aspect-ratio: 16 / 9; }
.lp-cta-orn { aspect-ratio: 1 / 1; }
.engrave-host { position: relative; }
.engrave-host > *:not(.engrave) { position: relative; z-index: 1; }
.engrave-on-canvas { opacity: var(--engrave-on-canvas); }
.engrave-on-dark { opacity: var(--engrave-on-dark); color: #ffffff; }

/* A reader who has asked for less motion has not asked for less ornament, so
   these stay. But `prefers-contrast: more` IS a request to remove anything
   competing with the text, and a full-bleed engraving behind a headline is
   exactly that. */
@media (prefers-contrast: more) {
  .engrave { display: none; }
}

/* ---- Titan's dark bands, which this app had no surface for until now -------
   The footer and the case-study band are `surface-dark`, and the two tokens
   below exist because Titan's own spec sets eyebrows in `slate` AND sets these
   grounds to near-black, without reconciling them: slate on #111111 measures
   2.93:1. `--on-dark-muted` is the token that does (5.45:1). */
.band-dark {
  background: var(--surface-dark);
  color: #ffffff;
}
.band-dark .eyebrow,
.band-dark .muted { color: var(--on-dark-muted); }
.band-dark hr,
.band-dark .rule { border: none; border-top: 1px solid var(--hairline-on-dark); }

/* ============================================================================
   TITAN PHASE 4 — THE LANDING COMPOSITION
   ---------------------------------------------------------------------------
   Miraaj's design, built to its STRUCTURE and hierarchy. One deliberate
   departure, flagged in the report: the comp renders its headlines and wordmark
   in a SERIF. Titan ships Geist and Geist Mono and nothing else, and the brand
   kit's own wordmark is sans — so every headline here is Geist at Titan's
   display scale and tracking, and the lockup is the kit's seal, not the comp's
   serif wordmark. Everything else is the comp: the full-bleed photographic
   hero, the search-first hierarchy, the section order, the numbered rail, the
   dark range band, the four-column transparency row and the closing CTA.

   The comp's green and red on the range band is duotone, which already ships.

   MARKETING REGISTER THROUGHOUT: these sections read from the top of Titan's
   ramp — 96px band rhythm (--rhythm-marketing), 32px radius, display type —
   not the in-app density register. That is what the two named registers are
   for.
   ========================================================================= */

/* ---- shared: the section furniture ---------------------------------------- */
.lp-eyebrow {
  margin: 0 0 var(--s-3);
  display: block;                 /* the app's .eyebrow is a flex row with a trailing rule;
                                     on a centred marketing band that rule reads as a stray
                                     hairline, so these opt out of it */
}
.lp-eyebrow::after { display: none; }
.lp-h2 {
  font-family: var(--sans);
  font-size: var(--t-display);
  font-weight: 500;               /* Titan sets its whole display ramp at 500 */
  line-height: var(--lead-display);
  letter-spacing: var(--track-display);
  color: var(--text);
  margin: 0 0 var(--s-4);
  text-wrap: balance;
}
.lp-lede {
  font-size: var(--t-body);
  line-height: 1.55;
  color: var(--muted);
  max-width: 46ch;
  margin: 0 0 var(--s-4);
}

/* ---- 1. HOW IT WORKS ------------------------------------------------------ */
/* A BAND IS FULL-BLEED OR IT IS A CARD; these were neither.

   `calc(-1 * var(--wrap-pad))` cancels the container's PADDING but not its gutter — and
   `main.wrap` is `min(1560px, 100% - 40px)`, so it holds 20px back at every width above the
   phone. Measured at 1440: the three bands rendered at x=20, w=1400 inside a 1440 viewport.
   Twenty pixels of page down each side turns a band into a very wide box, and a box with
   square corners sitting between 32px cards is what Miraaj saw as "keep shape consistency".

   `calc(50% - 50vw)` is the escape the watchlist rail already uses in this file (#mylist),
   and the matching `calc(50vw - 50%)` padding puts the content back on the same optical
   line as the sections above and below, so only the GROUND bleeds. Square corners are then
   correct rather than off-register: a full-bleed band has no corners to round.
   ========================================================================== */
.lp-steps {
  /* `--surface`, not `--bg`. The page ground IS the cream, so a cream band on it is not a
     band — it is a pair of hairlines with nothing between them. The design's how-it-works
     section reads as a lighter panel across the page, which is canvas. */
  background: var(--surface);
  margin: var(--rhythm-marketing) calc(50% - 50vw) 0;
  padding: var(--rhythm-marketing) calc(50vw - 50%);
  border-top: 1px solid var(--line);
  border-bottom: 1px solid var(--line);
  text-align: center;
  overflow: hidden;               /* the ornament is wider than the band on purpose */
}
.lp-steps-orn {
  width: 1400px; max-width: 160%; height: auto;
  left: 50%; top: 50%; transform: translate(-50%, -50%);
}
.lp-steps .lp-h2 { margin-bottom: var(--s-6); }
.lp-rail {
  list-style: none; margin: 0 0 var(--s-6); padding: 0;
  display: grid; grid-template-columns: repeat(4, minmax(0, 1fr)); gap: var(--s-5);
  text-align: left;
}
.lp-rail li { position: relative; padding-top: var(--s-5); min-width: 0; }
/* THE RAIL. The comp draws a hairline between the numbered discs. It is one line per gap,
   so it is drawn on the ITEM rather than the list — a single background line behind the row
   would run under the discs and out past the last one. */
.lp-rail li::before {
  content: ""; position: absolute; top: 17px; left: 44px; right: calc(-1 * var(--s-5));
  height: 1px; background: var(--line);
}
.lp-rail li:last-child::before { display: none; }
.lp-num {
  position: absolute; top: 0; left: 0;
  width: 34px; height: 34px; border-radius: var(--r-full);
  border: 1px solid var(--line); background: var(--surface);
  display: grid; place-items: center;
  font-family: var(--mono); font-size: var(--t-micro); font-weight: 500;
  letter-spacing: var(--track-eyebrow); color: var(--muted);
}
.lp-rail b { display: block; font-size: var(--t-title); font-weight: 500;
             letter-spacing: var(--track-title); line-height: var(--lead-title);
             margin: 0 0 var(--s-2); }
.lp-rail span:not(.lp-num) { display: block; font-size: var(--t-small); line-height: 1.55;
                             color: var(--muted); }
.lp-steps-cta { display: inline-flex; }

/* ---- 2. THE PRODUCT ------------------------------------------------------- */
.lp-product {
  margin: var(--rhythm-marketing) 0 0;
  display: grid; grid-template-columns: minmax(0, 5fr) minmax(0, 7fr);
  gap: var(--s-7); align-items: center;
}
.lp-product-copy .lp-h2 { margin-bottom: var(--s-4); }
.lp-card {
  background: var(--surface);
  border: 1px solid var(--line);
  border-radius: var(--r-lg);     /* Titan rounded.md — the same frame photography gets */
  padding: var(--s-5);
  min-width: 0;
}
.lp-card-head {
  display: flex; align-items: center; gap: var(--s-3); flex-wrap: wrap;
  padding-bottom: var(--s-4); border-bottom: 1px solid var(--line-soft);
}
.lp-card-co { font-size: var(--t-title); font-weight: 500; letter-spacing: var(--track-title); }
.lp-card-tag {
  margin-left: auto; font-size: var(--t-micro); letter-spacing: var(--track-eyebrow);
  text-transform: uppercase; color: var(--muted);
}
.lp-card-body {
  display: grid; grid-template-columns: repeat(3, minmax(0, 1fr));
  gap: var(--s-4); padding: var(--s-5) 0 var(--s-4);
}
.lp-fig { min-width: 0; }
.lp-fig-lab {
  font-size: var(--t-micro); letter-spacing: var(--track-eyebrow); text-transform: uppercase;
  color: var(--muted); margin-bottom: var(--s-2);
}
.lp-fig-val { font-size: 26px; font-weight: 500; letter-spacing: -0.02em;
              font-variant-numeric: tabular-nums; overflow-wrap: anywhere; }
.lp-card-note { font-size: var(--t-small); line-height: 1.55; color: var(--faint); }

/* ---- 3. THE RANGE (dark band) --------------------------------------------- */
.lp-range {
  margin: var(--rhythm-marketing) calc(50% - 50vw) 0;
  padding: var(--rhythm-marketing) calc(50vw - 50%);
  border-radius: 0;
  display: grid; grid-template-columns: minmax(0, 5fr) minmax(0, 7fr);
  gap: var(--s-7); align-items: center;
  overflow: hidden;
}
.lp-range .lp-h2 { color: #ffffff; }
.lp-range .lp-lede { color: var(--on-dark-muted); }
.lp-range-orn {
  width: 900px; max-width: 120%; height: auto;
  right: -6%; bottom: -14%;
}
.lp-range-figs {
  position: relative; z-index: 1;
  display: grid; grid-template-columns: repeat(3, minmax(0, 1fr));
  gap: var(--s-4); align-items: end;
}
.lp-rf { min-width: 0; text-align: center; }
.lp-rf-lab {
  font-size: var(--t-micro); letter-spacing: var(--track-eyebrow); text-transform: uppercase;
  color: var(--on-dark-muted); margin-bottom: var(--s-2);
}
.lp-rf-val { font-size: 24px; font-weight: 500; letter-spacing: -0.02em;
             font-variant-numeric: tabular-nums; color: #ffffff; overflow-wrap: anywhere; }
/* The blend is the answer; the two lenses either side are the spread. Weight, not colour —
   the two ends are NOT a bear and a bull and must not be dressed as one. */
.lp-rf-mid .lp-rf-val { font-size: 34px; }
.lp-rf-mid { position: relative; }
.lp-rf-mid::before {
  content: ""; position: absolute; left: 50%; bottom: -14px; width: 1px; height: 10px;
  background: var(--hairline-on-dark);
}
.lp-rf-axis {
  grid-column: 1 / -1; margin-top: var(--s-4); padding-top: var(--s-3);
  border-top: 1px solid var(--hairline-on-dark);
  text-align: center; font-size: var(--t-micro); letter-spacing: var(--track-eyebrow);
  text-transform: uppercase; color: var(--on-dark-muted);
}

/* ---- 4. NO BLACK BOXES ---------------------------------------------------- */
.lp-boxes {
  margin: var(--rhythm-marketing) 0 0;
  display: grid; grid-template-columns: minmax(0, 4fr) minmax(0, 8fr);
  gap: var(--s-7);
}
.lp-boxes-grid {
  display: grid; grid-template-columns: repeat(4, minmax(0, 1fr)); gap: var(--s-5);
}
.lp-box { min-width: 0; padding-top: var(--s-4); border-top: 1px solid var(--line); }
.lp-box b { display: block; font-size: var(--t-body); font-weight: 500;
            letter-spacing: var(--track-title); margin-bottom: var(--s-2); }
.lp-box span { display: block; font-size: var(--t-small); line-height: 1.55;
               color: var(--muted); margin-bottom: var(--s-3); }
.lp-box a { font-family: var(--mono); font-size: var(--t-meta); color: var(--text);
            text-decoration: none; }
.lp-box a:hover { text-decoration: underline; text-underline-offset: 3px; }

/* ---- 5. THE CLOSING CTA --------------------------------------------------- */
.lp-cta {
  margin: var(--rhythm-marketing) calc(50% - 50vw) 0;
  padding: var(--rhythm-marketing) calc(50vw - 50%);
  background: var(--surface);
  border-top: 1px solid var(--line);
  text-align: center;
  overflow: hidden;
}
/* THE CROP IS THE POINT. At `right: 2%` the rosette sat whole in the right margin with a
   sliver of air behind it — a disc parked beside centred content, related to nothing on
   the band. That is Miraaj's "too off and random", and it is not a position problem so
   much as an indecision one: 98% contained reads as an accident, where a clear crop reads
   as a printed plate running off the page.

   So it bleeds, deliberately and by a visible amount, the way `.lp-steps-orn` already does
   at both edges of the band above. It is NOT moved behind the headline and the search
   field: that is precisely the collision Miraaj photographed on the phone hero, and
   repeating it here two bands later would be the same mistake with a different rosette. */
.lp-cta-orn {
  width: 620px; max-width: 78%; height: auto;
  right: -7%; top: 50%; transform: translateY(-50%);
}
/* AND ON A PHONE IT IS NOT THERE AT ALL. The kit's rule is one ornament per surface, in
   NEGATIVE SPACE. A 402px band with centred content has none: the rosette landed behind
   "worth?", across the search field and through the sub-copy — the same collision Miraaj
   photographed on the hero, two bands later and with the same shape. An ornament with
   nowhere to sit is not a smaller ornament, it is no ornament. */
@media (max-width: 560px) { .lp-cta-orn { display: none; } }
.lp-cta .lp-h2 { margin: 0 auto var(--s-5); max-width: 14ch; }
.lp-cta-ask { margin: 0 auto; max-width: 560px; }
.lp-cta-sub { font-size: var(--t-small); color: var(--muted); margin: var(--s-4) 0 0; }

/* ---- responsive ----------------------------------------------------------- */
@media (max-width: 1023px) {
  .lp-rail { grid-template-columns: repeat(2, minmax(0, 1fr)); gap: var(--s-5) var(--s-5); }
  /* the rail line is a 4-across device; at 2-across it would point at nothing */
  .lp-rail li::before { display: none; }
  .lp-product, .lp-range, .lp-boxes { grid-template-columns: minmax(0, 1fr); gap: var(--s-5); }
  .lp-boxes-grid { grid-template-columns: repeat(2, minmax(0, 1fr)); }
}
@media (max-width: 620px) {
  .lp-rail, .lp-boxes-grid { grid-template-columns: minmax(0, 1fr); }
  .lp-card-body { grid-template-columns: minmax(0, 1fr); gap: var(--s-4); }
  .lp-range-figs { grid-template-columns: minmax(0, 1fr); gap: var(--s-5); text-align: left; }
  .lp-rf { text-align: left; }
  .lp-rf-mid::before { display: none; }
  .lp-rf-axis { text-align: left; }
  /* 96px between bands is Titan's marketing rhythm and it is right on a desktop band.
     On a phone the same rhythm is a screen and a half of nothing between two sentences,
     so the marketing register steps down here — the only place it does. */
  .lp-steps, .lp-product, .lp-range, .lp-boxes, .lp-cta { margin-top: var(--s-7); }
  .lp-steps, .lp-range, .lp-cta { padding-top: var(--s-7); padding-bottom: var(--s-7); }
}

/* ---- the hero's three trust markers --------------------------------------
   The honesty wedge used to be three stacked glass cards under the fold. The
   design puts the same three claims in a row across the bottom of the hero,
   separated by hairlines — smaller, and read before the first scroll rather
   than after it.

   These rules come after the `.hero-wedge li` glass-card block earlier in this
   file and replace it in place: same element, same three claims, different
   composition. The earlier rule is left where it is because `.wp-col` shares it
   and the why-pay columns still want the card. */
.hero .hero-wedge {
  display: flex; flex-wrap: wrap; align-items: flex-start;
  gap: 0;
  list-style: none; padding: 0;
  border-top: 1px solid var(--on-plate-rim-2);
  padding-top: var(--s-4);
}
.hero .hero-wedge li {
  /* everything the glass card set, undone: no fill, no blur, no frame, no lift */
  background: none; backdrop-filter: none; -webkit-backdrop-filter: none;
  border: 0; border-radius: 0; box-shadow: none; transform: none;
  flex: 1 1 220px; min-width: 0;
  display: grid; grid-template-columns: 22px minmax(0, 1fr); grid-row-gap: 2px; column-gap: var(--s-3);
  padding: 0 var(--s-5); font-size: var(--t-small); line-height: 1.5;
}
.hero .hero-wedge li:first-child { padding-left: 0; }
/* the divider is a border on the item, not a gap: at 2-across on a tablet the last one
   in a row must not draw a rule into empty space */
.hero .hero-wedge li + li { border-left: 1px solid var(--on-plate-rim-2); }
.hero .hero-wedge li::before { display: none; }   /* the old bullet dot */
.hw-ico {
  grid-row: 1 / span 2; width: 22px; height: 22px; align-self: start;
  margin-top: 2px; color: inherit; opacity: 0.85;
}
.hero .hero-wedge b {
  font-weight: 500; font-size: var(--t-small); letter-spacing: 0;
  color: var(--on-plate);
}
.hero .hero-wedge li > span { color: var(--on-plate-2); font-size: var(--t-meta);
                              line-height: 1.45; }
@media (max-width: 767px) {
  .hero .hero-wedge { display: grid; grid-template-columns: minmax(0, 1fr); gap: var(--s-4); }
  .hero .hero-wedge li { padding: 0; }
  .hero .hero-wedge li + li { border-left: 0; }
}

/* ---- the hero search, as the design draws it: one pill, arrow inside ------ */
.hero .hero-ask, .lp-cta-ask {
  background: var(--surface);
  border-radius: var(--r-pill);
  padding: 6px 6px 6px var(--s-5);
  gap: var(--s-3);
  align-items: center;
  box-shadow: none;
  border: 1px solid var(--line);
}
.hero .hero-ask input, .lp-cta-ask input {
  background: none; border: 0; outline: none; flex: 1; min-width: 0;
  font-family: var(--sans); font-size: var(--t-body); letter-spacing: 0;
  text-transform: none; color: var(--text); padding: 0;
}
.hero .hero-ask input::placeholder, .lp-cta-ask input::placeholder { color: var(--faint); text-transform: none; letter-spacing: 0; }
/* A LABELLED BUTTON, not a circular glyph. Width is now content + padding rather than a
   fixed `--tap` square; the height stays `--tap` so the tap target is unchanged. The input
   beside it is `flex: 1`, so it simply yields the extra width — the placeholder cannot
   collide with the button the way a hardcoded right padding would have let it.
   `white-space: nowrap` because "Search" wrapping to two lines inside a pill is the one way
   this gets worse than the arrow it replaced. */
.hero .hero-ask-go, .lp-cta-ask .hero-ask-go {
  flex: 0 0 auto; width: auto; min-width: var(--tap); height: var(--tap);
  padding: 0 var(--s-4); border-radius: var(--r-full);
  background: var(--on-plate); color: rgb(var(--plate-wash)); border: 0;
  display: grid; place-items: center; cursor: pointer;
  font-family: var(--sans); font-size: var(--t-small); font-weight: 600;
  letter-spacing: 0; text-transform: none; white-space: nowrap;
}
.hero .hero-ask-go:hover, .lp-cta-ask .hero-ask-go:hover { background: color-mix(in srgb, var(--on-plate) 86%, rgb(var(--plate-wash))); }

/* ---- the hero stack is LEFT, not centred ---------------------------------
   The shipped hero centred its stack (the Origin-shaped arrangement it was
   studied from). The design is left-aligned with the photograph carrying the
   right-hand half, and that is the composition being built — so the stack takes
   a left column and only the trust markers span the full width, which is where
   the design puts them.

   Scoped to the landing pane. `.hero` is also used by the signed-in Discover
   header, and the headline arithmetic in tests/test_hero_headline.py reads
   `max-width` off `.hero h1` — a narrower column here would change the ratio it
   computes, so the CAP IS UNCHANGED and only the alignment moves. */
body:has(#discover:not(.hidden)) .hero { text-align: left; }
body:has(#discover:not(.hidden)) .hero > *:not(.hero-bg):not(.hero-wedge) {
  margin-left: 0; margin-right: 0;
  max-width: 640px;
}
body:has(#discover:not(.hidden)) .hero > .hero-ask { max-width: 560px; }
body:has(#discover:not(.hidden)) .hero > .hero-wedge { max-width: none; }
body:has(#discover:not(.hidden)) .hero .hero-wedge li { text-align: left; }
/* the counters and the try-line follow the stack rather than floating centred under it */
body:has(#discover:not(.hidden)) .hero .hint,
body:has(#discover:not(.hidden)) .hero .usage-stat { text-align: left; max-width: 760px; }

/* The icons are line art on a photograph, so they need to be as bright as the
   label they sit beside — at 0.85 they read as smudges over a light patch of sky.

   THE COLOUR IS A TOKEN NOW. This was `rgba(255, 255, 255, 0.92)` in both themes, written
   when the plate was always a dark photograph. A light plate arrived and the icons stayed
   white on it, which is Miraaj's "the icons are too faint". The `<b>` beside each one has
   read `var(--on-plate)` all along; the glyph now agrees with its own label.

   test_titan_hero_plate.py has a guard for exactly this — it scans for a hardcoded
   `255, 255, 255` in any rule whose selector mentions `.hero` or `over-hero` — and it did
   not fire, because this selector is a bare `.hw-ico`. The guard is widened there. */
.hw-ico { opacity: 1; color: var(--on-plate); }

/* ---- THE HERO SCRIM ------------------------------------------------------
   The kit's `-scrim` crop is graded for type overlay, and it fixed the
   headline: 88px white went from 1.44:1 on the old provisional footage to
   3.90:1, clearing its large-text floor. It does NOT fix the 12-16px copy —
   the plate is a portrait macro and keeps bright passages, and pixel sampling
   found white body text sitting on rgb(241,241,240).

   So the page adds what the design itself shows: the photograph is bright on
   the right and the type sits on a darkened left. This is a GROUND, not a
   chromatic accent — Titan's near-black at an alpha — so it is legal here in a
   way a tint would not be.

   The two stops are measured, not chosen. To clear 4.5:1 for white text the
   brightest pixel under it has to come down to about rgb(118); the worst was
   241, which needs ~0.51 of black over it, so the text side sits at 0.62 with
   headroom. Below 900px the stack spans the full width, so the gradient has
   nowhere to fade to and the whole plate carries the floor. */
.hero-bg::after {
  content: ""; position: absolute; inset: 0; pointer-events: none;
  background:
    /* The stops are in PIXELS, not percentages, and that is the point: the text column is a
       fixed 640px, so at 1440 a 38% stop lands past it and at 1024 it lands inside it —
       which is exactly where the 12-16px copy measured 3.5-3.9:1. In px the dark region
       tracks the column at every width. */
    linear-gradient(100deg,
      rgba(var(--plate-wash), 0.80) 0px,
      rgba(var(--plate-wash), 0.72) 700px,
      rgba(var(--plate-wash), 0.30) 78%,
      rgba(var(--plate-wash), 0.14) 100%);
}

/* ---- the plate on a phone: a masthead, not a backdrop ---------------------- */
/* THE ART IS 16:9 AND THE PHONE HERO IS 402x1025. `object-fit: cover` therefore scales
   the plate 4.5x and throws away 711px off each side — measured, not estimated: the
   intrinsic box is 402x226 against a 402x1025 frame. What survives is an unreadable grey
   wash, and on the signed-in hero the guilloche rosette — a small element in the wide
   composition — lands blown up directly behind the search field and the sub-copy. That is
   the collision Miraaj photographed. It is not an ornament that was placed badly; it is a
   landscape photograph being asked to be a portrait one.

   So on a phone the plate stops being a backdrop and becomes a masthead band behind the
   headline, ending before the search field and fading out rather than cutting off. The
   scale factor drops from 4.5x to 1.65x, which is the whole fix — everything else here
   follows from it.

   AND IT NO LONGER REACHES UPWARD. `top: calc(-1 * var(--header-h))` exists so the plate
   slides under the sticky header, which is right when the hero is the first thing below
   it. It is wrong whenever anything sits in between: the "9 OF 10 LEFT" promo card does,
   and the plate was painting over its bottom edge — the sliced card in the phone
   screenshots. The header carries its own ground on a phone, so the extension buys
   nothing here and costs that. */
@media (max-width: 560px) {
  .hero-bg {
    top: 0;
    height: 300px;
    -webkit-mask-image: linear-gradient(to bottom, #000 0 58%, transparent 100%);
            mask-image: linear-gradient(to bottom, #000 0 58%, transparent 100%);
  }
  /* The 100deg wash was measured against a 640px text column at desktop widths; on a
     402px phone the entire viewport sits inside its first stop, so the plate resolved to
     a flat 0.78 grey everywhere. Vertical here, and lighter at the top where the image
     should actually read. Re-measured by pixel sampling after the change, both themes. */
  .hero-bg::after {
    background: linear-gradient(180deg,
      rgba(var(--plate-wash), 0.58) 0%,
      rgba(var(--plate-wash), 0.72) 62%,
      rgba(var(--plate-wash), 0.86) 100%);
  }
}

@media (max-width: 899px) {
  .hero-bg::after {
    background: linear-gradient(180deg,
      rgba(var(--plate-wash), 0.66) 0%,
      rgba(var(--plate-wash), 0.72) 55%,
      rgba(var(--plate-wash), 0.78) 100%);
  }
}

/* ---- landing refinements, all three found by looking ---------------------- */

/* 1. The value curves rise from the axis rule, which is where a distribution's baseline
      is. Anchored to the figures block rather than floating in the band, the middle peak
      sits under the blend and the axis stops cutting through them. */
.lp-range-figs { padding-bottom: var(--s-6); }
.lp-range-orn {
  width: 100%; height: auto;
  left: 50%; transform: translateX(-50%);
  bottom: var(--s-5);
  right: auto; top: auto;
}

/* 2. The full-band field reads from the SCALE-corrected step of the register, not from
      the discrete-ornament one. See --engrave-field in the token block. */
/* The band moved from `--bg` to `--surface` when it turned out a cream band on a cream
   page is not a band. White gives the ink more to work against than cream does, so the
   field steps down here for the same reason `--engrave-on-canvas` is below
   `--engrave-on-cream`. Measured at band width on the ground it actually sits on. */
.lp-steps-orn { opacity: calc(var(--engrave-field) * 0.62); }

/* 3. The engraving must not out-rank the content it sits behind. `.engrave-host > *` lifts
      real children to z-index 1, but the ornament is itself a child, so it needs to be
      explicitly below rather than merely earlier in the DOM. */
.engrave { z-index: 0; }
.lp-range-figs > *:not(.engrave) { position: relative; z-index: 1; }

/* ============================================================================
   TITAN — GEIST MONO IS NOT FOR RUNNING PROSE
   ---------------------------------------------------------------------------
   Titan states this twice: "Don't set Geist Mono in running prose; keep it to
   eyebrows, CTA labels, and stat figures", and "The mono is never used for
   running prose — only eyebrows, CTA labels, and numbers." A sentence is prose
   no matter how short it is or what container it sits in.

   THE SIZE OF IT. Miraaj spotted three elements. Sweeping every rendered text
   node on twelve surfaces at two widths in both themes, and classifying each by
   its COMPUTED font-family (not by grep — a rule can set the mono and be
   overridden, and a node can inherit it from three ancestors up), found
   **267 distinct prose strings across ~70 owning rules**. The three in the
   screenshot were `.hint`, `.screener-link` and `.free-meter`.

   THE CLASSIFIER, which is also the test: prose is text with more than one word
   that contains a fully-lowercase alphabetic word of two or more letters. That
   deliberately leaves alone eyebrows (uppercase), tickers (AAPL), figures
   ($208.64, +12.4%, 3/3), and title-case status labels (At Close, Market Open).
   It catches "5 of 5 free live valuations left today", which is the point.

   WHY ONE BLOCK RATHER THAN SEVENTY EDITS. These are seventy unrelated rules
   with one thing wrong. Correcting each in place would scatter the reasoning
   across 8,000 lines and leave nothing that says WHY; here the rule and its
   justification are in one place, and this file's established mechanism for a
   cross-cutting correction is the end-of-file block that wins on order.

   `font-variant-numeric: tabular-nums` comes along on purpose. Several of these
   are sentences WITH figures in them ("57% above fair value", "0/3 passed",
   "72.9% under"), and some sit in columns. Titan's answer, and the brief's, is
   that the sentence is sans and the numerals keep their tabular alignment —
   not that the sentence becomes mono to align four digits.
   ========================================================================= */

/* ---- status sentences, captions, disclaimers, notes ---------------------- */
.usage-stat, .usage-stat *,
.free-meter, .free-meter *,
.hint, .hint b,
.movers-note, .uv-note, .we-disc, .peer-src, .sc-basis, .score-meth,
.model-teaser, .scr-upsell, .scr-saved-hint, .scr-count, .sx-showing,
.seo-disc, .seo-updated, .factor-disc, .chk-meta, .dash-sub, .lens-src,
.street-disc, .vm-refnote, .vm-scale, .dl-cta-note, .mm-rule, .sub,
.tgt-conf, .we-driver-val, .sc-dmeta, .score-checked, .sc-basis,
.ins-cluster-tag, .ins-cluster-net, .mc-hz-when, .mcal-d, .hz-head,
.td-range-lab, .chart-delta, .wacc-meta, .street-sub, .street-vs-row,
.mc-count, .mc-sep, .heat, .heat th, .heat td,
/* figure-plus-unit captions: the figure keeps tabular alignment below, the
   unit stops pretending to be one */
.sc-cwt, .sc-depth, .chk-cat-tally, .chk-actual, .factor-lab, .factor-chip,
.ig-val, .rev-big, .rev-big-phrase, .sh-read .v, .cat-val,
.tgt-meta, .tgt-meta *, .tgt-now-p, .sb-chip, .lpath-chip,
/* the /stocks gap column. The <td> stays mono because it also carries pure
   figures; these are the spans that hold the phrase. */
.sx-under, .sx-over, .sx-level,
/* company names in the peer table are names, not data */
.peer-table td.pc-co, .pc-nm {
  font-family: var(--sans);
  font-variant-numeric: tabular-nums;
  letter-spacing: normal;
}

/* ---- nav and secondary actions in SENTENCE CASE -------------------------
   Titan's `button-nav` is {typography.caption} — Geist sans, sentence case.
   The earlier version of this block ALSO parked every ink-filled pill here
   ("flagged rather than taken"), which left the pricing page's most important
   button speaking a different voice from the landing's — sans-sentence beside
   mono-uppercase, for two actions of the same rank. The call is now taken, and
   THE FILL DECIDES THE VOICE: an ink-filled pill is button-primary and speaks
   mono-ui (the rule below); an outline or cream pill is the quiet register and
   stays sentence-case sans here. */
.hint-links .screener-link, .screener-link,
.footer-links a, .seo-foot a, .seo-cta, .skip-link,
.sc-methlink, .wp-pricing, .back, .signin-btn,
/* `.scr-export` ("Export CSV (Pro)") was a THIRD voice: Geist Mono, sentence case. Titan
   has two — mono UPPERCASE for an ink-filled pill, sans sentence case for the outline
   register — and this button is outline-filled (`background: var(--surface)`), so it is
   the quiet one. Found by reading every filled control on six surfaces of production and
   sorting them by voice; it was the only stray among 24. */
.scr-export,
.scr-save, .reset-w, .dt-more {
  font-family: var(--sans);
  font-variant-numeric: tabular-nums;
  letter-spacing: normal;
  text-transform: none;
}

/* ---- ink-filled pills are button-primary, and button-primary speaks mono-ui.
   DESIGN.md: "Set every pill-CTA label in {typography.mono-ui} UPPERCASE" —
   Geist Mono 13/500 — "the sole exception is the smaller button-nav, which is
   sentence case", and button-nav is the CREAM/outline register above, not an
   ink fill. One rule so the register cannot drift per-surface again; the
   letter-spacing is the landing's measured 0.06em, which uppercase mono needs
   at this size. `.dl-cta.ghost` rides along on purpose: Titan's outline pill
   (button-outline-on-dark) is mono-ui too, and the download pair must match. */
.hero-cta, .pr-cta, .wp-cta, .dl-cta, .lock-btn, .lock-card-cta,
.scr-run, .auth-submit, .mini-upgrade {
  font-family: var(--mono);
  font-size: 13px; font-weight: 500;
  letter-spacing: 0.06em; text-transform: uppercase;
  font-variant-numeric: tabular-nums;
}

/* ---- what STAYS mono, stated so it is a decision and not an oversight ----
   Eyebrows and uppercase labels (`.eyebrow`, `.t-eyebrow`, `.lp-eyebrow`,
   `.lp-fig-lab`, `.lp-rf-lab`, `.dt-title`, `.metric .lab`, every
   `text-transform: uppercase` mono rule), the ink-filled pill CTAs (the
   button-primary rule below the sentence-case block),
   tickers, and every actual stat figure (`.lp-fig-val`, `.lp-rf-val`,
   `.wl-price`, `.chg`, `.score-num`, `.sx-num` on its figure cells, the chart
   axes). Those are the three roles Titan licenses, and the sweep reports them
   as `eyebrow/CTA` and `figure` — 1,177 and 1,234 nodes that are correct. */

/* ---- the second pass: seventeen the first block did not reach -------------
   Two reasons they survived, both worth naming.

   SPECIFICITY. `.sub`, `.v` and `.val` are single classes that lose to the
   compound or descendant selectors that set their mono (`.panel .sub`,
   `.v.accent`, `.metric .val`). Matched at the same shape here rather than
   escalated with !important.

   SENTENCE-CASE CHILDREN OF AN EYEBROW. `.eyebrow` and `.panel h3` are mono
   uppercase, which is correct — but several carry a trailing span that resets
   `text-transform: none` to hold a phrase ("— per our blended fair value",
   "see the full list (Pro)", "overall 58/100"). The parent is an eyebrow; the
   child is prose wearing the eyebrow's font. */
.eyebrow .uv-sub, .eyebrow .movers-more, .eyebrow .movers-scope,
.uv-sub, .movers-more, .movers-scope, .cal-toggle, .brief-date,
.factor-overall, .hl-src, .news-live-panel .hl-src,
.horizon-panel .hz-vol,
.ins-act,
.panel .sub, .panel > .sub,
.rev-cmp .lab, .rev-cmp .v, .v.accent, .sh-read .v.accent,
.metric .val,
.mc-why > summary,
.lp-box a,
.sx-legend .dp, .dp,
.meth-toc a,
.scr-field label,
/* `.td-stat .val` is a genuine stat figure ("38.18M") and KEEPS its mono. What was prose
   is the comparison the template appends after it, which already has its own span — so
   this is one selector rather than a blanket change to a figure slot. */
.td-vr,
.seo-crumb, .seo-crumb a {
  font-family: var(--sans);
  font-variant-numeric: tabular-nums;
  letter-spacing: normal;
  text-transform: none;
}

/* ---- loading and note text: a class, not an inline style ------------------
   Thirty of these were `style="color:var(--muted);font-family:var(--mono);…"`
   written into the template, plus one on the sensitivity note. An inline style
   beats every stylesheet rule, which is exactly why a CSS sweep never found
   them and why they survived four phases of design work — the same "markup
   improvising what the design system owns" defect the static pages had with an
   inline `text-decoration:none`.

   They are also the FIRST thing a reader sees on a cold company page: ~20
   panels each spend their opening seconds saying "Loading the breakdown…" in
   what was, until now, Geist Mono. A sentence is prose. */
.panel-loading {
  color: var(--muted);
  font-size: var(--t-small);
  font-family: var(--sans);
}
.heat-note {
  font-size: var(--t-micro);
  color: var(--faint);
  margin-top: 10px;
  font-family: var(--sans);
  font-variant-numeric: tabular-nums;
}

/* ---- the third pass: empty, error and modal states ------------------------
   These are the ones the BROWSER sweep structurally could not find, because
   they only render when data is missing, a request failed, or a modal is open —
   and a sweep of the happy path never sees any of that. Phase 0 predicted
   exactly this: "this prototype has no loading, empty, or error states, and the
   real app is made of them."

   Found by the static half instead (tests/test_titan_tokens.py pairs every
   mono class with the literal strings the templates put in it), which is why
   both instruments are kept. Fifteen classes, nineteen strings. */
/* ---- the fourth pass: five that only PRODUCTION could show -----------------
   A sweep is only as complete as the data behind the surfaces it visits. The
   local server has no news provider configured, so every news panel rendered its
   empty state and these five strings never appeared — the earnings session
   labels, the headline disclaimer, and the two sentiment tags. Running the same
   probe against prod found them in a minute.

   The static test did not catch them either, and that is structural: their text
   is INTERPOLATED (`${notifEsc(e.session)}`), so no template scan can know what
   the string will be. Neither instrument covers interpolated text on a surface
   with no data — which is why tools/titan/monosweep.py is now documented as
   something to run against production before calling a sweep finished. */
.mc-esess, .hl-disc, .nw-sent,
.auth-tab, .auth-note,
.tip-sheet-close,
.mover-empty, .ins-empty, .adv-empty,
.price-lock,
.dl-btn, .fund-dl,
.td-loading,
.dash-btn,
.pf-note,
.notif-foot,
.trial-extend, .trial-cta {
  font-family: var(--sans);
  font-variant-numeric: tabular-nums;
  letter-spacing: normal;
  text-transform: none;
}

/* The 404's numeral. A figure, so the mono is right — but it was an INLINE STYLE, which
   is the one place a stylesheet can never reach. Same defect as the loading messages,
   opposite conclusion about the font. */
.err-code {
  font-family: var(--mono); font-size: 64px; font-weight: 700; color: var(--accent);
  font-variant-numeric: tabular-nums;
}
/* the 500's numeral is set a step smaller than the 404's */
.err-code-500 { font-size: 56px; }

/* ============================================================================
   DYNAMIC TILING — tiles size to their content, and reflow when it changes
   ---------------------------------------------------------------------------
   Reported three times on three surfaces, which is one defect wearing three sets
   of clothes: A CSS GRID ROW IS AS TALL AS ITS TALLEST MEMBER. Put a 190px
   "Nothing on the calendar today" beside a 450px morning brief and the short one
   leaves 260px of nothing.

   TWO MECHANISMS, BECAUSE THERE ARE TWO KINDS OF GRID — and the first thing
   tried, one mechanism for both, measured worse on half of them.

   Row-span masonry (fine `grid-auto-rows` plus a measured span per tile) was
   built first and rejected on evidence: it improved News but made the company
   page WORSE, 0.39M px2 of void to 0.63M, because that grid's tiles carry
   explicit column spans and full-width barriers that fine rows fight. Multicol
   was then measured on the company page too, and was worse again (1.27M) for the
   same reason in reverse — `column-span: all` chops the flow into short segments
   that each strand their own gap.

   So:

   · INDEPENDENT PANELS (News, the dashboard tiles) get CSS multi-column. It packs
     by height with no configuration and reflows on any change, including a panel
     that fills asynchronously. Reading order becomes column-major, which is
     correct here — these are a set of panels, not an argument.
     Measured on News at 1440: void 0.47M -> 0.19M px2, height 1401 -> 1078.

   · THE COMPANY VIEW keeps its span grid, because its order IS the argument —
     Score, then chart, then the sections — and column-major reading would break
     the spine. Its voids are addressed by span rules, not by a layout mode.

   The accordion case that motivated all of this (a tile going 90px -> 600px)
   needs nothing extra in multicol: the flow re-packs on its own.
   ========================================================================= */
.tile-flow {
  display: block;
  column-gap: var(--s-4);
}
.tile-flow > * {
  break-inside: avoid;
  -webkit-column-break-inside: avoid;
  display: block;
  width: 100%;
  margin: 0 0 var(--s-4) !important;
}
/* A panel that must run the full width of the flow — a stale-data banner, a
   footer note — opts out rather than being packed around. */
.tile-flow > .tile-span-all { column-span: all; }

/* SUPERSEDED — a fixed column COUNT at two breakpoints, which is what held News at
   three columns from 1280 all the way to 2560. The count now follows the available
   width; see "DYNAMIC TILES" at the end of this file. Deleted rather than left in
   place: it would still win if the cascade were ever reordered, and a rule that
   contradicts the one below it is how the four container opt-ins happened. */

/* ============================================================================
   THE HEADER — full width, and its ink follows the GROUND rather than the theme
   ---------------------------------------------------------------------------
   THE WORST DEFECT IN THE REVIEW, and it was invisible to every check that had
   been run because every one of them asked about the THEME. Pixel-measured over
   the landing hero in LIGHT theme:

       .brand .mark b  ("Worth")   1.07:1
       .brand-seal     (the seal)  1.06:1
       .theme-btn                  1.11:1

   The header is a translucent bar. In light theme its ink is #111 — correct
   over the cream page, and invisible over a near-black photograph. The kit
   ships both lockups for exactly this reason; the bug was picking one by theme.

   So the header asks what is BEHIND it. An IntersectionObserver puts
   `.over-hero` on <body> while the hero plate is under the bar and removes it
   the moment it scrolls away, and everything in the header reads its ink from
   that rather than from `--text`. Scroll off the hero and the bar returns to
   the theme's ink, in either theme, with no flash — it is a class swap on a
   property that is already transitioned.
   ========================================================================= */

/* ---- layout: use the whole bar ------------------------------------------
   "use the full header to keep perfect spacing." The nav was bunched into the
   middle of a wide bar with the actions crowded against it and "About Us"
   wrapping to two lines. Three zones: brand, nav, actions — the nav takes the
   space between and centres in it, so the bar is used rather than filled. */
.top-inner {
  display: flex; align-items: center; gap: var(--s-4);
  width: 100%; max-width: none;
  padding-inline: clamp(16px, 2.2vw, 34px);
}
.top-inner > .brand { flex: 0 0 auto; }
/* The nav's flex, justify-content and min-width all live in ONE rule now, further down
   (search for `.top-inner > .pnav-desk`). This declaration was here first and every one
   of its three properties was overridden by that later rule at equal specificity, so it
   had stopped deciding anything — the third dead duplicate of this selector found in one
   change. tests/test_nav_reachability.py now fails if a second live one reappears. */
/* `margin-left: auto` and not a spacer: on a desktop the flexible `.pnav-desk` pushes this
   to the right edge, but at phone widths the nav is hidden and collapses to zero, so the
   actions bunched against the wordmark with a third of the bar empty beside them. */
.top-actions { flex: 0 0 auto; display: flex; align-items: center; gap: var(--s-3); margin-left: auto; }
/* a nav label is a label; it does not wrap */
.pnav-desk .pnav-item { white-space: nowrap; }
.pnav-desk .pnav-lab { white-space: nowrap; }

/* ---- the search: one field, one action ---------------------------------- */
/* THE FLEX AND THE MIN-WIDTH ARE NOT DECIDED HERE. This rule used to declare
   `flex: 0 1 clamp(190px, 20vw, 320px); min-width: 0` and neither ever applied: a second
   `.search` rule 78 lines below re-declares both at equal specificity, so the cascade took
   that one and this half of the pair was dead. Two declarations of the same property on
   the same selector, close enough to be found together and far enough apart to be edited
   separately, is how a fix lands on the rule nobody is reading — it happened here first.
   The padding is the part this rule actually owns. */
/* 7px, not 5: the go button sat 6px from the pill's edge, which reads as flush at a 44px
   button on a coarse pointer and is what "no breathing room" describes. The left inset
   stays larger than the right — the placeholder needs the room and the button does not. */
/* LEFT PADDING HAS TO CLEAR THE CORNER. The pill is 46px tall and fully round, so its
   corner radius is 23px — text starting at 16px begins inside the curve, and the first glyph
   of the placeholder reads as clipped. Reported alongside the doubled border. 22px puts the
   text past the widest part of the arc; the trailing side needs no equivalent because the
   button sits there and is round itself. */
.search { padding: 7px 7px 7px 22px; }
.search input { font-family: var(--sans); text-transform: none; letter-spacing: normal;
                font-size: var(--t-small); }
.search input::placeholder { text-transform: none; letter-spacing: normal; }
.search-go {
  flex: 0 0 auto; width: 30px; height: 30px; border-radius: var(--r-full);
  background: var(--text); color: var(--bg); border: 0; cursor: pointer;
  display: grid; place-items: center; padding: 0;
}
.search-go:hover { background: color-mix(in srgb, var(--text) 86%, var(--bg)); }
@media (pointer: coarse) { .search-go { width: var(--tap); height: var(--tap); } }

/* ---- the ground-following ink ------------------------------------------- */
/* 66%, AND THE NUMBER IS MEASURED RATHER THAN CHOSEN. 42% was set against the local
   server's plate and measured clean there; against the plate production actually serves it
   composites to rgb(150) behind the bar, and white ink lands at 3.17:1 — the same
   "wordmark nearly invisible" Miraaj reported, still failing, on a page whose local check
   said it passed. The lesson is the one this batch keeps relearning: verify against the
   thing that is serving, not the thing that is convenient.

   Swept against tickerworth.com at 0.42 / 0.52 / 0.62 / 0.66 / 0.70 / 0.72 / 0.82, scoring
   white against the worst pixel under each glyph:

       0.42  wordmark 3.17   nav 3.25   toggle 2.73   field 2.72
       0.52  wordmark 4.32   nav 4.38   toggle 3.48   field 3.48
       0.62  wordmark 5.95   nav 6.05   toggle 4.56   field 4.52    <- first pass
       0.66  wordmark 6.75   nav 6.83   toggle 5.07   field 5.07    <- shipped
       0.70  wordmark 7.80   nav 7.92   toggle 5.64   field 5.64

   0.62 is the floor and 0.66 is the floor with somewhere to stand: the toggle and the field
   carry their own `rgba(255,255,255,0.12)` fill, which lightens their local ground, so they
   clear last and by the smallest margin. A photograph needs a scrim — that is not a
   departure from Titan, it is what "weight, not colour, creates hierarchy" means when the
   ground is a picture. */
body.over-hero header.top {
  background: color-mix(in srgb, rgb(var(--plate-wash)) 66%, transparent);
  border-bottom-color: var(--on-plate-rim);
}
body.over-hero header.top,
body.over-hero .brand .mark,
body.over-hero .brand .mark b,
body.over-hero .brand .tag,
body.over-hero .pnav-desk .pnav-item,
body.over-hero .pnav-desk .pnav-lab,
body.over-hero .theme-btn,
body.over-hero .account .acct-btn { color: var(--on-plate); }
body.over-hero .brand-seal { background-color: var(--on-plate); }
body.over-hero .brand .tag,
body.over-hero .pnav-desk .pnav-item:not(.is-on) { color: var(--on-plate-2); }
body.over-hero .pnav-desk .pnav-item.is-on { background: var(--on-plate-rim); color: var(--on-plate); }
body.over-hero .theme-btn { background: var(--on-plate-fill); border-color: var(--on-plate-rim-3); }
body.over-hero .search {
  background: var(--on-plate-fill); border-color: var(--on-plate-rim-3);
}
body.over-hero .search input { color: var(--on-plate); }
body.over-hero .search input::placeholder { color: var(--on-plate-3); }
body.over-hero .search-go { background: var(--on-plate); color: rgb(var(--plate-wash)); }

/* ---- the bar's ceiling is what FITS, not a number ------------------------
   The watchlist came back and the signed-in bar now carries seven nav items on
   top of a brand, a search field, a bell, a plan chip, an avatar and a theme
   toggle. Measured with the fullest possible nav (tools/titan/navfit.py), that
   overlaps below 1440 and squeezes the field under a usable width below 1180.

   So the labels go before anything else does. An icon-only nav is still a nav —
   every item keeps its `aria-label` and its title — whereas a nav that has
   dropped a destination is not, which is the trade that removed the watchlist
   in the first place. Nothing is ever hidden; only its label is. */
@media (min-width: 900px) and (max-width: 1439px) {
  .pnav-desk .pnav-lab { display: none; }
  .pnav-desk .pnav-item { padding-inline: 10px; }
}
/* Below 1180 the brand tagline goes too — it is a descriptor, not a destination. */
@media (max-width: 1179px) {
  .brand .tag { display: none; }
}
/* The field must never become an ellipsis with a button beside it. THE AUTHORITATIVE
   `.search` sizing — see the note ~80 lines above, where a second copy of these
   declarations sat dead.

   IT NO LONGER SHRINKS, and that is the "fix white space" in this bar. With
   `flex-shrink: 1` the field gave up its own 320px basis, and the space it gave up went to
   `.pnav-desk`, which is `flex: 1 1 auto` with `justify-content: center` — so the slack
   became EMPTY TRACK either side of six centred nav items rather than field. Measured
   signed in with the fullest bar:

       width   nav track   items in it   empty each side   search field
       1440      718px        657px           30px            176px
       1920     1194px        657px          268px            179px
       2560     1834px        657px          588px            179px

   Half a viewport of nothing in the middle while the one control that could use the width
   sat at its floor. Shrinking is unnecessary here by construction: below 1240 on a coarse
   pointer and 1180 on a fine one the field is not displayed at all — the magnifier
   replaces it — so every width where the field exists is a width where it fits.

   Measured after, both pointer types, no overflow at any width:
   1440 176 -> 200, 1920 179 -> 261, 2560 179 -> 261. The nav stays centred, which is
   deliberate; it simply stops absorbing space the field was asking for. */
/* WIDER, and the ceiling is the change that matters. Miraaj: "make it a bigger
   searchbar" — it is the primary action on the site and read as a minor control squeezed
   between the nav and the account chips. 320 -> 420 at the top end; the 18vw middle term
   means it still only takes the width where the width exists, and 4.1.0's `flex-shrink: 0`
   is what lets it actually reach the ceiling instead of donating it to the nav track. */
/* THE FIELD LEADS THE BAR AND IS THE WIDEST THING IN IT (2026-08-23).
   It used to be the LAST item in `.top-actions`, behind five nav tabs, taking whatever was
   left after the bell, the plan chip, the avatar and the theme toggle had been paid — a fixed
   `clamp(180px, 22vw, 420px)` that only ever reached its ceiling because 4.1.0 stopped it
   donating space to its neighbours. Typing a ticker is what this product is FOR, so it now
   sits between the wordmark and the nav and it GROWS: `flex: 1 1 auto` takes the leftover
   instead of the nav taking it.
   The nav gives up `flex: 1 1 auto` in the same move — it is a row of five labels with a
   natural width, and stretching it was only ever a way of consuming space nothing wanted. */
.top-inner > .search { flex: 1 1 auto; min-width: 200px; margin-left: 0; }
/* ONE declaration for the nav's flex, not two. The rule below used to read
   `flex: 1 1 auto` and sat AFTER the line above it — equal specificity, later wins — so a
   `flex: 0 0 auto` written above it would have been silently discarded. That is the fourth
   time this exact trap has cost a fix in this file; the answer is to amend the existing
   declaration rather than add a competing one. `overflow: hidden` and `min-width: 0` stay:
   the nav must still be allowed to clip rather than force the bar wider. */
.top-inner > .pnav-desk { min-width: 0; flex: 0 0 auto; overflow: hidden;
                          justify-content: flex-start; }
.search-compact {
  display: none; flex: 0 0 auto;
  width: 34px; height: 34px; border-radius: var(--r-full);
  background: var(--surface); border: 1px solid var(--line); color: var(--text);
  cursor: pointer; place-items: center; padding: 0;
}
@media (pointer: coarse) { .search-compact { width: var(--tap); height: var(--tap); } }
/* The second selector is not redundant. `body:has(#home:not(.hidden)) header.top .search`
   further up forces the field visible on the home page — and `:has()` inherits the
   specificity of its most specific argument, so that `#home` makes it an ID-weight rule
   that a plain `.top-actions > .search` cannot beat. Measured: the field came back at 900
   and 1024 at 121px and 125px, ellipsing its own placeholder, which is the exact "COM…"
   Miraaj reported. Matched at the same shape rather than escalated with !important. */
/* ONE RANGE, NOT TWO WITH A HOLE BETWEEN THEM. This rule used to start at 900px and the
   phone rule below used to stop at 719px, which left 720-899px — every tablet in portrait,
   and a good many small laptops — with NO SEARCH ENTRANCE IN THE HEADER AT ALL. The field
   is `flex: 0 1 clamp(190px, 20vw, 320px)` with `min-width: 0`, so in that band it did not
   stay at 190px, it shrank toward nothing; and the magnifier that replaces it was shown by
   neither rule, because each was scoped to its own side of the gap.

   It is also, almost certainly, the collision Miraaj reported. `.search-go` is
   `flex: 0 0 auto` at `var(--tap)` on a coarse pointer, so it does not shrink with its
   own field: as the field collapsed toward zero the 44px button kept its size and was
   pushed along the bar into the notification bell. One cause, two symptoms.

   The band was invisible to tools/titan/navfit.py because that probe sampled 600 and then
   900 — one width either side of the hole. It now reports "NO SEARCH ENTRANCE" as its own
   class of failure rather than only checking that the field is wide enough to use. */
/* AND A COARSE POINTER NEEDS MORE ROOM, because it is a different bar. Under
   `@media (pointer: coarse)` the go button, the bell and the theme toggle all take
   `var(--tap)` — 44px against 30px — so a touch laptop or an iPad in desktop mode gets
   ~40px less field at the same viewport width. Measured signed in with the fullest bar:

       coarse 1180 -> 139px    1200 -> 143px    1220 -> 147px    1240 -> 150px

   and below 150px the placeholder ellipses, which is the "COM…" this whole collapse was
   written to prevent. The threshold moves to 1239 on a coarse pointer so the magnifier
   takes over while the field is still too small to use, rather than 60px later.

   This state had never been swept: every header run used a mouse context, so the 44px bar
   at desktop widths simply did not exist in any measurement. */
@media (max-width: 1179px), (pointer: coarse) and (max-width: 1239px) {
  .top-inner > .search,
  body:has(#home:not(.hidden)) header.top .top-inner > .search { display: none; }
  .search-compact { display: grid; }
}
/* AND ON A PHONE, for the same reason at the other end of the range. A 180px minimum field
   plus the brand plus the gutters is 415px against a 390px viewport, so the bar WRAPPED —
   measured 128px of sticky header on an 844px screen, fifteen per cent of the display given
   over permanently to a search box that a magnifier says just as well. One row, 73px, the
   same height the bar has at every other width. The full-screen search sheet the magnifier
   opens is the better phone affordance anyway; the inline field was never the one being
   used here. */
@media (max-width: 719px) {
  .top-inner > .search,
  body:has(#home:not(.hidden)) header.top .top-inner > .search { display: none; }
  .search-compact { display: grid; }
}
body.over-hero .search-compact {
  background: var(--on-plate-fill); border-color: var(--on-plate-rim-3); color: var(--on-plate);
}

/* ---- the third pass: SEVENTEEN THAT ONLY EXIST BEHIND THE GATE ------------
   Miraaj named two of these by eye — the score explainer and "The full 4-tab
   workbook…" — after four clean runs of tools/titan/monosweep.py. The tool was
   not wrong about what it saw; it was looking at the wrong site. It swept
   logged out, and it never opened a `<details>`. Both of those describe most of
   this product: the DCF bridge, the dashboard tiles, the portfolio hints and
   the market-open panel are all either Pro-only or inside an accordion, so a
   cold sweep of a settled page walks past every one of them.

   The tool signs in and opens every accordion now, which is what turned 0 into
   17. The lesson is the one that keeps recurring in this batch: a checker is
   only as complete as the STATE it visits, and "the checks pass" is not the
   same claim as "the page is right".

   THE ARITHMETIC BRIDGE IS THE INTERESTING ONE. `.brow` rows read "Sum of PV of
   forecast FCF", "+ PV of terminal value", "− Net debt", "= Equity value" — and
   the mono there is doing real work, because the operators have to line up down
   the left edge for the column to read as a calculation. So this splits it
   rather than flattening it: the OPERATOR keeps the mono and its alignment, the
   words become sans. That is the same answer as everywhere else in this block —
   the sentence is sans, the figures stay tabular — applied to a glyph instead
   of a numeral.
   ========================================================================= */
.brow, .brow span,
.dt-li-r, .dt-meta, .adv-state, .opt-meta,
.mo-panel .mo-tone, .mo-clab,
.pf-add-hint {
  font-family: var(--sans);
  font-variant-numeric: tabular-nums;
  letter-spacing: normal;
}
/* the operator column keeps the mono it needs, and only that */
.brow .bop {
  font-family: var(--mono);
  display: inline-block; width: 1.1em; text-align: center;
}

/* A tip glyph promoted out of a de-duplicated panel heading and onto the tile title (see
   Tiles.dedupeHeading) lands as a bare sibling of the text — "MARKET OPEN OUTLOOK?" with
   nothing between them. It keeps its own spacing here rather than in the JS that moves it,
   so a heading that always had one looks the same as one that inherited it. */
.dt-title > .tip-ico, .dt-title > button, .dt-title > a { margin-left: 6px; vertical-align: middle; }

/* ---- A CARD INSIDE A CARD LOSES ITS FRAME -------------------------------
   Miraaj asked whether the tiles across the product share one vocabulary. Measured, they
   largely do — one radius, one hairline weight, an eyebrow header on 139 of 146 cards, no
   shadows outside the named --el-hero. This is the place they do not.

   `stockListCard()` returns a `.grp` — a full card with its own fill, hairline, 20px
   radius and elevation — because on the landing it IS the card. Dropped inside a dashboard
   tile or a panel it becomes a second frame 14px inside the first: two borders, two radii,
   two grounds, for one list. Seen on the Watchlist and Recently-valued tiles, and on the
   calendar's macro list.

   The fix is the rule rather than the call site, because the same helper is legitimately a
   card in one place and a list in another — what changes is the CONTEXT, so the context is
   what removes the frame. The rows keep their own dividers, so nothing about scanning the
   list changes; only the redundant box goes. ---- */
/* DESCENDANT, NOT CHILD. `> .panel` missed the market-open and recently-valued tiles,
   whose panel arrives inside an unclassed wrapper div — the same depth-anchoring mistake
   that made `Tiles.dedupeHeading` match nothing on its first attempt. A card nested at
   ANY depth inside a card is the defect; how many wrappers sit between them is not part
   of it. `.dash-tile .dash-tile` is excluded implicitly — tiles are never nested. */
.dash-tile .grp,
.dash-tile .panel,
.panel .grp,
.grp .grp {
  background: transparent;
  border: 0;
  border-radius: 0;
  box-shadow: none;
  padding-left: 0;
  padding-right: 0;
}
/* the top padding goes too when the frame does — the gap under the tile's own eyebrow is
   already the tile's padding, and keeping both reads as a dropped element */
.dash-tile .grp { padding-top: 0; }


/* ============================================================================
   TITAN — ONE EYEBROW
   ---------------------------------------------------------------------------
   Miraaj has raised consistency five times: fonts, shapes, tiling, tile
   vocabulary, and this. The eyebrow is the most repeated typographic primitive
   in the product — the small uppercase label at the top of a card, a section, a
   stat, a row — and it had no single definition.

   HOW BADLY, measured rather than felt. Every rule in this sheet that sets
   `text-transform: uppercase`, grouped by its type settings: ~78 distinct
   combinations. The same primitive rendered at 9, 9.5, 10, 10.5, 11, 11.5 and
   12px, at weights 400, 500, 600 and 700, across fourteen letter-spacings, and
   several times in the sans face. Walking the RENDERED pages instead of the
   sheet found 74 distinct settings on screen across eight surfaces.

   MY OWN SWEEP MISSED IT and it is worth recording why. I measured whether a
   card header WAS an eyebrow — uppercase and mono — and got 139 of 146, which
   reads as a healthy system. That question cannot see this defect. The right
   question is whether it is the SAME eyebrow, and the answer was no.

   THE TREATMENT is Titan's: Geist Mono, 11px, weight 500, +0.33px (0.03em at
   11px, which is what --track-eyebrow already encodes), uppercase, in slate.
   Both size and tracking were already tokens; nothing here invents a value.

   THREE REGISTERS, NOT ONE — because collapsing everything uppercase into the
   eyebrow would be a different kind of wrong:

     · LABELS (below) take the eyebrow. A label names the thing beside it and has
       no ground of its own.
     · DENSE COLUMN HEADERS take the ONE named second token. A `th` in a ten-column
       financial table is a genuinely different job — it must fit a column, not a
       card — so it gets `--t-eyebrow-dense` rather than a one-off per table. Nine
       tables had nine settings between them.
     · CHIPS AND BADGES are left alone. `.dp`, `.tier-tag`, `.deal-tag` and the
       other 24 carry their own fill and border: they are a component, not a
       label, and folding them in here would delete a distinction the design
       makes on purpose.

   Also excluded, deliberately: the button-primary CTA register (`.hero-cta`,
   `.scr-run`/`.auth-submit`/`.mini-upgrade`, `.hero-badge`), which is 13px/500 by
   its own declared rule and guarded by tests/test_titan_cta_voice.py; the two
   search inputs, where `text-transform` is uppercasing what a READER typed; and
   the duel's 16px "VS", which is display type.
   ========================================================================= */
/* titan:eyebrow-canonical */
.an h3, .an-kpi-l, .brief-stitle, .bull-bear .bx b, .cal-date, .chk-bignum-lab,
.chk-cat-lab, .cl-day > h2, .dl-lab, .dt-k, .dt-sub, .dt-title, .dtile-cap, .dtile-foot,
.duel .side .lab, .eyebrow, .factor-score small, .fld span, .fv-delayed, .fv-lab,
.grp-title, .hl-tone-cap, .horizon-panel .hz-conf, .horizon-panel .hz-head,
.horizon-panel .hz-more > summary, .horizon-panel .hz-when, .hw-mi-lab, .ig-lab,
.lens-gtitle, .lp-card-tag, .lp-fig-lab, .lp-rf-axis, .lp-rf-lab, .lv-chip-lab, .lv-kv .lab,
.lv-range-lab, .mc-day .mc-day-h, .mc-fig-l, .mc-sect, .mcal-h, .memo-call .mc-head,
.memo-call .mc-k, .memo-call .mc-lvl, .metric .lab, .mo-glab, .mo-stitle, .movers-h,
.mylist-lab, .nar-q, .notif-head, .nw-mine, .nw-mv-lab, .nw-sub, .opt-fact span, .opt-lab,
.opt-mkt-item span, .opt-strikes-h, .pa-h, .pa-sub-t, .panel h3, .pf-case-h, .pf-lab,
.pf-opt-e span, .pf-perf-head, .pf-sc-basis, .pr-nm, .quote-badge, .rdr-lab, .rev-cmp .lab,
.sb-lab, .sc-axk, .sc-c2head .sc-clab, .sc-conf-lab, .sc-havelab, .scr-adv-head,
.scr-field label, .scr-saved-lab, .sector-head, .sh-lab, .sh-read .lab, .sh-up .lab,
.ss-lab, .street-cap, .sx-table td[data-label]::before,
.targets-panel .tgt-assump > summary, .targets-panel .tgt-now-k, .targets-panel .tgt-when,
.td-slab, .td-stat .lab, .tip-sheet .tip-term, .tip-term, .we-num-lab, .we-sub, .wp-t {
  font-family: var(--mono);
  font-size: var(--t-eyebrow);
  font-weight: 500;
  letter-spacing: var(--track-eyebrow);
  text-transform: uppercase;
  color: var(--muted);
}

/* AN EYEBROW ON A DARK BAND KEEPS ITS REVERSED INK. `--muted` is Titan slate and is
   right on cream and on a white card; on the dark range band it measures 2.93:1, which
   the contrast sweep caught the moment the collapse landed. `--on-dark-muted` is the
   token that does the same job on that ground (5.45:1), and `.band-dark .eyebrow` was
   already using it — the collapse simply reached labels the old selector did not name.
   Placed after the collapse so it wins, and scoped to the band so it cannot leak. */
.band-dark .eyebrow, .band-dark .lp-fig-lab, .band-dark .lp-rf-lab,
.band-dark .lp-rf-axis, .band-dark .lp-card-tag, .band-dark .muted,
.lp-range .lp-fig-lab, .lp-range .lp-rf-lab, .lp-range .lp-rf-axis {
  color: var(--on-dark-muted);
}

/* titan:eyebrow-dense — the named second token, and the only one */
.ins-table th, .lv-table th, .meth-tbl th, .peer-table th, .pf-table th,
.seo-table thead th, .sx-table thead th, table.mc-scen th, table.waterfall th {
  font-family: var(--mono);
  font-size: var(--t-eyebrow-dense);
  font-weight: 600;
  letter-spacing: var(--track-eyebrow-dense);
  text-transform: uppercase;
  color: var(--muted);
}

/* ============================================================================
   DYNAMIC TILES — a tiled region uses the window, in both width and column count
   ---------------------------------------------------------------------------
   The other half of the tiling work. The vertical half is done: a card no longer
   inherits its neighbour's height, because the flow re-packs instead of sharing a
   grid row. This is the horizontal half, and it is the one Miraaj stated as a
   principle: "they should automatically resize themselves as the shape of the
   window changes."

   THE PAGE IS EDITORIAL; THE TILED REGIONS INSIDE IT ARE NOT. That distinction
   matters more than a per-route switch would, because the pages are HYBRIDS.
   Discover is a hero, three marketing bands and a Free-vs-Pro table — all of
   which want a reading measure — and then four rows of tiles that want the
   window. Widening the whole page would stretch the prose; leaving it narrow
   pins the tiles. So the tiles break out of the measure and everything else
   keeps it, which needs no knowledge of which route is showing.

   `calc(50% - var(--shell)/2)` is the centring escape: 50% resolves against the
   containing block, so a region of any width centres itself inside a container
   of any width. Same mechanism as the full-bleed marketing bands, stopped at a
   ceiling instead of running to the viewport edge.

   THE COLUMN COUNT FOLLOWS THE WIDTH. `columns: <length>` asks for as many
   columns of at least that width as fit, which is the responsive behaviour
   asked for — where `columns: 2` / `columns: 3` at breakpoints was three columns
   from 1280 to 2560. It also preserves the vertical fix exactly: multicol still
   re-packs when a tile changes height, at any column count.
   ========================================================================= */
/* THE SECTION BREAKS OUT, NOT THE GRID INSIDE IT. Applying this to `.grid` alone left
   every section HEADING behind at the reading measure while its own cards ran to the
   window — "MOST UNDERVALUED RIGHT NOW" sitting 380px inset from the first card it
   labels. A label that does not start where its content starts is worse than a narrow
   grid. So the unit is the section: heading, grid and footnote move together. */
#view,
.tile-flow, .nw-grid, .dash-grid,
/* AND THE FOOTNOTE UNDER THE TILES. "the unit is the section: heading, grid and footnote
   move together" is the rule this list is built on, and `.nw-disc` was the one part of
   News left behind by it — measured at x=197 under a grid starting at x=32. It is prose,
   so it keeps a measure of its own inside the wider shell rather than setting lines the
   full 1405px; the shell is what aligns its left edge with the cards it belongs to. */
.nw-disc,
#overnight-wrap, #movers-wrap, #undervalued-wrap, #rising-wrap, #trending-wrap,
.models-head, #company-grid,
/* `.scr-wrap` and `.pf-grid` were here and match NOTHING — zero occurrences of either in
   app.js or index.html. The screener renders `.scr-panel` / `#scr-saved` / `#scr-results`
   and the portfolio renders `.pf-cards`, so both pages have been at the reading measure the
   whole time while this list claimed otherwise. That stale claim is what made the screener
   look like a tiling page when its head was being classified above; it is not one. */
/* AND THE PAGE HEAD, for the same reason one level up. "Your dashboard" sat at the
   reading measure (x=422) while the tiles it heads began at x=40 — the title inset 380px
   from its own content. A heading that does not start where its content starts is the
   defect whether it is a section label or a page title.

   BUT ONLY WHERE THE CONTENT ACTUALLY BREAKS OUT, which is why this is `.dash-head-wide`
   and not `.dash-head`. `.dash-head` is shared by seven pages and only three of them tile;
   applied to all of them it produced the SAME defect in the opposite direction, measured on
   production: "My portfolio" and its subtitle sat at x=32 w=1405 while every card, holding
   row and note under them sat at x=197 w=1076 — the title hanging 165px to the LEFT of its
   own content. Earnings & economic calendar had it too (`.cal-cols` keeps the measure).
   A page opts in by tiling: dashboard, news, screener. The rest keep the measure, head and
   body together, which is the same rule read the other way round.

   `.back` needs its own selectors below rather than joining this list: `#view > .back`
   and `#dashboard > .back` set `margin: 18px 0 0`, an ID-weight shorthand that zeroes the
   inline margins and beats a bare `.back` whatever the order. Matched at the same weight
   there instead of escalated with !important. */
.dash-head-wide {
  --shell: min(var(--shell-tiles), calc(100vw - var(--sbw) - 2 * var(--shell-gutter)));
  width: var(--shell);
  margin-inline: calc(50% - var(--shell) / 2);
}
/* the column count, driven by width rather than by a breakpoint */
.tile-flow { columns: var(--tile-col); }

/* ---- THE LANDING'S FIGURE BANDS GET THE RULE TOO -------------------------
   Miraaj: the range band, the product band and the closing CTA "still sit narrow in a
   wide window — the app surfaces got the fluid rule and the landing sections never did."
   Measured, he is right and the reason is that these bands never had a shell at all:
   `.lp-range` and `.lp-product` are grids with HARD-CODED tracks (`425px 595px`),
   `.lp-boxes` is `340px 680px`. A fixed track cannot
   respond to anything. At 2560 the landing used 42% of the window.

   A THIRD REGISTER, NAMED, rather than four per-band overrides. The two existing ones are
   prose (`--shell-read`, a measure) and workbench (`--shell-tiles`, the window). An
   editorial band carrying a FIGURE is neither: the copy beside the figure still wants a
   measure, but the figure itself is a picture and gains from room. `--shell-band` is that
   ceiling, and the tracks become proportional so the copy column keeps roughly its old
   share while the figure takes the growth.

   The ceiling is 1760px rather than the workbench's 2160: past that the bell curves and
   the product shot stop reading as figures on a page and start reading as billboards.

   `.why-pay` is deliberately NOT in this list. It is a Free-versus-Pro comparison, which
   reads as prose in columns rather than as a figure, and it keeps the measure. It is also
   capped by `#discover > .why-pay`, an ID-weight selector that a bare class cannot beat —
   worth knowing before anyone tries, since matching it would mean escalating rather than
   deciding. */
:root { --shell-band: 1760px; }

/* ---- /methodology's tables stack on a phone -------------------------------
   Three columns — what, where, the detail — inside 362px gave 103/119/140px, and the
   middle one set "SEC EDGAR — Form 4 and 8-K filings" three lines deep at eleven
   characters a line. The markup is frozen (it is server-rendered in app/pages.py and its
   structure is asserted byte-for-byte), so this is CSS only: each cell becomes a block and
   takes the full measure, which is the classic responsive-table shape and needs no
   wrapper. The first cell carries the <b> label, so the stack reads as a titled entry. */
@media (max-width: 560px) {
  .meth-tbl, .meth-tbl tbody, .meth-tbl tr, .meth-tbl td, .meth-tbl th { display: block; width: auto; }
  .meth-tbl tr { padding: 12px 0; border-bottom: 1px solid var(--line-soft); }
  .meth-tbl tr:last-child { border-bottom: 0; }
  .meth-tbl td, .meth-tbl th { padding: 2px 0; border: 0; }
  .meth-tbl td:first-child { font-weight: 600; }
  /* The header row is meaningless once the cells stack — "WHAT / SOURCE / USED FOR" become
     three orphan labels above a list that no longer has columns for them to head. The
     stacked entry says the same thing by position: title, source, purpose. Hidden with
     `display: none` rather than clipped, so a screen reader is not read three labels that
     no longer correspond to anything either. */
  .meth-tbl thead { display: none; }
}

/* ---- the provenance line on the verdict block ----------------------------
   Metadata about the number, attached to the number. Sans rather than mono: these are
   short phrases, not figures or eyebrows, and mono on a phrase is the thing the type
   audit spent a day removing. */
.provenance {
  display: flex; flex-wrap: wrap; align-items: baseline; gap: 4px 8px;
  font-size: 11.5px; color: var(--muted); margin: -4px 0 0;
}
.prov-dot { color: var(--faint); }

/* ---- the two timestamps on the company page ------------------------------
   Named separately because they are different moments: a delayed quote from minutes ago
   and a model the cron may have rebuilt days ago. Keys are eyebrow-register mono, values
   are figures — both of which mono is for. */
.stamps { display: flex; flex-wrap: wrap; gap: 6px 20px; margin-top: 10px; }
.stamp { display: inline-flex; align-items: baseline; gap: 7px; }
.stamp-k {
  font-family: var(--mono); font-size: var(--t-eyebrow); font-weight: 500;
  letter-spacing: var(--track-eyebrow); text-transform: uppercase; color: var(--faint);
}
.stamp-v {
  font-family: var(--mono); font-size: 11px; color: var(--muted);
  font-variant-numeric: tabular-nums;
}
/* THE GAP BETWEEN THE TWO, when it is wide enough to matter. Warmer than its neighbours
   because it is the one that changes how they should be read — but still a stamp, not a
   warning banner: the page is not broken, it is being precise about what it compared. */
.stamp-gap .stamp-k, .stamp-gap .stamp-v { color: var(--warn, var(--down-text, var(--down))); }
.stamp-gap .stamp-v { font-weight: 600; }

/* ---- the three worked examples in the steps band ------------------------- */
.lp-works { margin-top: var(--s-6, 34px); }
.lp-works-row {
  display: grid; gap: 12px; margin-top: 14px;
  grid-template-columns: repeat(auto-fit, minmax(240px, 1fr));
}
.lp-work {
  display: grid; gap: 12px; align-content: start;
  padding: 18px; border: 1px solid var(--line); border-radius: var(--r);
  background: var(--surface); color: var(--text); text-decoration: none;
  transition: border-color var(--dur-2, .2s) var(--ease-out, ease),
              transform var(--dur-2, .2s) var(--ease-out, ease);
}
.lp-work:hover { border-color: var(--text); transform: translateY(-2px); }
.lp-work:focus-visible { outline: 2px solid var(--text); outline-offset: 3px; }
.lp-work-head { display: flex; align-items: baseline; gap: 8px; flex-wrap: wrap; min-width: 0; }
/* `min-width: 0` because the name is a flex item and its shrink floor is min-content — on a
   412px Android the card gave it 77px against "Microsoft" at 79px, and two pixels of a
   company name went missing. It may wrap; it may not be cut. */
.lp-work-co { font-size: 17px; font-weight: 600; letter-spacing: -0.01em; min-width: 0; overflow-wrap: anywhere; }
.lp-work-figs { display: grid; gap: 8px; }
.lp-work-f { display: flex; align-items: baseline; justify-content: space-between; gap: 10px; }
.lp-work-lab {
  font-family: var(--mono); font-size: var(--t-eyebrow); font-weight: 500;
  letter-spacing: var(--track-eyebrow); text-transform: uppercase; color: var(--muted);
}
.lp-work-v { font-size: 15px; font-variant-numeric: tabular-nums; }
/* SANS, not mono. Mono is for eyebrows, stat figures and uppercase CTA labels; this is a
   sentence-case link label and the note below it is a full sentence. Caught by
   test_no_mono_class_is_given_a_sentence_in_a_template on the first run of this markup. */
.lp-work-go { font-size: var(--t-small); color: var(--muted); }
.lp-work:hover .lp-work-go { color: var(--text); }
.lp-works-note { font-size: 11.5px; color: var(--faint); margin-top: 12px; }
.lp-product, .lp-boxes {
  --shell: min(var(--shell-band), calc(100vw - var(--sbw) - 2 * var(--shell-gutter)));
  width: var(--shell);
  margin-inline: calc(50% - var(--shell) / 2);
  max-width: none;
}
/* `.lp-range` IS DELIBERATELY LEFT AT ITS OLD MEASURE, and this is the interesting one.
   It was in Miraaj's list, widening it is a two-line change, and the result is wrong.

   Its figure is a `.engrave` whose box scales with the column and is anchored to the
   BOTTOM of the band, so it grows upward into a band that is a fixed 423px tall with
   `overflow: hidden`. Measured: at a 766px column the artwork is 766x431 sitting at
   y=-160; at 988px it is 988x556 at y=-285 — the bottom edge is pinned at the same
   y=271 in both, and every pixel of the extra height is taken off the top. At the
   original width the clip removed empty sky above the curves; at 988 it removes the
   PEAKS, and the three price labels stop sitting under their own maxima.

   So the band cannot be widened without re-composing the figure or growing the band from
   423px to roughly 750px, which is a design decision about the landing's vertical rhythm
   rather than a spacing fix. Left alone, with the numbers here, for Miraaj to call.
   Verified by looking rather than by the layout numbers, which improved. */
/* 42fr/58fr, NOT 0.42fr/0.58fr. When the flex factors in a grid template sum to 1 or
   less, the spec does not normalise them — each track gets that FRACTION OF THE LEFTOVER
   and the rest is simply not distributed. Written as 0.42/0.58 the range band's tracks
   measured 361px and 499px inside a 1760px section: 860px of content and 900px of nothing,
   narrower than the 1076px it started at. Factors summing above 1 normalise as expected. */
/* SCOPED TO WHERE THE TWO-COLUMN BAND ACTUALLY EXISTS. `@media (max-width: 1023px)`
   already stacks these to one column, and an unconditional rule here — later in the file,
   same specificity — beat it at every width. Caught on the production sweep: at 375px the
   "No black boxes." heading was handed a 103px column and set vertically, three lines of
   five characters. A responsive rule further up the file is not a suggestion. */
@media (min-width: 1024px) {
  .lp-range, .lp-product { grid-template-columns: minmax(0, 42fr) minmax(0, 58fr); }
  .lp-boxes { grid-template-columns: minmax(0, 33fr) minmax(0, 67fr); }
}
/* `min(..., 100%)` because a track floor larger than the container overflows it. At 320px
   — the narrowest phone still in use — `--tile-col` resolves to its 340px minimum and the
   column was 20px wider than the viewport, taking the document with it. Found while
   measuring the header at 320: it is not a header defect (it reproduces on both pointer
   types and the bar itself does not overflow), but it is one line and it is real. */
/* AUTO-FIT, NOT AUTO-FILL — a grid must not reserve a track it has no child for.
   `auto-fill` creates as many tracks as WILL fit and leaves the surplus empty; `auto-fit`
   collapses the empties so the real children share the whole width. Identical when the
   tracks are full, which is why the difference went unnoticed for so long: it only shows
   when the child count is smaller than the track count.

   Measured on Discover at 1920, where there are exactly TWO cards (gainers and losers) in
   a grid three tracks wide: the cards ended at x=1261 and 619px of the section sat empty
   down its whole height — Miraaj's "the cards stop at roughly x=1250, leaving about 650px
   of empty page". At 1440 it was 464px, at 2560 it was 725px.

   All six `auto-fill` grids in this file move together. The two with a small fixed child
   count are the ones that were visibly wrong — `.movers-grid` (always 2) and `.chk-cats`
   (5 check categories in a grid that fits 6 at width) — but the distinction is not "which
   ones are broken today", it is that reserving space for a child that does not exist is
   never what any of them wanted. */
.movers-grid { grid-template-columns: repeat(auto-fit, minmax(min(var(--tile-col), 100%), 1fr)); }

/* RUNNING PROSE KEEPS ITS MEASURE, and this is the part that makes the width
   safe rather than merely bigger. Measured on the company page BEFORE any of
   this: 65 prose blocks, none with a max-width, the widest already running to
   218 characters at 1440 and 245 at 1920 — the old 1560 cap was the only thing
   holding it back. Widening without this would have turned a bad line length
   into an unreadable one. `.meth p` and `.gl-list dd` already did this at 68ch;
   it is that rule extended to the surfaces that never had it. */
.panel > p, .dash-tile p, .sect-inner > p, .nw-card p, .dt-teach-copy,
.mo-summary, .mo-why, .model-intro, .lock-card-copy, .dt-note, .usage-stat {
  max-width: var(--prose-measure);
}

/* the back link, at the specificity its own rules already use */
/* THE BACK LINK RIDES WITH THE PAGE IT SITS ON, and for the same reason the page head
   does — see `.dash-head-wide` above. This was a bare `.back`, so "← Home" broke out to
   the tiles shell on EVERY page, including the ones whose body keeps the reading measure:
   measured on production, Portfolio's back link started at x=32 with the first card it
   sits above at x=197. Listed by section rather than given a modifier class because
   `.back` is emitted from a dozen places and the section already knows which register it
   is in. The ID weight is load-bearing for a second reason: `#view > .back` and
   `#dashboard > .back` set `margin: 18px 0 0`, an ID-weight shorthand that zeroes the
   inline margins and would beat a bare `.back` whatever the order — so the shell has to be
   matched at that weight here rather than escalated with !important. */
#view > .back {
  --shell: min(var(--shell-tiles), calc(100vw - var(--sbw) - 2 * var(--shell-gutter)));
  width: var(--shell);
  margin-inline: calc(50% - var(--shell) / 2);
}
/* AND THE TWO TILING PAGES — BUT ONLY WHEN THEY ARE ACTUALLY TILING. Both of these render
   a second, narrow branch: signed out, `#dashboard` is a lock card and nothing else, and a
   failed fetch leaves `#news` an error card. Keyed to the section id alone the back link
   broke out over those too — measured signed out, "← Home" at x=32 above a lock card at
   x=197, which is the same defect this whole block exists to remove. `:has()` asks the
   question that actually matters: is the tiled grid on the page.
   KEPT AS ITS OWN RULE, not merged into the one above. An invalid selector poisons the
   entire list it sits in, so an engine without `:has()` would take the company page's back
   link down with it; split, the worst case is these two links sitting at the measure. */
#dashboard:has(> .dash-grid) > .back,
#news:has(> .nw-grid) > .back {
  --shell: min(var(--shell-tiles), calc(100vw - var(--sbw) - 2 * var(--shell-gutter)));
  width: var(--shell);
  margin-inline: calc(50% - var(--shell) / 2);
}

/* ---- THE HEADLINE FEED IS A RAIL, NOT A TILE ----------------------------
   Found only on production, and only after the fluid tiles shipped — the eighth time in
   two days that local data hid something. The local server has no news provider, so
   `.nw-feed` renders its empty state and News balanced across three columns exactly as
   designed. Production has a live feed: forty headlines, 2,700-3,100px tall. Multicol
   gives an item that tall a column of its own and cannot balance anything around it, so
   News rendered at TWO columns with 42.9% of the window unused at 1920 — most of the
   fluid-width win handed straight back.

   A feed is not a tile. It is a list you scan and scroll, and its natural length has no
   relationship to the cards beside it. Capped to the viewport it stops setting the grid's
   height, and the flow re-balances. Measured on production data at 1440/1920/2560:

       as shipped              2 cols   42.3% / 42.9% / 48.7% unused   grid 3122px tall
       capped to 900px         3 cols   27.5% / 22.6% / 31.8%          grid 1535px
       capped to 1200px        3 cols   30.6% / 25.8% / 34.6%          grid 1737px
       capped to the viewport  3 cols   26.1% / 20.8% / 30.1%          grid 1485px   <-

   The viewport cap wins on every width AND is the only one that means something to a
   reader: the rail is as tall as the screen.

   FROM 768, NOT 1024, and the threshold was measured rather than assumed. It shipped at
   1024 on the reasoning "desktop only — on a phone a nested scroll region is worse than a
   long page". True of a phone; 768 is tablet portrait, where the flow is ALREADY two
   columns and the trade is identical to 1024's. Measured signed-in with a
   production-length feed: 768 went 34.6% unused -> 20.1%, with no change at 1024 or above.
   The phone stays at one column and keeps the long page, which is where that reasoning
   actually applies. ---- */
@media (min-width: 768px) {
  .nw-grid .nw-feed {
    max-height: calc(100vh - 150px);
    overflow-y: auto;
    overscroll-behavior: contain;   /* the page behind must not scroll with it */
  }
}


/* ---- WHAT IS LEFT OF THE VOID, AND WHY IT STAYS ----------------------------
   Measured 2026-08-22 after the reorder, company page, advanced chart both states:

       width   as-is            closed sections span 2
       1500    37.0% unused  ->  39.0%   (+2.0)
       1600    37.3%         ->  38.4%   (+1.1)
       1700    37.2%         ->  38.4%   (+1.2)
       1830    37.1%         ->  35.1%   (-2.0)
       1920    34.1%         ->  35.2%   (+1.1)
       2200    34.1%         ->  35.2%   (+1.1)

   Worse at five widths of six. `grid-auto-flow: dense` was ruled out the same way earlier,
   and making an open section span the full grid changed nothing at all. The cause is
   structural rather than a missing rule: a six-column grid holding items from 67px to
   1,642px IN A FIXED READING ORDER cannot pack tightly, and reading order is not
   negotiable on this page. The lever that does work is reducing the height disparity,
   which the 3.92.0 reorder did by drawering the 1,173px research write-up. Recorded here
   so the next person measures something else instead of re-running these two. ---- */

/* ---- THE SPINE'S VOID, and why this is a span rule rather than the shared flow ----
   Miraaj: "spacing. not dynamic in company analysis." At 1830 the company view left
   23.2% of its own area empty, in two holes an inch tall: a COLLAPSED accordion (67px,
   span 4) sitting beside the score panel (421px, span 8), and the outlook panel (297px,
   span 4) alone on the next row with eight columns of nothing to its right.

   THE ACCORDION BAND IS NOT THE PROBLEM, which is worth stating because it is what the
   report named. Measured, items 13-22 of #view are ten identical 67px collapsed cards at
   span 4 — three per row, no gaps. They are already the densest thing on the page.

   AND THE SHARED FLOW CANNOT APPLY HERE. `.tile-flow` is multicol, and multicol is
   column-major: it fills column one top to bottom before starting column two. The other
   surfaces can take that because a dashboard tile and a news card have no reading order.
   This page does — chart, then score, then the sections that justify it — and column-major
   would put the score panel halfway down column two. The comment on `.tile-flow` has said
   so since it was written; this is that constraint being load-bearing rather than
   theoretical.

   So the fix is the mechanism this page already declares: a span. Letting the score panel
   occupy two grid rows lets the collapsed accordion and the outlook stack in the 4-column
   track beside it instead of each leaving a hole. Measured, unused area inside #view:

       width      1280    1440    1600    1830    2560
       before    16.2%   16.6%   23.0%   23.2%   32.3%
       after     16.2%   16.6%   17.2%   17.7%   27.5%
       height     same    same   -331    -313    -297   px

   Scoped from 1500 because below it the grid is not wide enough for the hole to exist and
   the same rule costs 1.7 points — measured, which is why it is a media query and not an
   unconditional one. GRID-AUTO-FLOW: DENSE WAS TRIED FIRST and made it WORSE at every
   width (16.6 -> 22.2 at 1440, 23.2 -> 24.7 at 1830): the items that could backfill these
   holes are themselves full-width panels. ---- */
/* ============================================================================
   THE COMPANY VIEW IS ONE COLUMN ONCE IT IS WIDE, and this supersedes the span
   system above at this breakpoint. Miraaj has reported the same void four times.
   ---------------------------------------------------------------------------
   THE STRUCTURAL ANSWER, since it kept being asked: the company page DOES derive
   its WIDTH from the shared rule — `#view` is in the `--shell` breakout list and
   measures 1840px at 1920, exactly like News and Discover. What it never shared
   is the ARRANGEMENT. News packs with `.tile-flow` (`columns:`); `#view` is a
   twelve-track grid with per-panel spans. The shared rule was only ever about how
   wide a section is, never about how its children pack — which is why fixing one
   section never fixed the next.

   AND A GRID CANNOT PACK UNEQUAL HEIGHTS. A row is as tall as its tallest member,
   so any short panel beside a tall one leaves dead column, and no span assignment
   can fix it because the heights are content-dependent and unknowable in CSS. The
   rules above have tried three times — `[open]` spanning full width, three-across
   at 1600, the score panel spanning two rows — and each one moved the void rather
   than removing it. Masonry would solve it and is not in Chromium.

   So the pairing stops. Measured on MSFT, default state, both before and after:

       width   6-track grid              one column
       1600    31.3% dead, 925px void    9.4% dead, 0px void
       1920    30.7% dead, 895px void    9.5% dead, 0px void
       2560    30.1% dead, 859px void    9.5% dead, 0px void

   Dead space falls by two thirds and the worst void goes to ZERO, because with one
   child per row there is no sibling to be short beside. The residual 9.5% is the
   gaps between panels, which is the floor. The page is ~21% taller, and that is
   the trade: he has reported the void four times and has never once said the page
   was too long. The width is not wasted either — each panel now gets the full
   1840px instead of ~900, so the metric mosaics, the checks and the tables inside
   them spread wider rather than a second column standing empty beside them.

   It starts at 1024 — the width the grid itself starts at — because one column
   wins at every width from there up, including the band the old rules claimed was
   packing well. The span declarations have been REMOVED rather than left to sit
   inert above a rule that overrides them.
   ========================================================================= */
@media (min-width: 1024px) {
  #view { grid-template-columns: minmax(0, 1fr); }
  #view > *,
  #view > details.sect,
  #view > details.sect[open],
  #view > .score-panel,
  #view > .horizon-panel,
  #view > .price-panel { grid-column: 1 / -1; grid-row: auto; }
}

@media (min-width: 1500px) {
}


/* ---------- feedback form (one screen) ---------- */
/* The four kinds are chips, not a <select>: on a phone a select is a system sheet away from
   the thing being reported, and the whole point of this form is that it costs nothing to
   fill in. */
.fb-kinds { display: flex; flex-wrap: wrap; gap: 8px; margin: 0 0 16px; }
.fb-kind {
  font-family: var(--sans); font-size: var(--t-small); line-height: 1;
  padding: 9px 13px; border-radius: var(--r-full); cursor: pointer;
  background: var(--surface-2); border: 1px solid var(--line); color: var(--muted);
  transition: color var(--dur-2) var(--ease-out), border-color var(--dur-2) var(--ease-out),
              background var(--dur-2) var(--ease-out);
}
.fb-kind:hover { color: var(--text); border-color: var(--line-strong); }
.fb-kind.on { background: var(--text); border-color: var(--text); color: var(--bg); font-weight: 600; }
#fb-form textarea {
  width: 100%; resize: vertical; min-height: 96px;
  font-family: var(--sans); font-size: var(--t-small); line-height: 1.5;
  color: var(--text); background: var(--surface); border: 1px solid var(--line);
  border-radius: var(--r-sm); padding: 10px 12px;
}
#fb-form textarea:focus-visible { outline: none; box-shadow: var(--ring); border-color: var(--accent); }
.fb-opt { font-style: normal; font-weight: 400; color: var(--faint); }
/* Says what is being attached. Collecting context in the open is a different thing from
   collecting it quietly, and this form asks for enough trust to be worth the difference. */
.fb-context { font-family: var(--mono); font-size: 11px; color: var(--faint); margin: 2px 0 14px; }


/* ---------- allocation donut ---------- */
/* SIX STEPS, ONE HUE. Titan is a monochrome system and a rainbow pie would be the single
   most off-brand thing on the site — it would also imply the categories are unordered, when
   they are sorted largest-first and the ordering is the reading. So the ramp runs from the
   text colour down toward the ground: the biggest slice is the darkest, and the eye lands on
   it first without anyone being told to look. The "others" wedge is the faintest, because it
   is the one slice that is a summary rather than a thing. */
.dnt-wrap { display: flex; align-items: center; gap: var(--s-5); flex-wrap: wrap; margin: 4px 0 10px; }
.dnt { width: 160px; height: 160px; flex: 0 0 auto; }
.dnt-s { stroke: var(--surface); stroke-width: 1.5; transition: opacity var(--dur-2) var(--ease-out); }
.dnt-wrap:hover .dnt-s { opacity: 0.55; }
.dnt-wrap .dnt-s:hover { opacity: 1; }
.dnt-s0 { fill: var(--text); stroke: var(--surface); }
.dnt-s1 { fill: color-mix(in srgb, var(--text) 78%, var(--surface)); }
.dnt-s2 { fill: color-mix(in srgb, var(--text) 60%, var(--surface)); }
.dnt-s3 { fill: color-mix(in srgb, var(--text) 45%, var(--surface)); }
.dnt-s4 { fill: color-mix(in srgb, var(--text) 32%, var(--surface)); }
.dnt-s5 { fill: color-mix(in srgb, var(--text) 22%, var(--surface)); }
.dnt-rest { fill: color-mix(in srgb, var(--text) 14%, var(--surface)); }
/* The stroke form (a single 100% holding) needs the colour on `stroke`, not `fill`. */
circle.dnt-s0 { stroke: var(--text); fill: none; }
.dnt-c { font-family: var(--sans); font-size: 26px; font-weight: 700; fill: var(--text); }
.dnt-cs { font-family: var(--mono); font-size: 9.5px; letter-spacing: 0.12em;
          text-transform: uppercase; fill: var(--faint); }
.dnt-key { display: grid; gap: 6px; flex: 1 1 220px; min-width: 0; }
.dnt-k { display: grid; grid-template-columns: auto minmax(0, 1fr) auto; align-items: center;
         gap: 9px; font-size: var(--t-small); }
/* A DOT, not a rounded square: `3px` is not in Titan's radius vocabulary (0 / --r-sm /
   --r / --r-lg / --r-full / 50%), and a key swatch is the one shape where a true
   circle is what the eye is already reading it as. */
.dnt-sw { width: 10px; height: 10px; border-radius: 50%; flex: 0 0 auto; }
.dnt-kl { color: var(--text); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.dnt-kw { color: var(--muted); font-variant-numeric: tabular-nums; }
/* A swatch is an <i>, so the slice rules above (which set `fill`) do not reach it. */
.dnt-sw.dnt-s0 { background: var(--text); }
.dnt-sw.dnt-s1 { background: color-mix(in srgb, var(--text) 78%, var(--surface)); }
.dnt-sw.dnt-s2 { background: color-mix(in srgb, var(--text) 60%, var(--surface)); }
.dnt-sw.dnt-s3 { background: color-mix(in srgb, var(--text) 45%, var(--surface)); }
.dnt-sw.dnt-s4 { background: color-mix(in srgb, var(--text) 32%, var(--surface)); }
.dnt-sw.dnt-s5 { background: color-mix(in srgb, var(--text) 22%, var(--surface)); }
@media (max-width: 520px) { .dnt { width: 132px; height: 132px; } }
@media (prefers-reduced-motion: reduce) { .dnt-s { transition: none; } }

/* the book-level health radar reuses .rdr wholesale; it only needs somewhere to sit */
.pa-shape { display: flex; justify-content: center; margin: 6px 0 2px; }


/* The email is fixed once a code has been sent to it: editing it here would verify one
   address and create an account on another. Shown as settled rather than as an input. */
#auth-email[readonly] { color: var(--muted); cursor: default; }
#signup-verify .fld { margin-top: 0; }


/* ---------- verified signup (step 2) ---------- */
/* The code, the name and the experience level, shown only once the address has been proved.
   Same field furniture as the rest of the auth modal — this is one more step of the same
   form, not a different screen wearing the same frame. */
#signup-verify #auth-code {
  font-family: var(--mono); font-size: 20px; letter-spacing: 0.34em; text-align: center;
}
#signup-verify select {
  width: 100%; font-family: var(--sans); font-size: var(--t-small);
  color: var(--text); background: var(--surface); border: 1px solid var(--line);
  border-radius: var(--r-sm); padding: 10px 12px; appearance: none;
}
#signup-verify select:focus-visible { outline: none; box-shadow: var(--ring); border-color: var(--accent); }
/* "Didn't get it?" is not a nicety — a code that never arrived is the commonest way a
   verified signup dies, and without a way back the only route is closing the modal, which
   reads as the product being broken. Quiet, but never hidden. */
.auth-otp-again { font-size: var(--t-micro); color: var(--faint); margin: 2px 0 14px; }
.auth-otp-again a { color: var(--muted); }
.auth-otp-again a:hover { color: var(--accent); }


/* ---------- key moments ---------- */
/* The card sits BELOW the chart, never over it: a popover covers the line the reader is
   trying to look at, and this is meant to be read alongside the move rather than instead of
   it. Collapsed to nothing until a ring is clicked, so an unopened chart carries no
   furniture. */
.km-card { display: none; }
.km-card.on {
  display: block; margin: 12px 0 0; padding: 14px 16px;
  background: var(--surface-2); border: 1px solid var(--line-soft); border-radius: var(--r-sm);
}
.km-head { display: flex; align-items: center; gap: 12px; margin-bottom: 8px; }
.km-date { font-size: var(--t-meta); color: var(--muted); }
.km-move { font-size: 17px; font-weight: 700; font-variant-numeric: tabular-nums; }
.km-up { color: var(--up); }
.km-down { color: var(--down); }
.km-close {
  margin-left: auto; background: none; border: 0; cursor: pointer;
  color: var(--faint); font-size: 14px; line-height: 1; padding: 4px;
}
.km-close:hover { color: var(--text); }
.km-para { font-size: var(--t-small); line-height: 1.6; color: var(--text); margin: 0; }
/* "We couldn't find news that explains this" is a real answer, not an error — muted, not red.
   See docs/GOOGLE-FINANCE-TEARDOWN.md: printing it at all is the point. */
.km-none { color: var(--muted); font-style: italic; }
.km-load { font-size: var(--t-small); color: var(--faint); }
.km-src { display: flex; flex-wrap: wrap; gap: 6px; margin-top: 10px; }
.km-chip {
  font-family: var(--mono); font-size: 11px; letter-spacing: 0.02em;
  color: var(--muted); background: var(--surface); border: 1px solid var(--line);
  border-radius: var(--r-full); padding: 4px 10px; text-decoration: none;
}
.km-chip:hover { color: var(--accent); border-color: var(--accent); }
.km-disc { font-size: 11px; line-height: 1.5; color: var(--faint); margin: 10px 0 0; }

/* ---------- "build me a model" (the no-model page's front door) ----------
   Inherits .notice rather than inventing a surface: it sits inside "How honest is this
   number?" among the caveat bars, and it is the ANSWER to the one that says "No DCF for
   this one" — so it should read as part of that conversation, not as an ad dropped into it. */
.notice.model-request { margin-top: 12px; }
.mr-row { display: flex; flex-wrap: wrap; align-items: center; gap: 8px; margin-top: 12px; }
.mr-row[hidden] { display: none; }

.mr-go {
  display: inline-flex; align-items: center; gap: 6px;
  font-family: var(--mono); font-size: 12px; letter-spacing: 0.04em;
  padding: 8px 14px; border-radius: var(--r-full); cursor: pointer;
  background: var(--surface); border: 1px solid var(--line); color: var(--text);
  transition: border-color 0.2s, color 0.2s, transform 0.15s;
}
.mr-go:hover:not(:disabled) { color: var(--accent); border-color: var(--accent); transform: translateY(-1px); }
.mr-go:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }
/* NO :disabled RULE HERE ON PURPOSE. The shared `button:disabled` block already applies
   --state-disabled-opacity, cursor: not-allowed and pointer-events: none to every disabled
   control in the app. I had written `cursor: progress; opacity: 0.6` here on the argument
   that the disabled state is really an in-flight state and should read as "working" rather
   than "refusing" — but that token exists precisely because two surfaces had improvised 0.6
   and 0.7 and ended up looking differently disabled. A one-off deviation for a state that
   lasts under a second is not worth reopening that. It inherits the house standard.
   `pointer-events: none` also makes the `busy` flag in app.js belt-and-braces rather than
   the only thing standing between a double-click and two build promises. */

.mr-email {
  flex: 1 1 200px; min-width: 0; max-width: 280px;
  font-family: var(--mono); font-size: 12px;
  padding: 8px 12px; border-radius: var(--r-full);
  background: var(--surface); border: 1px solid var(--line); color: var(--text);
}
.mr-email:focus-visible { outline: 2px solid var(--accent); outline-offset: 1px; border-color: var(--accent); }

.mr-said { margin-top: 10px; font-size: 13px; color: var(--muted); }
.mr-said[hidden] { display: none; }
/* Colour is NOT the only carrier — the sentence itself says what went wrong, so this reads
   correctly to someone who cannot distinguish it from the muted default. */
.mr-said.bad { color: var(--down-text); }
/* ── Upcoming-earnings grid ────────────────────────────────────────────────────
   Density, so "is Thursday heavy?" is answerable before a name is read. Shaded
   against the peak the server measured (--d, 0..1) rather than a fixed ceiling,
   which would flatten a quiet fortnight and saturate a busy one.

   The grid scrolls INSIDE its own container. Seven columns will not sit down to
   375px without either wrapping the chips — which makes one cell taller than its
   neighbours and destroys the at-a-glance row comparison the grid exists for — or
   scrolling the whole page sideways. Scrolling the panel is the one option that
   costs nothing else. */
.mc-g-scroll { overflow-x: auto; margin-top: 14px; -webkit-overflow-scrolling: touch; }
.mc-g-dow, .mc-g-week {
  display: grid; grid-template-columns: repeat(7, minmax(92px, 1fr)); gap: 6px;
  min-width: 660px;
}
.mc-g-dow {
  font-family: var(--mono); font-size: 10px; letter-spacing: 0.1em;
  color: var(--faint); padding: 0 2px 6px;
}
.mc-g-week { margin-bottom: 6px; }
.mc-g-cell {
  /* Declared here so the variable has a home in the stylesheet — the client sets it
     per-cell inline from the day's share of the peak. A custom property that only ever
     appears in an inline style is invisible to the design-system guards. */
  --d: 0;
  background: var(--surface);
  background: color-mix(in srgb, var(--accent) calc(var(--d) * 13%), var(--surface));
  border: 1px solid var(--line); border-radius: var(--r-sm);
  padding: 8px 8px 9px; min-height: 86px;
  display: flex; flex-direction: column; gap: 6px;
}
.mc-g-quiet { background: var(--surface-2); }
.mc-g-past { opacity: 0.5; }
.mc-g-today { border-color: var(--accent); box-shadow: 0 0 0 1px var(--accent) inset; }
.mc-g-n {
  display: flex; align-items: baseline; gap: 6px;
  font-family: var(--mono); font-size: 12px; color: var(--muted);
  font-variant-numeric: tabular-nums;
}
.mc-g-tag {
  font-size: 9px; letter-spacing: 0.08em; text-transform: uppercase; color: var(--accent);
}
.mc-g-names { display: flex; flex-wrap: wrap; gap: 4px; align-content: flex-start; }
.mc-g-chip {
  font-family: var(--mono); font-size: 10.5px; line-height: 1.5;
  color: var(--muted); background: var(--surface-2);
  border: 1px solid var(--line-soft); border-radius: var(--r-full);
  padding: 1px 6px; text-decoration: none; white-space: nowrap;
}
/* A company we publish a fair value for is a link to an opinion; the rest are plain. */
a.mc-g-cov { color: var(--text); border-color: var(--line-strong); }
a.mc-g-cov:hover, a.mc-g-cov:focus-visible { color: var(--accent); border-color: var(--accent); }
.mc-g-ses { color: var(--faint); }
.mc-g-more { font-family: var(--mono); font-size: 10px; color: var(--faint); padding: 2px 2px 0; }
/* ── Phone touch and safe-area corrections ────────────────────────────────────
   Four measured defects, all of them invisible on a desktop and all of them the
   first thing a phone user meets. Appended together because they are one class
   of bug: the desktop layout was correct and the coarse-pointer case was not
   considered beside it. */

/* 1. iOS ZOOMS THE WHOLE PAGE when a focused input is under 16px, and the
      viewport meta deliberately sets no maximum-scale (pinch-zoom must stay
      available). So every phone sign-in and every add-holding began with an
      unrequested zoom the reader then had to pinch back out of.
      15px and 14px were fine choices for a mouse; they are a platform trigger
      for a thumb. Raised only under a coarse pointer so the desktop density the
      forms were designed at is untouched. */
@media (pointer: coarse) {
  .fld input,
  .pf-add input,
  .sx-search input { font-size: 16px; }
}

/* 2. THE TOAST LANDED ON THE PHONE TAB BAR. Measured at 375x812: the bar occupies
      y756-812 and the toast y755-796 — a 41px overlap, and that is with a ZERO
      safe-area inset. On a real iPhone the inset adds ~34px more, putting the
      confirmation almost entirely on top of the navigation and against the home
      indicator. Every "Added to watchlist" covered the thing you would tap next.

      --pnav-h is the bar's measured height without its inset. It is stated once
      here rather than repeated in a calc so there is a single number to correct
      if the bar's own metrics change; `test_toast_clears_the_phone_nav` pins the
      relationship rather than the value. */
:root { --pnav-h: 57px; }
@media (max-width: 899px) {
  .vm-toast { bottom: calc(var(--pnav-h) + env(safe-area-inset-bottom, 0px) + 12px); }
}

/* 3. THE ALERT-SETTINGS GEAR WAS 66x14 CSS PX — inside the notifications sheet,
      a surface that only exists on a phone. A 14px-tall control on a surface
      that cannot be reached with a mouse is the clearest possible case for the
      floor the design system already sets. */
@media (pointer: coarse) {
  .notif-gear { min-height: var(--tap); display: inline-flex; align-items: center; }
}

/* 4. THE EDIT-MODE DELETE HANDLE WAS A 24px DISC. Removing a watchlist row is
      destructive and undoable only by re-adding, so it is the last control that
      should be hard to hit accurately. The visible disc stays 24px — it is
      correct at that size next to the row — and gains the invisible hit area
      `.tip-ico::after` already uses for exactly this problem. */
.hw-del { position: relative; }
.hw-del::after {
  content: ""; position: absolute; top: 50%; left: 50%; transform: translate(-50%, -50%);
  width: 24px; height: 24px; border-radius: 50%;
}
@media (pointer: coarse) {
  .hw-del::after { width: var(--tap); height: var(--tap); }
}

/* ─────────────────────────────────────────────────────────────────────────────────────
   COMPANY ANALYSIS TILES  (web/app.js: analysisTile / plot / fillAnalysis)

   THE RULE THIS FILE ENFORCES: every analysis tile is the same object. One eyebrow, one body
   at one fixed height, one footer with controls left and legend right. The fixed body height
   is the load-bearing line — it is why a row of these reads as an instrument panel instead of
   four cards that happen to be adjacent. Content is NEVER allowed to set a tile's height,
   because the moment one chart has a taller legend the whole row steps and the grid stops
   looking designed.

   COLOUR: none is introduced. Series separate by WEIGHT and DASH against `--text`, and each
   line is named at its own right-hand endpoint so the legend is a confirmation rather than a
   lookup. The single exception is the earnings surprise, where the direction IS the fact —
   and it still carries a sign and a number, per DESIGN.md §2.
   ───────────────────────────────────────────────────────────────────────────────────── */

.tile-grid {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(288px, 1fr));
  gap: 14px;                      /* the .grid gap — this is not a new spacing step */
  margin-top: 4px;
}
.tile {
  display: flex;
  flex-direction: column;
  background: var(--surface);
  border: 1px solid var(--line-soft);
  border-radius: var(--r);
  padding: 16px 16px 12px;
  min-width: 0;                   /* or a wide SVG refuses to shrink and blows the grid out */
}
.tile-wide { grid-column: 1 / -1; }

.tile-head { display: flex; align-items: baseline; justify-content: space-between; gap: 10px; }
.tile-lab {
  font-family: var(--mono);
  font-size: var(--t-eyebrow);
  letter-spacing: var(--track-eyebrow);
  text-transform: uppercase;
  color: var(--muted);
}
.tile-note { font-family: var(--mono); font-size: 10px; color: var(--faint); white-space: nowrap; }

/* The fixed body, and the line that makes the grid a grid.

   `aspect-ratio` rather than a hard pixel height, so the tile stays honest at every column
   width — and, crucially, so a tile's height does not depend on WHAT IS IN IT. A locked tile
   measured 223px against a charted tile's 205px before this was pinned, which is exactly the
   stepped row the whole block exists to avoid. The chart's own viewBox ratio is used, so the
   SVG fills the box precisely instead of leaving a sliver. */
.tile-body {
  margin: 10px 0 8px;
  aspect-ratio: 460 / 210;
  min-height: 118px;
  display: flex; align-items: center;
}
.tile-body > * { width: 100%; }
.tile-body .tchart { display: block; width: 100%; height: auto; }

.tile-foot {
  display: flex; align-items: center; justify-content: space-between;
  gap: 10px; flex-wrap: wrap;
  border-top: 1px solid var(--line-soft);
  padding-top: 9px;
  margin-top: auto;               /* pins the footer to the bottom on a short tile */
  min-height: 26px;
}
.tile-legend { display: flex; gap: 12px; flex-wrap: wrap; margin-left: auto; }
.lg-item { display: inline-flex; align-items: center; gap: 5px; font-size: 11px; color: var(--faint); }

/* Legend swatches ARE the chart's marks, not a CSS imitation of them.

   The line swatches render as a tiny inline <svg> reusing the very `.tl-line` classes the
   chart draws with, so a dash pattern can never drift between the legend and the series it
   names — and no dashed CSS border is needed to fake one (which is not a Titan primitive;
   tests/test_titan_shapes.py). The block swatches stay CSS because a filled and a hollow
   square are exactly what the bars are. */
.lg-sw { display: inline-block; width: 10px; height: 10px; border-radius: var(--r-sm); }
.lg-fill { background: var(--text); }
.lg-hollow { background: transparent; border: 1.5px solid var(--text); opacity: .7; }
.lg-fill-dot, .lg-hollow-dot { width: 9px; height: 9px; border-radius: var(--r-full); }
.lg-fill-dot { background: var(--text); }
.lg-hollow-dot { background: transparent; border: 1.5px solid var(--text); opacity: .75; }
.lg-swline { display: block; width: 15px; height: 8px; overflow: visible; }

/* The block's source notes, gathered from the tiles that carry one. */
.tile-notes { margin: 12px 2px 0; display: grid; gap: 6px; }
.tile-notes p { font-size: 10.5px; line-height: 1.5; color: var(--faint); margin: 0; }
/* A locked tile is the same object as a charted one — same height, same footer line. It is
   selling the tile it replaces, so it must occupy exactly that tile's space. */
.tile-lock {
  display: flex; flex-direction: column; align-items: center; justify-content: center;
  text-align: center; padding: 8px; height: 100%;
}
.tile-lock p { font-size: 12.5px; color: var(--muted); margin: 0 0 12px; }

/* The table the charts came from — present, one click down, never the first thing. */
.tile-table { margin-top: 14px; }
.tile-table > summary {
  font-family: var(--mono); font-size: 11px; letter-spacing: .04em;
  text-transform: uppercase; color: var(--faint); cursor: pointer; padding: 6px 0;
}
.tile-table > summary:hover { color: var(--muted); }
.tile-table-in { padding-top: 6px; }
.tile-table-in h3 { display: none; }   /* the <summary> already names it */

/* ---- segmented control (Annual / Quarterly, 1Y / 2Y / 5Y) ---- */
.tile-seg { display: inline-flex; border: 1px solid var(--line-soft); border-radius: var(--r-sm); overflow: hidden; }
.tseg {
  font-family: var(--mono); font-size: 10.5px; letter-spacing: .03em;
  padding: 4px 9px; border: 0; background: transparent; color: var(--faint); cursor: pointer;
  transition: color var(--dur-2, .18s) var(--ease-out, ease), background var(--dur-2, .18s) var(--ease-out, ease);
}
.tseg:hover { color: var(--text); }
.tseg.on { background: var(--surface-3); color: var(--text); }

/* ---- the latest-quarter readout ---- */
.tq-latest { font-size: 11.5px; color: var(--faint); }
.tq-latest b { color: var(--text); font-weight: 600; font-variant-numeric: tabular-nums; }
.tq-lab { font-family: var(--mono); text-transform: uppercase; letter-spacing: .04em; }
/* The one direction colour in the block — and it never travels alone: the sign is in the
   text. `--up-text` / `--down-text`, never the fill tokens (DESIGN.md §2). */
.tq-surp { font-variant-numeric: tabular-nums; font-weight: 600; }
.tq-surp.up { color: var(--up-text); }
.tq-surp.down { color: var(--down-text); }
.tq-surp.flat { color: var(--muted); }

/* ---- chart marks ---- */
.tc-rule { stroke: var(--line-soft); stroke-width: 1; stroke-dasharray: 2 3; }
.tc-zero { stroke: var(--line); stroke-width: 1; }
.tc-vlab, .tc-clab {
  font-family: var(--mono); font-size: 9px; fill: var(--faint);
}
.tc-vlab { text-anchor: end; }
.tc-clab { text-anchor: middle; }

/* Bars: filled ink vs hollow ink. These are magnitudes — neither one has a direction, so
   neither one gets a direction colour. */
.tb-a { fill: var(--text); opacity: .88; }
.tb-b { fill: none; stroke: var(--text); stroke-width: 1.5; opacity: .62; }

/* Lines: one ink, three weights and dash patterns. */
.tl-line { fill: none; stroke: var(--text); stroke-width: 1.75; stroke-linejoin: round; stroke-linecap: round; }
.tl-line.tl-1 { opacity: .72; stroke-dasharray: 5 3; }
.tl-line.tl-2 { opacity: .55; stroke-dasharray: 1.5 2.5; }
.tl-line.tl-over { opacity: .62; stroke-width: 1.5; stroke-dasharray: 4 3; }
.tl-dot { fill: var(--text); }
.tl-dot.tl-1 { opacity: .72; }
.tl-dot.tl-2 { opacity: .55; }
.tl-dot.tl-over { opacity: .62; }

/* Estimate vs actual: hollow is what was expected, filled is what was reported, and the rule
   between them is the gap. A quarter that landed on its estimate draws the two on top of each
   other and is silent, which is the correct amount of noise for a quarter that held no news. */
.td-gap { stroke: var(--text); stroke-width: 1; opacity: .3; }
.td-est { fill: var(--surface); stroke: var(--text); stroke-width: 1.5; opacity: .8; }
.td-act { fill: var(--text); }

@media (max-width: 560px) {
  .tile { padding: 14px 13px 10px; }
  .tile-body { min-height: 118px; }
  .tile-legend { gap: 9px; }
  .lg-item { font-size: 10.5px; }
}

/* ─────────────────────────────────────────────────────────────────────────────────────
   HORIZON RANGES  (web/app.js: horizonRangesChart)

   The strip's two answers, drawn: hollow bands are statistical ranges with no view on
   direction, filled bands carry our estimate and a dot at it. Monochrome throughout —
   colouring a forward range would read as a call, which is the rule this strip has carried
   since it was text and is not relaxed by drawing it.
   ───────────────────────────────────────────────────────────────────────────────────── */
/* THE DRAWING IS CAPPED, and this is not a preference.

   SVG text scales with the box. The viewBox is 460 units wide and the type in it is 9-10
   units; in a tile that lands at roughly 10px, which is what it was drawn for. This panel is
   full-width, so at 1320px the same labels rendered at ~28px — three times the size of every
   other word on the page, and the row labels ended up larger than the section heading. Capping
   the drawing keeps its type near the size it was designed at; the panel keeps its width. */
.hz-chart { margin: 10px 0 14px; }
.hz-chart .hzchart {
  display: block; width: 100%; max-width: 620px; height: auto;
  margin-inline: auto;   /* capped AND centred — a 620px drawing pinned to the left of a
                            1300px panel reads as unfinished rather than as a decision */
}

.hz-row-lab {
  font-family: var(--mono); font-size: 9.5px; fill: var(--muted); text-anchor: end;
}
/* Today's price: the reference every band is read against. A hairline, not a rule that
   competes with the bands themselves. */
.hz-price-rule { stroke: var(--line-strong); stroke-width: 1; stroke-dasharray: 3 3; }
.hz-price-lab { fill: var(--muted); }

.hz-band-fill { fill: var(--text); opacity: .78; }
.hz-band-hollow { fill: var(--text); opacity: .16; }
.hz-cap { stroke: var(--text); stroke-width: 1.25; opacity: .55; }
/* The expected value, where one is published. Reads on both band treatments. */
.hz-point-dot { fill: var(--surface); stroke: var(--text); stroke-width: 2; }

@media (max-width: 560px) {
  .hz-row-lab { font-size: 9px; }
}

/* The horizon key. Same swatch vocabulary as the analysis tiles, same rule: the swatch is the
   mark, so the two band treatments cannot drift from the two things they mean. */
.hz-key { display: flex; gap: 14px; flex-wrap: wrap; justify-content: flex-end; margin: -6px 0 12px; }
.lg-hz-fill, .lg-hz-hollow { width: 16px; height: 9px; border-radius: var(--r-sm); }
.lg-hz-fill { background: var(--text); opacity: .78; }
.lg-hz-hollow { background: var(--text); opacity: .16; box-shadow: inset 0 0 0 1px var(--text); }

/* The score on the company's title line — ANCHORED RIGHT, and set at the size of the name.

   The first version put it immediately after the h2 at 20px, and it read as clutter: three
   different type sizes crowded into the left half of the line with the share button after
   them, and nothing holding the right-hand end. A title line has two ends. The name owns one,
   the verdict owns the other, and the eye has somewhere to land.

   The numeral is the same size as the name because they are the same order of information:
   what this is, and what we make of it. The caption stays small — it is a label for the
   number, not a third thing competing with it. */
/* The right-hand column: verdict on top, actions under it. The column owns the alignment,
   so the score no longer pushes itself right — it is simply the first row of something that
   already sits at the right end of the line. */
.co-head-right {
  margin-left: auto;
  display: flex; flex-direction: column; align-items: flex-end; gap: 10px;
}
.co-actions { display: flex; gap: 10px; align-items: center; }
.head-score { display: inline-flex; align-items: baseline; gap: 9px; }
.head-score[hidden] { display: none; }
.hs-cap {
  font-family: var(--mono); font-size: 10px; letter-spacing: var(--track-eyebrow);
  text-transform: uppercase; color: var(--faint);
}
.hs-num {
  font-family: var(--serif); font-weight: 400; letter-spacing: -0.02em;
  font-size: clamp(1.8rem, 1.2rem + 2.4vw, 2.9rem);   /* the h2 ramp, unmodified */
  font-variant-numeric: tabular-nums; line-height: 1;
}
.hs-grade {
  font-family: var(--mono); font-size: 15px; font-weight: 700; line-height: 1;
  border: 1.5px solid currentColor; border-radius: var(--r-sm); padding: 4px 7px;
}
@media (max-width: 700px) {
  /* Wrapped onto its own line, `margin-left: auto` would shove the column to the right of
     nothing. On a phone it sits under the name and aligns with it. */
  .co-head-right { margin-left: 0; align-items: flex-start; width: 100%; }
  .hs-cap { display: none; }
}

/* ─────────────────────────────────────────────────────────────────────────────────────
   YOUR POSITION  (web/app.js: fillPosition)
   Five label/value pairs on one row — the same shape whether the numbers are yours or zero,
   because a watchlist name and a held name are the same page.
   ───────────────────────────────────────────────────────────────────────────────────── */
.pos-strip {
  display: grid; grid-template-columns: repeat(auto-fit, minmax(132px, 1fr));
  gap: 14px 20px;
}
.pos-cell { min-width: 0; }
.pos-lab {
  font-size: 10px; letter-spacing: var(--track-eyebrow); text-transform: uppercase;
  color: var(--faint); margin-bottom: 5px;
}
.pos-val {
  font-size: 17px; color: var(--text); font-variant-numeric: tabular-nums;
  display: flex; align-items: baseline; gap: 7px; flex-wrap: wrap;
}
.pos-none { margin-top: 12px; font-size: 12px; color: var(--faint); }

/* The watchlist star, repeated at the point of interest — the price header. */
.chart-watch { margin-left: auto; }
.chart-watch:empty { display: none; }   /* signed out, `watchButton` renders nothing */

/* ═════════════════════════════════════════════════════════════════════════════════════
   NEWS — THE SESSION AND THE RUNWAY, DRAWN   (web/app.js: paintNewsCharts)

   Five sets of numbers on this page were only ever printed: six overnight closes, six
   movers, a VIX reading inside a sentence, a fortnight of scheduled events across two
   lists, and a wording tag on each of twenty-four headlines. Each one is a comparison,
   and the page was asking the reader to perform it.

   TWO COLOUR RULES, and they point in opposite directions on purpose.

   The SESSION charts are chromatic. A market that closed down closed down; that is a fact
   about a completed day, and it is the same fact the chips beside them have always drawn in
   `--up` / `--down`. Per DESIGN.md §2 the colour never travels alone — every bar's signed
   number is printed at the end of its own row, so the encoding survives colour-blindness, a
   screen reader, `data-ink="mono"` and a monochrome print.

   The RUNWAY is monochrome. It draws what has NOT happened, where a colour would be a claim
   about which way a scheduled release will go — the rule `fillHorizonStrip` has carried
   since it was text. Impact is height and fill instead, and both are named in a key.

   THE DRAWINGS ARE CAPPED, for the reason `.hz-chart` is capped: SVG text scales with the
   box, the viewBox is 460 units wide, and its 9-unit type lands at ~10px in a tile and at
   ~28px in a full-bleed panel — three times every other word on the page. Capping holds the
   type near the size it was drawn for; the panel keeps its own width.
   ═════════════════════════════════════════════════════════════════════════════════════ */
.nw-slot { margin: 12px 0 14px; }
.nw-slot .nwchart, .nw-slot .nwtemp, .nw-slot .nwrunway {
  display: block; width: 100%; max-width: 560px; height: auto;
  /* Capped AND centred: a 560px drawing pinned to the left of a 1300px panel reads as
     unfinished rather than as a decision. Same call as `.hz-chart`. */
  margin-inline: auto;
}

/* ---- diverging bars: the world's close, and the session's movers ---- */
.nw-zero { stroke: var(--line-strong); stroke-width: 1; }
.nw-bar-up { fill: var(--up); opacity: .85; }
.nw-bar-dn { fill: var(--down); opacity: .85; }
.nw-blab {
  font-family: var(--mono); font-size: 9.5px; fill: var(--muted); text-anchor: end;
}
.nw-bval {
  font-family: var(--mono); font-size: 9.5px; font-weight: 600; text-anchor: end;
  fill: var(--muted);
}
.nw-bval.up { fill: var(--up-text); }
.nw-bval.down { fill: var(--down-text); }

/* The row-wide hit area. Transparent, and `pointer-events` is set explicitly because a
   `fill: none` rect is not hittable at all — which is the difference between a link that
   works anywhere on its row and one that only works on a four-pixel bar. */
.nw-hit { fill: transparent; pointer-events: all; }
.nw-brow-link { cursor: pointer; }
.nw-brow-link:hover .nw-blab, .nw-brow-link:focus-visible .nw-blab { fill: var(--accent); }
.nw-brow-link:hover .nw-bar-up, .nw-brow-link:hover .nw-bar-dn { opacity: 1; }
.nw-brow-link:focus-visible { outline: 2px solid var(--accent); outline-offset: 1px; }

/* ---- the volatility gauge ---- */
/* Four bands of one ink at rising weight. NOT a red-to-green ramp: "stressed" is a
   description of what options cost, not a verdict on whether to be in the market, and a
   gauge that reddens as it fills would read as the second. */
.nw-tband { fill: var(--text); }
.nw-tband-0 { opacity: .07; }
.nw-tband-1 { opacity: .13; }
.nw-tband-2 { opacity: .21; }
.nw-tband-3 { opacity: .3; }
.nw-ttick {
  font-family: var(--mono); font-size: 8px; fill: var(--faint); text-anchor: middle;
}
.nw-tmark { stroke: var(--text); stroke-width: 1.5; }
.nw-tdot { fill: var(--text); stroke: var(--surface); stroke-width: 1.5; }
.nw-tval {
  font-family: var(--mono); font-size: 12px; font-weight: 700; fill: var(--text);
  text-anchor: middle;
}
.nw-tnote {
  font-size: 11.5px; line-height: 1.55; color: var(--faint);
  margin: 2px auto 0; max-width: 560px;
}

/* ---- the forward runway ---- */
.rw-axis { stroke: var(--line-strong); stroke-width: 1; }
.rw-tick { stroke: var(--line); stroke-width: 1; }
/* Impact as WEIGHT and FILL — never as hue. A high-impact stem is solid and tall, a low one
   is hairline and short, and the key under the drawing names all three. */
.rw-stem { stroke: var(--text); }
.rw-stem.rw-high { stroke-width: 1.8; opacity: .9; }
.rw-stem.rw-med { stroke-width: 1.3; opacity: .6; }
.rw-stem.rw-low { stroke-width: 1; opacity: .38; }
.rw-dot.rw-high { fill: var(--text); opacity: .9; }
.rw-dot.rw-med { fill: var(--surface); stroke: var(--text); stroke-width: 1.4; opacity: .7; }
.rw-dot.rw-low { fill: var(--surface); stroke: var(--text); stroke-width: 1; opacity: .45; }
.rw-lab {
  font-family: var(--mono); font-size: 9px; fill: var(--muted); text-anchor: middle;
}
.rw-ev { cursor: help; }
.rw-key { display: flex; gap: 14px; flex-wrap: wrap; justify-content: center; margin-top: 2px; }
/* The key's swatches are the stems' own encoding at rest — solid, hollow, hairline — for the
   same reason `.lg-swline` reuses the chart's path classes: a legend that only resembles the
   marks it names drifts away from them the first time either is touched. */
.rw-sw { width: 11px; height: 11px; border-radius: var(--r-full); }
.rw-sw.rw-high { background: var(--text); opacity: .9; }
.rw-sw.rw-med { background: transparent; border: 1.4px solid var(--text); opacity: .7; }
.rw-sw.rw-low { background: transparent; border: 1px solid var(--text); opacity: .45; }
.mc-runway .nw-slot { margin-bottom: 6px; }

/* ---- the day's wording: the split bar, and the filter beneath it ---- */
.nw-split-wrap { margin: 4px 0 14px; }
.nw-split {
  display: flex; height: 8px; border-radius: var(--r-full); overflow: hidden;
  background: var(--surface-3);
}
.nw-sp { display: block; min-width: 2px; }
.nw-sp-positive { background: var(--up); }
.nw-sp-negative { background: var(--down); }
.nw-sp-unclear { background: var(--line-strong); }
.nw-sp-all { background: var(--text); opacity: .55; }

.nw-fils { display: flex; flex-wrap: wrap; gap: 7px; margin-top: 10px; }
.nw-fil {
  display: inline-flex; align-items: center; gap: 6px;
  font-family: var(--mono); font-size: 10.5px; letter-spacing: .02em;
  padding: 4px 9px; border: 1px solid var(--line-soft); border-radius: var(--r-full);
  background: transparent; color: var(--faint); cursor: pointer;
  transition: color var(--dur-2, .18s) var(--ease-out, ease),
              border-color var(--dur-2, .18s) var(--ease-out, ease),
              background var(--dur-2, .18s) var(--ease-out, ease);
}
.nw-fil b { color: var(--text); font-weight: 700; font-variant-numeric: tabular-nums; }
.nw-fil:hover { color: var(--text); border-color: var(--line-strong); }
/* THE PRESSED STATE IS NOT A COLOUR. `aria-pressed` drives a filled ground and a strong
   border, so which filter is on survives a monochrome ground and does not depend on the
   reader distinguishing two greys. */
.nw-fil[aria-pressed="true"] {
  background: var(--surface-3); border-color: var(--line-strong); color: var(--text);
}
.nw-fil-sw { width: 8px; height: 8px; border-radius: var(--r-full); flex: 0 0 auto; }
.nw-none { font-size: 13px; color: var(--muted); padding: 14px 0; }

@media (max-width: 560px) {
  .nw-fils { gap: 6px; }
  .nw-fil { font-size: 10px; padding: 5px 9px; }   /* taller, not smaller: it is a tap target */
  .nw-tnote { font-size: 11px; }
}

/* ── /vs/{competitor} — the head-to-head pages (app/comparisons.py) ──────────
   Built on `.seo-table` rather than beside it: a comparison page that looked like a
   different product from the valuation pages would undercut the one thing it is arguing,
   which is that the same care went into both. Only the deltas live here.

   `.vs-wins` is deliberately the most prominent block on the page. It is the section
   listing what the COMPETITOR does better, and burying it in the same grey as everything
   else is the visual version of the omission `render_comparison` refuses to allow. */
.vs-intro p { font-size: 16px; line-height: 1.7; margin: 0 0 14px; }
.vs-block { margin: 38px 0; }
.vs-block h2 { font-size: 19px; margin-bottom: 14px; }
.vs-block h3 { font-size: 13px; font-family: var(--mono); text-transform: uppercase;
               letter-spacing: 0.04em; color: var(--faint); margin: 0 0 10px; font-weight: 500; }
.vs-table { margin-bottom: 12px; }
.vs-table td:first-child { font-weight: 600; }
/* Our own row, marked so a reader can find it without counting columns. A left rule
   rather than a fill: the table is read across, and a filled row fights the row rules. */
.vs-ours td { box-shadow: inset 2px 0 0 var(--accent); }
.vs-ours td:first-child { color: var(--accent); }
.vs-note { font-size: 12px; color: var(--faint); line-height: 1.6; }
.vs-note a { color: var(--accent); }
.vs-cols { display: grid; grid-template-columns: repeat(auto-fit, minmax(260px, 1fr)); gap: 26px; }
.vs-cols ul, .vs-block > ul { margin: 0; padding-left: 18px; }
.vs-cols li, .vs-block > ul li { font-size: 14px; line-height: 1.65; margin-bottom: 7px; }
.vs-cols p { font-size: 14px; line-height: 1.7; margin: 0; }
.vs-wins { border: 1px solid var(--line); border-radius: var(--r-sm); padding: 22px 24px; }
.vs-wins > p { font-size: 12.5px; color: var(--faint); line-height: 1.6; margin: -4px 0 14px; }
.vs-claim { font-size: 12px; color: var(--faint); line-height: 1.6; margin-top: 14px; }

/* ── the weekly-list signup (app/pages.newsletter_form) ─────────────────────
   Lives on the server-rendered hubs, which do not load app.js — so this is styled with the
   SEO page's own tokens and nothing from the SPA's component layer. */
.seo-uv-form { margin: 40px 0 8px; border: 1px solid var(--line); border-radius: var(--r-sm); padding: 24px; }
.seo-uv-form h2 { font-size: 19px; margin: 0 0 6px; }
.seo-uv-form p { font-size: 14px; color: var(--muted); line-height: 1.6; margin: 0 0 14px; }
.nl-form { display: flex; flex-wrap: wrap; gap: 10px; align-items: center; }
.nl-lab { position: absolute; width: 1px; height: 1px; overflow: hidden; clip: rect(0 0 0 0); white-space: nowrap; }
.nl-input { flex: 1 1 240px; min-width: 0; font: inherit; font-size: 15px; padding: 11px 13px;
            border: 1px solid var(--line); border-radius: var(--r-sm);
            background: var(--bg); color: var(--text); }
.nl-input:focus-visible { outline: 2px solid var(--accent); outline-offset: 1px; border-color: var(--accent); }
.nl-btn { flex: 0 0 auto; font: inherit; font-size: 15px; font-weight: 600; padding: 11px 18px;
          border: 1px solid var(--accent); border-radius: var(--r-sm);
          background: var(--accent); color: var(--bg); cursor: pointer;
          transition: opacity var(--dur-1, 150ms) ease; }
.nl-btn:hover { opacity: 0.88; }
.nl-btn:active { opacity: 0.75; }
.nl-btn[disabled] { opacity: 0.5; cursor: default; }
/* The honeypot. Hidden from sight AND from assistive tech, and never `display:none` — some
   bots skip what is display:none, which is the one thing this field must not let them do. */
.nl-hp { position: absolute; left: -9999px; width: 1px; height: 1px; opacity: 0; }
.nl-note { font-size: 12.5px; color: var(--faint); margin: 12px 0 0 !important; }
