/* Optic Terminal — dark theme.
   Colours are the data-viz reference palette's dark steps against its
   documented dark surface (#1a1a19), so its contrast and CVD guarantees hold. */

/* Two themes, both selected rather than derived.
 *
 * The dark steps below are the data-viz reference palette's dark column against
 * its dark surface; the light block further down is that palette's *light*
 * column, not the same hexes on a pale background — an automatic flip would
 * throw away the contrast and colour-vision guarantees the palette exists for.
 *
 * `light dark` (rather than `dark`) lets the browser follow the OS for form
 * controls, dropdowns and scrollbars, which is what previously had to be pinned
 * to dark to stop them rendering light against a dark page. */
/* ------------------------------------------------------------------- fonts
 *
 * No @font-face here on purpose. There were four, for Maison Neue, and the
 * files they pointed at do not exist — fonts/ holds a README and nothing else.
 *
 * The comment they carried said "absent files cost nothing: a face whose src
 * 404s is dropped by the browser and the next family in --sans is used". The
 * rendering half of that is true and the cost half is not: measured, every page
 * load spent four requests earning four 404s and four console errors before
 * falling back. Scaffolding for a file nobody has is not free, and --sans never
 * named the family anyway, so the fallback was the only path ever taken.
 *
 * Keeping the licensing note, because it is the part that took work to
 * establish and would otherwise be rediscovered: Robinhood's Capsule Sans is a
 * custom cut of Maison Neue commissioned for Robinhood and is not sold, so a
 * stack naming it points at something unobtainable. Maison Neue itself is
 * licensable from Milieu Grotesque and is what Capsule Sans is drawn from, so
 * it is the closest a licence gets to that look. Switzer is the closest freely
 * licensed stand-in and is what the terminal actually renders in.
 *
 * To install a licensed face: drop the .woff2 files in fonts/, add the
 * @font-face blocks back with font-display: swap, and put the family ahead of
 * "Switzer" in --sans. fonts/README.md has the same instructions.
 */

:root {
  color-scheme: light dark;

  /* Surfaces are near-black and almost flat against each other. The old palette
     stacked four visibly different greys — plane, surface, surface-2, raised —
     so every panel announced itself with a box. Keeping them within a few points
     of each other means depth comes from spacing and dividers instead, which is
     what makes a dense page read as calm. */
  /* Warm neutrals.
   *
   * The greys carry a few points of red and green over blue, so the surfaces
   * read as paper and ink under warm light rather than as cold slate. Chosen
   * at matched luminance, then re-measured: every text token still clears
   * 4.5:1 on the brightest surface it sits on, and --neg needed two points of
   * lightness back (#d95454 -> #d95656) because warming the surface raised it
   * enough to cost 0.05 of contrast. */
  --plane: #0b0a09;
  --surface: #121010;
  --surface-2: #1c1917;
  --border: rgba(255, 255, 255, 0.07);

  /* Glass. A translucent fill plus a backdrop blur, so what scrolls underneath
     shows through as colour and motion rather than being hidden by a slab.
     The fill has to be translucent or the blur has nothing to reveal, and it
     has to be dark enough that white text still clears 4.5:1 over whatever
     passes behind it. Measured against the brightest surface in the app
     (--surface-2 at #141414), white on this composite is 15.9:1. */
  --glass: rgba(10, 10, 10, 0.62);
  --glass-strong: rgba(10, 10, 10, 0.82);
  --glass-edge: rgba(255, 255, 255, 0.10);
  --glass-blur: 20px;
  --glass-sat: 180%;

  /* Grain. Opacity, not a colour: the texture itself is one SVG turbulence
     tile reused by both themes, and only its strength changes. Dark needs more
     because film grain reads as light specks on a dark field. */
  --grain-opacity: 0.035;
  --raised: rgba(255, 255, 255, 0.03);
  --border-strong: rgba(255, 255, 255, 0.13);

  --ink: #f5f1ec;
  /* Warm grey replaced with a neutral one. The old #c3c2b7 is a beige, which
     against a pure black plane reads as slightly dirty. */
  /* Cooled to match, which also lifts it from 9.75 to 10.66 on a panel. */
  --ink-2: #c2c6cb;
  /* Lifted and cooled. At #918879 this measured 5.23:1 on the panel surface:
     over the AA floor, but AA is a floor and this is 11.5px type carrying the
     timezone note and the session description. It also went muddy when the
     Asset restyle cooled every surface beneath it and left the ink warm.
     #9fa3a8 measures 7.83 on the plane and 7.22 on a panel, which clears AAA
     on both, and matches the surfaces it sits on. */
  --ink-muted: #9fa3a8;
  /* Pulse's width, and the space the page reserves for it. One variable because
     there were three numbers: the panel was 382px, main reserved 420px and the
     footer reserved 400px, so the page held open a 38px strip of nothing beside
     an already-narrow panel. Draggable at runtime; see the grip in app.js. */
  --chat-w: 520px;
  /* The hover wash. Three rules reached for var(--bg-hover, rgba(...)) and it
     was never declared, so every one of them took its fallback and the hover
     state did not follow the theme. Declared from the surface stack instead of
     a flat grey, which is what those fallbacks were approximating. */
  --bg-hover: rgba(255, 255, 255, 0.055);
  /* One backdrop for every modal, and one ink for every shadow.
     The backdrops were rgba(0,0,0,0.5) twice and 0.6 once for the same job, and
     the shadows used twelve different alphas of black between them. Naming both
     is what stops the next one being a thirteenth. */
  --scrim: rgba(0, 0, 0, 0.55);
  --shadow-ink: rgba(0, 0, 0, 0.25);
  /* Three elevations, and they exist because eleven `box-shadow`s in this file
   * hardcoded `rgba(0, 0, 0, ...)` at alphas from 0.25 to 0.7. In the dark
   * theme that is roughly right by accident; in the light theme it is a black
   * smear under a cream card, which is why elevated surfaces looked heavier
   * there than anywhere else in the app. --shadow-ink and --scroll-shade were
   * already themed and were already in use two shadows out of thirteen.
   *
   * Three steps because the file only ever wanted three: something resting
   * slightly above the page (a hovered card, a sticky bar), something floating
   * over it (a menu, a popover), and something covering it (the palette, a
   * modal, the Pulse panel). The near-duplicates each step replaces are listed
   * beside it. */
  --elev-1: 0 1px 2px var(--shadow-ink), 0 2px 8px -2px var(--shadow-ink);
  --elev-2: 0 8px 24px -6px var(--shadow-cast), 0 2px 6px -2px var(--shadow-ink);
  --elev-3: 0 24px 60px -16px var(--shadow-cast), 0 4px 12px -4px var(--shadow-ink);
  /* The cast is deeper than the contact shadow and carries the theme's own
   * black rather than a literal one. */
  --shadow-cast: rgba(0, 0, 0, 0.5);
  /* One colour for "the keyboard is here".
   *
   * Fifty-two focus rings, forty-eight on --s1 and four on --brand, which is
   * the logo's gold and reads as an accent rather than as a state. A reader
   * tabbing through Settings saw the ring change colour twice on the way
   * down. Keyed to a name rather than to --s1 directly so the decision can be
   * revisited in one place. */
  --focus: var(--s1);

  /* The accent. Green for up, red for down, applied to numbers and to the one
     control per view that actually does something. */
  --accent: #00c805;
  --accent-ink: #001a00;
  /* The filled button. Its own token rather than --s1, because --s1 also
     colours chart lines, where nothing sits on top of it and the contrast rule
     does not apply. Measured: white on --s1 (#3987e5) is 3.64:1, under the
     4.5:1 normal text needs — the original blue button had that flaw. This is
     the same hue (213 deg) walked dark enough to reach 4.72:1 on white. */
  /* Amber. Was #1c72d8.
   *
   * Measured rather than picked: white on it is 4.69:1 against the blue's
   * 4.72, and it sits 4.22:1 against the dark plane against the blue's 4.19,
   * so neither the label nor the button's own edge lost anything.
   *
   * The hue is the part that took the work. Every warmer amber tested sits
   * 6 to 30 degrees from --neg, the red every down move on this page is drawn
   * in, and in light mode a burnt orange lands 6 degrees away: the primary
   * action button would have been the same colour as a loss. This one is 42
   * degrees off in dark and 27 in light, which is the most separation an amber
   * can have while still carrying white text. The blue it replaces had 147. */
  /* ---- Asset restyle ---------------------------------------------------
     Measured off https://asset.framer.ai at 1440. Colour literals live in a
     token block because every literal in this stylesheet does; the rules that
     use them are in one block at the foot of the file, which is the whole of
     this change and the whole of its undo. */
  --asset-plane: #080a09;
  --asset-surface: #131517;
  --asset-raised: #232326;
  --asset-brand: #d4b144;
  --asset-brand-ink: #1a1400;
  --btn-primary: #9a6b00;
  --btn-primary-ink: #ffffff;
  /* Text on any saturated fill, not just the primary button. Five places set
     `color: #fff` over --btn-primary or --neg, which meant a theme could change
     the ink on the button and miss the badges: --btn-primary-ink is the right
     name for one of those and the wrong name for a count sitting on red. */
  --ink-on-solid: #ffffff;
  /* The plate behind a company logo, and the one colour in this file that is
     deliberately identical in both themes. Most brand marks ship on white, so
     a white plate stops a dark logo vanishing into a dark panel. A token rather
     than a literal so it reads as a decision: as a bare #ffffff in the middle
     of a rule it looks exactly like a token somebody forgot. */
  --logo-plate: #ffffff;
  /* The mark follows the accent, so the app has one of them rather than two.
     Its own token and not --s1: that is a chart series colour, chosen for
     separation from the other seven, and repainting the brand must not repaint
     every line on every chart. */
  --brand: var(--btn-primary);
  --grid: #2c2c2a;
  --baseline: #383835;

  /* categorical slots, dark steps, fixed order */
  --s1: #3987e5;
  --s2: #d95926;
  --s3: #199e70;
  --s4: #c98500;
  --s5: #d55181;
  /* Was #008300, which measured 3.83:1 as heading text on --surface and 3.54:1
     on --surface-2 — below the 4.5:1 body-text threshold on every surface in
     the app, including the plane. It reaches the Read card's heading through
     --accent, which is where it was caught.
     
     Lighter, and shifted towards lime rather than simply brightened: pure green
     at a readable lightness lands on top of --pos (#0ca30c), and these are
     supposed to be eight separable categorical hues. 5.79:1 on --surface-2. */
  --s6: #5fa617;
  --s7: #9085e9;
  --s8: #e66767;
  /* The moving averages, in order SMA 20, 50, 200 and EMA 9, 21, 50, and the
     EMA clouds over and under. Blues, violets and orchids only: the one part
     of the wheel no price, candle, stage or level colour uses. Measured in
     CIE76 each is at least 30 from every chart colour, including the session
     dividers' cyan and the support-and-resistance magenta, at least 15 degrees
     of hue from any of them, 4.5:1 or better on --surface, and at least 26
     from every other average. See tests/test_ma_palette.py. */
  --ma-1: #648fd8;
  --ma-2: #bd9efa;
  --ma-3: #c461c9;
  --ma-4: #f4c1f6;
  --ma-5: #6b6ff5;
  --ma-6: #a855f7;
  --cloud-up: #648fd8;
  --cloud-down: #c461c9;

  /* diverging poles + neutral midpoint.
     Up is green here rather than blue. The palette's blue was chosen for
     colour-vision safety against red, and that trade is still available — the
     greens below are the palette's own green steps, not an arbitrary hue. */
  /* Direction. Every "did it go up or down" mark in the app resolves through
     these two, which is the point: --pos and --neg are re-declared per theme, so
     the light theme can use a green dark enough to read on white. Forty rules
     painted direction with --good/--critical instead — fixed across every theme
     by design — so the light theme's own pair was reaching almost nothing, and
     "up" on a white background was #0ca30c at 3.35:1.

     The values are the ones those forty rules were already using, so nothing
     changes in the dark theme; what changes is that the light theme now reaches
     the marks it was always meant to. Deliberately not the accent
     green: #00c805 is the brand and a button, and a wall of it on every number
     is neon rather than legible. */
  /* Contrast is quoted against --surface-2 (#141414), the brightest panel these
     ever sit on, not against the plane. The old comments measured on pure black,
     which no text in this app is ever drawn on: --neg read "4.51:1" and was
     actually 3.96:1 where it appears. */
  --pos: #0ca30c;           /* on --surface-2: 5.49:1 */
  --neg: #d95656;           /* on --surface-2: 4.69:1, was #d43c3c at 3.96:1 */
  --mid: #383835;

  /* Status — "is something wrong", not "which way did it go". Reserved, never
     used as a series, and deliberately fixed across themes: a warning is the
     same warning on any background. Direction lives in --pos/--neg above. */
  --good: #0ca30c;
  --warn: #fab219;          /* on --surface-2: 10.04:1 */
  --serious: #ec835a;
  --critical: #d03b3b;

  /* Weinstein stages, which colour the price mark itself while Stages is on.
     Three are the terminal's own: an advance is --pos and a decline --neg, as
     every up and down figure here is, and a top is --warn because it is a
     caution rather than a direction. A base is a lighter green, --pos 40% into
     --ink, because it is on its way to an advance without being one; the
     reference chart this was built after draws it the same way. 10.81:1 on
     --surface, and 1.91:1 in luminance from --pos, so the two greens read as
     two marks rather than one. The light theme sets its own Stage 1; the
     other three follow its --pos, --warn and --neg through the references. */
  --stage-1: #98d292;
  --stage-2: var(--pos);
  --stage-3: var(--warn);
  --stage-4: var(--neg);

  /* Reference-line colours, deliberately outside the eight categorical slots —
     a level is annotation, not a series. Validated against every colour that can
     share a price chart with them: magenta for support/resistance, and a neutral
     for the Fibonacci grid. The neutral is the important part — all eight hue
     slots were already spoken for, and a ninth and tenth hue could not clear the
     colour-vision floors (violet against the blue close line measured ΔE 4.7
     under deuteranopia). Grey has no hue to confuse, so it survives every
     simulation, and ratios are conventionally drawn neutral anyway. */
  --ref-sr: #eb17bd;
  /* Period dividers. Cyan is the one hue nothing else on a price chart uses,
     which is the whole point: these were drawn in --baseline, the axis colour,
     so a time boundary looked like chart furniture. 7.87:1 on the plane. */
  --ref-session: #5ab7d1;
  --ref-fib: #c3c2b7;

  /* Tint for the scroll shadows below. Needs to darken on either surface, so it
     is declared per theme rather than derived from --ink. */
  --scroll-shade: rgba(0, 0, 0, 0.32);

  /* Switzer, then Inter, then the system stack.
   *
   * Chosen to sit in the same genre as the products this is measured against.
   * Checked directly rather than from articles: Robinhood's "Capsule Sans" is a
   * custom cut of Maison Neue, Kalshi ships a proprietary "Kalshi Sans" with
   * Graphik for display, and Polymarket serves plain Inter. All four are
   * neo-grotesques: neutral shapes, high x-height, tight apertures, real
   * tabular figures. Switzer is the closest freely licensed stand-in for Maison
   * Neue, so it lands nearest Robinhood of the free options.
   *
   * Corrects an earlier note here. It claimed Capsule was a reverse-stress,
   * essentially all-caps display face and therefore wrong for dense tables.
   * That described a different typeface of the same name, by Dave Rowland.
   * Robinhood's is a UI face and the objection did not apply.
   *
   * Geist was here before this and is Vercel's: more geometric and technical,
   * which reads as a developer tool rather than a consumer trading product.
   *
   * Inter stays behind Switzer, loaded from a different CDN, so a Fontshare
   * outage degrades to a chosen face rather than to Times New Roman. */
  /* Two faces, on purpose.
   *
   * --display is Space Grotesk: retro-futuristic, wide, with the flat-sided
   * bowls and the squared terminals of 1970s technical lettering. It carries
   * the headings and the wordmark, where character is the job.
   *
   * --sans stays Plus Jakarta Sans and keeps every number. Space Grotesk's
   * figures are distinctive enough to be a liability in a dense table: the
   * point of a price column is that no digit draws attention to itself. A
   * display face in the headings and a quiet one in the data is the ordinary
   * way to have both. */
  --display: "Space Grotesk Variable", "Space Grotesk", "Plus Jakarta Sans Variable",
             system-ui, sans-serif;
  --sans: "Plus Jakarta Sans Variable", "Plus Jakarta Sans", "Inter Variable",
          "Inter", system-ui, -apple-system, "Segoe UI", sans-serif;

  /* ---------------------------------------------------------------- scale
   *
   * Before this there were 21 distinct font sizes in here and 10 more inline in
   * app.js — 9.5, 10, 10.5, 11, 11.5, 12, 12.5, 13, 13.5, 14, 14.5, 15, 15.5,
   * 17, 21, 22, 31, 36, 42, 52. At that point size stops carrying meaning: two
   * things a half-pixel apart read as "different for no reason", and the eye
   * can't infer hierarchy from it. Seven steps plus three display sizes, so a
   * jump in size always means a jump in importance.
   */
  /* Spacing scale.
   *
   * There was a type scale and no equivalent for space, and it showed: 18
   * distinct paddings, 21 margins and 20 gaps rendered on a single page, drawn
   * from essentially every integer between 1 and 18. None was wrong on its own
   * — the problem is that 7px, 8px and 9px all appeared and none of them meant
   * anything, so the eye never found a rhythm and the page read as hand-placed
   * rather than designed.
   *
   * A 4pt grid. Nine steps rather than the tidier seven, because 20 and 40 are
   * both load-bearing here (panel padding is 20) and a scale that forces them
   * to 24 and 48 changes the look of the app to buy a rounder table of numbers
   * — which is the tail wagging the dog.
   *
   * --space-0 is the one sub-grid step. 2px is a real optical nudge — a caption
   * riding under a figure, a chip's inner lift — and rounding it to 4 doubles it.
   */
  /* ONE KNOB FOR THE WHOLE UI.
   *
   * Everything below is a multiple of this, so the spacing grid and the type
   * scale move together and their relationship to each other never changes.
   * That relationship is the design; the absolute size is a viewing preference,
   * and the two were tangled together because both were written as literal
   * pixels.
   *
   * 1.15 because 13px table body and 15px prose were measured against a modern
   * laptop and read as a small page inside a large window rather than as a
   * dense one. Most of this app's surface is 13px, which is where the web sat
   * when 1280 was a big screen.
   *
   * The 4px grid survives as a 4.6px grid: still one rhythm, still nine steps,
   * still every gap a multiple of one unit. Rounding each step back to a whole
   * pixel is what would break it, because the rounding error differs per step
   * and the rhythm is the ratio between them, not the numbers themselves. */
  --ui-scale: 1.15;

  --space-0: calc(2px * var(--ui-scale));
  --space-1: calc(4px * var(--ui-scale));
  --space-2: calc(8px * var(--ui-scale));
  --space-3: calc(12px * var(--ui-scale));
  --space-4: calc(16px * var(--ui-scale));
  --space-5: calc(20px * var(--ui-scale));
  --space-6: calc(24px * var(--ui-scale));
  --space-7: calc(32px * var(--ui-scale));
  --space-8: calc(40px * var(--ui-scale));
  --space-9: calc(48px * var(--ui-scale));

  /* Seven distinct sizes, down from ten.
   *
   * The old scale packed six values into a 4px band — 10.5, 11.5, 12.5, 13, 14,
   * 15.5. At those distances size stops carrying meaning: nobody perceives 12.5
   * against 13 as hierarchy, they perceive it as inconsistency, and the page
   * pays the cost of the variation without getting the signal.
   *
   * Three pairs have therefore collapsed onto shared values. The names are kept
   * because they say what the text is for, and renaming them would mean editing
   * 104 call sites to change nothing about the render — but --t-micro and
   * --t-caption are now one size, as are --t-small/--t-body and
   * --t-base/--t-lead. Do not re-split them without a reason a reader could see.
   *
   * --t-micro and --t-caption are the one deliberate 1px gap. Everything at
   * micro is uppercase, everything at caption is not, and uppercase reads
   * noticeably larger at the same nominal size — no descenders, and the cap
   * height IS the size. 10px uppercase and 11px lowercase land at the same
   * apparent size, which is the point; setting both to 11 made every label
   * shout over its own caption.
   *
   * The base moved 14 -> 15. Inter sets on a larger x-height than SF Pro but
   * slightly narrower, so 15px Inter reads at about the optical size of the 14px
   * system font it replaces; holding 14 would have made the app quietly smaller
   * than it was. */
  --t-micro: calc(10px * var(--ui-scale));   /* uppercase micro-labels, legends, chart ticks */
  --t-caption: calc(11px * var(--ui-scale)); /* captions, caveats, secondary notes */
  --t-small: calc(13px * var(--ui-scale));   /* dense table cells */
  --t-body: calc(13px * var(--ui-scale));    /* table body, key-value readings */
  --t-base: calc(15px * var(--ui-scale));    /* prose default */
  --t-lead: calc(15px * var(--ui-scale));    /* navigation, emphasised UI */
  --t-heading: calc(18px * var(--ui-scale)); /* sub-headings */
  /* The top of the heading ramp, and the two tiers that were missing from it.
   *
   * Page titles were a literal `32px` and the home display line a literal
   * `clamp(34px, 3.4vw, 48px)`, which made them the only type in the product
   * that --ui-scale could not reach. Measured on the home page at 1440px
   * across the three settings: body went 11 -> 12.65 -> 14.3 while the page
   * title stayed at 32 and the hero at 48, so the title-to-body ratio ran
   * from 2.9x down to 2.24x. The whole point of that control, in its own
   * words, is to change "the absolute size of everything and none of the
   * relationships between any two things".
   *
   * 28 and 42 are chosen to land on what those literals already rendered at
   * the default scale -- 32.2px and 48.3px -- so this changes how the sizes
   * respond, not what they are. The d-tiers below are for figures, which is
   * why a heading could not simply borrow one: --t-d3 would have taken the
   * page title from 32px to 27.6px. */
  --t-title: calc(28px * var(--ui-scale));   /* page and section titles */
  --t-hero: calc(42px * var(--ui-scale));    /* the home page's one display line */
  --t-d3: calc(24px * var(--ui-scale));      /* tile values */
  --t-d2: calc(32px * var(--ui-scale));      /* secondary figures */
  --t-d1: calc(44px * var(--ui-scale));      /* the one number a panel is about */

  /* Radii, likewise collapsed from eight values to five. Panels sit at 12px:
     softer than the old 8px, which is what makes a dense dashboard read as
     calm rather than boxed-in. */
  /* Two motion bands, not one. --dur-ui at 120ms is right for a hover, where a
     colour change wants to feel immediate. It is too fast for something that
     opens: at 120ms a menu reads as a jump rather than a movement, so menus,
     panels and inline explanations get their own.
     Measured against the spec this was built to, which asks for hover at
     120-180ms and transitions at 180-240ms. One value was serving both. */
  --dur-panel: 200ms;

  /* Six steps, because six is what the app actually draws.
   *
   * Measured before this: 75 hardcoded radii against five tokens, and the two
   * most-drawn surfaces in the product were both bypassing the scale. `.panel`
   * rendered at 14px while --r-lg, whose own comment says "panels", was 16px
   * and used by nothing that is a panel. `.btn` rendered at 10px against a
   * --r-md of 8px. Everything that wanted "rounder than a button, less round
   * than a card" -- the command palette, the section index, the home skeleton
   * -- had no token to reach for and invented 10px, 11px, 12px or 14px.
   *
   * So the two missing steps are named rather than the drawn values being
   * moved onto the nearest existing token. Adopting them changed no pixels.
   *
   * --r-lg is the one exception and the one judgement call: it moved 16px ->
   * 14px so that the card token matches the card. Seventeen secondary surfaces
   * (the home search field, chat bubbles, several callouts) lose 2px and land
   * on the same radius as the panels they sit beside. Moving `.panel` the
   * other way would have changed every panel in the app instead of seventeen
   * pieces of chrome; the canonical surface wins. One number to reverse it.
   *
   * 1px, 2px and 3px stay bare. They are bar fills and legend keys 3-8px tall,
   * where the radius is a function of the element's own height rather than a
   * step on a scale -- see the exceptions block at the foot of this file. */
  --r-xs: 4px;          /* inline marks, bars, strip segments */
  --r-sm: 8px;          /* inputs, small controls */
  --r-ctl: 10px;        /* buttons, selects, text inputs */
  --r-chrome: 12px;     /* sticky bars, the palette, skeletons */
  --r-md: 8px;          /* buttons, selects, callouts — merged with --r-sm,
                           which sat two pixels away and read as a mistake */
  --r-lg: 14px;         /* panels, cards, tiles */
  --r-pill: 999px;
  /* Nothing else may hold a bare pixel radius. The exceptions are 1px, 2px and
   * 3px on bar fills, legend keys, skeleton bars and dots -- elements 3 to 8
   * pixels tall, where the radius is a proportion of the element's own height
   * rather than a step on this scale, and rounding them to --r-xs (4px) would
   * turn a 3px bar into a lozenge. tests/test_radius_scale.py holds the line:
   * any new bare radius of 4px or more fails, and the 1-3px set is allowed by
   * value rather than by an ever-growing list of selectors. */

  /* Spacing rhythm. Everything is a multiple of 4 so vertical alignment falls
     out for free instead of being nudged per-panel. */

}

/* Light theme. Selected by the data-theme attribute rather than a media query:
   the inline script in index.html stamps it before first paint from the stored
   choice or, absent one, from the OS setting. That keeps one light block here
   instead of the same values in both a media query and a toggle scope. */
:root[data-theme="light"] {
  /* Pure white, and the same near-flat stack as the dark theme. */
  --plane: #fffdfb;
  --surface: #fffdfb;
  --surface-2: #f7f3ee;
  --border: rgba(11, 11, 11, 0.08);

  /* Light theme glass. Lighter fill, and less grain: on a white field the same
     texture reads as dirt rather than as film. */
  --glass: rgba(255, 255, 255, 0.68);
  --glass-strong: rgba(255, 255, 255, 0.86);
  --glass-edge: rgba(11, 11, 11, 0.08);
  --glass-blur: 20px;
  --glass-sat: 180%;
  --grain-opacity: 0.022;
  --raised: rgba(11, 11, 11, 0.02);
  --border-strong: rgba(11, 11, 11, 0.14);

  --ink: #14110e;
  --ink-2: #4a4238;
  --ink-muted: #6e6558;   /* on --surface-2: 4.55:1, was #7b7b7b at 3.95:1 */
  --bg-hover: rgba(11, 11, 11, 0.05);
  --scrim: rgba(11, 11, 11, 0.38);
  --shadow-ink: rgba(11, 11, 11, 0.18);
  /* Lighter and warmer: the same geometry over a cream plane needs far less
   * alpha or it reads as dirt rather than depth. */
  --shadow-cast: rgba(11, 11, 11, 0.16);
  /* Darker green on white. #00c805 measures 1.8:1 against white and fails any
     text use, so the light theme takes a step down.
     
     The step used to be #00a504 with a comment claiming it cleared 4.5:1. It
     does not — measured, it is 3.29:1, and white on it as a button label is the
     same 3.29:1. #e03600 was 4.47:1, near enough to look right and still short.
     These are the nearest values on the same hues that actually clear the
     threshold, so the claim and the colour now agree. */
  --accent: #008903;        /* white on this: 4.59:1 */
  --accent-ink: #ffffff;
  /* Unchanged between themes on purpose: the button paints its own fill, so its
     contrast does not depend on the page behind it. */
  /* Amber. Was #1c72d8.
   *
   * Measured rather than picked: white on it is 4.69:1 against the blue's
   * 4.72, and it sits 4.22:1 against the dark plane against the blue's 4.19,
   * so neither the label nor the button's own edge lost anything.
   *
   * The hue is the part that took the work. Every warmer amber tested sits
   * 6 to 30 degrees from --neg, the red every down move on this page is drawn
   * in, and in light mode a burnt orange lands 6 degrees away: the primary
   * action button would have been the same colour as a loss. This one is 42
   * degrees off in dark and 27 in light, which is the most separation an amber
   * can have while still carrying white text. The blue it replaces had 147. */
  --btn-primary: #9a6b00;
  --btn-primary-ink: #ffffff;
  /* Text on any saturated fill, not just the primary button. Five places set
     `color: #fff` over --btn-primary or --neg, which meant a theme could change
     the ink on the button and miss the badges: --btn-primary-ink is the right
     name for one of those and the wrong name for a count sitting on red. */
  --ink-on-solid: #ffffff;
  /* The plate behind a company logo, and the one colour in this file that is
     deliberately identical in both themes. Most brand marks ship on white, so
     a white plate stops a dark logo vanishing into a dark panel. A token rather
     than a literal so it reads as a decision: as a bare #ffffff in the middle
     of a rule it looks exactly like a token somebody forgot. */
  --logo-plate: #ffffff;
  /* The mark follows the accent, so the app has one of them rather than two.
     Its own token and not --s1: that is a chart series colour, chosen for
     separation from the other seven, and repainting the brand must not repaint
     every line on every chart. */
  --brand: var(--btn-primary);
  --grid: #e1e0d9;
  --baseline: #c3c2b7;

  /* Same correction as the dark theme: measured on --surface-2 (#f6f7f8),
     not on white. Both were failing where they are actually drawn. */
  --pos: #007a03;           /* on --surface-2: 5.17:1, was #008903 at 4.28:1 */
  --neg: #d13200;           /* on --surface-2: 4.70:1, was #de3500 at 4.24:1 */

  /* The palette's light column: same eight hues, stepped for a light surface. */
  --s1: #2771ca;          /* on --surface-2: 4.55:1, was #2a78d6 at 4.12:1 */
  /* Light theme needs its own amber. It had none, so it inherited the dark
     theme's #fab219 and drew yellow text on white at 1.71:1, the worst
     contrast in the app. A dark amber keeps the "caution" reading without
     relying on a hue that cannot carry text on a light ground. */
  --warn: #956703;          /* on --surface-2: 4.64:1 */
  /* Stage 1 is --pos 70% into --surface here. Any lighter fails the 3:1 a
     candle needs to read as a shape on white: 60% measured 2.65:1. This is
     3.17:1 on --surface and 1.72:1 in luminance from --pos. */
  --stage-1: #4da14d;
  --s2: #eb6834;
  --s3: #1baf7a;
  --s4: #eda100;
  --s5: #e87ba4;
  /* Same hue shift on the light column, for the same reason plus its own: on
     --surface-2 (#f7f3ee) the old value measured 4.48:1, which misses by two
     hundredths. 4.82:1 now. */
  --s6: #357a0f;
  --s7: #4a3aa7;
  --s8: #e34948;
  /* The averages' light column: the same families, and the same clearances,
     at 4.5:1 or better on the light surface. */
  --ma-1: #29558e;
  --ma-2: #2f148a;
  --ma-3: #c124e0;
  --ma-4: #5d2b73;
  --ma-5: #2456e0;
  --ma-6: #461dcd;
  --cloud-up: #29558e;
  --cloud-down: #c124e0;

  /* --pos and --neg are NOT redeclared here.
   *
   * They used to be, twice: the accessible green and red above, and then this
   * blue-and-red pair copied down with the categorical palette's light column —
   * note that it repeats --s1 and --s8 exactly. The later pair won, so "up" in
   * the light theme has been rendering blue while the dark theme renders green,
   * and the green declared for the light surface was dead the moment it was
   * written. Nobody noticed because only one strip on the Read tab resolved
   * through --pos at the time; now that every directional mark does, it would
   * have turned most of the light theme blue.
   *
   * There is no colour-vision-safe theme to justify it either — the settings
   * offer system, light and dark, and nothing else. */
  --mid: #f0efec;

  /* Status colours are fixed across themes by design — a warning is the same
     warning either way — so they are not re-declared here. On a light surface
     warning and serious fall below 3:1, which is why every status in this app
     ships with a word beside it and never relies on the colour alone. */

  --ref-sr: #a10591;
  --ref-session: #17758a;        /* 5.24:1 on the light plane */
  --ref-fib: #52514e;
  --scroll-shade: rgba(11, 11, 11, 0.14);
}

* { box-sizing: border-box; }
*::-webkit-scrollbar { width: 10px; height: 10px; }
*::-webkit-scrollbar-track { background: transparent; }
*::-webkit-scrollbar-thumb { background: var(--border-strong); border-radius: var(--r-md); border: 2px solid var(--surface); }
*::-webkit-scrollbar-thumb:hover { background: var(--ink-muted); }
* { scrollbar-width: thin; scrollbar-color: var(--border-strong) transparent; }

:focus-visible {
  outline: 2px solid var(--focus);
  outline-offset: 1px;
  border-radius: var(--r-sm);
}

html, body {
  margin: 0;
  padding: 0;
  background: var(--plane);
  color: var(--ink);
  font-family: var(--sans);
  font-size: var(--t-base);
  line-height: 1.5;
  -webkit-font-smoothing: antialiased;
  /* Inter ships contextual alternates on by default, which turns some digit and
     punctuation pairs into ligatures that break a column of figures. Off here,
     on nowhere. */
  font-variant-ligatures: none;

  /* Smooth scrolling for jumps, not for the wheel.
   *
   * This covers anchor navigation and every scrollIntoView in the app: the
   * glossary's "learn more", the chart scrolling a panel into view, the jump
   * back to a table after a filter. Those are the places where an instant jump
   * loses the reader's position.
   *
   * Deliberately NOT a scroll-hijacking library. Locomotive and its kind
   * replace native scrolling with a transform on a wrapper, which costs a frame
   * of latency on every wheel event, breaks position: sticky (the header above
   * took a fix today to work at all), and disconnects the scrollbar from the
   * document. On a terminal whose chart must answer a wheel immediately and
   * whose chrome is sticky by design, that trade is the wrong way round. The
   * "premium" part of that effect is content revealing as it arrives, which is
   * the IntersectionObserver stagger in revealPanels. */
  scroll-behavior: smooth;

  /* No overflow property on the root. See the note on `main` below.
   *
   * This carried overflow-x: hidden, which forces the other axis to compute to
   * `auto` and turns body into a scroll container. A position: sticky child
   * then sticks to that container rather than the viewport and never pins, so
   * header.topbar scrolled away despite being sticky: measured at scrollY 600,
   * the bar sat at -600.
   *
   * Switching it to `clip` fixed the header and broke IntersectionObserver.
   * overflow: clip ON THE ROOT ELEMENT degenerates the implicit root that
   * IntersectionObserver measures against, so no observer fired anywhere in the
   * app: measured, a plain observer on a panel at top 456 in a 971px viewport
   * reported nothing. That took the panel reveals with it.
   *
   * Both properties want the root left alone, so the clipping moved to main. */
}

button, input, select, textarea { font: inherit; color: inherit; }

/* ------------------------------------------------------------------ chrome */

header.topbar {
  /* Promoted to its own compositing layer: a sticky bar over a long scrolling
     document leaves stale paint artifacts in some embedded renderers otherwise. */
  position: sticky;
  top: 0;
  z-index: 50;
  transform: translateZ(0);
  backface-visibility: hidden;
  display: flex;
  align-items: center;
  /* space-2 rather than space-4 between the blocks. Six gaps at 18.4px spent
     110px of the row on separation alone, and the strip beside them was folding
     for want of rather less than that. */
  gap: var(--space-2);
  padding: var(--space-2) var(--space-4);
  background: var(--plane);
  border-bottom: 1px solid var(--border);
  /* Single row. With eight tabs plus the search box and the assistant button,
     wrapping used to drop Pulse onto its own line and break tab labels across
     two — the tab strip scrolls instead. */
  flex-wrap: nowrap;
}

.brand {
  display: flex;
  align-items: center;
  gap: var(--space-2);
  flex: none;
  font-weight: 600;
  letter-spacing: 0.02em;
  white-space: nowrap;
  /* Reset the button chrome — it needs button *semantics*, not button looks. */
  padding: var(--space-2) var(--space-2);
  margin-left: -8px;
  background: none;
  border: 1px solid transparent;
  border-radius: var(--r-md);
  color: inherit;
  font-family: inherit;
  font-size: inherit;
  cursor: pointer;
  transition: background-color 0.12s ease, border-color 0.12s ease;
}
/* Hover is the only affordance that it's clickable at all — without it a logo
   reads as decoration and nobody tries. */
.brand:hover { background: var(--surface); border-color: var(--border); }
.brand:hover .brand-mark { color: var(--ink); }
/* 28px, up from 22. At 22 the mark was the smallest thing in the bar and read
   as an icon next to the wordmark rather than as the logo; the row has the
   height for more, since the buttons beside it stand 40px tall. */
/* The mark alone, at 34px.
 *
 * "OPTIC TERMINAL" beside it was about 115px of the top row restating what the
 * mark and the browser tab already say, on every render and at every width.
 * The button keeps `aria-label="Optic Terminal. Go to home"`, so the name is
 * still there for a screen reader and only the duplicate pixels are gone.
 *
 * Resolution is not a consideration: the mark is an inline SVG on a 32-unit
 * viewBox, so it is resampled by the renderer at whatever size and pixel ratio
 * it lands on. Growing it costs nothing and loses nothing. */
.brand-mark { width: 34px; height: 34px; flex: none; color: var(--ink-2); }
.brand-text { display: none; }

.search {
  display: flex;
  align-items: center;
  gap: var(--space-2);
  flex: none;
}
/* Pinned to the right edge so the scrolling tab strip can't displace it,
   and sized to match the enlarged tabs beside it. */
#chat-toggle {
  flex: none;
  margin-left: var(--space-2);
  padding: var(--space-2) var(--space-3);
  font-size: var(--t-lead);
  font-weight: 500;
  border-radius: var(--r-md);
}
.search input {
  /* Width and type size are one decision here, because the thing being fitted is
     a fixed string in a tracked uppercase face.
     "SEARCH OR ASK  ⌘K" renders 186px at the inherited 17.25px, and 30px of
     padding and border puts the box at 216px to show it. So 210 never did: it
     clipped the K, and 195 — picked off a measurement taken before
     `text-transform` was applied, which is the mistake — clipped 21px and lost
     the shortcut hint entirely.
     Dropping to the body size needs 191px instead of 216, which is what lets
     this box both hold its own placeholder and leave the row enough width to
     stay on one line. A search field is the one control here that gains nothing
     from being the largest type in the bar.
     Re-measure if the placeholder or the face changes, and measure it by
     putting the placeholder in as a *value* and comparing scrollWidth to
     clientWidth. Measuring a probe span is what produced both wrong numbers
     here: once by forgetting `text-transform`, once by trusting the `font`
     shorthand. The real minimum at this size is 191px; 200 leaves nine. */
  width: 200px;
  font-size: var(--t-body);
  padding: var(--space-2) var(--space-3);
  background: var(--surface);
  border: 1px solid var(--border-strong);
  border-radius: var(--r-md);
  text-transform: uppercase;
  letter-spacing: 0.06em;
  font-variant-numeric: tabular-nums;
  transition: border-color var(--dur-ui) ease, box-shadow var(--dur-ui) ease;
}
/* The outline is the accessible focus ring and stays. The border and the faint
   wash under it are the premium half: the product's primary entry point reads
   as live while it holds the caret, in the brand accent the rail and palette
   already use, rather than only gaining a system outline. */
.search input:focus {
  outline: 2px solid var(--focus);
  outline-offset: 1px;
  border-color: color-mix(in srgb, var(--btn-primary) 55%, var(--border-strong));
  box-shadow: 0 0 0 3px color-mix(in srgb, var(--btn-primary) 16%, transparent);
}

.btn {
  padding: var(--space-2) var(--space-3);
  background: var(--surface);
  border: 1px solid var(--border-strong);
  border-radius: var(--r-md);
  cursor: pointer;
  white-space: nowrap;
  transition: background-color 0.12s ease, border-color 0.12s ease, filter 0.12s ease;
}
.btn:hover { background: var(--surface-2); }
.btn:active { filter: brightness(0.94); }
/* .btn.primary is defined once, further down with the other button rules.
   It used to be declared here too, as var(--s1) with white text, and again
   later as var(--accent) — same selector, same specificity, so the later one
   won and this one was dead. That is why the buttons were green while the
   stylesheet appeared to say blue in two places. */
.btn[disabled] { opacity: 0.45; cursor: not-allowed; }

/* Pulse's mark, and its face.
 *
 * Two drawings, and the split is measured: at 16 and 20px the mascot's eyes
 * stop resolving and the whole thing turns to mush, while the circle and the
 * waveform stay legible all the way down. So the face is used where there is
 * room and the reduced mark everywhere else.
 *
 * Both are `currentColor` throughout, which is what lets the same drawing sit
 * on the primary button in white and in the panel header in ink. The accent
 * belongs to the trace only where there is a surface behind it to contrast
 * against; on the button there is not, and a two-tone glyph on a brand fill
 * reads as a rendering fault. */
.pulse-mark { display: inline-flex; align-items: center; }
.pulse-glyph { display: block; }
.pulse-glyph-sm { width: 17px; height: 17px; }
/* The empty state's anchor is the Optic aperture, not the Pulse waveform.
   The waveform is drawn for 17px and reads thin at 76; the aperture is the
   mark the product is named for and is already the header's and the home
   page's. Same drawing, third placement -- `opticMarkHTML` renders all three.

   It does not blink or iris open here. Those belong to arrival: the header
   mark blinks everywhere except Home so it has something to do, and the home
   logo opens once on load. This sits above four cards a reader is reading,
   and motion beside text you are reading is motion nobody asked for. */
.pulse-logo { width: 76px; height: 76px; color: var(--ink-2); }

/* The trace redraws itself, the same idiom and the same timing as the brand
   mark's chart line -- `pathLength="100"` normalises the dash maths so the
   keyframes do not have to know the path's real length. It is the only thing
   that moves: a mark that scales in the corner of a button is motion the
   reader did not ask for, which is what the bullet here used to do. */
.pulse-trace {
  stroke-dasharray: 100;
  animation: pulse-trace-draw 5.5s ease-in-out infinite;
}
@keyframes pulse-trace-draw {
  0%, 62%, 100% { stroke-dashoffset: 0; }
  12% { stroke-dashoffset: 100; }
  38% { stroke-dashoffset: 0; }
}

/* The figure, in the panel's empty state.
 *
 * A height and no width: the viewBox is 32x38 because he stands up, and
 * setting both to the same number squashes him. */
/* 104, not 92: the aperture in each eye is 5.4 units on a 32-unit box, so
   it needs the pixels to read as a lens rather than as a circle. */


/* In `.pulse-empty`, which is the intro that already existed. A first draft
   put the face in a block of its own and the panel greeted a reader twice. */

/* `pulse-beat` is still here and the mark no longer uses it.
 *
 * Deleting it with the bullet took the animation off `.ses-dot.p-regular` and
 * off the refreshing indicator app.js sets inline, both of which still name
 * it -- a keyframe that does not exist does not error, the animation simply
 * never runs. Caught by the test that asserts the mark has stopped scaling,
 * which had to be told the keyframe itself is not the thing being removed. */
@keyframes pulse-beat {
  0%, 100% { opacity: 0.55; transform: scale(0.85); }
  50% { opacity: 1; transform: scale(1); }
}

@media (prefers-reduced-motion: reduce) {
  .pulse-trace { animation: none; stroke-dasharray: none; }
  /* The two remaining users of pulse-beat. The old rule here covered only the
     mark, so a reader who asks for less motion still got a pulsing dot. */
  .ses-dot.p-regular { animation: none; }
}

nav.tabs {
  display: flex;
  /* No gap between tabs: each button carries its own horizontal padding, so
     the labels are already 18.4px apart and the gap was only ever separating
     two transparent backgrounds. Worth 14px across seven groups, which is the
     difference between this row fitting a 1440 screen and not. */
  gap: 0;
  margin-left: auto;
  /* min-width:0 lets the strip shrink below its content width so it scrolls
     rather than shoving the assistant button off the row. */
  min-width: 0;
  overflow-x: auto;
  scrollbar-width: none;
}
nav.tabs::-webkit-scrollbar { display: none; }
nav.tabs button {
  /* Sized up from 14px/6-12px once, because at desktop widths the strip read as
     small print next to the panels it navigates. Both halves of that came back
     down when a seventh group arrived and the strip started folding.
     Measured as the width the whole header needs to stay on one line:
     1653px as it stood, 1538 on padding alone, 1451 with the header gap too,
     and 1408 here. A 1440 laptop is the case that matters and only the last
     number clears it.
     The padding went first and further — seven groups at 18.4px a side spent
     258px of the row on empty space inside the buttons, which is the single
     largest thing that was pushing the last group onto a second line. t-body is
     the same size the panel headings use, so the strip is no longer small print
     against them; it is simply no longer larger than them. */
  padding: var(--space-2) var(--space-2);
  font-size: var(--t-lead);
  font-weight: 500;
  flex: none;
  white-space: nowrap;
  background: transparent;
  border: 1px solid transparent;
  border-radius: var(--r-md);
  color: var(--ink-2);
  cursor: pointer;
  transition: background-color 0.12s ease, color 0.12s ease, border-color 0.12s ease;
}
nav.tabs button:hover { color: var(--ink); background: var(--surface); }
nav.tabs button { position: relative; }
/* The selected tab now carries the logo's blue as a 2px underline. The only cue
   before was a grey box, which reads as "hovered" rather than "you are here" — and
   the accent colour from the mark in the corner appeared nowhere in navigation. */
nav.tabs button[aria-selected="true"] {
  color: var(--ink);
  background: var(--surface);
  border-color: var(--border-strong);
}
nav.tabs button[aria-selected="true"]::after {
  content: "";
  position: absolute;
  left: var(--space-4);
  right: var(--space-4);
  bottom: -1px;
  height: 2px;
  border-radius: var(--r-xs) var(--r-xs) 0 0;
  background: var(--s1);
}

/* Sticky under the top bar, so it floats over content and takes glass too.

   No clamp and no disclosure. This was one clipped row with a More/Less
   button, and both are gone -- see setStatus for the measurement. The short
   version: at 1512px every view's strip is one row and 29px either way, so
   dropping the clamp changed no height a reader sees; a clipped row with no
   button left is the one state that hides a reading for good. */
.statusline {
  padding: var(--space-2) var(--space-5);
  font-size: var(--t-small);
  color: var(--ink-muted);
  background: var(--glass);
  border-bottom: 1px solid var(--glass-edge);
  display: flex;
  align-items: flex-start;
  gap: var(--space-3);
}
.sl-parts {
  display: flex;
  gap: var(--space-4);
  flex-wrap: wrap;
  flex: 1 1 auto;
  min-width: 0;
}
@supports ((backdrop-filter: blur(1px)) or (-webkit-backdrop-filter: blur(1px))) {
  .statusline {
    -webkit-backdrop-filter: blur(var(--glass-blur)) saturate(var(--glass-sat));
    backdrop-filter: blur(var(--glass-blur)) saturate(var(--glass-sat));
  }
}
@supports not ((backdrop-filter: blur(1px)) or (-webkit-backdrop-filter: blur(1px))) {
  .statusline { background: var(--plane); }
}

/* No transition on padding: animating a layout property forces full-document
   reflow on every frame and is the usual source of ghosted repaints. */
main { padding: var(--space-6) var(--space-6) var(--space-8); isolation: isolate; }
/* The reserve is a margin set further down, from --chat-w. This rule used to
   set padding-right: 400px and was already overridden there, so the number was
   decoration. */

.view { display: none; }
.view.active { display: block; }

/* ------------------------------------------------------------------ panels */

.grid { display: grid; gap: var(--space-5); }
.grid.c2 {
  grid-template-columns: repeat(auto-fit, minmax(420px, 1fr));
  /* Panels size to their content instead of to the tallest one in the row.
   *
   * Grid defaults to align-items: stretch, which paired the swing verdict — a
   * long panel with the score table and every factor definition — against the
   * quote panel, and stretched the quote's box to match. That left roughly 500px
   * of bordered empty space below "Sector", which reads as a section that failed
   * to load rather than as one that simply has less to say.
   *
   * Only c2 is changed. c3 and c4 hold rows of tiles where equal heights are the
   * point, and letting those size individually makes a ragged row. */
  align-items: start;
}
.grid.c3 { grid-template-columns: repeat(auto-fit, minmax(300px, 1fr)); }
.grid.c4 { grid-template-columns: repeat(auto-fit, minmax(210px, 1fr)); }

/* ------------------------------------------------------------------ panel ---
 *
 * One rule. This was declared FIVE times at top level, and the duplication was
 * not harmless: with equal specificity the last declaration won, which killed
 * three things their authors meant to happen.
 *
 * - The scroll shadows below were dead. A later rule used the `background`
 *   SHORTHAND, which resets every background-* longhand, so background-image,
 *   -repeat, -size, -position and -attachment all reverted to their defaults.
 *   Measured: a panel computed `background-image: none`. `background-color` is
 *   used here for exactly that reason.
 * - `@media (max-width: 640px) { .panel { padding: var(--space-3) } }` was dead
 *   for the same reason, the later rule sitting after it in the file. Measured
 *   at 375px: 23px of padding where 13.8px was intended.
 * - Of two paddings and three border-radii declared, one of each applied and
 *   the rest were decoration. Nobody could predict a panel's box by reading any
 *   single rule.
 *
 * Borders, and the author was right to put them back: most panels here are
 * collapsible, and a collapsed panel with no border is a floating heading with
 * no visible extent, so you cannot see what you are about to open. Contained,
 * not boxed-in.
 *
 * The asymmetric radius is deliberate. Equal corners read as a box; a slightly
 * longer top-left reads as a card that was placed rather than generated. Two
 * pixels, not ten. The curve lives on things you touch and stops at things you
 * read, which is why tiles, cells and candles stay square.
 *
 * Panels scroll their own overflow (wide tables, leg rows) rather than forcing
 * the page wider than the viewport, and the scroll was invisible: the
 * candidate-strikes table is 1035px inside an 804px panel and nothing said so.
 * Two gradients pinned `local` move with the content and vanish at each end;
 * two pinned `scroll` cast a soft edge that shows only while there is more to
 * reach. No measurement, and nothing on panels that do not overflow.
 *
 * Hover outlines the whole card rather than the heading, because the whole card
 * is the target.
 */
.panel {
  background-color: var(--surface);
  border: 1px solid var(--border);
  border-radius: var(--r-lg);   /* Asset restyle */
  /* 20/24 rather than 24/32.
   *
   * Measured at 1280px: 27.6px vertical and 36.8px horizontal inside every
   * card, against 13px table text. On a page carrying twenty panels that is
   * the difference between a research terminal and a marketing site -- the
   * padding was doing more work than most of the content in it, and the
   * horizontal 36.8 was taken twice per panel out of the width tables have to
   * fit their columns into.
   *
   * Not smaller than this: the panel border has to read as a container rather
   * than as a line drawn around text, and --space-5 is where that still
   * holds. */
  padding: var(--space-5) var(--space-6);
  min-width: 0;
  overflow-x: auto;
  background-image:
    linear-gradient(to right, var(--surface) 40%, transparent),
    linear-gradient(to left, var(--surface) 40%, transparent),
    linear-gradient(to right, var(--scroll-shade), transparent),
    linear-gradient(to left, var(--scroll-shade), transparent);
  background-repeat: no-repeat;
  background-size: 34px 100%, 34px 100%, 16px 100%, 16px 100%;
  background-position: 0 0, 100% 0, 0 0, 100% 0;
  background-attachment: local, local, scroll, scroll;
  transition: border-color 140ms ease;
}
.panel.span2 { grid-column: span 2; }
@media (max-width: 900px) { .panel.span2 { grid-column: auto; } }

/* Staggered reveal on a fresh load. The delay is set per-panel in JS, and the
   class is only added on a foreground load — a background refresh tick must not
   re-animate a dashboard the user is already reading. `both` fill mode holds the
   opening state through the delay so nothing flashes in early. */
@keyframes panel-in {
  from { opacity: 0; transform: translateY(14px) scale(0.99); }
  to   { opacity: 1; transform: none; }
}
/* Not scoped to `.panel` any more. The home page has no panels above its tour,
   so the first screen anyone sees was the one screen with no entrance at all:
   the shell painted, and then the strip and the market block each snapped in
   whole the instant /api/home landed. Reported as looking laggy, which is
   exactly what an instant state change reads as when everything around it
   moves. Same motion, same curve, now available to any block. */
.reveal { animation: panel-in 0.5s cubic-bezier(0.22, 0.61, 0.36, 1) both; }

/* The reveal reads as one motion only if the whole view starts clean. */
@keyframes view-in {
  from { opacity: 0; }
  to   { opacity: 1; }
}
.view.revealing { animation: view-in 0.25s ease-out both; }

@media (prefers-reduced-motion: reduce) {
  .reveal, .view.revealing { animation: none; }
}

/* -------------------------------------------------------------------- home */

.home {
  /* Follows `main` now, by way of 1000 and 1280.
     Keeping it narrower than the shell was the wrong instinct: this page holds
     the eight-cell market strip, the watchlist and the movers table, all of
     which are the same shape as the tables everywhere else and were being asked
     to live in a column 320px narrower than those. A reader on a wide screen
     saw the whole page inset inside an app that was not, which reads as the
     page having failed to fill rather than as a measured column.
     The search lockup is what must not grow, and it is pinned and centred
     below rather than the whole page being held in to protect it. */
  margin: 0 auto;
  /* Was --space-9 at the top. The market strip is the first element now and it
     wants to sit close to the session bar above it, not a screen's height
     below it. */
  padding: var(--space-4) var(--space-1) 60px;
}

/* The one thing on this page that must not grow with it. A search field the
   full width of the column reads as a URL bar rather than as a place to type
   four letters, and the ticker pills under it would spread across the whole
   screen with acres between them.
   Centred rather than left-aligned, now that the column around it is the full
   width of the app: an 860px block pinned to the left of a 1600px page is not
   a measured element, it is an element that ran out. Centred it reads as the
   thing the page is built around, which on this page it is. */
/* 720, down from 860. The field only ever holds four characters, and the width
   it needs is set by its placeholder rather than by the page: at 860 the
   sentence ended a third of the way across and the rest was an empty box, which
   reads as a field waiting for a sentence rather than a ticker. */
.home-search, .home-quick { max-width: 720px; margin-left: auto; margin-right: auto; }

/* The mark and the wordmark, one compact row.
   Both were much larger and stacked: a 92px aperture, then "Optic Terminal" at
   --t-hero, then a three-line lede. That is about 340px of branding above the
   fold, on a page whose own header already says OPTIC TERMINAL in the top-left
   corner, and it pushed the market state off the first screen entirely. The
   brand still opens the page; it just no longer *is* the page. */
.home-brand {
  display: flex;
  align-items: center;
  gap: var(--space-2);
  margin: var(--space-5) 0 var(--space-3);
  /* Centred over the search field rather than over the page: it shares that
     field's width, so the mark sits above the middle of the box it introduces
     instead of above the middle of a column the field does not fill. Left
     aligned it read as a label on the field; centred it reads as the name of
     the thing you are about to search. */
  max-width: 720px;
  justify-content: center;
  margin-left: auto;
  margin-right: auto;
}

.home-logo {
  /* 60, by way of 30, 38 and 52, each step asked for after looking at the last.
     The ceiling has always been the search field directly underneath, so that
     the two read as one lockup rather than a mark floating above a control. At
     the original type scale that field was 53px tall and the mark stopped at
     52. The UI scale since took the field to 61, so the same rule now puts the
     mark at 60: the number moved because the thing it is measured against did,
     not because the rule changed. */
  width: 60px; height: 60px;
  color: var(--ink-2);
  margin-bottom: 0;
  flex: 0 0 auto;
}
/* The aperture ring drifts open once on arrival — a single quiet gesture, not
   an animation the eye has to keep tracking. */
@keyframes iris-open {
  from { opacity: 0; transform: scale(0.9) rotate(-12deg); }
  to   { opacity: 1; transform: none; }
}
.home-logo { animation: iris-open 0.7s cubic-bezier(0.22, 0.61, 0.36, 1) both; }

/* The eye blinks, and the line draws itself back on when it opens.
 *
 * Three animations on one 6s clock so they cannot drift apart. Written as
 * percentages of a shared duration rather than as separate durations with
 * delays: delays drift the moment any one of them is retimed, and a blink whose
 * lid and contents disagree by 40ms reads as a rendering fault.
 *
 * 6s because that is roughly a human blink interval. Faster looked nervous.
 *
 * Only the eye group scales — the outer ring holds still. Scaling the whole
 * mark read as the logo flickering rather than as something blinking.
 *
 * scaleY(0.06) rather than 0: an exactly-zero scale collapses the transform
 * matrix and some engines drop the element's stroke rendering for that frame,
 * which shows up as a flash of nothing rather than a closed lid. */
@keyframes eye-blink {
  0%, 80%   { transform: scaleY(1); }
  85%, 88%  { transform: scaleY(0.06); }
  93%, 100% { transform: scaleY(1); }
}

/* The line is offset out of sight while the lid is down, then drawn back from
   its start once the eye is open — so the reveal is the payoff of the blink
   rather than something happening behind a closed lid. */
@keyframes line-redraw {
  0%, 84%  { stroke-dashoffset: 0; }
  85%, 91% { stroke-dashoffset: 100; }
  100%     { stroke-dashoffset: 0; }
}

/* The dot waits for the line to reach it. Its 96% start is deliberately after
   line-redraw is most of the way home, so it lands on the end of the stroke
   instead of hanging in space ahead of it. */
@keyframes dot-reveal {
  0%, 84%  { opacity: 1; }
  85%, 96% { opacity: 0; }
  100%     { opacity: 1; }
}

/* The delay is negative on purpose: -4.25s.
 *
 * The blink lives at 80-93% of the cycle, so a positive delay put the first one
 * about 5.9s after load — long enough that most people never saw it. A negative
 * delay starts the animation already part-way through instead of waiting: at
 * -4.25s the clock begins at 70.8% and reaches the 80% blink 0.55s later.
 *
 * 0.55s rather than 0s so the blink lands just after the logo's own fade-up
 * (0.5s) has finished. Blinking through the fade read as a glitch in the
 * entrance rather than as a blink.
 *
 * All three share the same negative delay, which is what keeps the lid, the
 * line and the dot in phase — that is also why they share one duration. */
.home-logo-eye {
  animation: eye-blink 6s ease-in-out -4.25s infinite;
  /* fill-box so the origin is the eye's own centre, not the SVG viewport's. */
  transform-box: fill-box;
  transform-origin: center;
}
.home-logo-line {
  stroke-dasharray: 100;
  animation: line-redraw 6s ease-in-out -4.25s infinite;
}
.home-logo-dot { animation: dot-reveal 6s ease-in-out -4.25s infinite; }

/* The header mark blinks too, on every page except Home.
 *
 * Same keyframes and the same 6s clock as .home-logo, not a copy of them: two
 * marks blinking to two sets of timings drift apart the first time either is
 * retimed, and a blink is only legible because the lid and the line agree.
 *
 * Not on Home, and that is the whole of the rule. The 64px home logo is
 * already blinking three feet lower down the same screen, and two apertures
 * closing half a second apart reads as a rendering fault rather than as one
 * mark with a tic. `body[data-view]` is published by switchView.
 *
 * The dot is driven from here too, on the same clock as the other two. It has
 * one job -- not hanging in space ahead of a half-drawn stroke -- and it can
 * only do it if it is on the same 6s cycle as the line it belongs to. Left
 * unanimated it stayed lit through the blink, so the closed lid had a gold
 * speck floating over it.
 */
body:not([data-view="home"]) .brand-mark-eye {
  animation: eye-blink 6s ease-in-out -4.25s infinite;
  /* fill-box, so the lid closes on the eye's own centre rather than the
     viewport's. Without it the mark slides as it scales. */
  transform-box: fill-box;
  transform-origin: center;
}
body:not([data-view="home"]) .brand-mark-line {
  stroke-dasharray: 100;
  animation: line-redraw 6s ease-in-out -4.25s infinite;
}
body:not([data-view="home"]) .brand-mark-dot {
  animation: dot-reveal 6s ease-in-out -4.25s infinite;
}

/* Everything on the home page fades up on arrival, in reading order.
 *
 * One shared keyframe with a per-element delay, not six keyframes: the order is
 * the only thing that differs, and the stagger is the whole effect —
 * simultaneous fades read as the page being slow rather than as motion.
 *
 * `both` holds the from-state through the delay, so nothing paints at full
 * opacity and then jumps back to start. The last element is in by ~0.62s:
 * renderHome runs on every visit to the tab, and an entrance you wait through
 * stops being an entrance. */
@keyframes fade-up {
  from { opacity: 0; transform: translateY(8px); }
  to   { opacity: 1; transform: none; }
}
.home-logo,
.home-title,
.home-search,
.home-quick,
.home-foot {
  animation: fade-up 0.5s cubic-bezier(0.22, 0.61, 0.36, 1) both;
}
.home-logo   { animation-delay: 0s; }
.home-title  { animation-delay: 0.08s; }
.home-search { animation-delay: 0.32s; }
.home-quick  { animation-delay: 0.40s; }
.home-foot   { animation-delay: 0.56s; }

/* Every animation above is decorative — nothing in them carries information
   that is not also in the static layout, so switching them all off costs
   nothing. iris-open was previously unguarded and is covered here too. */
@media (prefers-reduced-motion: reduce) {
  .home-logo,
  .home-logo-eye,
  .home-logo-line,
  .home-logo-dot,
  .brand-mark-eye,
  .brand-mark-line,
  .brand-mark-dot,
  .home-title,
  .home-search,
  .home-quick,
  .home-foot {
    animation: none;
    opacity: 1;
    transform: none;
    stroke-dashoffset: 0;
  }
}

/* --t-lead, not --t-d1. This is a label beside a 30px mark now, not a hero. */
.home-title {
  /* Grows with the mark. They are one lockup, and a mark enlarged on its own
     stops reading as a logo beside a name and starts reading as a logo with a
     caption: at 52px against 15px type the mark was three and a half times the
     words next to it. --t-d3 holds the ratio near two, which is where it was
     at 30 and 15. */
  font-size: var(--t-d3);
  font-weight: 600;
  letter-spacing: -0.01em;
  line-height: 1;
  margin: 0;
}
/* One colour across the whole wordmark.
 *
 * "Terminal" was set in --ink-muted against --ink on "Optic", which reads as
 * two words in two voices rather than one name. It was inherited from the
 * topbar's `.brand-text span`, where the same two-tone treatment had the same
 * problem and where the wordmark is now hidden entirely.
 *
 * The weight stays lighter. That is the part of the original that was doing
 * real work: it separates the two words without breaking the name into two
 * colours. */
.home-title span { font-weight: 400; }

/* The hovered bar's date, shown in the chart header only while scrubbing.
   Tabular figures so the row does not jitter as the digits change under the
   cursor — the whole point is to read it while moving, and a header that
   reflows on every bar is unreadable. */
.ws-ohlc-date {
  color: var(--ink-muted);
  font-variant-numeric: tabular-nums;
  margin-right: var(--space-1);
}
.ws-ohlc { font-variant-numeric: tabular-nums; }


/* The ticker box is the one thing to do on this page, so it gets real weight. */
.home-search { display: flex; gap: var(--space-2); flex-wrap: wrap; margin-bottom: var(--space-3); }
/* A layer of its own, above the blocks after it. The results list's z-index
   80 only counts inside this form, because `.reveal`'s entrance leaves every
   revealed block a stacking context of its own (fill-mode both keeps the
   transform). The "Or jump to" chips and the Today card are revealed after
   this form, so they painted over its results list: ORCL's matches came out
   under SPY, QQQ and NVDA. */
.home-search { position: relative; z-index: 6; }
.home-search input {
  flex: 1 1 240px;
  min-width: 0;
  background: var(--surface);
  border: 1px solid var(--border-strong);
  border-radius: var(--r-lg);
  color: var(--ink);
  font: inherit;
  font-size: var(--t-heading);
  letter-spacing: 0.06em;
  text-transform: uppercase;
  padding: var(--space-3) var(--space-4);
}
.home-search input::placeholder { color: var(--ink-muted); letter-spacing: 0.02em; text-transform: none; }
.home-search input:focus { outline: none; border-color: var(--s1); }
.home-search button { padding: var(--space-3) var(--space-6); font-size: var(--t-lead); border-radius: var(--r-lg); }

.home-quick { display: flex; align-items: center; gap: var(--space-2); flex-wrap: wrap; margin-bottom: var(--space-7); }
.home-quick .label { font-size: var(--t-small); color: var(--ink-muted); }
.home-quick button {
  background: var(--surface-2);
  border: 1px solid var(--border);
  border-radius: var(--r-pill);
  color: var(--ink-2);
  font: inherit;
  font-size: var(--t-small);
  letter-spacing: 0.04em;
  padding: var(--space-1) var(--space-3);
  cursor: pointer;
}
.home-quick button:hover { color: var(--ink); border-color: var(--s1); }

.home-foot {
  margin-top: var(--space-7);
  padding-top: var(--space-5);
  border-top: 1px solid var(--border);
  display: flex;
  gap: var(--space-2) var(--space-5);
  flex-wrap: wrap;
  font-size: var(--t-small);
  color: var(--ink-muted);
}
.home-foot .dot-sep { display: flex; align-items: center; gap: var(--space-2); }

/* A button that reads as the footer's own text, because that is what it is:
   navigation to the control in Settings, at the footer's weight. Left as a bare
   <button> it took the UA's grey box and 13px system font, which in a row of
   11px muted chips looked like a rendering fault rather than a link. */
.foot-link {
  padding: 0;
  border: 0;
  background: none;
  font: inherit;
  color: var(--ink-2);
  text-decoration: underline;
  text-underline-offset: 2px;
  cursor: pointer;
  transition: color var(--dur-ui) var(--ease-ui);
}
.foot-link:hover { color: var(--ink); }
.foot-link:focus-visible { outline: 2px solid var(--focus); outline-offset: 2px; }

@media (max-width: 640px) {
  .home { padding: var(--space-3) var(--space-0) var(--space-9); }
}

/* Segmented control — chart style toggle. */
.chart-toolbar {
  display: flex;
  justify-content: flex-end;
  align-items: center;
  gap: var(--space-3);
  margin: -4px 0 var(--space-2);
  flex-wrap: wrap;
}
/* The pair is mandatory, and this one was missing.
 *
 * `swingPriceBlock` renders this label as `<label class="range-pick" hidden>`
 * whenever the range is intraday, because there is no interval to choose on a
 * one-minute series. An author `display` beats the UA stylesheet's
 * `[hidden] { display: none }` whatever the specificity, so the picker kept its
 * 62px and stayed on screen offering a choice that does not apply. Measured
 * with the attribute set: computed display `inline-flex`, width 62px.
 *
 * Same fault CLAUDE.md records against `.set-pw`, which rendered its password
 * form open on every visit to Settings. Any rule that gives an element a
 * display needs this line beside it. */
.range-pick[hidden] { display: none; }
.range-pick {
  display: inline-flex;
  align-items: center;
  gap: var(--space-2);
  font-size: var(--t-caption);
  color: var(--ink-muted);
  text-transform: uppercase;
  letter-spacing: 0.05em;
}
.range-pick select {
  background: var(--surface-2);
  border: 1px solid var(--border-strong);
  border-radius: var(--r-md);
  color: var(--ink);
  font: inherit;
  font-size: var(--t-small);
  text-transform: none;
  letter-spacing: 0;
  padding: var(--space-1) var(--space-2);
}
.range-pick select:focus { outline: none; border-color: var(--s1); }
.seg { display: inline-flex; border: 1px solid var(--border-strong); border-radius: var(--r-md); overflow: hidden; }
.seg button {
  background: transparent;
  border: none;
  color: var(--ink-2);
  font: inherit;
  font-size: var(--t-small);
  padding: var(--space-1) var(--space-3);
  cursor: pointer;
}
.seg button + button { border-left: 1px solid var(--border-strong); }
.seg button:hover { background: var(--surface-2); color: var(--ink); }
/* --btn-primary, not --s1.
 *
 * White on --s1 measures 3.64:1, which fails body text at this size — and
 * these are the segmented controls that carry Line/Candles and the interval,
 * so the label a reader most needs is the one on the selected half.
 *
 * --btn-primary exists for exactly this: it is the one blue in the palette
 * chosen for white text sitting ON it rather than for separation from the
 * seven other series hues, and it is deliberately identical in both themes
 * because a filled control's contrast does not depend on the page behind it.
 * 4.72:1. Hardcoded #fff also becomes the token, so a theme cannot change the
 * fill and leave the ink behind. */
.seg button[aria-pressed="true"] {
  background: var(--btn-primary);
  color: var(--btn-primary-ink);
}

/* The chart style as three icons: candles, a line, an area. The segmented
   control's own look, with the glyphs centred in square-ish cells and the
   selected one filled the way a selected word is. */
.seg.chart-style button {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  padding: var(--space-1) var(--space-2);
  min-width: 34px;
}
.seg.chart-style button svg { display: block; }
.seg.chart-style button[disabled] { opacity: 0.4; cursor: not-allowed; }

/* Term-and-definition list used under the composite-score tables. */
.factor-defs { margin: var(--space-3) 0 0; font-size: var(--t-small); }
.factor-defs dt {
  color: var(--ink);
  font-weight: 600;
  text-transform: capitalize;
  margin-top: var(--space-2);
}
.factor-defs dd {
  margin: var(--space-0) 0 0;
  color: var(--ink-2);
  line-height: 1.5;
}

/* ------------------------------------------------------------ roth controls */

.roth-controls {
  display: flex;
  gap: var(--space-3);
  flex-wrap: wrap;
  align-items: flex-end;
  padding: var(--space-3);
  background: var(--surface-2);
  border: 1px solid var(--border);
  border-radius: var(--r-lg);
}
.roth-controls label {
  display: flex;
  flex-direction: column;
  gap: var(--space-1);
  font-size: var(--t-caption);
  color: var(--ink-muted);
  text-transform: uppercase;
  letter-spacing: 0.04em;
}
.roth-controls textarea {
  background: var(--plane);
  border: 1px solid var(--border-strong);
  border-radius: var(--r-md);
  color: var(--ink);
  font-family: inherit;
  font-size: var(--t-base);
  padding: var(--space-2) var(--space-3);
  resize: vertical;
  text-transform: none;
  letter-spacing: 0;
  width: 100%;
  box-sizing: border-box;
}
.roth-controls textarea:focus { outline: none; border-color: var(--s1); }
.roth-controls input,
.roth-controls select {
  background: var(--plane);
  border: 1px solid var(--border-strong);
  border-radius: var(--r-md);
  color: var(--ink);
  font: inherit;
  font-size: var(--t-base);
  padding: var(--space-2) var(--space-3);
  min-width: 130px;
  text-transform: none;
  letter-spacing: 0;
}
.roth-controls input:focus,
.roth-controls select:focus { outline: none; border-color: var(--s1); }
.roth-controls button { padding: var(--space-2) var(--space-5); }

/* Tracker action bar. The hint sits inline rather than under the buttons so the
   "this takes a minute" warning is read before the button is pressed. */
.tracker-bar {
  display: flex;
  gap: var(--space-3);
  flex-wrap: wrap;
  align-items: center;
  padding: var(--space-3) var(--space-3);
  margin-top: var(--space-3);
  background: var(--surface-2);
  border: 1px solid var(--border);
  border-radius: var(--r-lg);
}
.tracker-bar button { padding: var(--space-2) var(--space-4); }
.tracker-bar button[disabled] { opacity: 0.55; cursor: progress; }
.tracker-hint {
  flex: 1 1 320px;
  font-size: var(--t-caption);
  line-height: 1.5;
  color: var(--ink-muted);
}

/* Scan progress. A NASDAQ-wide scan runs for minutes, so "it's working" has to be
   visible or the page looks broken. */
.tracker-progress { margin-top: var(--space-3); }
.tracker-progress-head {
  display: flex;
  gap: var(--space-3);
  align-items: center;
  flex-wrap: wrap;
  font-size: var(--t-small);
  color: var(--ink-2);
  margin-bottom: var(--space-2);
}
.tracker-progress-head .mono {
  font-variant-numeric: tabular-nums;
  color: var(--ink-muted);
  font-size: var(--t-caption);
}
.tracker-progress-track {
  height: 5px;
  border-radius: var(--r-xs);
  background: var(--surface-2);
  overflow: hidden;
}
.tracker-progress-fill {
  height: 100%;
  background: var(--s1);
  border-radius: var(--r-xs);
  transition: width 0.5s ease;
}

/* Funnel: universe → gates → shortlist. Laid out horizontally because the point
   is the drop between steps, and a vertical list hides it. */
.funnel {
  display: flex;
  align-items: stretch;
  gap: var(--space-2);
  flex-wrap: wrap;
  margin: var(--space-3) 0 var(--space-2);
}
.funnel-step {
  flex: 1 1 130px;
  display: flex;
  flex-direction: column;
  gap: var(--space-0);
  padding: var(--space-3) var(--space-3);
  background: var(--surface-2);
  border: 1px solid var(--border);
  border-radius: var(--r-lg);
}
.funnel-value {
  font-size: var(--t-d3);
  font-weight: 600;
  color: var(--ink);
  font-variant-numeric: tabular-nums;
}
.funnel-label {
  font-size: var(--t-micro);
  letter-spacing: 0.01em;
  color: var(--ink-2);
}
.funnel-note { font-size: var(--t-caption); color: var(--ink-muted); }
.funnel-arrow {
  align-self: center;
  color: var(--ink-muted);
  font-size: var(--t-lead);
}
@media (max-width: 720px) {
  .funnel-arrow { display: none; }
}

table.data { min-width: 480px; }
/* Tables sitting side-by-side in a nested grid are narrow (3-4 columns), and the
   480px floor makes them overflow their grid cell and collide with the table
   next to it. Inside a nested grid, let them size to their content instead. */
.panel .grid table.data { min-width: 0; }
/* Nested grid cells must contain their own overflow.
   `min-width: auto` is the default for a grid item, so a table wider than its
   column pushed the column open and painted over the table beside it — three
   panels' worth of text stacked on top of each other. min-width:0 lets the
   column hold its share, and overflow-x scrolls anything that still won't fit
   instead of spilling sideways. */
.panel .grid > * { min-width: 0; overflow-x: auto; }
/* And let multi-word headers wrap in these narrow tables. `nowrap` is right for
   a full-width table, but here it forced "Distance from spot" to hold a single
   line and dragged a 320px column out to 565px. */
.panel .grid table.data th { white-space: normal; }
/* Same problem for a two-column table in a half-width panel: the 480px floor
   pushed the value column off the right edge, so the readings were invisible. */
table.data.narrow { min-width: 0; }
.leg { min-width: 480px; }

/* Panel headings.
 *
 * These were 13px uppercase in secondary ink, which put them *below* the data
 * they introduce: a tile value is 22px and a hero figure 44px, so the heading
 * was the quietest thing in its own panel. Inverted hierarchy is why the page
 * read as one undifferentiated wall — nothing announced where a section began.
 *
 * Sentence case rather than uppercase because the strings are already written
 * that way in the renderers ("Support & resistance", "Where to enter"), and
 * uppercasing prose costs legibility for emphasis that weight already provides. */
/* The one declaration of this selector. There were three; the other two are
 * gone, and the reasoning from all of them is here.
 *
 * It was 13px uppercase in secondary ink once, which put the heading *below*
 * the data it introduces -- a tile value is 24px and a hero figure 44px, so
 * the heading was the quietest thing in its own panel. The correction
 * overshot to a 32px display face. `--t-heading` is the step between: plainly
 * a heading against 13px body text, plainly not the thing the page is about.
 *
 * Sentence case rather than uppercase because the renderers already write the
 * strings that way ("Support & resistance"), and uppercasing prose costs
 * legibility for emphasis that weight already provides.
 *
 * The UI face, not the display one: the display face is for the single figure
 * or title a page is built around, and a panel heading repeats down the view. */
.panel > h2 {
  margin: 0 0 var(--space-1);
  font-size: var(--t-heading);
  font-weight: 600;
  letter-spacing: -0.01em;
  line-height: 1.25;
  color: var(--ink);
}
.panel > .sub {
  margin: 0 0 var(--space-3);
  font-size: var(--t-small);
  color: var(--ink-muted);
}
.panel h3 {
  margin: var(--space-5) 0 var(--space-2);
  font-size: var(--t-base);
  font-weight: 600;
  letter-spacing: -0.005em;
  color: var(--ink-2);
}

/* ------------------------------------------------------- figures & tiles */

.hero {
  font-size: var(--t-d1);
  font-weight: 600;
  line-height: 1;
  letter-spacing: -0.02em;
}
.hero-label { font-size: var(--t-small); color: var(--ink-muted); text-transform: uppercase; letter-spacing: 0.06em; }

/* Stat tiles.
 *
 * These were bare stacked text with no container, so a row of them read as loose
 * words floating on the panel rather than as discrete readings. A quiet surface
 * and real padding is the whole difference between "unstyled" and "designed",
 * and it costs nothing in density.
 *
 * The label keeps its uppercase micro-treatment: at 11.5px it functions as a
 * legend for the number below, not as prose, which is the one place caps earn
 * their keep. */
/* One reading, framed, and centred.
 *
 * A tile IS a box, so it keeps a surface; softer and flatter than the original
 * so a row of them reads as a set rather than as four separate panels.
 *
 * Consolidated from two rules 4,200 lines apart. The second declared only
 * background, border and padding and therefore silently owned all three, so the
 * values here are the ones that were actually rendering. Two declarations of
 * one class is the last-declaration-wins trap this file has been caught by more
 * than once; `.panel` was five of them.
 *
 * Centred rather than left-aligned. These are single figures under short
 * uppercase captions, and ragged-left numbers of different widths in a row of
 * equal boxes read as a column of accidents. `tabular-nums` on the value is
 * what makes centring safe: the digits are the same width, so a figure does not
 * shift sideways as it ticks.
 */
.tile {
  display: flex;
  flex-direction: column;
  gap: var(--space-1);
  /* --space-3 on the sides, not --space-5. At 23px a side, a 134px tile spent
     34% of itself on padding and left an 88px content box for figures that
     need more than that. */
  padding: var(--space-4) var(--space-3);
  background: var(--surface-2);
  /* The scroll shadows' covers, in the tile's own fill: see `.panel .grid > *`. */
  --scroll-cover: var(--surface-2);
  border: 0;
  border-radius: var(--r-md);
  text-align: center;
  /* Centred down the box as well as across it.
   *
   * The grid stretches every tile to the tallest in the row, and the content
   * was top-aligned inside that, so a tile with no delta and no note spent the
   * extra height as a gap under its figure. Measured on the instrument row at
   * 1440px: every tile 142px, label at 18 and value at 58 in all of them, and
   * the space under the content 18px on the three-line tiles against 42px on
   * the two-line ones. The figures lined up and the boxes did not, which reads
   * as the short ones having slipped upward inside their own card.
   *
   * Reserving an invisible third line was the first attempt and it is a no-op:
   * an empty line occupies exactly the space the gap already did, so nothing
   * moves. The content has to move, which means centring it. */
  justify-content: center;
}
/* Two lines' worth, whether it uses them or not.
 *
 * `FROM 52-WEEK HIGH` wraps and `RSI` does not, so in a row of six the values
 * sat at two different heights and the row read as broken. Reserving the second
 * line costs about 16px on the short captions and buys one baseline across the
 * whole row. `line-height` is declared here because the reservation is computed
 * from it. */
.tile .label {
  /* Sentence case at the caption size. Same argument as the table header:
     a label over a figure is read, not shouted, and both references put these
     in plain sentence case. The two-line reservation stays, for the reason
     recorded on it. */
  font-size: var(--t-caption);
  line-height: 1.4;
  min-height: calc(2 * 1.4 * var(--t-caption));
  color: var(--ink-muted);
  font-weight: 500;
}

/* The delta, which the tile did not have.
 *
 * Both references carry label / value / delta where this carried label /
 * value: Tremor's KPI card is "Unique visitors / 10,450 / -12.5%", and
 * shadcn's adds a trend caption under it. The figure was already in the
 * payload on every tile that wants one, so this was a missing component
 * rather than a restyle. Directional colour, because on a delta the sign is
 * the content. */
.tile .delta {
  font-size: var(--t-caption);
  font-weight: 600;
  font-variant-numeric: tabular-nums;
  letter-spacing: -0.01em;
}
.tile .delta.up { color: var(--pos); }
.tile .delta.down { color: var(--neg); }
.tile .delta.flat { color: var(--ink-muted); }
.tile .value {
  font-size: var(--t-d3);
  font-weight: 600;
  letter-spacing: -0.02em;
  font-variant-numeric: tabular-nums;
  /* The figure gets the whole tile, padding included.
   *
   * `text-align: center` cannot centre a line that is wider than the box it
   * sits in: the line box anchors at the content edge and the overflow all
   * goes one way. Measured at 134px: "7,704.28" is 116px against an 88px
   * content box, so it sat 14px right of the tile's centre and 4px past its
   * edge, while "54" and "-1.2%" -- which fit -- were dead centre. Only the
   * long values looked wrong, which is what made it read as a rendering
   * glitch rather than a padding problem.
   *
   * Reclaiming the side padding gives the figure the full tile width and
   * centring works again. The label, delta and note keep the padding: they
   * are the things that should stay inside it. */
  margin-inline: calc(-1 * var(--space-3));
}
/* No longer pushed to the bottom. `margin-top: auto` was how the notes were
   lined up with each other while the content was top-aligned; against
   `justify-content: center` an auto margin wins outright, which would splay
   the three-line tiles open while the two-line ones sat centred. The notes
   still line up, because every tile carrying one now centres the same three
   lines. */
.tile .note {
  font-size: var(--t-caption);
  color: var(--ink-2);
  line-height: 1.5;
}

.kv { display: grid; grid-template-columns: auto 1fr; gap: var(--space-1) var(--space-4); font-size: var(--t-body); }
.kv dt { color: var(--ink-muted); }
.kv dd { margin: 0; text-align: right; font-variant-numeric: tabular-nums; }

.up { color: var(--pos); }
.down { color: var(--neg); }
.flat { color: var(--ink-2); }

.chip {
  display: inline-flex;
  align-items: center;
  gap: var(--space-2);
  padding: var(--space-0) var(--space-2);
  border-radius: var(--r-pill);
  border: 1px solid var(--border-strong);
  font-size: var(--t-small);
  color: var(--ink-2);
  background: var(--surface-2);
}
.chip .dot { width: 8px; height: 8px; border-radius: 50%; background: var(--ink-muted); flex: none; }
.chip.bull .dot { background: var(--pos); }
.chip.bear .dot { background: var(--neg); }
.chip.warn .dot { background: var(--warn); }
.chip.neutral .dot { background: var(--ink-muted); }

ul.reasons { margin: var(--space-2) 0 0; padding-left: var(--space-5); font-size: var(--t-body); color: var(--ink-2); }
ul.reasons li { margin-bottom: var(--space-1); }

.callout {
  margin-top: var(--space-3);
  padding: var(--space-2) var(--space-3);
  border-left: 3px solid var(--warn);
  background: var(--surface-2);
  border-radius: 0 var(--r-md) var(--r-md) 0;
  font-size: var(--t-body);
  color: var(--ink-2);
}
.callout.info { border-left-color: var(--s1); }
.callout.bad { border-left-color: var(--critical); }

/* The house convention: every panel states what it cannot tell you. It is
 * load-bearing and none of it is deleted here -- what changed is that it
 * stopped being a wall.
 *
 * Measured with NVDA loaded: the Options facet carries 8,291 characters of
 * caveat across 28 of them, a quarter of everything on the page, and Investing
 * is 37% prose. All of it was set in italic at 12.65px. Italic is for
 * emphasis; three hundred characters of it, twenty-eight times, is harder to
 * read than the text it is qualifying and reads as noise rather than as care.
 *
 * Upright now, and the long ones clamp to two lines with the rest one click
 * away. A caveat short enough to fit is left alone entirely -- see
 * markClampedCaveats, which only marks the ones that actually overflow. */
.caveat {
  margin-top: var(--space-3);
  font-size: var(--t-caption);
  color: var(--ink-muted);
  line-height: 1.55;
}
.caveat.is-clamped {
  display: -webkit-box;
  -webkit-line-clamp: 2;
  -webkit-box-orient: vertical;
  overflow: hidden;
  cursor: pointer;
  border-radius: var(--r-xs);
  transition: color var(--dur-ui, 120ms) ease;
}
.caveat.is-clamped:hover { color: var(--ink-2); }
.caveat.is-clamped:focus-visible { outline: 2px solid var(--focus); outline-offset: 2px; }
.caveat.is-open { cursor: pointer; }
/* The affordance, on its own line so it cannot be mistaken for the sentence
   it follows. */
.caveat.is-clamped::after { content: ' more'; color: var(--s1); }
.caveat.is-open::after { content: ' less'; color: var(--s1); }

.gloss-term {
  font-style: normal;
  text-decoration: underline dotted var(--ink-muted);
  text-underline-offset: 2px;
  cursor: help;
  border-radius: var(--r-xs);
}
.gloss-term:hover {
  color: var(--s1);
  text-decoration-color: var(--s1);
  outline: none;
}
/* Split from the hover rule it used to share. Colour alone is the same signal
 * hover gives, so a keyboard reader could not tell which term was focused from
 * one they happened to be pointing at, and WCAG 1.4.1 asks for more than hue
 * either way. Tight offset because these sit inline in running text. */
.gloss-term:focus-visible {
  color: var(--s1);
  text-decoration-color: var(--s1);
  outline: 2px solid var(--focus);
  outline-offset: 2px;
}
/* Headings are uppercase and letter-spaced, so a dotted rule sitting tight under
   the caps reads as noise — push it further down. Colour stays at ink-muted so
   it's clearly there against the brighter heading text without shouting. */
h2 .gloss-term, h3 .gloss-term {
  text-decoration-color: var(--ink-muted);
  text-underline-offset: 4px;
}
h2 .gloss-term:hover, h3 .gloss-term:hover,
h2 .gloss-term:focus-visible, h3 .gloss-term:focus-visible {
  text-decoration-color: var(--s1);
}

/* ------------------------------------------------------------------ tables */

table.data {
  width: 100%;
  border-collapse: collapse;
  font-size: var(--t-small);
  font-variant-numeric: tabular-nums;
}
/* Row hover. Scanning a ten-row table across four columns is exactly where a
   highlight earns its place; only the hovered row repaints. */
table.data tbody tr { transition: background 90ms ease; }
table.data tbody tr:hover { background: var(--raised); }
@media (prefers-reduced-motion: reduce) {
  table.data tbody tr { transition: none; }
}

table.data th {
  text-align: right;
  padding: var(--space-1) var(--space-2);
  /* Sentence case, at the small size, in secondary rather than muted ink.
     It was 12.65px uppercase with 0.04em tracking in --ink-muted, which made
     the row you scan to find a column the least legible text in the table.
     Measured against the two references that do this well: shadcn's dashboard
     runs 14px sentence case at near-full ink and Tremor the same. Neither
     uppercases anything. */
  color: var(--ink-2);
  font-weight: 500;
  font-size: var(--t-small);
  border-bottom: 1px solid var(--baseline);
  white-space: nowrap;
}
table.data th:first-child, table.data td:first-child { text-align: left; }
table.data td {
  text-align: right;
  padding: var(--space-1) var(--space-2);
  border-bottom: 1px solid var(--grid);
  white-space: nowrap;
  /* Lining figures, on the densest surface in the product.
   *
   * The intent was already here and only the base rule was missing: `td.name`
   * sets `font-variant-numeric: normal` to opt a text column back OUT, which
   * only makes sense against a default that opts every other column in. So
   * every price column in every table has been rendering in proportional
   * figures, where a 1 is narrower than a 7 and a column of numbers does not
   * line up under itself. It is the single clearest difference between a page
   * of data and a terminal. */
  font-variant-numeric: tabular-nums;
}
table.data tr:last-child td { border-bottom: none; }
table.data td { transition: background-color 0.1s ease; }
table.data tr:hover td { background: var(--surface-2); }
table.data td.name { color: var(--ink); font-variant-numeric: normal; }
/* A first column that holds numbers rather than a name.
 *
 * `td.name` opts a TEXT column out of lining figures, which is right for
 * company names and wrong for the nine prices the chart dock's Key levels
 * widget puts in that column: measured on AMD, 223.58 / 197.34 / 491.24 down
 * the left edge with no two decimal points under each other. `.num` opts it
 * back in without giving up the left alignment `.name` is really being used
 * for there. Same meaning the class already has in `.seg-table td.num` and
 * `.sector-board .num`. */
table.data td.name.num { font-variant-numeric: tabular-nums; }
/* Sticky column labels for a vertically scrolled table: the header holds while
   the body scrolls under it, which is what a table capped at a height wants and
   what a reader loses first in a long ownership or levels table. A solid
   background is load-bearing here -- a transparent sticky header lets the rows
   scroll straight through it, the same failure the sec-head had -- and the
   underline is redrawn as an inset shadow because border-collapse drops a
   sticky cell's own border in several engines. Scoped to .scroll-y, the one
   container that scrolls vertically; .table-scroll is horizontal only. */
.scroll-y table.data thead th {
  position: sticky;
  top: 0;
  z-index: 1;
  background: var(--surface);
  box-shadow: inset 0 -1px 0 var(--baseline);
}
.scroll-y { max-height: 340px; overflow-y: auto; }

/* ------------------------------------------------------------------ charts */

.chart { width: 100%; display: block; overflow: visible; }
.chart text { font-family: var(--sans); }
.legend {
  display: flex;
  gap: var(--space-4);
  flex-wrap: wrap;
  font-size: var(--t-caption);
  color: var(--ink-2);
  margin-bottom: var(--space-2);
}
.legend .key { display: inline-flex; align-items: center; gap: var(--space-1); }
.legend .swatch { width: 12px; height: 2px; border-radius: var(--r-xs); }
.legend .swatch.box { height: 10px; width: 10px; border-radius: var(--r-xs); }

.tooltip {
  position: fixed;
  z-index: 100;
  pointer-events: none;
  padding: var(--space-2) var(--space-2);
  background: var(--surface-2);
  border: 1px solid var(--border-strong);
  border-radius: var(--r-md);
  font-size: var(--t-small);
  font-variant-numeric: tabular-nums;
  box-shadow: var(--elev-2);
  opacity: 0;
  transition: opacity 0.1s;
  max-width: 260px;
}
.tooltip.on { opacity: 1; }
.tooltip .t-title { color: var(--ink); font-weight: 600; margin-bottom: var(--space-1); }
.tooltip .t-row { color: var(--ink-2); display: flex; justify-content: space-between; gap: var(--space-3); }
/* The line under a bar's insider trades saying a click opens them. */
.tooltip .t-hint { color: var(--ink-muted); }

/* ------------------------------------------------------------- trade ideas */

.idea {
  border: 1px solid var(--border);
  border-radius: var(--r-md);
  padding: var(--space-3) var(--space-3);
  margin-bottom: var(--space-3);
  background: var(--surface-2);
}
.idea .idea-head { display: flex; justify-content: space-between; align-items: baseline; gap: var(--space-3); flex-wrap: wrap; }
.idea .idea-name { font-weight: 600; }
.idea .idea-why { font-size: var(--t-small); color: var(--ink-2); margin: var(--space-2) 0 var(--space-2); }
.leg {
  display: grid;
  grid-template-columns: 44px 52px 1fr repeat(4, auto);
  gap: var(--space-2);
  font-size: var(--t-small);
  font-variant-numeric: tabular-nums;
  padding: var(--space-1) 0;
  border-top: 1px solid var(--grid);
  align-items: center;
}
.leg .buy { color: var(--good); font-weight: 600; }
.leg .sell { color: var(--serious); font-weight: 600; }
.leg .muted { color: var(--ink-muted); }

/* ------------------------------------------------------------------- chat */

aside#chat {
  position: fixed;
  top: 0;
  right: 0;
  width: var(--chat-w);
  height: 100vh;
  transform: translateZ(0);
  background: var(--surface);
  border-left: 1px solid var(--border);
  display: none;
  flex-direction: column;
  z-index: 60;
}
body.chat-open aside#chat { display: flex; }


/* The drag handle. A separator, not a button: it has an orientation and a
   value, and a reader on a keyboard resizes it with the arrow keys. */
.chat-grip {
  position: absolute;
  left: 0;
  top: 0;
  width: 10px;
  height: 100%;
  margin-left: -5px;          /* straddles the border, so the target is 10px */
  cursor: col-resize;
  background: none;
  border: 0;
  padding: 0;
  z-index: 2;
  touch-action: none;         /* the drag must not become a page scroll */
  /* The affordance is the element's own background, not a pseudo-element.
     A 2px stripe painted with a gradient means the thing that transitions is
     the thing whose :hover fires it -- with the stripe on ::after, the hover
     state had no transition of its own, which is the rule the stylesheet holds
     everywhere else and a test asserts. */
  background-image: linear-gradient(to right,
    transparent 4px, var(--s1) 4px, var(--s1) 6px, transparent 6px);
  opacity: 0;
  transition: opacity var(--dur-ui) var(--ease-ui);
}
.chat-grip:hover,
.chat-grip:focus-visible,
body.chat-resizing .chat-grip { opacity: 0.9; }
/* Invisible at rest but never unreachable: opacity does not remove the target,
   so the 10px strip still takes the pointer and the keyboard. */
/* The ring is the whole point here, not an afterthought. This handle takes
 * ArrowLeft/Right/Home/End and resizes the chat panel with them, so a keyboard
 * reader who tabs onto an invisible 10px strip finds their arrow keys have
 * silently stopped scrolling the page. `outline: none` was inherited from the
 * hover rule above, where it is correct -- a pointer already shows where it
 * is. Offset outwards so the ring sits clear of the 10px strip. */
.chat-grip:focus-visible {
  outline: 2px solid var(--focus);
  outline-offset: 2px;
}
/* While dragging, nothing else should take the pointer or select text. */
body.chat-resizing { cursor: col-resize; user-select: none; }
body.chat-resizing iframe,
body.chat-resizing svg { pointer-events: none; }

#chat .chat-head {
  padding: var(--space-3) var(--space-4);
  border-bottom: 1px solid var(--border);
  display: flex;
  align-items: center;
  gap: var(--space-2);
}
#chat .chat-head .title { font-weight: 600; font-size: var(--t-body); text-transform: uppercase; letter-spacing: 0.04em; }
#chat .chat-head .close { background: none; border: none; color: var(--ink-muted); cursor: pointer; font-size: var(--t-heading); }
/* The chip takes the slack, not the close button.
 *
 * `margin-left: auto` sat on `.close`, which pushed only the x to the far edge
 * and left the history button stranded directly after the chip -- so it moved
 * every time the chip's text changed length, from "No context" to "NVDA
 * options" to a list of four. The auto margin now sits on the chip's right, so
 * everything after it is pushed to the edge as one group and the two buttons
 * stay side by side whatever the chip says.
 *
 * `min-width: 0` and the ellipsis so a long context list shortens itself
 * instead of shoving the buttons off the panel: the controls must survive the
 * label, not the other way round. The full list is in the chip's title. */
#chat .chat-head #chat-ctx { margin-right: auto; min-width: 0; }
#chat .chat-head #chat-ctx > span:last-child {
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
#chat .chat-head .icon-btn, #chat .chat-head .close { flex-shrink: 0; }

/* The one scroller in the panel. `min-height: 0` because a flex item's
   automatic minimum is its content height, which would let it grow past the
   panel instead of scrolling inside it -- the same shape of fault that crushed
   the log. `overscroll-behavior: contain` so reaching either end does not hand
   the wheel to the page behind the fixed panel: a reader scrolling Pulse should
   never find the terminal moving underneath it. */
#chat .chat-body {
  flex: 1 1 auto;
  min-height: 0;
  overflow-y: auto;
  overscroll-behavior: contain;
}
/* Content-sized now. It was the scroller, and `flex: 1` inside a column whose
   other children were all fixed is what squeezed it to 28px. */
#chat .chat-log { padding: var(--space-3) var(--space-4); display: flex; flex-direction: column; gap: var(--space-3); }
/* The inset the footer used to give it, and the log's own horizontal step so
   the chips line up with the messages above them. */
#chat .chat-body > .suggest { padding: 0 var(--space-4) var(--space-3); margin-bottom: 0; }
.msg { font-size: var(--t-base); }
.msg .who { font-size: var(--t-caption); text-transform: uppercase; letter-spacing: 0.05em; color: var(--ink-muted); margin-bottom: var(--space-1); }
.msg.user .bubble { background: var(--surface-2); border: 1px solid var(--border); border-radius: var(--r-lg); padding: var(--space-2) var(--space-3); }
/* The answer is the primary content of this panel, so it takes the primary ink.
 *
 * It was --ink-2, which is the colour used for captions, notes and metadata
 * everywhere else. The contrast measured 9.72:1 — comfortably past WCAG AA — so
 * this was never a legibility failure in the technical sense. It read as
 * *secondary* because it was literally the secondary colour, which is why long
 * answers felt like they sank into the panel.
 *
 * The looser line height is for the same reason: these are multi-sentence
 * paragraphs, not table cells, and 1.5 is tight for prose at this width. */
.msg .bubble {
  white-space: pre-wrap;
  word-wrap: break-word;
  color: var(--ink);
  font-size: var(--t-base);
  line-height: 1.65;
}
.msg.assistant .bubble { color: var(--ink); }
/* Metadata stays quiet so the hierarchy still reads. */
.msg .bubble .caveat, .msg .status { color: var(--ink-muted); }
.msg .bubble strong { color: var(--ink); }
.msg .bubble code { background: var(--surface-2); padding: var(--space-0) var(--space-1); border-radius: var(--r-xs); font-size: var(--t-small); }
.msg .status { font-size: var(--t-small); color: var(--ink-muted); font-style: italic; }
/* What to ask next, under a reply. The Home page's question rows, one per line,
   with room above them so they read as a separate thing from the answer. */
.msg .pulse-ups { display: flex; flex-direction: column; gap: var(--space-1); margin-top: var(--space-3); }
.msg .sources { margin-top: var(--space-2); font-size: var(--t-caption); }
.msg .sources a { color: var(--s1); display: block; text-decoration: none; margin-bottom: var(--space-0); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.msg .sources a:hover { text-decoration: underline; }
.msg.err .bubble { color: var(--serious); }

#chat .chat-foot { border-top: 1px solid var(--border); padding: var(--space-3) var(--space-3); }
#chat textarea {
  width: 100%;
  min-height: 62px;
  max-height: 160px;
  resize: vertical;
  padding: var(--space-2) var(--space-2);
  background: var(--surface-2);
  border: 1px solid var(--border-strong);
  border-radius: var(--r-md);
}
#chat textarea:focus { outline: 2px solid var(--focus); outline-offset: 1px; }
#chat .chat-actions { display: flex; align-items: center; gap: var(--space-2); margin-top: var(--space-2); flex-wrap: wrap; }
#chat .chat-actions label { font-size: var(--t-small); color: var(--ink-2); display: flex; align-items: center; gap: var(--space-1); }
.suggest { display: flex; flex-wrap: wrap; gap: var(--space-2); margin-bottom: var(--space-2); }
.suggest button {
  font-size: var(--t-caption);
  padding: var(--space-1) var(--space-2);
  border-radius: var(--r-pill);
  border: 1px solid var(--border-strong);
  background: var(--surface-2);
  color: var(--ink-2);
  cursor: pointer;
}
.suggest button:hover { color: var(--ink); border-color: var(--s1); }

/* ------------------------------------------------------------------ misc */

.loading { color: var(--ink-muted); font-size: var(--t-body); padding: var(--space-5) 0; }
.spinner {
  display: inline-block;
  width: 11px; height: 11px;
  border: 2px solid var(--border-strong);
  border-top-color: var(--s1);
  border-radius: 50%;
  animation: spin 0.7s linear infinite;
  vertical-align: -1px;
  margin-right: var(--space-2);
}
@keyframes spin { to { transform: rotate(360deg); } }
.error-box {
  padding: var(--space-3) var(--space-4);
  border: 1px solid var(--serious);
  border-left-width: 3px;
  border-radius: var(--r-md);
  background: var(--surface);
  color: var(--ink-2);
  font-size: var(--t-body);
}
/* Visually hidden, still announced.
 *
 * Defined twice, identically, three thousand lines apart — one copy is gone.
 *
 * `clip` is deprecated and some engines now ignore it, which would leave a
 * 1px-square element with its text overflowing and occasionally painting a
 * stray pixel. clip-path is the supported form; clip stays underneath it as the
 * fallback for anything old enough to need it. */
.sr-only {
  position: absolute; width: 1px; height: 1px; padding: 0; margin: -1px;
  overflow: hidden; white-space: nowrap; border: 0;
  clip: rect(0, 0, 0, 0);
  clip-path: inset(50%);
}

/* ---------------------------------------------------------------- phone */

/* Brand + search + the tab strip + Pulse need ~1275px to sit on one line at the
   current tab size (measured, not guessed — enlarging the tabs pushed this up
   from ~1020px). Below that, give the tabs their own full-width row rather than
   squeezing them into a narrow horizontal scroller — the row is still tidy, and
   every tab stays visible and clickable. */
/* Below this the strip takes a row of its own, because it no longer fits beside
   the brand, the search box and the three controls on the right. Raised from
   1280 when the strip was changed to nowrap: it has no wrap left to fall back
   on, so this number is the only thing standing between a slightly wider label
   and the tabs running underneath Pulse. The inline row measures 1400px. */
@media (max-width: 1410px) {
  header.topbar { flex-wrap: wrap; row-gap: var(--space-2); }
  nav.tabs { order: 3; flex: 1 0 100%; margin-left: 0; overflow-x: auto; }
  /* Re-asserted at higher specificity, and it is not the duplicate it looks
     like. `nav.tabs-group { flex: none; min-width: auto }` fires unconditionally
     further down the file to hold the strip on one row, and against
     `nav.tabs` it is equal specificity and later source, so it wins here too.
     What that produced: `order: 3` still applied, so the strip moved to the end
     of the row, but `flex: 1 0 100%` did not, so it never took a line of its
     own. Measured at 1410 with the whole row needing 1400: everything fitted,
     nothing wrapped, and the tabs rendered to the right of Pulse.
     `nav.tabs.tabs-group` is two classes to one and wins on specificity rather
     than on position. */
  nav.tabs.tabs-group { flex: 1 0 100%; min-width: 0; }
  /* Given room, but not the whole row.
     Above this width the strip is inline beside the search and every pixel is
     contested, which is why the buttons carry no gap at all. On its own row
     there is width going spare, and the tabs were huddled in the first two
     thirds of it.
     `space-between` was the first answer and it overcorrected: measured at
     1200 it put 93px between every label, which stops reading as one strip of
     navigation and starts reading as seven separate things. A fixed gap gives
     about 41px, roughly halfway, and keeps the group left-aligned with the
     content beneath it rather than stretched to meet the far edge. */
  nav.tabs.tabs-group { justify-content: flex-start; gap: var(--space-5); }
  /* The tab strip drops to its own row here, so it no longer claims the free
     space on row one. The account slot takes over that job and keeps Pulse
     beside it -- without this the pair was stranded next to the Load button.
     This was `#settings-btn` until the gear moved into the rail footer, at
     which point it was a push applied to an element no longer in the bar. */
  .account-slot { margin-left: auto; }
  #chat-toggle { margin-left: var(--space-2); }
}

@media (max-width: 860px) {
  /* Below this width the chat panel can't push content aside without
     squeezing every panel unreadably narrow — let it overlay instead. */
  body.chat-open main { padding-right: var(--space-5); }
  aside#chat { width: 100vw; }
  /* No split to resize when the panel is the window. */
  .chat-grip { display: none; }
}

@media (max-width: 640px) {
  .grid.c2, .grid.c3, .grid.c4 { grid-template-columns: 1fr; }
  main { padding: var(--space-3); }
  .panel { padding: var(--space-3); }
  header.topbar { padding: var(--space-2) var(--space-3); gap: var(--space-2); }
  .brand { font-size: var(--t-body); }
  .search input { width: 88px; }
  .hero { font-size: var(--t-d2); }
  .idea .leg { font-size: var(--t-caption); }
}

/* ---------------------------------------------------------- session strip
   Which trading session is running, the regular-hours close, and where the
   stock is actually trading. Sits under the status line so it reads as part of
   the header rather than as another panel competing for attention. */

.sessionbar {
  padding: var(--space-2) var(--space-5) var(--space-3);
  border-bottom: 1px solid var(--border);
  background: var(--surface);
}
.ses-main {
  display: flex;
  align-items: center;
  gap: var(--space-5);
  flex-wrap: wrap;
}
.ses-dot {
  width: 8px;
  height: 8px;
  border-radius: 50%;
  flex: 0 0 auto;
  background: var(--ink-muted);
}
/* One hue per session — violet through the small hours, amber at dawn, blue for
   the main session, orange at dusk. Declared once and shared by the dot, the
   strip and the legend so a colour can't drift between the three. */
.p-overnight { --ses-hue: var(--s7); }
.p-pre       { --ses-hue: var(--s4); }
.p-regular   { --ses-hue: var(--s1); }
.p-after     { --ses-hue: var(--s2); }
.p-closed    { --ses-hue: var(--ink-muted); }

.ses-dot { background: var(--ses-hue, var(--ink-muted)); }
/* Only the live session pulses, and only when the market is actually open. */
.ses-dot.p-regular { animation: pulse-beat 1.8s ease-in-out infinite; }

.ses-head { display: flex; flex-direction: column; gap: var(--space-0); min-width: 190px; }
.ses-phase {
  font-size: var(--t-small);
  font-weight: 600;
  letter-spacing: 0.03em;
  text-transform: uppercase;
  color: var(--ink-2);
}
.ses-sub { font-size: var(--t-caption); color: var(--ink-muted); }

/* The two prices sit side by side on purpose: the whole point is the comparison. */
.ses-price { display: flex; flex-direction: column; gap: var(--space-0); }
.ses-plabel {
  font-size: var(--t-micro);
  text-transform: uppercase;
  letter-spacing: 0.05em;
  color: var(--ink-muted);
}
.ses-pval {
  font-size: var(--t-heading);
  font-weight: 600;
  font-variant-numeric: tabular-nums;
  color: var(--ink);
}
.ses-pval.up { color: var(--pos); }
.ses-pval.down { color: var(--neg); }
.ses-pnote { font-size: var(--t-caption); }

/* 24-hour strip with the four sessions laid out to scale, and a marker at now. */
.ses-strip {
  position: relative;
  flex: 1 1 200px;
  height: 6px;
  min-width: 160px;
  border-radius: var(--r-xs);
  background: var(--surface-2);
  overflow: hidden;
}
.ses-seg {
  position: absolute;
  top: 0;
  height: 100%;
  border-radius: var(--r-xs);
  background: var(--ses-hue, var(--border-strong));
  /* Every session keeps its own colour; the inactive ones just recede, so the
     day still reads as four phases rather than one block and some grey. */
  opacity: 0.38;
}
.ses-seg.active {
  opacity: 1;
  box-shadow: 0 0 6px -1px var(--ses-hue);
}
.ses-now {
  position: absolute;
  top: -2px;
  width: 2px;
  height: calc(100% + 4px);
  background: var(--ink);
  border-radius: var(--r-xs);
  /* A dark hairline either side keeps the white marker legible wherever it
     lands, including on top of the pale amber and violet segments. */
  box-shadow: 0 0 0 1px var(--plane);
}

.ses-detail { margin-top: var(--space-4); }

/* The detail fold, and the shape of it is the point.
 *
 * `#ses-detail` carries the phase legend, the timezone note, the company
 * block and the description: measured at 375x812, 165px of every page, above
 * every view.
 *
 * The last attempt at paying that down put these details behind an "Hours &
 * company" button and broke the desktop, because the two rules meant for a
 * width query ended up outside one -- so `display: none` applied at every
 * width and a desktop with room to spare hid the company behind a click. The
 * comment on that markup in app.js asks, in as many words, that the next
 * attempt pay it inside a width query and verify the rule is actually in one.
 *
 * So the default here, unmediated, IS the desktop's: the panel shown and no
 * control at all. Only the phone block below adds a fold. A rule that escapes
 * the query fails towards "not folded on a phone either", which is the state
 * this shipped in for months, rather than towards a desktop that has lost its
 * company details with no button to get them back.
 *
 * `.is-closed` is left inert here on purpose rather than being kept out of
 * the markup: a reader who folded it on a phone and opened the same account
 * on a laptop would otherwise carry a hidden panel across with them. */
/* Shown at every width now, and that is a change of direction with a reason.
 *
 * This started phone-only because an earlier attempt at a disclosure here
 * broke the desktop: its rules escaped the width query, so the details were
 * hidden everywhere AND the button that revealed them was hidden too, leaving
 * no way back. The fold was then scoped to phones to be safe.
 *
 * What makes it safe everywhere is that the button is visible everywhere. The
 * failure last time was not "the desktop folded", it was "the desktop folded
 * with no control". Measured at 1440x950: the bar is 150px of every page above
 * every view, and the part that never changes during a day -- the phase
 * legend, the timezone note, the company profile, the description -- is most
 * of it. The live part, which session and the clock and the 24-hour strip,
 * stays unconditional. */
.ses-detail-btn {
  display: inline-flex;
  align-items: center;
  gap: var(--space-2);
  margin-top: var(--space-3);
  padding: var(--space-1) var(--space-3);
  background: var(--surface-2);
  border: 1px solid var(--border-strong);
  border-radius: var(--r-md);
  color: var(--ink-2);
  font: inherit;
  font-size: var(--t-small);
  cursor: pointer;
  transition: color var(--dur-ui) var(--ease-ui),
              border-color var(--dur-ui) var(--ease-ui);
}
.ses-detail-btn:hover { color: var(--ink); border-color: var(--ink-muted); }
.ses-detail-btn::after {
  content: "";
  width: 0;
  height: 0;
  border-left: 3.5px solid transparent;
  border-right: 3.5px solid transparent;
  border-top: 4px solid currentColor;
  opacity: 0.6;
  transition: transform var(--dur-ui) var(--ease-ui);
}
.ses-detail-btn[aria-expanded="true"]::after { transform: rotate(180deg); }
.ses-detail.is-closed { display: none; }
.ses-desc { margin-top: var(--space-3); }
.ses-legend {
  display: flex;
  gap: var(--space-4);
  flex-wrap: wrap;
  margin-top: var(--space-2);
}
.ses-key {
  display: inline-flex;
  align-items: center;
  gap: var(--space-1);
  font-size: var(--t-micro);
  text-transform: uppercase;
  letter-spacing: 0.04em;
  color: var(--ink-muted);
}
.ses-key i {
  width: 9px;
  height: 9px;
  border-radius: var(--r-xs);
  background: var(--ses-hue);
  opacity: 0.45;
  flex: none;
}
.ses-key em {
  font-style: normal;
  text-transform: none;
  letter-spacing: 0;
  opacity: 0.72;
}
/* The live session is named in the header too, but calling it out here means the
   bar and the legend can be read in either order. */
.ses-key.live { color: var(--ink); }
.ses-key.live i { opacity: 1; }

.ses-warn {
  margin-top: var(--space-2);
  font-size: var(--t-caption);
  line-height: 1.5;
  color: var(--ink-2);
  border-left: 2px solid var(--warn);
  padding-left: var(--space-2);
}
/* The disclosure for the session legend and description, at every width.
   It used to be phone-only, on the reasoning that "the desktop has the room".
   The desktop has the room and spends it badly: measured on AAPL at 727px, the
   four phase keys, the timezone note and the description put the company name
   and its price at y=516 in an 835px viewport, so 62% of the first screen was
   market-hours reference material on the page a reader opened to see a price.

   Which session is running, the clock and the 24-hour strip all stay visible
   unconditionally — those are the parts that change during a day. What is
   behind the button is a fixed timetable and a paragraph explaining it, which
   is reference, not news. */

.ses-desc {
  margin-top: var(--space-2);
  font-size: var(--t-caption);
  line-height: 1.5;
  color: var(--ink-muted);
}

@media (max-width: 900px) {
  .ses-strip { flex-basis: 100%; order: 9; }
}

/* Company line at the top of the session strip: who this is and what they do,
   established once per ticker rather than repeated on every tab. */
.ses-company {
  /* The strip ran straight up against the company name: measured, six pixels
     of coloured bar with ten to the heading below it, which reads as the two
     touching. The phase strip is the busiest thing in this bar and the company
     name is the largest, so they need more between them than any other pair
     here. */
  padding-bottom: var(--space-4);
  margin-bottom: var(--space-4);
  border-bottom: 1px solid var(--border);
}
.ses-co-head {
  display: flex;
  align-items: baseline;
  gap: var(--space-3);
  flex-wrap: wrap;
}
.ses-co-name { font-size: var(--t-lead); font-weight: 600; color: var(--ink); }
.ses-co-meta { font-size: var(--t-caption); color: var(--ink-muted); }
.ses-co-link { font-size: var(--t-caption); color: var(--s1); text-decoration: none; }
.ses-co-link:hover { text-decoration: underline; }

.ses-co-sum {
  margin: var(--space-1) 0 0;
  font-size: var(--t-small);
  line-height: 1.5;
  color: var(--ink-2);
  max-width: 105ch;
  /* Clamped rather than cut in JS: no broken sentences, and it adapts to width. */
  display: -webkit-box;
  -webkit-line-clamp: 2;
  -webkit-box-orient: vertical;
  overflow: hidden;
}
.ses-co-sum.expanded { -webkit-line-clamp: unset; display: block; }

.ses-co-more {
  margin-top: var(--space-0);
  padding: 0;
  background: none;
  border: 0;
  color: var(--s1);
  font-size: var(--t-caption);
  cursor: pointer;
}
.ses-co-more:hover { text-decoration: underline; }

/* Level-visibility toggles. The little swatch matches the line colour on the
   chart, so the button says which family it controls without reading the label. */
.seg button .lvl-key {
  display: inline-block;
  width: 12px;
  height: 3px;
  border-radius: var(--r-xs);
  margin-right: var(--space-2);
  vertical-align: 2px;
}
/* Both families share one dash rhythm; only the hue differs. */
.seg button .lvl-key.fib {
  background: repeating-linear-gradient(90deg, var(--ref-fib) 0 5px, transparent 5px 8px);
}
.seg button .lvl-key.sr {
  background: repeating-linear-gradient(90deg, var(--ref-sr) 0 5px, transparent 5px 8px);
}
/* An off toggle should look off: the swatch and label both recede. */
.seg button[aria-pressed="false"] .lvl-key { opacity: 0.35; }

/* ------------------------------------------------------------------- legal
   Always present, never dismissible. The home-page notice covers one tab and
   can be hidden; a tool that prints strike prices and a simulated track record
   needs the disclaimer on screen wherever those numbers are. */
footer.legal {
  border-top: 1px solid var(--border);
  background: var(--surface);
  padding: var(--space-4) var(--space-5) var(--space-5);
  margin-top: var(--space-2);
}
body.chat-open footer.legal { padding-right: var(--chat-w); }
.legal-line {
  margin: 0;
  max-width: 110ch;
  font-size: var(--t-caption);
  line-height: 1.65;
  color: var(--ink-muted);
}
.legal-line strong { color: var(--ink-2); }
#legal-more {
  margin-left: var(--space-1);
  padding: 0;
  background: none;
  border: 0;
  color: var(--s1);
  font-size: var(--t-caption);
  cursor: pointer;
  text-decoration: underline;
}
.legal-full {
  margin-top: var(--space-3);
  max-width: 110ch;
  padding: var(--space-3) var(--space-4);
  background: var(--surface-2);
  border: 1px solid var(--border);
  border-radius: var(--r-lg);
}
.legal-full p {
  margin: 0 0 var(--space-2);
  font-size: var(--t-caption);
  line-height: 1.65;
  color: var(--ink-2);
}
.legal-full p:last-child { margin-bottom: 0; }

/* Per-tab notice, at the top of any view that emits actionable-looking output. */
/* A block, not a flex row.
 *
 * It was `display: flex`, which made the bold lead-in and the sentence after it
 * two flex items. Flex items shrink to min-content, so in a narrow container —
 * the Pulse panel, at roughly 380px while the window is wide — both columns
 * collapsed and the notice rendered one word per line: "Not / financial /
 * advice." beside "Pulse / states / which / way / the".
 *
 * A viewport media query cannot fix that, because the viewport is not what is
 * narrow. A container query could, but every ancestor would need a containment
 * context declared for one notice. A bold lead-in on a paragraph is the ordinary
 * way to set this and it cannot break at any width. */
.legal-area {
  display: block;
  margin-bottom: var(--space-4);
  padding: var(--space-2) var(--space-3);
  background: var(--surface);
  border: 1px solid var(--border);
  border-left: 2px solid var(--warn);
  border-radius: var(--r-md);
  font-size: var(--t-caption);
  line-height: 1.65;
  color: var(--ink-2);
}
.legal-area strong { color: var(--ink); margin-right: var(--space-0); }

/* ---------------------------------------------------------------- settings */
.settings-row {
  display: flex;
  gap: var(--space-5);
  align-items: flex-start;
  justify-content: space-between;
  flex-wrap: wrap;
  padding: var(--space-3);
  margin-top: var(--space-3);
  background: var(--surface-2);
  border: 1px solid var(--border);
  border-radius: var(--r-lg);
}
.settings-label {
  display: flex;
  flex-direction: column;
  gap: var(--space-1);
  font-size: var(--t-base);
  color: var(--ink);
  /* Never wider than the row: at 320 with Large text the row's content box
     is under 220px. */
  min-width: min(220px, 100%);
  flex: 1 1 260px;
}
.settings-hint {
  font-size: var(--t-caption);
  line-height: 1.5;
  color: var(--ink-muted);
  font-weight: 400;
}
.settings-select {
  flex: 0 1 340px;
  /* A select's automatic minimum is its longest option, so it would not
     shrink into its row. Measured at 390x844: the time zone picker was 401px
     wide in a 314px row, and the panel scrolled sideways to show the rest.
     A max-width caps that minimum too, so a phone gets a picker the width of
     its row and a desktop keeps the 401px it had. */
  max-width: 100%;
  padding: var(--space-2) var(--space-3);
  background: var(--surface);
  border: 1px solid var(--border-strong);
  border-radius: var(--r-md);
  color: var(--ink);
  font-size: var(--t-base);
}
.settings-select:focus { outline: 2px solid var(--focus); outline-offset: 1px; }

/* Segmented option group, reused from the chart toolbar's look. */
/* `.seg-opt` was a second, near-identical segmented control with its own radius,
   its own active class and its own border rules. Two controls that look almost the
   same are worse than two that look clearly different, so it now shares the base
   styling and differs only in how the active state is flagged. */
.seg-opt {
  padding: var(--space-2) var(--space-4);
  background: var(--surface);
  border: 1px solid var(--border-strong);
  color: var(--ink-2);
  font-size: var(--t-body);
  cursor: pointer;
}
.seg-opt:first-child { border-radius: var(--r-md) 0 0 var(--r-md); }
.seg-opt:last-child { border-radius: 0 var(--r-md) var(--r-md) 0; }
.seg-opt + .seg-opt { border-left: 0; }
.seg-opt:hover { background: var(--surface-2); color: var(--ink); }
/* Same correction as .seg button above, same 3.64:1 reason. */
.seg-opt.on {
  background: var(--btn-primary);
  border-color: var(--btn-primary);
  color: var(--btn-primary-ink);
}
/* A segmented control in a narrow settings row.
 *
 * `.seg` clips what does not fit (overflow: hidden, for its rounded ends),
 * and that overflow also lets it shrink below its buttons. So the last choice
 * was cut off, not wrapped: at 320x640 the Text size control needed 232px of
 * a 214px row and lost 18px of "Large", and at 375x812 with Large text it
 * needed 265px of 257.
 *
 * The room it needs grows with the text size, so the test is in em, which in
 * a container query is the row's own font size and scales with it. Below 14em
 * the side padding narrows, which fits all three settings at 320 in the
 * default size and at 375 in Large. Where even that is not enough (320 in
 * Large) the control scrolls sideways rather than hide a choice. */
.settings-row { container-type: inline-size; }
.settings-row .seg { overflow-x: auto; }
@container (max-width: 14em) {
  .settings-row .seg button { padding-left: var(--space-2); padding-right: var(--space-2); }
}

/* Zone note at the end of the session legend. */
.ses-key.ses-zone { color: var(--ink-muted); text-transform: none; letter-spacing: 0; }
.ses-key.ses-zone button {
  margin-left: var(--space-1);
  padding: 0;
  background: none;
  border: 0;
  color: var(--s1);
  font-size: var(--t-micro);
  cursor: pointer;
  text-decoration: underline;
}

/* ------------------------------------------------------------- settings gear
   In the top bar rather than the tab strip: the strip wraps to its own row at
   narrow widths, which would drop the gear below the bar instead of leaving it
   beside Pulse where it belongs. */
.icon-btn {
  flex: none;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 38px;
  height: 38px;
  margin-left: auto;
  padding: 0;
  background: transparent;
  border: 1px solid transparent;
  /* --r-ctl, the button step, not --r-md. The gear sits in the same bar
     as `.btn` and was two pixels squarer than it. */
  border-radius: var(--r-ctl);
  color: var(--ink-2);
  cursor: pointer;
  transition: background-color 0.12s ease, color 0.12s ease, border-color 0.12s ease;
}
/* The tab strip already claims the free space with margin-left: auto; without
   this the gear and the strip fight over it and the gear jumps. */
nav.tabs ~ .icon-btn { margin-left: var(--space-2); }
.icon-btn:hover { background: var(--surface); color: var(--ink); }
.icon-btn[aria-pressed="true"] {
  background: var(--surface);
  border-color: var(--border-strong);
  color: var(--ink);
}

.gear {
  width: 21px;
  height: 21px;
  /* Spun from the centre — the default transform-origin on an SVG child is the
     viewport corner, which would swing it in an arc instead of rotating it. */
  transform-origin: 50% 50%;
  transition: transform 0.5s cubic-bezier(0.34, 0.9, 0.3, 1);
}
.icon-btn:hover .gear,
.icon-btn:focus-visible .gear { transform: rotate(90deg); }
/* On the settings view it keeps turning slowly, so the gear reads as "this is
   where you are" without needing a text label beside it. */
.icon-btn[aria-pressed="true"] .gear {
  animation: gear-spin 9s linear infinite;
  transition: none;
}
@keyframes gear-spin {
  from { transform: rotate(0deg); }
  to   { transform: rotate(360deg); }
}
@media (prefers-reduced-motion: reduce) {
  .gear, .icon-btn:hover .gear, .icon-btn:focus-visible .gear { transition: none; transform: none; }
  .icon-btn[aria-pressed="true"] .gear { animation: none; }
}

/* ------------------------------------------------------------ symbol search
   Typeahead for the ticker boxes. Absolute so the list overlays the page rather
   than pushing the header taller as results arrive. */
.combo { position: relative; display: flex; }
.combo-list {
  position: absolute;
  top: calc(100% + 4px);
  left: 0;
  right: 0;
  z-index: 80;
  margin: 0;
  padding: var(--space-1);
  list-style: none;
  min-width: 300px;
  max-height: 340px;
  overflow-y: auto;
  background: var(--surface);
  border: 1px solid var(--border-strong);
  border-radius: var(--r-lg);
  box-shadow: var(--elev-2);
}
.combo-list li {
  display: flex;
  /* Centre, not baseline. A 22px logo next to baseline-aligned text sits low and
     drags the row's apparent height down; the text no longer shares a baseline
     with anything now that there is a graphic in the row. */
  align-items: center;
  gap: var(--space-2);
  padding: var(--space-2) var(--space-2);
  border-radius: var(--r-md);
  cursor: pointer;
  font-size: var(--t-body);
}
.combo-list li.on,
.combo-list li:hover { background: var(--surface-2); }
.combo-sym {
  flex: 0 0 auto;
  min-width: 58px;
  font-weight: 600;
  color: var(--ink);
  font-variant-numeric: tabular-nums;
}
.combo-name {
  flex: 1 1 auto;
  color: var(--ink-2);
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}
/* The matched run, so a long list can be scanned rather than read. */
.combo-list b { color: var(--s1); font-weight: 600; }
.combo-tag {
  flex: 0 0 auto;
  font-size: var(--t-micro);
  letter-spacing: 0.06em;
  text-transform: uppercase;
  color: var(--ink-muted);
  border: 1px solid var(--border);
  border-radius: var(--r-sm);
  padding: 0 var(--space-1);
}
/* The header box is narrow; give its dropdown room to show company names. */
#ticker-results { min-width: 330px; }
.home-search .combo { flex: 1 1 auto; }
.home-search .combo input { width: 100%; }

ul.brief-list {
  list-style: none;
  margin: var(--space-2) 0 0;
  padding: 0;
}
.brief-item {
  padding: var(--space-2) 0 var(--space-2) var(--space-3);
  border-left: 2px solid var(--border);
  border-bottom: 1px solid var(--border);
}
.brief-item:last-child { border-bottom: 0; }
.brief-item.tone-positive { border-left-color: var(--pos); }
.brief-item.tone-negative { border-left-color: var(--neg); }

.brief-head {
  font-size: var(--t-body);
  line-height: 1.5;
  color: var(--ink);
}
.brief-head a { color: inherit; text-decoration: none; }
.brief-head a:hover,
.brief-head a:focus-visible { text-decoration: underline; }
/* Outbound links get a marker, since every item on this page leaves the site. */
.brief-head a::after {
  content: '↗';
  font-size: 0.8em;
  margin-left: var(--space-1);
  color: var(--ink-muted);
  vertical-align: super;
}

.brief-meta {
  display: flex;
  align-items: center;
  flex-wrap: wrap;
  gap: var(--space-2);
  margin-top: var(--space-1);
  font-size: var(--t-small);
  color: var(--ink-muted);
}
.brief-src { color: var(--ink-2); }
.brief-dot { opacity: 0.5; }
.brief-tags { display: inline-flex; flex-wrap: wrap; gap: var(--space-1); margin-left: var(--space-0); }
/* Denser than a standalone chip: several sit on one metadata line. */
.brief-tags .chip { padding: var(--space-0) var(--space-2); font-size: var(--t-micro); }
.brief-tags .chip .dot { width: 6px; height: 6px; }

/* "Subscription" on a headline whose publisher will ask for one.
   Outlined rather than filled: it is a fact about the link, not a warning about
   the story, and a solid chip here would read as louder than the catalyst chips
   beside it that describe what actually happened. --ink-2 rather than
   --ink-muted because at --t-micro this has to be legible at a glance to do its
   job at all — 9.31:1 dark, 8.93:1 light. */
.brief-paid {
  flex: none;
  padding: 0 var(--space-1);
  border: 1px solid var(--border-strong);
  border-radius: var(--r-sm);
  font-size: var(--t-micro);
  letter-spacing: 0.04em;
  text-transform: uppercase;
  color: var(--ink-2);
  cursor: help;
}

/* The daily summary reads as prose, so it gets prose measure and leading rather
   than the tighter panel defaults used for label/value text. */
.brief-summary p {
  margin: 0 0 var(--space-2);
  font-size: var(--t-body);
  line-height: 1.5;
  color: var(--ink-2);
  max-width: 74ch;
}
.brief-summary p:first-child { color: var(--ink); }
.brief-summary p:last-child { margin-bottom: 0; }

/* ------------------------------------------------------- Optic's Read: desks
 *
 * A wire front page rather than one column: an index strip, a lead story with
 * room to breathe, then five desks in a responsive grid. The grid is the whole
 * point — five short lists side by side are scannable in a way that one list of
 * thirty is not, which was the complaint about the first version.
 */

.read-strip {
  display: flex;
  gap: 0;
  overflow-x: auto;
  border: 1px solid var(--border);
  border-radius: var(--r-md);
  background: var(--surface-2);
  margin-bottom: var(--space-4);
  scrollbar-width: none;
}
.read-strip::-webkit-scrollbar { display: none; }
.read-tick {
  display: flex;
  align-items: baseline;
  gap: var(--space-2);
  padding: var(--space-2) var(--space-4);
  border-right: 1px solid var(--border);
  white-space: nowrap;
  flex: none;
}
.read-tick:last-child { border-right: 0; }
.read-tick-name {
  font-size: var(--t-micro);
  text-transform: uppercase;
  letter-spacing: 0.06em;
  color: var(--ink-muted);
}
.read-tick-last { font-size: var(--t-body); font-variant-numeric: tabular-nums; }
.read-tick-day { font-size: var(--t-micro); font-variant-numeric: tabular-nums; }
.read-tick-day.up { color: var(--pos); }
.read-tick-day.down { color: var(--neg); }

/* The read's own headline, now that it sits under the title that names it.
   Larger than a `.sub` because it is the first sentence of the thing the page
   is for, and it used to be the subtitle of a panel one further down. */
.read-lede {
  margin: var(--space-4) 0 var(--space-2);
  font-size: var(--t-lead);
  font-weight: 600;
  color: var(--ink);
  line-height: 1.35;
}

.read-hero-top {
  display: flex;
  align-items: flex-start;
  justify-content: space-between;
  gap: var(--space-4);
  flex-wrap: wrap;
}
.read-search { flex: 1 1 240px; max-width: 340px; }
.read-search input {
  width: 100%;
  padding: var(--space-2) var(--space-3);
  font: inherit;
  font-size: var(--t-body);
  color: var(--ink);
  background: var(--surface-2);
  border: 1px solid var(--border-strong);
  border-radius: var(--r-md);
}
.read-search input:focus-visible {
  outline: 2px solid var(--focus);
  outline-offset: 1px;
}
#read-results:not(:empty) {
  margin-top: var(--space-4);
  padding-top: var(--space-4);
  border-top: 1px solid var(--border);
}

/* The lead. Deliberately not a panel — it should read as the top of the page,
 * not as one more box in the stack. */
.read-lead {
  padding: var(--space-5) var(--space-5);
  margin-bottom: var(--space-4);
  background: var(--surface-2);
  border: 1px solid var(--border);
  border-left: 3px solid var(--s1);
  border-radius: var(--r-lg);
}
.read-lead.tone-positive { border-left-color: var(--pos); }
.read-lead.tone-negative { border-left-color: var(--neg); }
.read-lead-kicker {
  font-size: var(--t-micro);
  letter-spacing: 0.01em;
  color: var(--ink-muted);
  margin-bottom: var(--space-2);
}
.read-lead-title {
  margin: 0 0 var(--space-2);
  font-size: var(--t-heading);
  line-height: 1.3;
  font-weight: 600;
}
.read-lead-title a { color: inherit; text-decoration: none; }
.read-lead-title a:hover,
.read-lead-title a:focus-visible { text-decoration: underline; }
.read-lead-sum {
  margin: 0 0 var(--space-2);
  font-size: var(--t-body);
  color: var(--ink-2);
  line-height: 1.5;
}

/* Jump links. Real anchors, so they work with the keyboard and survive a reload. */
.read-nav {
  display: flex;
  gap: var(--space-2);
  flex-wrap: wrap;
  margin-bottom: var(--space-4);
}
.read-nav a {
  padding: var(--space-1) var(--space-3);
  font-size: var(--t-micro);
  text-transform: uppercase;
  letter-spacing: 0.06em;
  color: var(--ink-2);
  text-decoration: none;
  background: var(--surface-2);
  border: 1px solid var(--border);
  border-radius: var(--r-pill);
}
.read-nav a:hover,
.read-nav a:focus-visible { color: var(--ink); border-color: var(--border-strong); }

.read-desks {
  display: grid;
  gap: var(--space-4);
  grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
  margin-bottom: var(--space-4);
}
.read-desk { margin: 0; scroll-margin-top: 96px; }
.read-desk h2 { margin-bottom: var(--space-0); }

/* Calendar. Grouped by day, because "what's on Wednesday" is the question. */
.read-cal { margin-top: var(--space-3); }
.read-cal-day {
  display: grid;
  grid-template-columns: 108px 1fr;
  gap: var(--space-3);
  padding: var(--space-2) 0;
  border-top: 1px solid var(--border);
}
.read-cal-day:first-child { border-top: 0; }
.read-cal-date {
  font-size: var(--t-micro);
  text-transform: uppercase;
  letter-spacing: 0.06em;
  color: var(--ink-muted);
  padding-top: var(--space-0);
}
.read-cal-rows { display: flex; flex-direction: column; gap: var(--space-2); min-width: 0; }
.read-cal-row {
  display: grid;
  /* 138px column holds the prev / est / actual strip. */
  grid-template-columns: 92px 1fr 138px auto auto;
  gap: var(--space-3);
  align-items: baseline;
  font-size: var(--t-body);
}
.read-cal-row.major { font-weight: 600; }

/* An expandable row carries the grid on its summary instead, because the
 * <details> box also has to hold the drawer underneath it. */
details.read-cal-row { display: block; }
details.read-cal-row > summary {
  display: grid;
  grid-template-columns: 92px 1fr 138px auto auto 14px;
  gap: var(--space-3);
  align-items: baseline;
  cursor: pointer;
  list-style: none;
  border-radius: var(--r-sm);
  padding: var(--space-0) var(--space-1);
  margin: -2px -4px;
}
details.read-cal-row > summary::-webkit-details-marker { display: none; }
details.read-cal-row > summary:hover { background: var(--bg-hover); }
details.read-cal-row > summary:focus-visible {
  outline: 2px solid var(--focus);
  outline-offset: 1px;
}
.cal-caret {
  width: 0; height: 0;
  border-left: 4px solid currentColor;
  border-top: 3.5px solid transparent;
  border-bottom: 3.5px solid transparent;
  opacity: 0.45;
  /* Aligned to the first line, not the cell's centre: a title that wraps to two
   * lines otherwise pushes the caret to the bottom-right, away from the row it
   * belongs to. */
  align-self: start;
  margin-top: var(--space-2);
  transition: transform 140ms ease;
}
details[open] > summary .cal-caret { transform: rotate(90deg); }

.cal-why {
  grid-column: 1 / -1;
  /* 102px is deliberate and off-grid: it aligns this row's left edge with the
     start of the column above it, whose width is set by content rather than by
     the spacing scale. Snapping it to 104 would misalign it by 2px. */
  margin: var(--space-2) 0 var(--space-1) 102px;
  padding-left: var(--space-3);
  border-left: 2px solid var(--border);
  font-weight: 400;
}
.cal-why p {
  margin: 0;
  font-size: var(--t-micro);
  line-height: 1.65;
  color: var(--ink-2);
}
.cal-prev { margin-top: var(--space-2); font-size: var(--t-micro); line-height: 1.5; }
.cal-prev-tag {
  text-transform: uppercase;
  letter-spacing: 0.05em;
  font-size: var(--t-micro);
  color: var(--ink-muted);
  margin-right: var(--space-2);
}
.cal-prev a { color: var(--ink-2); }
.cal-prev-age { color: var(--ink-muted); margin-left: var(--space-2); }

/* Impact reads as text, not as a colour alone — the band has to survive both a
 * greyscale print and the ~8% of male readers with a red/green deficiency. */
.cal-impact {
  font-size: var(--t-micro);
  text-transform: uppercase;
  letter-spacing: 0.05em;
  white-space: nowrap;
  padding: var(--space-0) var(--space-2);
  border-radius: var(--r-pill);
  border: 1px solid var(--border);
  color: var(--ink-muted);
  font-weight: 500;
}
.cal-impact.high {
  color: var(--warn);
  border-color: color-mix(in srgb, var(--warn) 45%, transparent);
  background: color-mix(in srgb, var(--warn) 10%, transparent);
  font-weight: 600;
}
.cal-impact.medium { color: var(--ink-2); border-color: var(--border-strong); }
/* The low band is de-emphasised through its border, not by fading the text.
 * opacity: 0.7 here measured 3.05:1 against the panel — below the 4.5:1 that
 * 10px text needs — because opacity blends toward the background and the
 * computed `color` still reports the unfaded value, so it looks compliant when
 * it is not. The border carries the hierarchy and the label stays legible. */
.cal-impact.low {
  color: var(--ink-muted);
  border-color: color-mix(in srgb, var(--border) 55%, transparent);
}
.read-cal-time {
  font-size: var(--t-micro);
  color: var(--ink-muted);
  font-variant-numeric: tabular-nums;
}
.read-cal-what { min-width: 0; }
.read-cal-what a { color: inherit; text-decoration: none; }
.read-cal-what a:hover,
.read-cal-what a:focus-visible { text-decoration: underline; }
.read-cal-who {
  font-size: var(--t-micro);
  color: var(--ink-muted);
  white-space: nowrap;
}
.chip.strong { border-color: var(--border-strong); color: var(--ink); }
.chip.strong .dot { background: var(--s1); }

@media (max-width: 620px) {
  .read-cal-day { grid-template-columns: 1fr; gap: var(--space-1); }
  .read-cal-row { grid-template-columns: 74px 1fr; }
  details.read-cal-row > summary { grid-template-columns: 74px 1fr 14px; }
  .read-cal-who { grid-column: 2; }
  .cal-impact { grid-column: 2; justify-self: start; }
  .cal-nums { grid-column: 2; justify-content: start; max-width: 200px; }
  .cal-why { margin-left: 0; }
  .read-lead-title { font-size: var(--t-heading); }
}

/* Collapsible glossary inside the score panel. See the comment at its use site:
 * five paragraphs of static explanation on every ticker made the verdict panel
 * three times the height of the quote panel next to it. */
.factor-defs-wrap { margin-top: var(--space-3); }
.factor-defs-wrap > summary {
  cursor: pointer;
  font-size: var(--t-small);
  color: var(--ink-2);
  padding: var(--space-1) 0;
  list-style: none;
  display: flex;
  align-items: center;
  gap: var(--space-2);
  user-select: none;
}
.factor-defs-wrap > summary::-webkit-details-marker { display: none; }
.factor-defs-wrap > summary::before {
  content: '▸';
  font-size: 0.9em;
  color: var(--ink-muted);
  transition: transform 140ms ease;
}
.factor-defs-wrap[open] > summary::before { transform: rotate(90deg); }
.factor-defs-wrap > summary:hover { color: var(--ink); }
.factor-defs-wrap > summary:focus-visible {
  outline: 2px solid var(--focus);
  outline-offset: 2px;
  border-radius: var(--r-xs);
}
.factor-defs-wrap .factor-defs { margin-top: var(--space-1); }

@media (prefers-reduced-motion: reduce) {
  .factor-defs-wrap > summary::before { transition: none; }
}

/* A grid child that should occupy the whole row instead of one cell. Used where
 * an odd number of blocks would otherwise leave an empty cell inside a panel. */
.grid > .span-all { grid-column: 1 / -1; }

/* Vertical rhythm between stacked blocks.
 *
 * This was `style="margin-bottom:14px"` written out 48 times in app.js. 14px is
 * not on the spacing scale (4/8/12/16/20/28), so the most common gap in the whole
 * interface was the one value the scale did not contain — and it could only be
 * changed by editing 48 string literals. Snapped to --s5. */
.gap { margin-bottom: var(--space-5); }

/* ------------------------------------------------------- the page rhythm ---
 *
 * `.gap` above is opt-in, and eighty-one blocks opt in. The defect is the ones
 * that forget: measured across all nineteen views, sixteen boundaries had NO
 * space at all, so a panel's closing caveat ran straight into the next heading.
 * The daily Read was the worst, with seven flush boundaries down one page.
 *
 * Opt-in spacing cannot be kept consistent by hand. A new panel is correct only
 * if whoever wrote it remembered a class that does nothing visible when it is
 * absent, and the symptom is two blocks touching rather than anything that
 * looks broken on its own. So the view carries a default and a block no longer
 * has to ask.
 *
 * ZERO SPECIFICITY, which is the whole design. Everything sits inside
 * `:where()`, so this rule loses every argument it has: `.gap`'s own
 * --space-5, `.sec-head`'s --space-3, and the Read view's four --space-4
 * boundaries (`.read-strip`, `.read-lead`, `.read-nav`, `.read-desks`) all
 * win and keep the spacing they were given. It only fills in where nothing was
 * declared at all, which is exactly the set that was broken: `.panel` declares
 * no margin of its own.
 *
 * A first attempt used `> * + * { margin-top }` and relied on adjacent sibling
 * margins collapsing so the eighty-one opt-ins would not double. The collapsing
 * worked, but the rule outranked the deliberate values: measured, six chrome
 * boundaries that were 14px and 18px by choice became 23px, including the
 * header above every dossier page. A default has to be beatable, so it is
 * expressed as one.
 *
 * `:not(:last-child)` so a view does not end on 23px of nothing, and
 * `:not(:empty)` so neither does a host that has not filled yet. The tracker
 * has two of those: `#roth-host` and the portfolio-risk host are empty divs in
 * index.html that a later fetch writes into, and spacing something with no
 * content in it is the same 23px-of-nothing in a different place. Measured
 * after this rule shipped: one such spacer on the tracker, which is how it was
 * found. `:empty` stops matching the moment content lands, so the rhythm
 * arrives with it.
 */
:where([role="tabpanel"] > *:not(:last-child):not(:empty)) { margin-bottom: var(--space-5); }

/* The charting view is a grid: margins do not collapse in one, and its layout
   is the dock beside the chart, spaced by the grid rather than by this. An id
   so it beats the default outright rather than by source order. */
#view-chart > * { margin-bottom: 0; }

/* ------------------------------------------------------- scroll affordance, pt 2
 *
 * `.panel` already carries scroll shadows (see the block above). Measured on a
 * 375px viewport, twelve containers on the swing view clip horizontally and only
 * the panels among them said so — `.scroll-y` boxes were hiding 468px and 476px
 * of table with no visual cue at all, and nested grid cells another 73-247px.
 *
 * Same four-gradient technique, factored into a class so there is one definition
 * rather than a second copy that drifts. `local` gradients mask the content edge
 * and travel with it; `scroll` gradients stay put and only show while there is
 * more to reach.
 */
/* .scroll-shadow-x used to lead this list and was applied to nothing — the
   comment above describes "a class so there is one definition", and the class
   itself never made it into any markup. The two live selectors are the whole
   of it. */
.scroll-y,
.panel .grid > * {
  background-image:
    linear-gradient(to right, var(--scroll-cover, var(--surface)) 40%, transparent),
    linear-gradient(to left, var(--scroll-cover, var(--surface)) 40%, transparent),
    linear-gradient(to right, var(--scroll-shade), transparent),
    linear-gradient(to left, var(--scroll-shade), transparent);
  background-repeat: no-repeat;
  background-size: 34px 100%, 34px 100%, 16px 100%, 16px 100%;
  background-position: 0 0, 100% 0, 0 0, 100% 0;
  background-attachment: local, local, scroll, scroll;
}
/* The covers are the box's own fill.
 *
 * They hide the shades by matching what is under them, so they have to be the
 * colour of the box they sit in. A tile is a box with a fill of its own, and in
 * the panel's colour the covers painted a 34px fade over both edges of every
 * tile in a panel grid: 53 of them across Options, Earnings, Investing, Macro
 * and Indices. Reported as "these boxes are fading away, fix this and keep the
 * text within the boxes": with the edges faded, the box read narrower than its
 * text. A box with its own fill names it as --scroll-cover, as `.tile` does. */

/* `.scroll-y` also scrolls vertically, so it needs the horizontal pair plus a
   matching pair on the y axis — otherwise a 340px-capped box gives no hint that
   the list continues below the fold of the box. */
.scroll-y {
  background-image:
    linear-gradient(to right, var(--surface) 40%, transparent),
    linear-gradient(to left, var(--surface) 40%, transparent),
    linear-gradient(to bottom, var(--surface) 40%, transparent),
    linear-gradient(to top, var(--surface) 40%, transparent),
    linear-gradient(to right, var(--scroll-shade), transparent),
    linear-gradient(to left, var(--scroll-shade), transparent),
    linear-gradient(to bottom, var(--scroll-shade), transparent),
    linear-gradient(to top, var(--scroll-shade), transparent);
  background-size:
    34px 100%, 34px 100%, 100% 26px, 100% 26px,
    16px 100%, 16px 100%, 100% 12px, 100% 12px;
  background-position:
    0 0, 100% 0, 0 0, 0 100%,
    0 0, 100% 0, 0 0, 0 100%;
  background-attachment:
    local, local, local, local,
    scroll, scroll, scroll, scroll;
}

/* ----------------------------------------------------------- touch targets
 *
 * Measured at 375px: fifteen controls under 40px tall, the worst being inline
 * text buttons at 16-17px ("change", "More", "Site"). Apple asks for 44pt and
 * Material for 48dp; 17px is a coin toss with a thumb.
 *
 * The hit area is expanded with a pseudo-element rather than padding, so the
 * text stays exactly where it is and nothing reflows. Only under `pointer:
 * coarse`, because on a mouse these are correctly sized and a 44px invisible
 * box would swallow clicks meant for neighbouring text.
 */
@media (pointer: coarse) {
  .ses-co-more,
  .ses-co-link,
  [data-goto-settings],
  .legal-area a,
  .home-foot a {
    position: relative;
  }
  .ses-co-more::after,
  .ses-co-link::after,
  [data-goto-settings]::after,
  .legal-area a::after,
  .home-foot a::after {
    content: '';
    position: absolute;
    /* 17px text + 14px either side ≈ 45px of reachable height. */
    inset: -14px -8px;
  }
  /* The header controls and tabs sit just under the 44px line; lift them onto it.
     The tabs measured 43px — one pixel, but the whole point of a guideline is
     that it is a floor and not a target to land near. */
  #ticker-input, #ticker-form button, .icon-btn, #chat-toggle,
  nav.tabs button {
    min-height: 44px;
  }
}

/* ------------------------------------------ balanced columns in two-up rows
 *
 * There is no cap here any more. What used to be:
 *
 *   .grid.c2 > .panel:not(.span2):not(:has(svg.chart)):not(:has(.chart-controls))
 *     { max-height: 640px; overflow-y: auto; }
 *
 * capped a tall panel in a two-column row so both columns ended at roughly the
 * same place, on the argument that "no information is removed". Two things were
 * wrong with that.
 *
 * It swallowed the wheel. `.panel` carries `overflow-x: auto` for wide tables,
 * and a box with one axis scrollable and the other `visible` computes the
 * visible axis to `auto` — so the cap turned those panels into VERTICAL scroll
 * containers too. Scrolling the page with the pointer over one of them scrolled
 * the panel instead, stopped, and then chained to the document. Reported as
 * "scrolling up and down is messed up", and measured on Investing at both
 * 720px and 1400px: "Long-term view" 638px of 754, "Valuation vs its own
 * history" 638px of 850 — 116px and 212px sitting behind an inner scrollbar on
 * a page that already scrolls.
 *
 * And information WAS removed, in the only sense that matters: behind a
 * scrollbar nobody expects inside a panel on a scrolling page.
 *
 * The phone escape hatch that was supposed to limit this had never worked. It
 * read `.grid.c2 > .panel:not(.span2)` at max-width 860px — four classes
 * against the six in the capping selector, and a media query adds no
 * specificity — so the cap applied at every width from the day the `:has()`
 * exemptions were added.
 *
 * Uneven columns are the honest outcome, and `align-items: start` already
 * handles them. A reader who finds a panel too long can collapse it: every
 * panel with a heading is collapsible, which is a better answer to "this is
 * long" than a scroll box, and it did not exist when the cap was written.
 */

/* -------------------------------------------------- tile rows fill their width
 *
 * `.grid.c4` and `.grid.c3` hold rows of small stat tiles. As CSS grid, a row
 * that isn't full leaves real empty cells: measured on Optic Portfolio, four
 * tiles resolved to three columns, so the fourth sat alone with 589px of nothing
 * beside it — five times on that tab, and the same shape on Earnings, Roth and
 * Long-Term.
 *
 * Grid cannot stretch a short final row; flex can. `flex: 1 1 <basis>` lets the
 * leftover tiles grow to fill the line, so an orphan tile spans the width instead
 * of leaving a hole. The wrap points stay where they were because the basis
 * matches the old minmax() minimums.
 *
 * c2 stays grid — it holds panels, where equal column widths matter more than a
 * filled last row.
 */
.grid.c3,
.grid.c4 {
  display: flex;
  flex-wrap: wrap;
}
.grid.c3 > * { flex: 1 1 300px; min-width: 0; }
/* 172px, not 210px: a panel's inner width is ~740px, so at 210px only three
   tiles fit and the fourth stretches across a row on its own. Four is what
   c4 means, and 172px is still comfortable for a label and a figure. */
.grid.c4 > * { flex: 1 1 172px; min-width: 0; }
/* span-all still means "take the whole line". */
.grid.c3 > .span-all,
.grid.c4 > .span-all { flex-basis: 100%; }

/* A lone panel on a two-up row takes the whole row.
 *
 * With `align-items: start`, a single item in a two-column grid occupies one
 * column and leaves the other empty — measured at 463x472 and 463x558 on Swing
 * and Long-Term. That happens whenever a c2 grid holds an odd number of panels.
 *
 * An earlier attempt spanned *chart* panels instead, on the theory that charts
 * want width. It made things worse (Swing dead space 303K -> 534K px2): spanning
 * the chart pushed its partner onto a row by itself, which is exactly the case
 * this rule exists to catch. Fixing the lone row directly covers both.
 */
.grid.c2 > *:last-child:nth-child(odd) {
  grid-column: 1 / -1;
}

/* The same rule, counting only what is on screen.
 *
 * `:last-child` and `:nth-child` count hidden elements, and these grids carry
 * them: a panel switched off by Simple mode or the panel chooser keeps its
 * `hidden` attribute and its place in the child order. So on Investing the
 * rule above was matching "Valuation & accumulation", which is hidden, while
 * "Valuation vs its own history" -- the actual last visible panel, 743px tall
 * -- sat in one column with 577px of nothing beside it. Measured at 1182px.
 *
 * `of :not([hidden])` is what makes the selector count the visible ones. Kept
 * alongside the original rather than replacing it: where nothing is hidden the
 * two agree, and where the syntax is unsupported the old behaviour stands. */
.grid.c2 > *:nth-last-child(1 of :not([hidden])):nth-child(odd of :not([hidden])) {
  grid-column: 1 / -1;
}

/* And the case parity cannot see at all: a full-width sibling.
 *
 * `nth-child` counts children, not columns, so one `.span-all` or `.span2` in
 * the grid puts every later panel's parity one out. Measured on Investing:
 * "Valuation vs its own history" is the last visible child and sits at visible
 * position four -- even -- so the parity rule above declined it, while the
 * `.span-all` wrapper two places before it had already closed that row. 743px
 * tall in one column with 577 of nothing beside it.
 *
 * Adjacency answers it without counting. An element that directly follows a
 * full-width sibling starts a row; if it is also the last visible child,
 * nothing can pair with it, so it takes the row. A panel preceded by an
 * ordinary half-width panel is left alone, because that one has a partner. */
.grid.c2 > :is(
    :first-child,
    .span-all + *,
    .span2 + *,
    .panel.is-closed + *
  ):is(
    :has(+ .span-all),
    :has(+ .span2),
    :has(+ .panel.is-closed),
    :nth-last-child(1 of :not([hidden]))
  ) {
  grid-column: 1 / -1;
}

/* A collapsed panel is a title bar, and a title bar has no reason to take half
   a row.
 *
 * Measured on Investing at 1182px: "Drawdown" closed to 65px and held one
 * column, leaving 577x65 empty beside it, and the span2 panels above and below
 * it could not pair with anything. Full width turns the closed ones into a
 * clean stack you can read down, and leaves the two-up layout to the panels
 * that actually have content in them. */
.grid.c2 > .panel.is-closed,
.grid.c3 > .panel.is-closed,
.grid.c4 > .panel.is-closed {
  grid-column: 1 / -1;
}

/* A ticker that opens its own analysis. Styled as text rather than a button so a
   table of holdings still reads as a table, with the affordance on hover. */
.tkr {
  appearance: none;
  background: none;
  border: 0;
  padding: 0;
  font: inherit;
  font-weight: 700;
  color: var(--ink);
  cursor: pointer;
  border-bottom: 1px solid transparent;
}
.tkr:hover { color: var(--s1); border-bottom-color: currentColor; }
.tkr:focus-visible { outline: 2px solid var(--focus); outline-offset: 2px; border-radius: 2px; }

/* Live marker on the leading point of a chart's primary series.
 *
 * The halo expands and fades on a loop; the dot itself breathes very slightly.
 * Set only while the session is open — see setChartLive(). A blinking dot on a
 * closed market would be claiming movement that isn't happening. */
.live-halo {
  transform-box: fill-box;
  transform-origin: center;
  animation: live-halo 1.9s ease-out infinite;
}
@keyframes live-halo {
  0%   { transform: scale(1);   opacity: 0.55; }
  70%  { transform: scale(2.6); opacity: 0; }
  100% { transform: scale(2.6); opacity: 0; }
}
.live-dot {
  transform-box: fill-box;
  transform-origin: center;
  animation: live-dot 1.9s ease-in-out infinite;
}
@keyframes live-dot {
  0%, 100% { transform: scale(1); }
  50%      { transform: scale(1.14); }
}
@media (prefers-reduced-motion: reduce) {
  .live-halo { animation: none; opacity: 0.4; }
  .live-dot { animation: none; }
}

/* Timeframe pills. One tap to switch, whole set visible — the dropdown they
   replaced hid its options and cost two interactions, and on a phone the native
   picker covered the chart you were switching timeframe to compare. */
.pills {
  display: inline-flex;
  gap: var(--space-0);
  padding: var(--space-0);
  background: var(--surface-2, var(--surface));
  border: 1px solid var(--border);
  border-radius: var(--r-pill);
}
/* Styled on the class itself, not on `.pills .pill`.
 *
 * The descendant selector required a .pills wrapper that almost nothing
 * renders: pills live in .econ-pills and .map-picker instead. Measured on the
 * Macro tab, all 31 pills were unstyled native buttons — ButtonFace grey with
 * inherited near-black text at 3.69:1. They looked like OS buttons dropped into
 * the page, which is what they were.
 *
 * Dropping the ancestor lowers specificity from (0,2,0) to (0,1,0). Safe: no
 * other rule in this file sets a background on .pill. */
.pill {
  appearance: none;
  border: 0;
  background: none;
  color: var(--ink-2);
  font: inherit;
  font-size: var(--t-caption);
  font-weight: 600;
  letter-spacing: 0.02em;
  padding: var(--space-1) var(--space-3);
  border-radius: var(--r-pill);
  cursor: pointer;
  transition: background 120ms ease, color 120ms ease;
  min-width: 34px;
}
.pill:hover { color: var(--ink); }
.pill.on {
  /* --btn-primary, not --s1. White on --s1 (#3987e5) is 3.64:1; --btn-primary
     exists precisely because a fill that carries white text needs to clear
     4.5:1, and it does at 4.72. --s1 stays what it is: a line colour on charts,
     where nothing sits on top of it. */
  background: var(--btn-primary);
  color: var(--btn-primary-ink);
}
.pill:focus-visible { outline: 2px solid var(--focus); outline-offset: 2px; }

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

/* ------------------------------------------------- the case both ways
 * Two columns of arguments, each citing the panel that produced it. Colour is
 * carried by a small dot rather than the text, so a long bear list doesn't turn
 * the page red — the arguments should read as evidence, not as an alarm. */
.case-col h3 { display: flex; align-items: center; gap: var(--space-2); margin: 0 0 var(--space-2); }
.case-tag {
  font-size: var(--t-micro);
  font-weight: 700;
  letter-spacing: 0.06em;
  text-transform: uppercase;
  padding: var(--space-0) var(--space-2);
  border-radius: var(--r-pill);
}
.case-tag.bull { background: color-mix(in oklab, var(--pos) 18%, transparent); color: var(--pos); }
.case-tag.bear { background: color-mix(in oklab, var(--neg) 18%, transparent); color: var(--neg); }

.case-list { list-style: none; margin: 0; padding: 0; display: grid; gap: var(--space-3); }
.case-row { display: flex; gap: var(--space-2); align-items: flex-start; }
.case-row strong { font-size: var(--t-body); font-weight: 600; }

/* Dot size encodes how much weight the argument carries. */
.case-dot {
  flex: none;
  width: 7px; height: 7px;
  border-radius: 50%;
  margin-top: var(--space-2);
  opacity: 0.45;
}
.case-dot.s2 { width: 9px; height: 9px; opacity: 0.7; margin-top: var(--space-1); }
.case-dot.s3 { width: 11px; height: 11px; opacity: 1; margin-top: var(--space-1); }
.case-dot.bull { background: var(--pos); }
.case-dot.bear { background: var(--neg); }

.case-src {
  font-size: var(--t-micro);
  color: var(--ink-muted);
  text-transform: uppercase;
  letter-spacing: 0.05em;
  margin-left: var(--space-2);
}
.case-detail { font-size: var(--t-small); color: var(--ink-2); margin-top: var(--space-0); }

/* Staged chat attachments. Chips rather than thumbnails: the panel is ~330px and
   a grid of previews would push the composer off-screen. */
.chat-attachments {
  display: flex;
  flex-wrap: wrap;
  gap: var(--space-2);
  margin: 0 0 var(--space-2);
}
.chat-attachments:empty { display: none; }

/* Drop target feedback while dragging a file over the panel. */
#chat.dropping { outline: 2px dashed var(--s1); outline-offset: -6px; }

/* An excluded composite input. Dimmed but legible — the point is that you can
   read which category dropped out, so hiding it defeats the purpose. */
.row-excluded > td { opacity: 0.72; }
.row-excluded .name { color: var(--ink-2); }

/* ------------------------------------------------- collapsible panel headings
 *
 * The heading is the control. A chevron on the right rather than the left, so the
 * headings stay flush with the panels above and below them and the row does not
 * read as an indented list item. */
.panel > h2.is-toggle {
  display: flex;
  align-items: center;
  gap: var(--space-2);
  cursor: pointer;
  user-select: none;
  margin-bottom: 0;
}
.panel.is-open > h2.is-toggle { margin-bottom: var(--space-4); }
.panel > h2.is-toggle:hover { color: var(--ink); }

.panel-toggle {
  appearance: none;
  background: none;
  border: 0;
  margin-left: auto;
  padding: var(--space-0) var(--space-1);
  cursor: pointer;
  color: var(--ink-muted);
  line-height: 1;
  flex: none;
}
.panel-toggle::before {
  content: '';
  display: block;
  width: 7px;
  height: 7px;
  border-right: 1.6px solid currentColor;
  border-bottom: 1.6px solid currentColor;
  transform: rotate(45deg);
  transition: transform 150ms ease;
}
.panel.is-open .panel-toggle::before { transform: rotate(-135deg); }
.panel > h2.is-toggle:hover .panel-toggle { color: var(--ink); }
.panel-toggle:focus-visible {
  outline: 2px solid var(--focus);
  outline-offset: 2px;
  border-radius: var(--r-xs);
}

/* Hidden rather than height-animated: these bodies contain SVG charts that get
   measured on insert, and animating a container they live in makes getTotalLength
   and getBoundingClientRect return mid-transition values. */
.panel.is-closed > .panel-body { display: none; }
/* Every collapsible panel's content sits in one of these, and it had no
 * containment of its own.
 *
 * Measured at 768px on the Options facet: a `table.data` ran 8px past its
 * `.panel-body` with nothing able to scroll to it. Small, but it is the same
 * fault as the catalyst table 144px past its card -- `table.data` carries a
 * 480px floor, and an overflow escaping a parent with no width constraint
 * does not reliably give the scrolling ancestor anything to scroll.
 *
 * Fixed here rather than per-panel because this wrapper is created by
 * `makePanelsCollapsible` for every panel in the product, so the containment
 * belongs to the wrapper rather than to whichever table happens to be too
 * wide this week. */
.panel > .panel-body { min-width: 0; overflow-x: auto; }

/* A closed panel is a row, not a card — drop the tall padding so a stack of them
   reads as a list of sections you can open. */
.panel.is-closed {
  padding-top: var(--space-4);
  padding-bottom: var(--space-4);
}

@media (prefers-reduced-motion: reduce) {
  .panel-toggle::before { transition: none; }
}

/* ---------------------------------------------------- written briefs ------ */

/* Section headings inside model-written prose. Sized between a panel h2 and body
 * text: these divide one panel's argument, they do not introduce a new panel. */
.prose-h {
  font-size: var(--t-body);
  font-weight: 600;
  letter-spacing: -0.005em;
  color: var(--ink);
  margin: var(--space-5) 0 var(--space-2);
}
.prose-h:first-child { margin-top: 0; }

.earn-brief-head {
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  gap: var(--space-3);
  flex-wrap: wrap;
}
.earn-brief-lede {
  font-size: var(--t-base);
  line-height: 1.5;
  color: var(--ink);
  margin: var(--space-0) 0 var(--space-3);
  font-weight: 500;
}
/* Measure. Prose set the full width of a wide panel is genuinely harder to
 * read — the eye loses the line it is returning to — so the brief is capped
 * where the tables around it are not. */
.earn-brief, .brief-summary { max-width: 74ch; }
.earn-brief p, .brief-summary p {
  margin: 0 0 var(--space-3);
  line-height: 1.65;
  color: var(--ink-2);
}
.earn-brief p:last-child, .brief-summary p:last-child { margin-bottom: 0; }
.earn-brief strong, .brief-summary strong { color: var(--ink); font-weight: 600; }

/* The stance. Colour is a reinforcement, never the carrier — the words
 * "leaning bearish" say it outright, so the badge still reads in greyscale. */
.earn-stance {
  font-size: var(--t-micro);
  text-transform: uppercase;
  letter-spacing: 0.05em;
  font-weight: 600;
  padding: var(--space-0) var(--space-2);
  border-radius: var(--r-pill);
  border: 1px solid var(--border-strong);
  color: var(--ink-2);
  white-space: nowrap;
}
.earn-stance.up { color: var(--pos); border-color: color-mix(in srgb, var(--pos) 45%, transparent); }
.earn-stance.down { color: var(--neg); border-color: color-mix(in srgb, var(--neg) 45%, transparent); }

/* ----------------------------------------------------- index regime ------ */

.regime-head {
  display: flex;
  align-items: baseline;
  gap: var(--space-6);
  flex-wrap: wrap;
  margin-bottom: var(--space-4);
}
.regime-verdict { min-width: 0; }
.regime-stance {
  font-size: var(--t-micro);
  text-transform: uppercase;
  letter-spacing: 0.05em;
  font-weight: 600;
  color: var(--ink-2);
}
.regime-plain {
  margin: var(--space-1) 0 0;
  font-size: var(--t-body);
  color: var(--ink);
  line-height: 1.5;
}

/* Centre-anchored so the zero line is visible as a line, not implied. A bar
 * growing left reads as negative without needing to also colour-code it. */
.regime-bar {
  position: relative;
  display: block;
  height: 7px;
  border-radius: var(--r-xs);
  background: var(--surface-2);
  overflow: hidden;
}
.regime-bar::before {
  content: '';
  position: absolute;
  left: 50%;
  top: 0;
  bottom: 0;
  width: 1px;
  background: var(--border-strong);
  z-index: 1;
}
.regime-fill {
  position: absolute;
  top: 0;
  bottom: 0;
  border-radius: 3px;
}
.regime-fill.pos { background: var(--pos); }
.regime-fill.neg { background: var(--neg); }

.regime-table td { vertical-align: middle; }
.regime-notes {
  margin: var(--space-3) 0 0;
  padding-left: var(--space-5);
  font-size: var(--t-micro);
  line-height: 1.65;
  color: var(--ink-2);
}
.regime-notes li { margin-bottom: var(--space-0); }

@media (max-width: 620px) {
  .regime-head { gap: var(--space-3); }
}

/* ---------------------------------------------------------- scanners ------ */

/* The group's scans: the chart pages' own tab strip (`.sec-tabs`), with a
 * rule under it so the open tab's results read as the strip's, and the
 * results themselves in the same panel below. */
.scan-tabs {
  margin-top: var(--space-4);
  border-bottom: 1px solid var(--border);
  /* Wrapped rather than scrolled, as the chart pages' strip is: a scan's name
     is a phrase ("Extended. Stretched from the mean"), and the strip's hidden
     scrollbar cut the last one off mid-word with nothing to say there was
     more. */
  flex-wrap: wrap;
}
.scan-tabs .sec-tab { margin-bottom: -1px; }
.sec-tab:focus-visible { outline: 2px solid var(--focus); outline-offset: -2px; }
.scan-result { padding-top: var(--space-3); }
.scan-reads-h { margin: var(--space-4) 0 var(--space-1); font-size: var(--t-body); }

/* The blind-spot note. Muted rather than alarming — it is a standing property of
 * the scan, not a warning that something has gone wrong. */
/* How far the universe ranking's build has got, under the sentence saying so.
   See scanBuildBar. */
.scan-build-bar {
  height: 6px;
  margin-top: var(--space-2);
  border-radius: var(--r-pill);
  background: var(--surface-2);
  overflow: hidden;
}
.scan-build-bar > span {
  display: block;
  height: 100%;
  background: var(--accent);
  border-radius: inherit;
  transition: width var(--dur-ui) var(--ease-ui);
}
@media (prefers-reduced-motion: reduce) { .scan-build-bar > span { transition: none; } }
.scan-blind {
  margin-top: var(--space-4);
  font-size: var(--t-micro);
  line-height: 1.65;
  color: var(--ink-2);
}

/* --------------------------------------------------------- Ask Pulse ------ */

/* Sits beside a heading, so it must not compete with it: normal weight, muted
 * until hovered. It is an offer, not a call to action. */
.ask-pulse {
  font: inherit;
  font-size: var(--t-micro);
  font-weight: 500;
  line-height: 1;
  margin-left: var(--space-3);
  padding: var(--space-1) var(--space-2);
  border-radius: var(--r-pill);
  border: 1px solid var(--border);
  background: transparent;
  color: var(--ink-muted);
  cursor: pointer;
  vertical-align: middle;
  white-space: nowrap;
  transition: border-color 120ms ease, color 120ms ease, background 120ms ease;
}
.ask-pulse:hover {
  border-color: var(--s1);
  color: var(--ink);
  background: color-mix(in srgb, var(--s1) 12%, transparent);
}
.ask-pulse:focus-visible { outline: 2px solid var(--focus); outline-offset: 2px; }

h3 .ask-pulse { font-size: var(--t-micro); padding: var(--space-0) var(--space-2); }

@media (max-width: 620px) {
  .ask-pulse { margin-left: var(--space-2); }
}

/* ------------------------------------------------------ bulk collapse ----- */

.panel-bulk {
  display: flex;
  justify-content: flex-end;
  gap: var(--space-2);
  margin: 0 0 -4px;
}
.bulk-btn {
  font: inherit;
  font-size: var(--t-micro);
  padding: var(--space-1) var(--space-3);
  border-radius: var(--r-pill);
  border: 1px solid var(--border);
  background: transparent;
  color: var(--ink-muted);
  cursor: pointer;
  transition: border-color 120ms ease, color 120ms ease;
}
.bulk-btn:hover { border-color: var(--border-strong); color: var(--ink); }
.bulk-btn:focus-visible { outline: 2px solid var(--focus); outline-offset: 2px; }

/* A disabled chart control still has to be readable — it is explaining why an
 * option does not apply here, not hiding that it exists. */
.seg button[disabled] {
  opacity: 1;
  color: var(--ink-muted);
  cursor: not-allowed;
  background: transparent;
}
.seg button[disabled] .lvl-key { opacity: 0.4; }

/* ------------------------------------------------------ sector board ----- */

.table-scroll { overflow-x: auto; }
.sector-board td, .sector-board th { white-space: nowrap; }
.sector-board .num { font-variant-numeric: tabular-nums; }

/* Trend and rotation both read as words first. Colour reinforces; it never
 * carries the meaning on its own. */
.sec-trend, .sec-rot {
  display: inline-block;
  font-size: var(--t-micro);
  font-weight: 600;
  padding: var(--space-0) var(--space-2);
  border-radius: var(--r-pill);
  border: 1px solid var(--border);
  color: var(--ink-2);
  white-space: nowrap;
}
.sec-trend.up, .sec-rot.up {
  color: var(--pos);
  border-color: color-mix(in srgb, var(--pos) 45%, transparent);
  background: color-mix(in srgb, var(--pos) 10%, transparent);
}
.sec-trend.down, .sec-rot.down {
  color: var(--neg);
  border-color: color-mix(in srgb, var(--neg) 45%, transparent);
  background: color-mix(in srgb, var(--neg) 10%, transparent);
}
.sec-rot.warn {
  color: var(--warn);
  border-color: color-mix(in srgb, var(--warn) 45%, transparent);
  background: color-mix(in srgb, var(--warn) 10%, transparent);
}
.sec-trend.flat, .sec-rot.flat { color: var(--ink-muted); }

/* "held" / "changed today" — the trend's own history, kept subordinate to the
 * state itself so the column still scans as one word per row. */
.sec-note {
  margin-left: var(--space-2);
  font-size: var(--t-micro);
  text-transform: uppercase;
  letter-spacing: 0.04em;
  color: var(--ink-muted);
}
.sec-note.flip { color: var(--warn); }

/* -------------------------------------------------- index board cards ---- */

.idx-cards {
  display: flex;
  flex-wrap: wrap;
  gap: var(--space-3);
  margin-top: var(--space-3);
}
.idx-card {
  flex: 1 1 240px;
  min-width: 0;
  padding: var(--space-4) var(--space-4);
  border: 1px solid var(--border);
  border-radius: var(--r-md);
  background: var(--surface-2);
}
/* The trending cards are outlined, not filled: a card is a lot of area, and a
 * tinted block that size reads as an alert rather than a state. */
.idx-card.up { border-color: color-mix(in srgb, var(--pos) 50%, transparent); }
.idx-card.down { border-color: color-mix(in srgb, var(--neg) 50%, transparent); }

.idx-card-head {
  display: flex;
  justify-content: space-between;
  align-items: flex-start;
  gap: var(--space-3);
  margin-bottom: var(--space-3);
}
.idx-sym {
  font-size: var(--t-heading);
  font-weight: 700;
  letter-spacing: -0.01em;
  font-variant-numeric: tabular-nums;
}
.idx-name { font-size: var(--t-micro); color: var(--ink-muted); margin-top: var(--space-0); }

.idx-nums { display: flex; gap: var(--space-4); flex-wrap: wrap; }
.idx-nums > div { display: flex; flex-direction: column; gap: var(--space-0); }
.idx-lbl {
  font-size: var(--t-micro);
  letter-spacing: 0.01em;
  color: var(--ink-muted);
}
.idx-val { font-size: var(--t-body); font-weight: 600; font-variant-numeric: tabular-nums; }
.idx-val.pos { color: var(--pos); }
.idx-val.neg { color: var(--neg); }

.idx-summary {
  margin: var(--space-3) 0 var(--space-3);
  font-size: var(--t-micro);
  line-height: 1.5;
  color: var(--ink-2);
}

.sector-read-btn {
  font: inherit;
  font-size: var(--t-micro);
  padding: 0;
  border: 0;
  background: transparent;
  color: var(--s1);
  cursor: pointer;
  white-space: nowrap;
}
.sector-read-btn:hover { text-decoration: underline; }
.sector-read-btn:focus-visible { outline: 2px solid var(--focus); outline-offset: 2px; }

.sector-read { margin-top: var(--space-4); border-color: var(--border-strong); }
.fg-score { font-size: var(--t-d2); font-weight: 700; line-height: 1; font-variant-numeric: tabular-nums; }
.fg-label {
  margin-top: var(--space-1);
  font-size: var(--t-micro);
  letter-spacing: 0.01em;
  font-weight: 600;
}

/* The five bands. Colour reinforces the word, which is always present — the
 * dial reads correctly in greyscale because "Extreme Greed" is written on it. */
.fg-xfear { color: var(--critical); border-color: color-mix(in srgb, var(--critical) 60%, transparent); }
.fg-fear { color: var(--critical); border-color: color-mix(in srgb, var(--critical) 35%, transparent); }
.fg-neutral { color: var(--ink-2); }
.fg-greed { color: var(--good); border-color: color-mix(in srgb, var(--good) 35%, transparent); }
.fg-xgreed { color: var(--good); border-color: color-mix(in srgb, var(--good) 60%, transparent); }

.fg-hist { display: flex; gap: var(--space-5); flex-wrap: wrap; }
.fg-hist > div { display: flex; flex-direction: column; gap: var(--space-0); }
.fg-hl {
  font-size: var(--t-micro);
  text-transform: uppercase;
  letter-spacing: 0.05em;
  color: var(--ink-muted);
}
.fg-hv { font-size: var(--t-heading); font-weight: 600; font-variant-numeric: tabular-nums; }
.fg-delta { font-size: var(--t-micro); font-weight: 600; color: var(--ink-muted); }
.fg-delta.pos { color: var(--pos); }
.fg-delta.neg { color: var(--neg); }

/* ------------------------------------------------ weekly update ---------- */

.weekly-kicker {
  font-size: var(--t-micro);
  letter-spacing: 0.01em;
  color: var(--ink-muted);
  margin-bottom: var(--space-2);
}
.weekly-title {
  font-size: clamp(22px, 2.4vw, 30px);
  line-height: 1.3;
  letter-spacing: -0.015em;
  font-weight: 700;
  margin: 0 0 var(--space-2);
  text-transform: none;
}
.weekly-sub {
  font-size: var(--t-body);
  color: var(--ink-2);
  margin: 0 0 var(--space-4);
  max-width: 70ch;
}
/* Capped measure: prose across a full-width panel loses the reader on the
 * return sweep, which is the whole reason newspapers use columns. */
.weekly-body, .weekly-earnings { max-width: 74ch; }
.weekly-body p {
  margin: 0 0 var(--space-3);
  line-height: 1.65;
  color: var(--ink-2);
}
.weekly-body strong { color: var(--ink); font-weight: 600; }
.weekly-earnings ul { margin: var(--space-2) 0 0; padding-left: var(--space-5); }
.weekly-earnings li { margin-bottom: var(--space-1); line-height: 1.65; color: var(--ink-2); }
.weekly-earnings li strong { color: var(--ink); }

/* ---------------------------------------------------- catalyst library --- */

.cat-controls { display: flex; flex-direction: column; gap: var(--space-3); margin-top: var(--space-4); }
.cat-search { display: flex; gap: var(--space-2); }
.cat-search input {
  flex: 1 1 auto;
  font: inherit;
  padding: var(--space-2) var(--space-3);
  border-radius: var(--r-sm);
  border: 1px solid var(--border);
  background: var(--surface-2);
  color: var(--ink);
}
.cat-search input:focus-visible { outline: 2px solid var(--focus); outline-offset: 1px; }
.cat-filters { display: flex; gap: var(--space-4); flex-wrap: wrap; }
.cat-filters label {
  display: flex;
  align-items: center;
  gap: var(--space-2);
  font-size: var(--t-micro);
  color: var(--ink-muted);
}
.cat-filters select {
  font: inherit;
  font-size: var(--t-small);
  padding: var(--space-1) var(--space-2);
  border-radius: var(--r-sm);
  border: 1px solid var(--border);
  background: var(--surface-2);
  color: var(--ink);
}

.cat-list { display: flex; flex-direction: column; gap: var(--space-3); }
.cat-card {
  border: 1px solid var(--border);
  border-radius: var(--r-md);
  background: var(--surface-2);
}
.cat-card > summary {
  padding: var(--space-4) var(--space-4);
  cursor: pointer;
  list-style: none;
  position: relative;
}
.cat-card > summary::-webkit-details-marker { display: none; }
.cat-card > summary:hover { background: color-mix(in srgb, var(--s1) 5%, transparent); }
.cat-card > summary:focus-visible { outline: 2px solid var(--focus); outline-offset: -2px; }
.cat-card .cal-caret { position: absolute; right: 16px; top: 20px; }

.cat-meta { display: flex; align-items: center; gap: var(--space-3); flex-wrap: wrap; margin-bottom: var(--space-2); }
.cat-horizon {
  font-size: var(--t-micro);
  text-transform: uppercase;
  letter-spacing: 0.05em;
  font-weight: 600;
  padding: var(--space-0) var(--space-2);
  border-radius: var(--r-pill);
  border: 1px solid currentColor;
}
.cat-horizon.up { color: var(--pos); }
.cat-horizon.warn { color: var(--warn); }
.cat-horizon.flat { color: var(--ink-muted); }
.cat-cat, .cat-date {
  font-size: var(--t-micro);
  text-transform: uppercase;
  letter-spacing: 0.05em;
  color: var(--ink-muted);
}
.cat-archived {
  font-size: var(--t-micro);
  text-transform: uppercase;
  letter-spacing: 0.05em;
  color: var(--warn);
}
.cat-title { font-size: var(--t-heading); font-weight: 600; margin: 0 0 var(--space-1); line-height: 1.3; }
.cat-summary {
  margin: 0 0 var(--space-2);
  font-size: var(--t-small);
  line-height: 1.65;
  color: var(--ink-2);
  max-width: 78ch;
}
.cat-themes { font-size: var(--t-micro); color: var(--ink-muted); }
/* The catalyst table is wider than the card and nothing could scroll to it.
 *
 * Measured at 1280px: the table ran to x=1395 against a panel ending at 1251,
 * and the panel was NOT scrollable -- scrollWidth equalled clientWidth -- so
 * 144px of columns were clipped and unreachable rather than merely off to one
 * side. `table.data` carries `min-width: 480px`, and an overflow that escapes
 * a `visible` parent does not always give the scrolling ancestor anything to
 * scroll.
 *
 * `min-width: 0` plus a scroller of its own is the same containment
 * `.panel .grid > *` already uses a few hundred lines up, for the same reason
 * and after the same symptom. */
.cat-body {
  padding: 0 var(--space-4) var(--space-4);
  min-width: 0;
  overflow-x: auto;
}

.cat-tag {
  font-size: var(--t-micro);
  text-transform: uppercase;
  letter-spacing: 0.04em;
  font-weight: 600;
  padding: var(--space-0) var(--space-2);
  border-radius: var(--r-pill);
  border: 1px solid currentColor;
  white-space: nowrap;
}
.cat-tag.up { color: var(--pos); }
.cat-tag.down { color: var(--neg); }
.cat-tag.flat { color: var(--ink-muted); }

/* One company's own catalysts, above the library's, for a ticker search. */
.cat-ticker {
  margin: 0 0 var(--space-5);
  padding-bottom: var(--space-4);
  border-bottom: 1px solid var(--border);
}
.cat-ticker-title { font-size: var(--t-heading); font-weight: 600; margin: 0 0 var(--space-2); }
.cat-ticker .note { color: var(--ink-muted); margin: 0 0 var(--space-3); }
.cat-ticker .cat-list { margin-bottom: var(--space-3); }
.cat-factors { display: flex; flex-wrap: wrap; gap: var(--space-2); margin: 0 0 var(--space-3); }
.cat-factor {
  font-size: var(--t-micro);
  color: var(--ink-2);
  padding: var(--space-0) var(--space-2);
  border-radius: var(--r-pill);
  border: 1px solid var(--border);
  white-space: nowrap;
}
.cat-factor strong { color: var(--ink); font-weight: 600; }
.cat-upcoming, .cat-reach {
  font-size: var(--t-micro);
  text-transform: uppercase;
  letter-spacing: 0.05em;
}
.cat-upcoming { color: var(--pos); }
.cat-reach { color: var(--warn); }

/* ------------------------------------------------- market catalyst mode -- */

.catmode { border-color: color-mix(in srgb, var(--s1) 30%, var(--border)); }
.catmode-head { display: flex; justify-content: space-between; align-items: center; gap: var(--space-3); }
.catmode-kicker {
  font-size: var(--t-micro);
  letter-spacing: 0.01em;
  color: var(--s1);
  font-weight: 600;
}
.catmode-cat {
  margin-left: var(--space-3);
  font-size: var(--t-micro);
  text-transform: uppercase;
  letter-spacing: 0.05em;
  color: var(--ink-muted);
  border: 1px solid var(--border);
  border-radius: var(--r-pill);
  padding: var(--space-0) var(--space-2);
}
.catmode-title {
  font-size: clamp(19px, 2vw, 25px);
  line-height: 1.3;
  font-weight: 700;
  text-transform: none;
  letter-spacing: -0.01em;
  margin: var(--space-2) 0 var(--space-1);
}
.catmode-when { font-size: var(--t-micro); color: var(--ink-muted); margin: 0; }

.catmode-box {
  padding: var(--space-3) var(--space-4);
  border: 1px solid var(--border);
  border-radius: var(--r-md);
  background: var(--surface-2);
}
.catmode-ago { font-size: var(--t-heading); font-weight: 600; color: var(--s1); margin-top: var(--space-1); }
.catmode-noconsensus { font-size: var(--t-heading); font-weight: 600; color: var(--ink-muted); margin-top: var(--space-1); }
.catmode-sum { margin: var(--space-2) 0 0; font-size: var(--t-micro); line-height: 1.5; color: var(--ink-2); }

.catmode-steps { list-style: none; margin: var(--space-2) 0 0; padding: 0; }
.catmode-steps li {
  display: flex;
  align-items: center;
  gap: var(--space-2);
  font-size: var(--t-micro);
  color: var(--ink-muted);
  margin-bottom: var(--space-1);
}
.catmode-steps li.done { color: var(--ink-2); }
.step-dot {
  width: 8px; height: 8px;
  border-radius: 50%;
  border: 1px solid var(--border-strong);
  flex: none;
}
.catmode-steps li.done .step-dot { background: var(--good); border-color: var(--good); }

.catmode-reaction { margin-top: var(--space-4); }
.rx-chips { display: flex; flex-wrap: wrap; gap: var(--space-2); margin-top: var(--space-2); }
.rx-chip {
  display: inline-flex;
  align-items: baseline;
  gap: var(--space-2);
  padding: var(--space-1) var(--space-3);
  border-radius: var(--r-pill);
  border: 1px solid var(--border);
  background: var(--surface-2);
  font-size: var(--t-micro);
  font-variant-numeric: tabular-nums;
}
.rx-chip strong { color: var(--ink); font-weight: 600; }

/* ------------------------------------------ fear & greed, full breakdown -- */

.fg-top { display: flex; justify-content: space-between; align-items: flex-start; gap: var(--space-5); flex-wrap: wrap; }
.fg-panel .fg-score { font-size: var(--t-d1); font-weight: 700; line-height: 1; margin-top: var(--space-1); }
.fg-label-lg { font-size: var(--t-heading); font-weight: 600; margin-top: var(--space-1); }
.fg-meta { text-align: right; font-size: var(--t-micro); color: var(--ink-muted); line-height: 1.65; }
.fg-meta strong { font-weight: 600; }
.fg-meta .pos { color: var(--pos); }
.fg-meta .warn { color: var(--warn); }

/* The scale. A gradient the marker sits on, so position alone carries the
 * reading — the number is above it and the band names are below, which means
 * the graphic is redundant with the text rather than the only way to read it. */
.fg-scale { margin: var(--space-5) 0 var(--space-1); }
.fg-scale-bar {
  position: relative;
  height: 9px;
  border-radius: var(--r-pill);
  background: linear-gradient(90deg,
    var(--critical) 0%,
    color-mix(in srgb, var(--critical) 55%, var(--ink-muted)) 25%,
    var(--ink-muted) 50%,
    color-mix(in srgb, var(--good) 55%, var(--ink-muted)) 75%,
    var(--good) 100%);
}
.fg-marker {
  position: absolute;
  top: 50%;
  width: 15px; height: 15px;
  /* Centred by transform rather than a -7.5px margin: the margin had to equal
     half the width, so any change to the width silently un-centred it. */
  transform: translateX(-50%);
  border-radius: 50%;
  background: var(--ink);
  border: 2px solid var(--surface);
  transform: translateY(-50%);
  box-shadow: var(--elev-1);
}
.fg-scale-labels {
  display: flex;
  justify-content: space-between;
  margin-top: var(--space-2);
  font-size: var(--t-micro);
  text-transform: uppercase;
  letter-spacing: 0.04em;
  color: var(--ink-muted);
}

.fg-hist-row { display: flex; gap: var(--space-3); flex-wrap: wrap; margin-top: var(--space-5); }
.fg-hbox {
  flex: 1 1 170px;
  padding: var(--space-3) var(--space-4);
  border: 1px solid var(--border);
  border-radius: var(--r-md);
  background: var(--surface-2);
}
.fg-hrow { display: flex; align-items: baseline; justify-content: space-between; gap: var(--space-2); margin-top: var(--space-1); }

.fg-comps { display: flex; flex-direction: column; gap: var(--space-3); margin-top: var(--space-3); }
.fg-comp {
  padding: var(--space-3) var(--space-4);
  border: 1px solid var(--border);
  border-radius: var(--r-md);
  background: var(--surface-2);
}
.fg-comp-head { display: flex; align-items: baseline; gap: var(--space-3); }
.fg-comp-name { font-size: var(--t-body); font-weight: 600; }
.fg-comp-weight {
  font-size: var(--t-micro);
  letter-spacing: 0.01em;
  color: var(--ink-muted);
}
.fg-comp-score { margin-left: auto; font-size: var(--t-heading); font-weight: 700; font-variant-numeric: tabular-nums; }
.fg-comp-score.up { color: var(--pos); }
.fg-comp-score.down { color: var(--neg); }
.fg-of { font-size: var(--t-micro); font-weight: 400; color: var(--ink-muted); }
.fg-comp-q { margin: var(--space-1) 0 var(--space-2); font-size: var(--t-small); color: var(--ink-2); }
.fg-comp-bar { height: 6px; border-radius: var(--r-pill); background: var(--surface); overflow: hidden; }
.fg-comp-fill { display: block; height: 100%; border-radius: var(--r-pill); background: var(--ink-muted); }
.fg-comp-fill.up { background: var(--pos); }
.fg-comp-fill.down { background: var(--neg); }
.fg-comp-foot {
  display: flex;
  justify-content: space-between;
  gap: var(--space-4);
  margin-top: var(--space-2);
  font-size: var(--t-micro);
}
.fg-comp-band { font-weight: 600; color: var(--ink-muted); }
.fg-comp-band.up { color: var(--pos); }
.fg-comp-band.down { color: var(--neg); }
.fg-comp-reading { color: var(--ink-2); text-align: right; }
.fg-comp-under {
  margin: var(--space-2) 0 0;
  font-size: var(--t-micro);
  line-height: 1.5;
  color: var(--ink-muted);
}
.fg-comp-under span { text-transform: uppercase; letter-spacing: 0.05em; }

@media (max-width: 620px) {
  .fg-comp-foot { flex-direction: column; gap: var(--space-1); }
  .fg-comp-reading { text-align: left; }
  .fg-meta { text-align: left; }
}

/* --------------------------------------------- technical levels menu ----- */

.lvl-menu { position: relative; }
.lvl-menu > summary {
  display: inline-flex;
  align-items: center;
  gap: var(--space-2);
  font-size: var(--t-small);
  padding: var(--space-2) var(--space-3);
  border-radius: var(--r-sm);
  border: 1px solid var(--border);
  background: var(--surface-2);
  color: var(--ink-2);
  cursor: pointer;
  list-style: none;
  white-space: nowrap;
}
.lvl-menu > summary::-webkit-details-marker { display: none; }
.lvl-menu > summary:hover { border-color: var(--border-strong); color: var(--ink); }
.lvl-menu > summary:focus-visible { outline: 2px solid var(--focus); outline-offset: 2px; }
.lvl-menu[open] > summary { border-color: var(--s1); color: var(--ink); }
.lvl-menu[open] > summary .cal-caret { transform: rotate(90deg); }

/* Three sliders, drawn in CSS rather than shipping an icon font for one glyph. */
.lvl-icon {
  width: 12px; height: 10px;
  flex: none;
  background:
    linear-gradient(currentColor, currentColor) 0 1px / 12px 1.5px no-repeat,
    linear-gradient(currentColor, currentColor) 0 5px / 12px 1.5px no-repeat,
    linear-gradient(currentColor, currentColor) 0 9px / 12px 1.5px no-repeat;
  position: relative;
  opacity: 0.75;
}
.lvl-icon::before, .lvl-icon::after {
  content: '';
  position: absolute;
  width: 2.5px; height: 4px;
  background: currentColor;
}
.lvl-icon::before { left: 3px; top: -0.5px; }
.lvl-icon::after { left: 7px; top: 3.5px; }

.lvl-pop {
  position: absolute;
  right: 0;
  top: calc(100% + 6px);
  z-index: 40;
  min-width: 232px;
  padding: var(--space-3);
  border-radius: var(--r-md);
  border: 1px solid var(--border-strong);
  background: var(--surface);
  box-shadow: var(--elev-2);
}
.lvl-opt {
  display: flex;
  align-items: center;
  gap: var(--space-2);
  padding: var(--space-2) var(--space-2);
  border-radius: var(--r-sm);
  font-size: var(--t-small);
  color: var(--ink-2);
  cursor: pointer;
}
.lvl-opt:hover { background: var(--surface-2); color: var(--ink); }
.lvl-opt input { accent-color: var(--s1); cursor: pointer; }
.lvl-opt input:disabled { cursor: not-allowed; }
.lvl-opt:has(input:disabled) { opacity: 0.55; cursor: not-allowed; }
.lvl-note {
  margin: var(--space-0) var(--space-1) var(--space-2);
  font-size: var(--t-micro);
  line-height: 1.5;
  color: var(--ink-muted);
}
.lvl-opt .lvl-key {
  display: inline-block;
  width: 14px;
  height: 3px;
  border-radius: var(--r-xs);
  flex: none;
}
.lvl-opt .lvl-key.fib {
  background: repeating-linear-gradient(90deg, var(--ref-fib) 0 5px, transparent 5px 8px);
}
.lvl-opt .lvl-key.sr {
  background: repeating-linear-gradient(90deg, var(--ref-sr) 0 5px, transparent 5px 8px);
}
/* --s1 directly. This read var(--ma-mid, var(--s1)) and --ma-mid was never
   declared anywhere, so the fallback was the value every time. The real SMA 50
   colour comes from the style store at runtime and CSS cannot read it; --s1 is
   that store's default, so this swatch matches an untouched chart. */
.lvl-opt .lvl-key.ma { background: var(--s1); }
.lvl-opt .lvl-key.vol { background: var(--ink-muted); height: 7px; border-radius: 1px; }

/* ------------------------------------------------ sector confirmation ---- */

.sc-sub { margin-top: var(--space-1); font-size: var(--t-micro); color: var(--ink-muted); line-height: 1.5; }
.sc-note {
  margin: var(--space-4) 0 0;
  font-size: var(--t-small);
  line-height: 1.65;
  color: var(--ink-2);
  max-width: 78ch;
}

/* ------------------------------------------------------ grouped nav ------ */

/* The top row carries the weight; the second row is subordinate to it. Making
 * them look identical would be two rows of equals, which is the flat structure
 * this replaced. */
.tabs-group button { font-weight: 600; }

.tabs-sub {
  gap: var(--space-0);
  padding-top: 0;
  padding-bottom: var(--space-2);
  border-bottom: 1px solid var(--border);
}
.tabs-sub button {
  font-size: var(--t-small);
  font-weight: 500;
  color: var(--ink-muted);
  padding: var(--space-1) var(--space-3);
  border-radius: var(--r-sm);
}
.tabs-sub button:hover { color: var(--ink); background: var(--surface-2); }
.tabs-sub button[aria-selected="true"] {
  color: var(--ink);
  background: var(--surface-2);
  font-weight: 600;
}
.tabs-sub[hidden] { display: none; }

/* ------------------------------------------------- signal evaluation ----- */

.eval-h {
  border: 1px solid var(--border);
  border-radius: var(--r-md);
  background: var(--surface-2);
  margin-bottom: var(--space-2);
}
.eval-h > summary {
  padding: var(--space-3) var(--space-4);
  cursor: pointer;
  list-style: none;
  position: relative;
  font-size: var(--t-small);
  color: var(--ink-2);
}
.eval-h > summary::-webkit-details-marker { display: none; }
.eval-h > summary:hover { color: var(--ink); }
.eval-h > summary:focus-visible { outline: 2px solid var(--focus); outline-offset: -2px; }
.eval-h .cal-caret { position: absolute; right: 14px; top: 15px; }
.eval-h table { margin: 0 var(--space-4) var(--space-3); width: calc(100% - 28px); }
.callout.warn { border-left-color: var(--warn); }

/* -------------------------------------------- staged attachment tiles ---- */

.chat-attachments { display: flex; flex-wrap: wrap; gap: var(--space-2); }

.att-tile {
  position: relative;
  display: flex;
  align-items: center;
  gap: var(--space-2);
  padding: var(--space-2) var(--space-7) var(--space-2) var(--space-2);
  border-radius: var(--r-sm);
  border: 1px solid var(--border);
  background: var(--surface-2);
  max-width: 240px;
}
.att-tile.bad { border-color: color-mix(in srgb, var(--critical) 55%, transparent); }

/* The thumbnail. Fixed box with cover so a tall screenshot and a wide chart
 * produce the same tile rather than a ragged row. */
.att-thumb {
  width: 44px;
  height: 44px;
  object-fit: cover;
  border-radius: var(--r-xs);
  flex: none;
  background: var(--surface);
  display: block;
}
.att-doc {
  width: 44px;
  height: 44px;
  flex: none;
  display: flex;
  align-items: center;
  justify-content: center;
  border-radius: var(--r-xs);
  background: var(--surface);
  border: 1px solid var(--border);
  font-size: var(--t-micro);
  font-weight: 700;
  letter-spacing: 0.04em;
  color: var(--ink-muted);
}

.att-meta { display: flex; flex-direction: column; min-width: 0; gap: var(--space-0); }
.att-meta .nm {
  font-size: var(--t-micro);
  color: var(--ink-2);
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}
.att-meta .sz { font-size: var(--t-micro); color: var(--ink-muted); }
.att-tile.bad .sz { color: var(--critical); }

.att-x {
  position: absolute;
  top: 4px;
  right: 4px;
  width: 20px;
  height: 20px;
  border-radius: 50%;
  border: 0;
  background: transparent;
  color: var(--ink-muted);
  cursor: pointer;
  font-size: var(--t-base);
  line-height: 1;
}
.att-x:hover { background: var(--surface); color: var(--ink); }
.att-x:focus-visible { outline: 2px solid var(--focus); outline-offset: 1px; }

/* ------------------------------------------------------ momentum row ----- */

/* RSI and MACD are read as a pair, so they must be the same height. They were
 * not: RSI's legend carries five entries and wraps to two lines while MACD's
 * four fit on one, which pushed one chart down relative to the other and left
 * the panels visibly mismatched.
 *
 * Stretch is scoped to this row rather than restoring it globally — the
 * `align-items: start` elsewhere is what stops a short panel being inflated
 * into a mostly-empty bordered box. */
.grid.momentum-row { align-items: stretch; }
.grid.momentum-row > .panel { display: flex; flex-direction: column; }
/* A collapsed panel opts out of the stretch, or the row inflates it to match
   whichever sibling is still open — a 66px header in a 466px box. */
.grid.momentum-row > .panel.is-closed { align-self: start; }

/* Both the intro line and the legend get a consistent floor. Equal panel
 * heights alone were not enough: RSI's explanation wraps to two lines and
 * MACD's fits on one, which left the two charts starting 27px apart inside
 * boxes that were themselves the same size. */
/* The panel body is wrapped by the collapse mechanism, so `.panel > .sub` never
 * matched — the intro is a child of .panel-body, not of .panel. */
.grid.momentum-row .panel-body > .sub { min-height: 2.9em; }
/* `:not(.is-closed)` is load-bearing, not defensive.
 *
 * Four classes here (.grid .momentum-row .panel .panel-body) outrank the three
 * in `.panel.is-closed > .panel-body { display: none }`, so this rule won and
 * the collapse did nothing: clicking either heading rotated the chevron, set
 * aria-expanded="false" and left a 466px panel on screen. Measured on AAPL —
 * both panels reported is-closed with panel-body computing to display:flex and
 * a 400px body. 930px of chart the reader had asked to put away.
 *
 * Excluding the closed state rather than raising the other rule's specificity,
 * because the equal-height flexing this exists for is only meaningful between
 * two panels that are actually showing a chart. */
.grid.momentum-row > .panel:not(.is-closed) > .panel-body {
  display: flex;
  flex-direction: column;
  flex: 1 1 auto;
}
/* Two lines' worth, measured: RSI's five legend entries wrap to 49px at this
 * column width while MACD's four fit in one line. Reserving the taller of the
 * two is what puts both charts on the same y. */
.grid.momentum-row .legend { min-height: 49px; align-content: flex-start; }

/* The caption is pushed to the bottom, so both charts sit at the same y even
 * when one explanation runs longer than the other. */
.grid.momentum-row > .panel > .caveat:last-child { margin-top: auto; }
.regime-effect {
  margin: var(--space-2) 0 0;
  font-size: var(--t-small);
  line-height: 1.5;
  color: var(--ink-2);
  max-width: 60ch;
}
.lvl-opt .lvl-key.vbp {
  background: var(--ink-muted);
  height: 8px;
  width: 10px;
  border-radius: 1px;
  opacity: 0.55;
}
.lvl-opt .lvl-key.ins {
  background: linear-gradient(90deg, var(--good) 0 50%, var(--critical) 50% 100%);
  height: 8px;
  width: 12px;
  border-radius: 2px;
}

/* ------------------------------------------- revenue and the multiple ---- */

/* stretch, not flex-end: with flex-end the columns shrink to their own content
 * height, so the bars' percentage heights resolved against a collapsed parent
 * and rendered as hairlines. The columns must fill the chart for a percentage
 * height to mean anything. */
.rm-chart {
  display: flex;
  align-items: stretch;
  gap: var(--space-3);
  height: 230px;
  margin-top: var(--space-4);
}
.rm-col { flex: 1 1 0; display: flex; flex-direction: column; min-width: 0; }
.rm-plot { position: relative; flex: 1 1 auto; min-height: 0; display: flex; align-items: flex-end; }
.rm-bar {
  width: 100%;
  background: linear-gradient(180deg,
    color-mix(in srgb, var(--s3) 70%, transparent),
    color-mix(in srgb, var(--s3) 30%, transparent));
  border-radius: 3px 3px 0 0;
  min-height: 2px;
}
/* The multiple floats on its own scale inside the column, so it can sit low on a
 * tall bar — which is the whole point when growth and rerating disagree. */
.rm-pe {
  position: absolute;
  left: 50%;
  transform: translate(-50%, -50%);
  font-size: var(--t-micro);
  font-weight: 600;
  font-variant-numeric: tabular-nums;
  color: var(--ink);
  background: var(--surface);
  border: 1px solid var(--s4);
  border-radius: var(--r-pill);
  padding: var(--space-0) var(--space-2);
  white-space: nowrap;
}
.rm-rev {
  margin-top: var(--space-2);
  font-size: var(--t-micro);
  color: var(--ink-2);
  text-align: center;
  font-variant-numeric: tabular-nums;
}
.rm-label {
  font-size: var(--t-micro);
  color: var(--ink-muted);
  text-align: center;
  margin-top: var(--space-0);
}
.rm-legend {
  display: flex;
  gap: var(--space-4);
  margin-top: var(--space-3);
  font-size: var(--t-micro);
  color: var(--ink-muted);
}
.rm-legend span { display: inline-flex; align-items: center; gap: var(--space-2); }
.rm-key { width: 12px; height: 8px; border-radius: 2px; flex: none; }
.rm-key.bar { background: color-mix(in srgb, var(--s3) 60%, transparent); }
.rm-key.pe { background: var(--surface); border: 1px solid var(--s4); border-radius: var(--r-pill); height: 10px; }

/* --------------------------------------------------- panel hover accent -- */

.panel:hover {
  border-color: color-mix(in srgb, var(--s1) 45%, var(--border));
}
/* A collapsed panel gets a touch more, since the heading is all there is to aim
 * at and the affordance has to carry further. */
.panel.is-closed:hover {
  border-color: color-mix(in srgb, var(--s1) 60%, var(--border));
}
@media (prefers-reduced-motion: reduce) {
  .panel { transition: none; }
}

/* ------------------------------------------------------- scanner groups --- */

.scan-groups {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(190px, 1fr));
  gap: var(--space-3);
  margin: var(--space-4) 0 var(--space-3);
}
.scan-group {
  position: relative;
  text-align: left;
  font: inherit;
  padding: var(--space-3) var(--space-4);
  border-radius: var(--r-md);
  border: 1px solid var(--border);
  background: var(--surface-2);
  color: var(--ink-2);
  cursor: pointer;
  transition: border-color 130ms ease, color 130ms ease;
}
.scan-group:hover { border-color: var(--border-strong); color: var(--ink); }
.scan-group.on { border-color: var(--s1); color: var(--ink); }
.scan-group:focus-visible { outline: 2px solid var(--focus); outline-offset: 2px; }
.sg-name { display: block; font-weight: 600; font-size: var(--t-body); }
.sg-count {
  position: absolute;
  top: 11px; right: 13px;
  font-size: var(--t-micro);
  font-variant-numeric: tabular-nums;
  color: var(--ink-muted);
  border: 1px solid var(--border);
  border-radius: var(--r-pill);
  padding: 0 var(--space-2);
}
.scan-group.on .sg-count { color: var(--s1); border-color: var(--s1); }
.sg-blurb {
  display: block;
  margin-top: var(--space-1);
  font-size: var(--t-micro);
  line-height: 1.5;
  color: var(--ink-muted);
}

/* ------------------------------------------------------ priority board --- */

.pri-board {
  display: grid;
  /* Four columns of fixed identity, so the track count has to divide four.
     auto-fit resolved to three at mid widths and pushed the fourth column onto
     a row of its own, leaving two thirds of that row as empty panel — a hole
     several hundred pixels tall that read as a rendering failure. Only 1, 2 and
     4 tile four items completely, so those are the only counts used. */
  grid-template-columns: repeat(4, minmax(0, 1fr));
  gap: var(--space-4);
  margin-top: var(--space-4);
  align-items: start;
}
/* Below the width where four 230px columns fit, two rows of two — never three. */
@media (max-width: 1080px) {
  .pri-board { grid-template-columns: repeat(2, minmax(0, 1fr)); }
}
@media (max-width: 620px) {
  .pri-board { grid-template-columns: 1fr; }
}
.pri-col {
  padding: var(--space-3) var(--space-4);
  border: 1px solid var(--border);
  border-radius: var(--r-md);
  background: var(--surface-2);
}
.pri-col-title {
  font-size: var(--t-micro);
  letter-spacing: 0.01em;
  color: var(--ink);
  font-weight: 600;
  margin: 0;
  display: flex;
  justify-content: space-between;
  align-items: baseline;
  gap: var(--space-2);
}
.pri-count {
  font-size: var(--t-micro);
  font-variant-numeric: tabular-nums;
  color: var(--ink-muted);
  border: 1px solid var(--border);
  border-radius: var(--r-pill);
  padding: 0 var(--space-2);
}
.pri-col-blurb {
  margin: var(--space-1) 0 var(--space-3);
  font-size: var(--t-micro);
  line-height: 1.5;
  color: var(--ink-muted);
}
.pri-row { padding: var(--space-2) 0; border-top: 1px solid var(--border); }
.pri-row:first-of-type { border-top: 0; padding-top: 0; }
.pri-row-top { display: flex; align-items: center; gap: var(--space-2); flex-wrap: wrap; margin-bottom: var(--space-1); }
.pri-when { font-size: var(--t-micro); color: var(--ink-muted); }
.pri-title { font-size: var(--t-small); font-weight: 600; color: var(--ink); line-height: 1.3; }
.pri-why {
  margin: var(--space-1) 0 0;
  font-size: var(--t-micro);
  line-height: 1.5;
  color: var(--ink-2);
}
.pri-more { margin-top: var(--space-3); }
.pri-more > summary {
  position: relative;
  cursor: pointer;
  list-style: none;
  font-size: var(--t-micro);
  color: var(--s1);
  padding: var(--space-1) 0;
}
.pri-more > summary::-webkit-details-marker { display: none; }
.pri-more .cal-caret { position: absolute; right: 2px; top: 9px; }

/* --------------------------------------------------------- explainers ----- */

.explainer {
  position: relative;
  display: flex;
  gap: var(--space-3);
  padding: var(--space-3) var(--space-8) var(--space-3) var(--space-4);
  margin-bottom: var(--space-4);
  border: 1px solid color-mix(in srgb, var(--s1) 35%, var(--border));
  border-radius: var(--r-md);
  background: color-mix(in srgb, var(--s1) 6%, transparent);
}
.ex-icon {
  flex: none;
  width: 18px; height: 18px;
  border-radius: 50%;
  border: 1px solid var(--s1);
  color: var(--s1);
  font-size: var(--t-caption);
  font-style: italic;
  font-weight: 700;
  display: flex;
  align-items: center;
  justify-content: center;
  margin-top: var(--space-0);
}
.ex-kicker {
  font-size: var(--t-micro);
  letter-spacing: 0.01em;
  color: var(--s1);
  font-weight: 600;
}
.ex-text { margin: var(--space-1) 0 0; font-size: var(--t-small); line-height: 1.5; color: var(--ink-2); }
.ex-actions { display: flex; gap: var(--space-2); margin-top: var(--space-3); }
.ex-got, .ex-more {
  font: inherit;
  font-size: var(--t-micro);
  padding: var(--space-1) var(--space-3);
  border-radius: var(--r-sm);
  cursor: pointer;
  border: 1px solid transparent;
}
/* Same correction again. */
.ex-got {
  background: var(--btn-primary);
  color: var(--btn-primary-ink);
  font-weight: 600;
}
.ex-got:hover { filter: brightness(1.08); }
.ex-more { background: var(--surface-2); border-color: var(--border); color: var(--ink-2); }
.ex-more:hover { border-color: var(--border-strong); color: var(--ink); }
.ex-got:focus-visible, .ex-more:focus-visible { outline: 2px solid var(--focus); outline-offset: 2px; }
.ex-close {
  position: absolute;
  top: 8px; right: 8px;
  width: 22px; height: 22px;
  border: 0; background: transparent;
  color: var(--ink-muted);
  font-size: var(--t-base);
  line-height: 1;
  cursor: pointer;
  border-radius: 50%;
}
.ex-close:hover { background: var(--surface-2); color: var(--ink); }

/* Where "Learn more" lands. Brief, because a permanent highlight would just be
 * a differently-coloured paragraph. */
.ex-flash {
  background: color-mix(in srgb, var(--s1) 14%, transparent);
  border-radius: var(--r-sm);
  transition: background 700ms ease;
}

.cat-archive-link {
  font: inherit;
  font-size: var(--t-micro);
  background: none;
  border: 0;
  padding: 0;
  color: var(--s1);
  cursor: pointer;
  text-decoration: underline;
  text-underline-offset: 2px;
}

/* ---------------------------------------------------------- three books --- */

.book-cards {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
  gap: var(--space-3);
  margin-top: var(--space-4);
}
.book-card {
  text-align: left;
  font: inherit;
  padding: var(--space-3) var(--space-4);
  border-radius: var(--r-md);
  border: 1px solid var(--border);
  background: var(--surface-2);
  color: var(--ink-2);
  cursor: pointer;
  transition: border-color 130ms ease;
}
.book-card:hover { border-color: var(--border-strong); }
.book-card.on { border-color: var(--s1); }
.book-card:focus-visible { outline: 2px solid var(--focus); outline-offset: 2px; }
.bk-top { display: flex; align-items: center; gap: var(--space-2); flex-wrap: wrap; }
.bk-name { font-weight: 600; font-size: var(--t-body); color: var(--ink); }
.bk-figs {
  display: grid;
  grid-template-columns: repeat(4, 1fr);
  gap: var(--space-2);
  margin: var(--space-3) 0 var(--space-2);
}
/* Centred, like every other figure in a row of equal boxes. Safe because the
   values below are tabular: the digits are one width, so a number does not
   shift sideways as it ticks. */
.bk-figs > span {
  display: flex;
  flex-direction: column;
  gap: var(--space-0);
  text-align: center;
  /* Down the box as well, for the reason `.tile` carries it. These cells are
     two lines each so they are usually level anyway, but the grid stretches
     them to the tallest and the empty book's "No trades yet" wraps on a narrow
     card -- which leaves its neighbour's figure hanging at the top of a taller
     box. Every other figure in a row of equal boxes here is centred both ways:
     .tile, .ms-cell and .map-tile. */
  justify-content: center;
}
.bk-figs i {
  font-style: normal;
  font-size: var(--t-micro);
  /* One line reserved, as on `.tile`: "Closed" is one word and "Return" is one
     word, but a narrow card wraps whichever is longest and then the values in
     that row sit at two different heights. */
  line-height: 1.4;
  min-height: calc(1.4 * var(--t-micro));
  letter-spacing: 0.01em;
  color: var(--ink-muted);
}
.bk-figs b, .bk-figs > span { font-variant-numeric: tabular-nums; }
.bk-figs > span { font-size: var(--t-small); color: var(--ink); font-weight: 600; }

/* Two cells, not four, because there is no record to lay out. Sized to the
   content so "No trades yet" is not stretched across a four-column grid with
   two of the columns empty. */
.bk-figs-empty { grid-template-columns: max-content max-content; }
.bk-untraded {
  font-weight: 500 !important;
  color: var(--ink-muted) !important;
  /* Not tabular and not signed: it is a sentence, not a figure. */
  font-variant-numeric: normal;
}
.bk-rules, .bk-blurb {
  display: block;
  font-size: var(--t-micro);
  line-height: 1.5;
  color: var(--ink-muted);
}
.bk-rules { margin-bottom: var(--space-1); color: var(--ink-2); }

/* ------------------------------------------------------ earnings week ---- */

.ew-head { display: flex; justify-content: space-between; align-items: flex-start; gap: var(--space-4); flex-wrap: wrap; }
.ew-nav { display: flex; gap: var(--space-2); flex: none; }

.ew-board {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(180px, 1fr));
  gap: var(--space-3);
  margin-top: var(--space-4);
  align-items: start;
}
.ew-day {
  border: 1px solid var(--border);
  border-radius: var(--r-md);
  background: var(--surface-2);
  padding: var(--space-3);
}
.ew-day.today { border-color: var(--s1); }
/* A day already gone is context, not a to-do list. */
.ew-day.past { opacity: 0.62; }
.ew-day-head { display: flex; align-items: baseline; gap: var(--space-2); margin-bottom: var(--space-3); }
.ew-dow {
  font-size: var(--t-micro);
  text-transform: uppercase;
  letter-spacing: 0.06em;
  color: var(--ink-muted);
}
.ew-dom { font-size: var(--t-heading); font-weight: 700; color: var(--ink); }
.ew-today {
  font-size: var(--t-micro);
  text-transform: uppercase;
  letter-spacing: 0.05em;
  color: var(--s1);
  border: 1px solid var(--s1);
  border-radius: var(--r-pill);
  padding: 0 var(--space-1);
}
.ew-count {
  margin-left: auto;
  font-size: var(--t-micro);
  font-variant-numeric: tabular-nums;
  color: var(--ink-muted);
  border: 1px solid var(--border);
  border-radius: var(--r-pill);
  padding: 0 var(--space-2);
}

.ew-row {
  /* A grid so the logo occupies its own column and the three text rows stack
     beside it, rather than the logo being inline and pushing the name onto a
     second line. */
  display: grid;
  grid-template-columns: auto 1fr;
  column-gap: var(--space-3);
  align-items: start;
  width: 100%;
  text-align: left;
  font: inherit;
  background: transparent;
  border: 1px solid transparent;
  border-radius: var(--r-sm);
  padding: var(--space-2) var(--space-2);
  margin-bottom: var(--space-1);
  cursor: pointer;
}
.ew-row:hover { background: var(--surface); border-color: var(--border-strong); }
.ew-row:focus-visible { outline: 2px solid var(--focus); outline-offset: 1px; }
.ew-row.major { border-color: color-mix(in srgb, var(--s1) 40%, transparent); }
.ew-tick {
  display: flex;
  align-items: center;
  gap: var(--space-2);
  font-weight: 700;
  font-size: var(--t-small);
  color: var(--ink);
}
.ew-major {
  font-size: var(--t-micro);
  text-transform: uppercase;
  letter-spacing: 0.05em;
  font-weight: 600;
  color: var(--s1);
  border: 1px solid var(--s1);
  border-radius: var(--r-pill);
  padding: 0 var(--space-1);
}
.ew-name {
  display: block;
  font-size: var(--t-micro);
  color: var(--ink-muted);
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}
.ew-eps { display: block; margin-top: var(--space-1); font-size: var(--t-micro); color: var(--ink-2); }
.ew-eps b { color: var(--ink); font-variant-numeric: tabular-nums; }
.ew-eps i { color: var(--ink-muted); font-style: italic; }
.ew-unconf { display: block; margin-top: var(--space-0); font-size: var(--t-micro); color: var(--warn); }
.ew-empty { margin: 0; font-size: var(--t-micro); color: var(--ink-muted); font-style: italic; }

/* ------------------------------------------------------------- compare ---- */

.cmp-hz {
  padding: var(--space-3) var(--space-4);
  border: 1px solid var(--border);
  border-radius: var(--r-md);
  background: var(--surface-2);
}
.cmp-hz-h { margin-top: var(--space-1); font-size: var(--t-small); font-weight: 600; color: var(--ink); }
.cmp-hz-b { margin: var(--space-2) 0 0; font-size: var(--t-micro); line-height: 1.5; color: var(--ink-muted); }

.cmp-controls { display: flex; gap: var(--space-2); flex-wrap: wrap; align-items: center; margin-top: var(--space-4); }
/* Each box sits in a combo wrapper for its suggestion list, which is what the
   row lays out now. */
.cmp-combo { flex: 0 1 auto; }
.cmp-input {
  font: inherit;
  text-transform: uppercase;
  letter-spacing: 0.06em;
  font-variant-numeric: tabular-nums;
  width: 130px;
  padding: var(--space-2) var(--space-3);
  border-radius: var(--r-sm);
  border: 1px solid var(--border);
  background: var(--surface-2);
  color: var(--ink);
}
.cmp-input:focus-visible { outline: 2px solid var(--focus); outline-offset: 1px; }

.cmp-table th { vertical-align: bottom; }
.cmp-table .cmp-name, .cmp-table .cmp-sector {
  display: block;
  font-weight: 400;
  text-transform: none;
  letter-spacing: 0;
}
.cmp-name { font-size: var(--t-micro); color: var(--ink-2); }
.cmp-sector { font-size: var(--t-micro); color: var(--ink-muted); }
.cmp-na { color: var(--ink-muted); }
.cmp-note {
  display: block;
  font-size: var(--t-micro);
  color: var(--ink-muted);
  text-transform: none;
  letter-spacing: 0;
}

/* The rank rows sit above the raw metrics and are the summary, so they get the
 * only emphasis in the table. */
.cmp-rank { background: var(--surface-2); }
.cmp-rank td { font-weight: 600; }
.cmp-badge {
  font-size: var(--t-micro);
  text-transform: uppercase;
  letter-spacing: 0.04em;
  border: 1px solid var(--border-strong);
  border-radius: var(--r-pill);
  padding: var(--space-0) var(--space-2);
  color: var(--ink-muted);
}
.cmp-badge.win { color: var(--pos); border-color: var(--pos); }

/* --------------------------------------------- economic calendar filters ---- */

.cal-filters {
  display: flex;
  justify-content: space-between;
  gap: var(--space-3);
  flex-wrap: wrap;
  margin: var(--space-4) 0 var(--space-1);
  padding-bottom: var(--space-3);
  border-bottom: 1px solid var(--border);
}
.cal-pills { display: flex; gap: var(--space-2); flex-wrap: wrap; }
.cal-pill {
  display: inline-flex;
  align-items: center;
  gap: var(--space-1);
  padding: var(--space-1) var(--space-3);
  font-size: var(--t-micro);
  border: 1px solid var(--border);
  border-radius: var(--r-pill);
  background: transparent;
  color: var(--ink-2);
  cursor: pointer;
}
.cal-pill:hover { border-color: var(--border-strong); color: var(--ink); }
.cal-pill:focus-visible { outline: 2px solid var(--focus); outline-offset: 1px; }
.cal-pill.on { border-color: var(--s1); color: var(--ink); background: var(--surface-2); }
/* The count is the useful part of a filter you have not pressed yet — it says
   whether pressing it leads anywhere. */
.cal-pill-n { font-variant-numeric: tabular-nums; color: var(--ink-muted); }
.cal-pill.on .cal-pill-n { color: var(--s1); }

/* Prev / est / actual. A fixed width so the numbers line up down the column
   even though each row's title is a different length. The grid columns that
   carry them are set on the base rules above, not here, so the existing
   narrow-screen media block still wins on mobile. */
.cal-nums { display: grid; grid-template-columns: repeat(3, 1fr); gap: var(--space-1); }
.cal-nums.empty { display: block; }
.cal-num { display: flex; flex-direction: column; gap: var(--space-0); text-align: right; }
.cal-num-lbl {
  font-size: var(--t-micro);
  letter-spacing: 0.01em;
  color: var(--ink-muted);
  font-weight: 400;
}
.cal-num-val {
  font-size: var(--t-micro);
  font-variant-numeric: tabular-nums;
  color: var(--ink-2);
}
.cal-num.act .cal-num-val { color: var(--ink); }
/* The estimate column is structurally empty — dimmed rather than hidden so the
   gap is visible and explained, not silently absent. */
.cal-num.est .cal-num-val { color: var(--ink-muted); }

.cal-detail { margin-top: var(--space-2); }
.cal-detail-grid {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(140px, 1fr));
  gap: var(--space-3);
  padding: var(--space-2) var(--space-3);
  border: 1px solid var(--border);
  border-radius: var(--r-sm);
  background: var(--surface-2);
}
.cal-detail-lbl {
  display: block;
  font-size: var(--t-micro);
  letter-spacing: 0.01em;
  color: var(--ink-muted);
}
.cal-detail-val {
  display: block;
  margin-top: var(--space-0);
  font-size: var(--t-micro);
  color: var(--ink);
}
.cal-detail-id { color: var(--ink-muted); }
.cal-detail-note {
  margin: var(--space-2) 0 0;
  font-size: var(--t-micro);
  line-height: 1.5;
  color: var(--ink-muted);
}

/* No intermediate breakpoint. Dropping the numbers onto their own line at
   mid-width left the row with more children than columns, so the impact badge
   and agency wrapped and stretched across the second line. The 138px strip fits
   beside the title everywhere above the existing 620px block, and below it the
   whole row already reflows. */
/* "pending" is a state, not a value — it should not read as a number. */
.cal-num-val.pending { color: var(--ink-muted); font-size: var(--t-micro); }

/* The reading under each metric label. Small and quiet: it is there for the
   reader who does not know the term, and must not compete with the numbers for
   the reader who does. */
.cmp-means {
  display: block;
  margin-top: var(--space-1);
  max-width: 44ch;
  font-size: var(--t-micro);
  line-height: 1.5;
  font-weight: 400;
  text-transform: none;
  letter-spacing: 0;
  color: var(--ink-muted);
}
.cmp-rank .cmp-means { color: var(--ink-2); }

/* Group headings inside the table. A row, not a separate table, so every column
   stays in one alignment context. */
.cmp-group td {
  padding-top: var(--space-5);
  padding-bottom: var(--space-1);
  font-size: var(--t-micro);
  font-weight: 600;
  text-transform: uppercase;
  letter-spacing: 0.09em;
  color: var(--ink-muted);
  border-bottom: 1px solid var(--border);
}
.cmp-table tbody tr:first-child .cmp-group td,
.cmp-group:first-child td { padding-top: var(--space-2); }

/* The leader marker. A dot, not a colour or an arrow: the cell may already be
   red or green from its own sign, and a second colour on top of that would read
   as a verdict rather than a comparison. */
/* A real 5px dot rather than a middot glyph — the character rendered at the
   same weight as the surrounding digits and disappeared into them. */
.cmp-lead-dot {
  display: inline-block;
  width: 5px;
  height: 5px;
  border-radius: 50%;
  background: var(--s1);
  margin-left: var(--space-2);
  vertical-align: 2px;
  overflow: hidden;
  text-indent: -99px;
}
.cmp-sub-read {
  display: block;
  font-size: var(--t-micro);
  font-weight: 400;
  color: var(--ink-muted);
  text-transform: none;
  letter-spacing: 0;
}

/* Loading skeleton. Shaped like the table that is coming so the panel does not
   change height when the numbers arrive. */
.cmp-loading .loading { padding: var(--space-1) 0 var(--space-4); }
.cmp-skel { display: flex; flex-direction: column; gap: var(--space-3); }
/* The track count is set inline from the number of tickers. auto-fit made as
   many tracks as would fit rather than as many as there are bars, so a
   two-ticker skeleton left a column-wide hole in the middle. */
.cmp-skel-row {
  display: grid;
  gap: var(--space-3);
  align-items: center;
}
.cmp-skel-bar {
  height: 9px;
  border-radius: 3px;
  background: var(--border);
  animation: skel-pulse 1.5s ease-in-out infinite both;
}
.cmp-skel-bar.lbl { max-width: 190px; }
@keyframes skel-pulse {
  0%, 100% { opacity: 0.30; }
  50%      { opacity: 0.75; }
}
@media (prefers-reduced-motion: reduce) {
  .cmp-skel-bar { animation: none; opacity: 0.4; }
}

/* A deferred panel's host is an empty div until its payload lands. In a grid an
   empty span-all child still consumes a row gap, so five of them stacked up as
   dead space above the fold on every Read load. */
#priority-host:empty,
#catmode-host:empty,
#weekly-host:empty,
#sentiment-host:empty,
#catalyst-host:empty { display: none; }

/* ------------------------------------------------------- busy / skeletons ---- */

/* A view refetching with content already on it. The old numbers stay put and
   stay readable — the point is that nothing moves and nothing is thrown away,
   not that the screen looks disabled. */
.view.is-busy { position: relative; }
.view.is-busy > .panel,
.view.is-busy > .grid,
.view.is-busy > section,
.view.is-busy > div:not(.busy-bar) {
  opacity: 0.62;
  transition: opacity 160ms ease-out;
}
.busy-bar {
  position: sticky;
  top: 0;
  z-index: 40;
  height: 2px;
  border-radius: 2px;
  overflow: hidden;
  background: var(--border);
  grid-column: 1 / -1;
}
.busy-bar::after {
  content: '';
  position: absolute;
  inset: 0 auto 0 0;
  width: 38%;
  border-radius: 2px;
  background: var(--s1);
  animation: busy-slide 1.05s cubic-bezier(0.45, 0.05, 0.55, 0.95) infinite;
}
@keyframes busy-slide {
  from { transform: translateX(-100%); }
  to   { transform: translateX(365%); }
}

/* First visit, nothing to preserve. Bars in the shape of the text that is
   coming, so the panel does not resize under the reader when it lands. */
.skel-stack { display: flex; flex-direction: column; gap: var(--space-3); margin-top: var(--space-1); }
.skel-bar {
  height: 10px;
  border-radius: 3px;
  background: var(--border);
  animation: skel-pulse 1.5s ease-in-out infinite both;
}

@media (prefers-reduced-motion: reduce) {
  .busy-bar::after { animation: none; width: 100%; opacity: 0.5; }
  .skel-bar { animation: none; opacity: 0.4; }
  .view.is-busy > .panel { transition: none; }
}
.lvl-key.zone { background: linear-gradient(90deg, var(--s3) 50%, var(--neg) 50%); }

/* -------------------------------------------------------- chart patterns ---- */

.pat-head {
  margin: var(--space-5) 0 var(--space-2);
  font-size: var(--t-micro);
  font-weight: 600;
  letter-spacing: 0.01em;
  color: var(--ink-muted);
}
.pat-table td { vertical-align: top; }
.pat-sub {
  display: block;
  margin-top: var(--space-1);
  max-width: 52ch;
  font-size: var(--t-micro);
  line-height: 1.5;
  font-weight: 400;
  text-transform: none;
  letter-spacing: 0;
  color: var(--ink-muted);
}
/* The conventional reading sits under the pattern name rather than in its own
   column: four columns pushed the measured base rate off the right edge, and
   that column is the reason the panel exists. */
.pat-conv {
  display: block;
  margin-top: var(--space-1);
  max-width: 52ch;
  font-size: var(--t-micro);
  line-height: 1.5;
  font-weight: 400;
  text-transform: none;
  letter-spacing: 0;
  color: var(--ink-2);
}
.pat-table th.num { max-width: 15ch; white-space: normal; }

.pat-chip {
  display: inline-block;
  font-size: var(--t-micro);
  text-transform: uppercase;
  letter-spacing: 0.05em;
  padding: var(--space-0) var(--space-2);
  border-radius: var(--r-pill);
  border: 1px solid var(--border-strong);
  color: var(--ink-muted);
  white-space: nowrap;
}
.pat-chip.conf { border-color: var(--good); color: var(--good); }
/* Not a warning colour. An unconfirmed pattern is not a bad pattern, it is an
   incomplete one, and on this data it was the more informative case. */
.pat-chip.unconf, .pat-chip.open { border-color: var(--border-strong); color: var(--ink-2); }
.pat-chip.up { border-color: var(--s3); color: var(--s3); }
.pat-chip.down { border-color: var(--neg); color: var(--neg); }

/* The measured column. Scored against a coin toss rather than against zero:
   the claim under test is "better than chance", and 52% is not that. */
.pat-rate { font-size: var(--t-micro); font-variant-numeric: tabular-nums; }
.pat-rate.worked { color: var(--pos); }
.pat-rate.failed { color: var(--neg); }
.pat-rate.flat { color: var(--ink-2); }
.pat-rate.none { color: var(--ink-muted); font-variant-numeric: normal; }
.pat-edge {
  display: block;
  font-size: var(--t-micro);
  font-weight: 400;
  color: var(--ink-muted);
}
#pe-host:empty { display: none; }

/* Four tiles of fixed identity, so the track count has to divide four.
   The shared .grid.c4 uses auto-fit, which is right for its seventeen other call
   sites with varying child counts — here it resolved to three tracks and pushed
   the cross-check tile onto a row of its own at full width, the same failure the
   priority board had. Only 1, 2 and 4 tile four items. */
.pe-tiles {
  display: grid;
  grid-template-columns: repeat(4, minmax(0, 1fr));
  gap: var(--space-2);
}
@media (max-width: 1080px) {
  .pe-tiles { grid-template-columns: repeat(2, minmax(0, 1fr)); }
}
@media (max-width: 620px) {
  .pe-tiles { grid-template-columns: 1fr; }
}

.ind-panes { display: flex; flex-direction: column; gap: var(--space-5); margin-top: var(--space-5); }
.ind-pane {
  padding-top: var(--space-4);
  border-top: 1px solid var(--border);
}
.ind-fit {
  display: flex;
  gap: var(--space-5);
  flex-wrap: wrap;
  margin-top: var(--space-2);
  font-size: var(--t-small);
  color: var(--ink-2);
  font-variant-numeric: tabular-nums;
}
#ind-host:empty { display: none; }

/* ------------------------------------------------- macro score breakdown ---- */

.mf-list { display: flex; flex-direction: column; gap: var(--space-0); margin-top: var(--space-3); }
.mf-row {
  display: grid;
  grid-template-columns: minmax(0, 1fr) 110px 46px;
  gap: var(--space-3);
  align-items: center;
  padding: var(--space-1) 0;
  border-bottom: 1px solid var(--border);
}
.mf-row:last-child { border-bottom: 0; }
.mf-name {
  font-size: var(--t-small);
  color: var(--ink);
  min-width: 0;
}
.mf-rule {
  display: block;
  font-size: var(--t-micro);
  line-height: 1.5;
  color: var(--ink-muted);
}

/* A diverging bar: negative grows left of the axis, positive right. Two fixed
   halves rather than one bar with a transform, so the zero line stays in the same
   place on every row and the eye can scan down it. */
.mf-bar { display: grid; grid-template-columns: 1fr 1px 1fr; align-items: center; height: 12px; }
.mf-neg, .mf-pos { display: flex; height: 7px; }
.mf-neg { justify-content: flex-end; }
.mf-neg i, .mf-pos i { display: block; height: 100%; }
.mf-neg i { background: var(--neg); border-radius: 2px 0 0 2px; }
.mf-pos i { background: var(--s3); border-radius: 0 2px 2px 0; }
.mf-axis { background: var(--border-strong); height: 12px; }

.mf-val {
  text-align: right;
  font-size: var(--t-small);
  font-variant-numeric: tabular-nums;
}
/* A term contributing nothing is information, not absence — but it should not
   read as loudly as one that is moving the score. */
.mf-val.zero { color: var(--ink-muted); }

.mf-total {
  display: flex;
  justify-content: space-between;
  align-items: baseline;
  margin-top: var(--space-3);
  padding-top: var(--space-2);
  border-top: 1px solid var(--border-strong);
  font-size: var(--t-small);
  color: var(--ink-muted);
}
.mf-sum { font-size: var(--t-body); font-weight: 600; font-variant-numeric: tabular-nums; }

/* ------------------------------------------------ clickable instrument rows ---- */

.inst-clickable { cursor: pointer; }
.inst-clickable:hover { background: var(--bg-hover); }
.inst-clickable:focus-visible { outline: 2px solid var(--focus); outline-offset: -2px; }
/* The affordance sits on the name rather than the whole row: underlining twelve
   numbers to say "this is clickable" would compete with the data. */
.inst-link { border-bottom: 1px dotted var(--border-strong); }
.inst-clickable:hover .inst-link { border-bottom-color: var(--s1); color: var(--s1); }
.sub-close {
  font-style: normal;
  font-size: var(--t-body);
  line-height: 1;
  opacity: 0.55;
  padding: 0 var(--space-0);
  border-radius: 3px;
}
.sub-close:hover { opacity: 1; background: var(--bg-hover); }

.inst-stats {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(130px, 1fr));
  gap: var(--space-3);
  margin-top: var(--space-4);
}
/* The captions on one line across the row, the figures on the next, the third
 * lines on the one after, and every tile centred.
 *
 * Asked for as "center all of the text as well and keep them horizontally
 * aligned", over a row where Last and RSI carried a third line and the other
 * three did not. Centring each tile on its own content put the two kinds at
 * different heights: the captions 15px apart, the figures 14px. Every tile has
 * a third line now, and the three lines are rows of the row itself (a
 * subgrid), so a caption that wraps or a third line that is missing moves its
 * line in every tile together, and no tile has space the others do not. */
.inst-stats .tile {
  display: grid;
  grid-row: span 3;
  grid-template-rows: subgrid;
  justify-content: normal;
}
/* The rows do the reserving the two-line caption did, and a caption sits on
   its figure rather than at the top of a taller row. */
.inst-stats .tile .label { min-height: 0; align-self: end; }
.inst-stats .tile .delta, .inst-stats .tile .note { line-height: 1.5; }

/* The instrument page's heading, centred over its row of centred tiles. */
.inst-head { text-align: center; }
.inst-head .weekly-sub { margin-inline: auto; }

/* ---------------------------------------------- chrome under magnification ----
 *
 * Browser zoom shrinks the viewport measured in CSS pixels, so a magnified tab
 * looks to CSS exactly like a small window. That is what makes this fixable: the
 * same rules serve a short window and a zoomed-in one, keyed on height rather
 * than width.
 *
 * The problem being solved is specific. Below 1280px the top bar wraps its two
 * nav rows onto their own lines, which is right — eleven tabs in one row
 * overflowed. But wrapping makes the bar three rows tall, and at 700x450 the
 * header, subnav and status line together took 263 of 450 pixels: 58% of the
 * screen was chrome and 187px was left for the dashboard.
 *
 * So the padding scales down as vertical room runs out. Nothing is hidden and no
 * row is removed — every tab stays reachable, which is the point of keeping the
 * two-row structure at all. Only the whitespace gives way.
 */
@media (max-height: 780px) {
  header.topbar { padding: var(--space-1) var(--space-4); gap: var(--space-3); }
  header.topbar { row-gap: var(--space-1); }
  nav.tabs button { padding: var(--space-1) var(--space-3); font-size: var(--t-body); }
  .statusline { padding: var(--space-1) var(--space-4); }
  .brand { padding: var(--space-1) var(--space-2); }
  main { padding: var(--space-3) var(--space-4) var(--space-5); }
}

@media (max-height: 560px) {
  /* Deeper zoom. The search field keeps a usable target but stops claiming a
     third of the bar, and the brand text goes — the mark alone still identifies
     the app and still navigates home. */
  header.topbar { padding: var(--space-1) var(--space-3); gap: var(--space-2); }
  nav.tabs button { padding: var(--space-1) var(--space-2); font-size: var(--t-small); }
  nav.tabs { gap: var(--space-0); }
  .statusline { padding: var(--space-0) var(--space-3); font-size: var(--t-micro); }
  .brand-text { display: none; }
  .search input { padding: var(--space-1) var(--space-2); }
  .btn, .btn.primary { padding: var(--space-1) var(--space-3); }
  main { padding: var(--space-2) var(--space-3) var(--space-4); }
  /* A sticky three-row bar over a 400px viewport is worse than one that scrolls
     away: at this size the reader needs the pixels more than the persistence. */
  header.topbar { position: static; }
}

/* The 44px touch-target floor under `pointer: coarse` sets the real height of the
 * bar: two rows of 44px controls is 100px before any padding. Relaxing it is only
 * correct where the small viewport comes from ZOOM rather than from a small
 * device, so this is scoped to a fine pointer — a phone in landscape is also a
 * short viewport, and shrinking its tap targets would trade one problem for a
 * worse one.
 *
 * 32px stays comfortably above the 24x24 minimum in WCAG 2.2 target size, and a
 * mouse or trackpad is a precise pointer to begin with. */
@media (pointer: fine) and (max-height: 560px) {
  #ticker-input, #ticker-form button, .icon-btn, #chat-toggle,
  nav.tabs button {
    min-height: 32px;
  }
}

/* ------------------------------------------------------ full-screen on phone ---
 *
 * `viewport-fit=cover` in the head lets the page paint edge to edge on a notched
 * phone. That alone would run content under the notch and the home indicator, so
 * the insets come back as padding here — the background covers the whole screen,
 * the content stays clear of the hardware.
 *
 * env() is unknown to desktop browsers and resolves to 0px, so these are inert
 * everywhere else and need no media query.
 */
body {
  padding-left: env(safe-area-inset-left);
  padding-right: env(safe-area-inset-right);
}
header.topbar {
  padding-top: calc(var(--space-2) + env(safe-area-inset-top));
}
main {
  padding-bottom: calc(28px + env(safe-area-inset-bottom));
}

/* 100vh on a mobile browser is the viewport WITHOUT the address bar, so a
   full-height element is taller than what you can see and its bottom is
   unreachable behind the toolbar. dvh tracks the visible height as the bar
   hides and shows; the vh line stays first as the fallback for anything that
   does not support it. */
aside#chat { height: 100vh; }
@supports (height: 100dvh) {
  aside#chat { height: 100dvh; }
}

/* The correlation column is the point of the overnight panel, so the two states
   read differently at a glance — but neither is a warning colour: a market that
   moves on its own is not a problem, it is just not a read-across. */
.gm-corr { font-variant-numeric: tabular-nums; font-weight: 600; }
.gm-corr.linked { color: var(--s1); }
.gm-corr.loose { color: var(--ink-muted); }
#global-host:empty { display: none; }
#patterns-host:empty { display: none; }

/* ----------------------------------------------------- Pulse history drawer ---- */

.pulse-history {
  border-bottom: 1px solid var(--border);
  background: var(--surface-2);
  padding: var(--space-2) var(--space-3) var(--space-3);
  max-height: 46%;
  overflow-y: auto;
}
.ph-head {
  display: flex;
  justify-content: space-between;
  align-items: center;
  font-size: var(--t-micro);
  color: var(--ink-muted);
  margin-bottom: var(--space-2);
}
.ph-list { display: flex; flex-direction: column; gap: var(--space-1); }
.ph-item {
  display: grid;
  grid-template-columns: 1fr auto;
  align-items: center;
  gap: var(--space-1);
  border-radius: var(--r-sm);
}
.ph-item:hover { background: var(--surface); }
/* The live conversation is marked so resuming another one is an obvious change of
   place rather than a silent swap. */
.ph-item.on { background: var(--surface); box-shadow: inset 2px 0 0 var(--s1); }
.ph-open {
  display: block;
  text-align: left;
  background: none;
  border: 0;
  padding: var(--space-2) var(--space-2);
  cursor: pointer;
  color: inherit;
  font: inherit;
  min-width: 0;
}
.ph-title {
  display: block;
  font-size: var(--t-small);
  color: var(--ink);
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}
.ph-meta { display: block; font-size: var(--t-micro); color: var(--ink-muted); margin-top: var(--space-0); }
.ph-del {
  background: none;
  border: 0;
  color: var(--ink-muted);
  font-size: var(--t-base);
  line-height: 1;
  padding: var(--space-1) var(--space-2);
  cursor: pointer;
  border-radius: var(--r-sm);
}
.ph-del:hover { color: var(--neg); background: var(--surface-2); }

/* The indicator dropdown on the price chart. Wider than the levels menu because
   each row carries a name and where it draws. */
.lvl-pop.wide { min-width: 268px; }
.lvl-count {
  display: inline-block;
  min-width: 15px;
  padding: 0 var(--space-1);
  border-radius: var(--r-pill);
  background: var(--s1);
  color: var(--plane);
  font-size: var(--t-micro);
  font-weight: 700;
  text-align: center;
}
.ind-drop-name { font-size: var(--t-small); color: var(--ink); }
/* Says which of the two places it will appear, because "on the chart" and "own
   pane" are the difference between an overlay and a new block below. */
.ind-drop-where {
  grid-column: 2;
  font-size: var(--t-micro);
  color: var(--ink-muted);
  text-transform: uppercase;
  letter-spacing: 0.05em;
}

/* One click back to a bare chart. Sits at the foot of the dropdown it clears,
   styled as a quiet action rather than a primary button — it is a way out, not the
   thing you came here to do. */
.lvl-clear {
  display: block;
  width: 100%;
  margin-top: var(--space-2);
  padding: var(--space-2) var(--space-2);
  font: inherit;
  font-size: var(--t-micro);
  text-align: center;
  color: var(--ink-muted);
  background: none;
  border: 1px solid var(--border);
  border-radius: var(--r-sm);
  cursor: pointer;
}
.lvl-clear:hover { color: var(--ink); border-color: var(--border-strong); }
.lvl-clear:focus-visible { outline: 2px solid var(--focus); outline-offset: 1px; }
/* The panes now live inside the price panel, so they need a divider from the
   tiles above rather than a panel border of their own. renderIndicatorPanes emits
   the .ind-pane blocks directly with no wrapper, so the spacing goes on the host. */
#ind-panes-host:empty { display: none; }
#ind-panes-host {
  display: flex;
  flex-direction: column;
  gap: var(--space-5);
  margin-top: var(--space-5);
}

/* ------------------------------------------------- indicator explanations ---- */

/* The live reading. Above the chart because it is the line that changes and the
   one most likely to be read — the static prose can wait below. */
.ind-reading {
  margin: var(--space-1) 0 var(--space-3);
  max-width: 82ch;
  font-size: var(--t-small);
  line-height: 1.5;
  color: var(--ink-2);
}

/* Collapsed by default. The explanation is worth having and worth not having to
   scroll past every time — five open essays between five charts would bury the
   charts they describe. */
details.ind-explain { margin-top: var(--space-3); }
details.ind-explain > summary {
  display: inline-flex;
  align-items: center;
  gap: var(--space-2);
  cursor: pointer;
  list-style: none;
  font-size: var(--t-micro);
  color: var(--ink-muted);
  padding: var(--space-1) 0;
}
details.ind-explain > summary::-webkit-details-marker { display: none; }
details.ind-explain > summary:hover { color: var(--ink); }
details.ind-explain > summary:focus-visible { outline: 2px solid var(--focus); outline-offset: 2px; }
details.ind-explain[open] > summary .cal-caret { transform: rotate(90deg); }

.ind-explain-body {
  margin-top: var(--space-2);
  padding-left: var(--space-3);
  border-left: 2px solid var(--border);
  max-width: 86ch;
}
.ind-measures {
  margin: 0 0 var(--space-2);
  font-size: var(--t-micro);
  line-height: 1.65;
  color: var(--ink-muted);
}
.ind-explain-row { margin-bottom: var(--space-2); }
.ind-explain-row:last-child { margin-bottom: 0; }
.ind-explain-row p {
  margin: var(--space-0) 0 0;
  font-size: var(--t-micro);
  line-height: 1.65;
  color: var(--ink-2);
}
.ind-explain-label {
  display: block;
  font-size: var(--t-micro);
  font-weight: 600;
  letter-spacing: 0.01em;
  color: var(--ink-muted);
}

/* ------------------------------------------------ on-chart overlay readings ---- */

.ov-block { display: flex; flex-direction: column; gap: var(--space-4); }
.ov-row { padding-top: var(--space-3); border-top: 1px solid var(--border); }
.ov-block > .idx-lbl { margin-bottom: -2px; }
.ov-head { display: flex; align-items: center; gap: var(--space-2); }
/* The same colour as the line it describes. Without this the reader has to match
   a name against a legend entry against a line — three hops for one fact. */
.ov-dot {
  width: 9px;
  height: 9px;
  border-radius: 2px;
  flex: none;
}
.ov-name { font-size: var(--t-small); font-weight: 600; color: var(--ink); }
.ov-where {
  font-size: var(--t-micro);
  text-transform: uppercase;
  letter-spacing: 0.06em;
  color: var(--ink-muted);
}
.ov-row .ind-reading { margin: var(--space-2) 0 0; }
/* What period the morning read covers. Sits under the headline because "is the
   weekend in this?" is the first question a Monday reader has. */
.brief-window {
  margin: var(--space-0) 0 var(--space-3);
  font-size: var(--t-micro);
  color: var(--ink-muted);
}
.lvl-key.ema { background: linear-gradient(90deg, var(--s5) 33%, var(--s7) 33% 66%, var(--s6) 66%); }
/* Two-tone, like the supply-and-demand key above it: an EMA cloud is drawn in
 * one of two colours depending on which average is on top, and a single-colour
 * key would name it after half of what it does. Taller than a line key because
 * the thing it stands for is a filled area, not a stroke. */
.lvl-opt .lvl-key.cloud {
  height: 8px;
  border-radius: 2px;
  background: linear-gradient(90deg, var(--pos) 50%, var(--neg) 50%);
  opacity: 0.75;
}

/* =========================================================== navigation ====
 *
 * One row. Each section with more than one page opens a menu, by three routes
 * that suit three input devices: hover for a mouse, :focus-within for a
 * keyboard, and an explicit .open class for a tap. Where there is no hover the
 * .open class also serves the keyboard (see the hover block below).
 *
 * The tap route was missing, and the first two do not cover for it. A phone has
 * no hover, and Safari does not focus a <button> when you tap it, so on a phone
 * the caret pointed at a menu that could not be opened at all. See
 * navMenusOpenOnTap in app.js for what the tap does instead of navigating.
 *
 * Hover therefore stays an accelerator for a mouse rather than the only way in,
 * which is what the old note here claimed it already was.
 */
.nav-item { position: relative; display: inline-flex; }
/* Out of the flow, in the corner it already appeared in.
 *
 * It was inline with `margin-left: var(--space-2)`, so a tab with a menu was
 * 16px wider than one without and the strip's rhythm alternated: measured at
 * 1500, the ink gap ran 20px between Home and Dossier, 37px between Dossier and
 * Compare, 20px, then 37px again after Market. Two of seven tabs carrying a
 * caret is what made it read as uneven spacing rather than as a wider button.
 *
 * Absolute against the button, which `nav.tabs button { position: relative }`
 * already sets up. The caret sits in the top-right padding corner, which is
 * where the old inline box ended up anyway once the flex row placed a 7x4
 * element in a 40px-tall button. Nothing moves visually; the 16px it used to
 * claim in the layout goes back to the row, so every gap is the two buttons'
 * padding and nothing else. */
.nav-caret {
  position: absolute;
  top: var(--space-1);
  right: var(--space-1);
  width: 0; height: 0;
  border-left: 3.5px solid transparent;
  border-right: 3.5px solid transparent;
  border-top: 4px solid currentColor;
  opacity: 0.45;
  transition: transform 140ms ease, opacity 140ms ease;
}
.nav-item.open .nav-caret { opacity: 0.9; transform: rotate(180deg); }

.nav-menu {
  position: absolute;
  top: calc(100% + 4px);
  left: 0;
  z-index: 60;
  min-width: 190px;
  padding: var(--space-1);
  background: var(--surface);
  border: 1px solid var(--border-strong);
  border-radius: var(--r-md);
  box-shadow: var(--elev-2);
  display: flex;
  flex-direction: column;
  gap: var(--space-0);
  /* A menu taller than the space under the bar loses its last items, and there
     is no scrolling out of it: the window does not scroll (the app scrolls
     `main`, which is overflow: clip) and the phone rule below makes the menu
     position: fixed, which the page cannot reach into either. Measured at
     740x360, which is a phone in landscape: the seven Dossier pages ended at
     y=433 in a 360px viewport, so News and Long-Term were off screen and
     elementFromPoint found page content where they should have been.
     --topbar-h is the strip's own bottom edge, so this is exactly the room
     left under it. */
  max-height: calc(100vh - var(--topbar-h, 106px) - var(--space-4));
  overflow-y: auto;
  -webkit-overflow-scrolling: touch;
  /* Hidden by visibility rather than display:none so the transition runs and
     focus-within can still reach it from the keyboard. */
  opacity: 0;
  visibility: hidden;
  transform: translateY(-3px);
  transition: opacity var(--dur-panel) var(--ease-ui),
              transform var(--dur-panel) var(--ease-ui),
              visibility var(--dur-panel);
}
.nav-item.open .nav-menu {
  opacity: 1;
  visibility: visible;
  transform: none;
}

/* Hover is scoped to devices that have one.
 *
 * Unscoped, :hover still fires on a touchscreen: a tap sets it and it sticks
 * until you tap elsewhere. That is worse than nothing here, because the tap also
 * navigates, so the menu appeared over the page it had just taken you to. It is
 * also why the bridge below is in here: a 6px invisible strip under every
 * section is a mouse affordance, and on a phone it is 6px of the page content
 * underneath that cannot be tapped.
 *
 * :focus-within is in here for the same reason. Chrome focuses a button you
 * tap, so on an Android phone the tap that closed a menu took .open away and
 * the focus it left behind kept the menu on screen. Measured at 375x812 in
 * Chrome: after the second tap on Dossier no item was open, the Dossier button
 * had focus, and its menu was still visible. Where there is no hover the .open
 * class is the only way in, and a keyboard takes it too: Enter on the section
 * button is a click, and the click opens the menu. */
@media (hover: hover) {
  .nav-item:hover .nav-caret,
  .nav-item:focus-within .nav-caret { opacity: 0.9; transform: rotate(180deg); }
  .nav-item:hover .nav-menu,
  .nav-item:focus-within .nav-menu {
    opacity: 1;
    visibility: visible;
    transform: none;
  }
  /* A gap between the button and the menu would close it as the pointer crosses,
     so the trigger area is padded to bridge it. */
  .nav-item::after {
    content: '';
    position: absolute;
    top: 100%;
    left: 0;
    right: 0;
    height: 6px;
  }
}

.nav-page {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--space-2);
  width: 100%;
  padding: var(--space-2) var(--space-3);
  font: inherit;
  font-size: var(--t-body);
  text-align: left;
  white-space: nowrap;
  background: none;
  border: 0;
  border-radius: var(--r-sm);
  color: var(--ink-2);
  cursor: pointer;
}
.nav-page:hover { background: var(--surface-2); color: var(--ink); }
.nav-page:focus-visible { outline: 2px solid var(--focus); outline-offset: -2px; }
.nav-page.current { color: var(--ink); background: var(--surface-2); }
/* Absolutely positioned, not a flex child.
 *
 * As a ::before inside `justify-content: space-between` it counted as a second
 * item, so the marker went hard left and pushed its own label hard right — the
 * current row's text sat several pixels out of line with every other row in the
 * menu, which read as a rendering fault rather than as a selection. Taking it out
 * of flow leaves the labels aligned. */
.nav-page { position: relative; justify-content: flex-start; }
.nav-page.current::before {
  content: '';
  position: absolute;
  left: 3px;
  top: 50%;
  transform: translateY(-50%);
  width: 3px;
  height: 15px;
  border-radius: 2px;
  background: var(--accent, var(--s1));
}

/* The second row is gone; nothing should reserve height for it. */
nav.tabs-sub, #subnav { display: none !important; }

/* ==================================================== flat layout (v2) =====
 *
 * The structural half of the restyle, and the part that actually changes how the
 * page reads.
 *
 * Before: every block was a bordered, filled card on a slightly different grey,
 * full-bleed across a 1400px window. Twenty-seven of those on the Swing tab is
 * twenty-seven boxes competing for attention, and the eye has nowhere to rest.
 *
 * After: one measured column, and sections separated by a hairline instead of
 * enclosed by a border. Nothing is hidden and no panel is removed — the same
 * content, with the boxes taken off it.
 */

/* A measured column, remeasured.
 *
 * It was 1120px, on the grounds that this is about 90 characters at the body
 * size and prose stops being comfortable past that. The premise was right and
 * the conclusion was wrong for this page: almost nothing here is prose. It is
 * tables, charts, tile grids and option chains, all of which want the width,
 * and the prose that does exist already carries its own cap in `ch` (74, 78,
 * 105 at various panels), so the shell was never what protected it.
 *
 * Measured on the reader's own screen at 2560px wide: 1120px of content and
 * 720px of empty page on each side, with the eight-cell market strip clipped
 * at 990px inside it because that is all the room the narrower `.home` left.
 * The strip needs 1056.
 *
 * 1600 rather than the full width: past that, a table row puts its first and
 * last column an arm's length apart and the eye loses the row on the way
 * across. This is the width where the content fills a laptop entirely and a
 * large monitor still frames it. */
main {
  /* No cap. The header and the session bar span the window, so a content column
     stopped 150px short of each edge read as the page having failed to fill
     rather than as a measured column, which is what a reader circled after the
     third attempt at this.
     1120 then 1600 were both attempts to protect prose by holding the whole app
     in, and both were solving it in the wrong place: every prose block here
     already carries its own cap in `ch` (74, 78, 105 at various panels) and the
     home page's search lockup is pinned at 860. What was left inside the cap
     was tables, charts and tile grids, none of which wanted protecting. */
  /* `0 auto` until the workspace frame. With no cap above it the centring was
     already a no-op in block layout -- auto margins resolve to zero when the
     width is auto -- but `main` is a flex item now, and there an auto cross-axis
     margin turns OFF stretch and sizes the element to its max-content width.
     Measured at 1280: main came out 1129px inside a 1068px column and ran 61px
     past the viewport, which was the whole of the horizontal page scroll.
     `body.chat-open main` below still sets its own margin-right; this only
     stops the element asking to be centred in a column it should fill. */
  margin: 0;
  padding: var(--space-6) var(--space-5) var(--space-8);
}
body.chat-open main { padding-right: var(--space-5); margin-right: var(--chat-w); }

/* Dragged wide enough, the panel covers rather than splits.
 *
 * Without this, reserving the full width leaves `main` zero pixels across, and
 * a zero-width main is not merely invisible: every chart measures its host
 * before building, so they all resolve to 0 and the page has to be resized to
 * recover. Past the threshold the page keeps its layout and the panel sits over
 * it, which is what a browser does when a split is dragged to the edge.
 *
 * Directly after the rule above, not up with the panel's own styles. Both
 * selectors are (0,1,2), so source order decides and nothing else does -- this
 * first sat ~3,900 lines earlier and lost silently, leaving main 46px wide at
 * full cover. */
body.chat-full main,
body.chat-full footer.legal { margin-right: 0; }

/* The header needs the reservation too, and never had it.
 *
 * `main` gets `margin-right: var(--chat-w)` above and the footer gets a
 * matching padding, so both stay clear of the panel. The top bar got neither
 * and laid out across the whole window, which put its right-hand end
 * underneath the panel.
 *
 * Measured in a browser at 1100px with Pulse open, panel edge at x=769: the
 * settings gear ran 828-866, the account slot 875-961 and the Pulse toggle
 * itself 979-1066, all three entirely behind it. With the rule they sit at
 * 508-546, 555-641 and 659-746, clearing the edge by the 23px gutter. The
 * panel has its own close button so this was never a trap, but Settings and
 * Sign in could not be reached without closing what you wanted open.
 *
 * It surfaced with the eighth nav group -- seven tabs ended at 758 and
 * cleared the panel edge by 22px -- but the three buttons were already
 * hidden, at every width where the panel splits rather than covers.
 *
 * Padding rather than margin: the bar is the sticky, full-bleed surface the
 * glass and the bottom border are drawn on, and pulling its box in would
 * leave a 320px strip of unpainted page beside it -- `footer.legal` reserves
 * the panel with padding for exactly this reason and is the precedent.
 * `chat-full` drops it for the same reason it drops the others: past that
 * width the panel covers the page instead of splitting it. It also has to
 * stay BELOW the `chat-open` line, because `applyChatWidth` adds `chat-full`
 * alongside `chat-open` rather than in place of it, both selectors are
 * (0,2,2), and source order is the only thing left to decide.
 *
 * The panel is drag-resizable, which is why both sides read `--chat-w`:
 * swept at 320/500/700/860 against a 1100px window and nothing overlaps at
 * any of them. The bar does grow -- 185px tall at 320, 564px at 860, since
 * everything wraps -- but it degrades without overlap or a scrollbar. */
body.chat-open header.topbar { padding-right: calc(var(--chat-w) + var(--space-5)); }
body.chat-full header.topbar { padding-right: var(--space-5); }

/* The status line and the session strip: the same fault, a third and fourth
 * time.
 *
 * Both are siblings of `main` inside `.app-main` rather than children of it, so
 * main's `margin-right` never reached them, and neither had a reservation of
 * its own. Measured at 1600px with Pulse dragged to 560px, panel edge at
 * x=1040: the status line and the session strip both ran to 1600, so the
 * session phase legend and its paragraph were cut off under the panel mid-word.
 * main, the topbar and the footer all stopped at the edge. With this, both wrap
 * inside the remaining width instead -- which is what "collapse when Pulse is
 * dragged to the side" asked for.
 *
 * Padding rather than margin, for the reason the topbar records above: both
 * are painted, full-bleed surfaces, and pulling the box in would leave an
 * unpainted strip beside the panel. `chat-full` after `chat-open`, because
 * applyChatWidth adds one alongside the other and source order decides. */
body.chat-open .statusline,
body.chat-open .sessionbar { padding-right: calc(var(--chat-w) + var(--space-5)); }
body.chat-full .statusline,
body.chat-full .sessionbar { padding-right: var(--space-5); }

.grid { gap: var(--space-4) var(--space-5); }

/* Buttons: one filled accent per view, everything else quiet. */
.btn.primary {
  background: var(--btn-primary);
  color: var(--btn-primary-ink);
  border-color: transparent;
  font-weight: 600;
}
/* Lift and glow.
 *
 * A 1px rise plus a coloured shadow drawn from the button's own fill, so the
 * glow belongs to the control rather than being a generic drop shadow. Both are
 * compositor properties (transform, box-shadow), so the hover costs no layout.
 *
 * 1px, not 2 or 3. The button sits in a toolbar next to inputs of the same
 * height; a larger lift makes the row look misaligned while the cursor rests on
 * it. The brightness bump stays, because on a small control the shadow alone is
 * easy to miss.
 *
 * color-mix keeps one source of truth: change --btn-primary and the glow
 * follows. Guarded by @supports because the fallback should be a plain shadow
 * rather than no hover state at all. */
.btn.primary {
  transition: background-color 0.12s ease, border-color 0.12s ease,
              filter 0.12s ease, transform 0.16s cubic-bezier(0.22, 0.61, 0.36, 1),
              box-shadow 0.16s ease;
}
.btn.primary:hover {
  filter: brightness(1.08);
  transform: translateY(-1px);
  /* Derived from --btn-primary, not a literal. This was
     rgba(28, 114, 216, 0.38) -- #1c72d8, a blue from before the brand became
     gold -- so every primary button in the app cast a blue glow on hover
     against its own gold fill. Mixed from the token so it follows the brand and
     cannot drift again. */
  box-shadow: 0 4px 14px color-mix(in srgb, var(--btn-primary) 38%, transparent),
              0 1px 3px var(--shadow-ink);
}
@supports (color: color-mix(in srgb, red 50%, blue)) {
  .btn.primary:hover {
    box-shadow: 0 4px 14px color-mix(in srgb, var(--btn-primary) 42%, transparent),
                0 1px 3px var(--shadow-ink);
  }
}
/* Pressing puts it back down. Without this the button stays lifted through the
   click, which reads as the press not registering. */
.btn.primary:active {
  transform: translateY(0);
  box-shadow: none;
  filter: brightness(0.96);
}
/* Keyboard users get the same affordance; :focus-visible only, so a mouse click
   does not leave a lifted button behind after the pointer has gone. */
.btn.primary:focus-visible {
  transform: translateY(-1px);
  box-shadow: 0 4px 14px color-mix(in srgb, var(--btn-primary) 38%, transparent);
}
@media (prefers-reduced-motion: reduce) {
  .btn.primary { transition: background-color 0.12s ease, filter 0.12s ease; }
  .btn.primary:hover, .btn.primary:focus-visible { transform: none; }
}

/* The top bar loses its fill and keeps only a divider. */
/* The topbar's fill lives here, once. It was declared at both ends of this
   file — the later rule silently won — which is how the buttons ended up green
   while two places said blue. */
header.topbar {
  background: var(--glass);
  border-bottom: 1px solid var(--glass-edge);
}
@supports ((backdrop-filter: blur(1px)) or (-webkit-backdrop-filter: blur(1px))) {
  header.topbar {
    -webkit-backdrop-filter: blur(var(--glass-blur)) saturate(var(--glass-sat));
    backdrop-filter: blur(var(--glass-blur)) saturate(var(--glass-sat));
  }
}
@supports not ((backdrop-filter: blur(1px)) or (-webkit-backdrop-filter: blur(1px))) {
  header.topbar { background: var(--plane); }
}

/* Big numbers get tighter tracking — at 30px and up the default spacing reads
   loose next to a small label. */
.hero, .value, .mf-sum { letter-spacing: -0.02em; }

/* The dropdowns were invisible, and this is why: nav.tabs carries
   `overflow-x: auto` so the old eleven-tab strip could scroll instead of pushing
   Pulse off the row. That makes it a scroll container, which clips on BOTH axes —
   so a 188px menu hanging out of a 43px-tall nav was cut off entirely. The menu
   was rendering correctly the whole time, at the right size and position.
   With five sections instead of eleven there is nothing to scroll, so the strip
   wraps if it ever needs to and the menus can escape. */
nav.tabs-group {
  overflow: visible;
  /* One row, and it claims the width it needs to stay that way.
   *
   * `wrap` was the answer when the comment above was written and there were
   * five sections. At seven it stopped being a safety valve and became the
   * normal case: measured at 1440, six groups sat on row one and "Optic's
   * Positions" alone on row two — a 114px header where 66px does the job.
   *
   * `flex: none; min-width: auto` is the half that matters, and it is undoing
   * the base rule rather than adding to it. `nav.tabs` sets `min-width: 0` so
   * the strip can shrink and scroll, and a shrinkable box whose content is
   * allowed to wrap will always wrap before anything else on the row gives up
   * a pixel. Refusing to shrink is what makes the row honest about its width:
   * either the strip fits beside everything else or it does not, and when it
   * does not the 1320px rule below moves it to a full-width row of its own.
   *
   * The two must stay in step. There is no wrap left to absorb an overflow
   * here, so if the strip ever grows past what that breakpoint reserves for it
   * the tabs run under Pulse instead of folding. Measured after the type and
   * padding came down: the inline row needs 1408px, so the breakpoint sits at
   * 1420 for a little slack against a font that renders wider than it does
   * here. Change either number and re-measure the other.
   *
   * Re-measured when Watchlist became an eighth group: the inline row now
   * needs 1393px and the breakpoint is 1410, so the slack is 17px where it
   * used to be 183. At 1411 the last tab clears the settings gear by 18px.
   * There is no room for a ninth group or a longer label without moving the
   * breakpoint up in the same commit. */
  flex: none;
  min-width: auto;
  flex-wrap: nowrap;
}

/* ------------------------------------------------------- company marks ---- */

/* The monogram sits underneath and the logo covers it. Not a loading
   placeholder: a logo service 404s for plenty of real companies, and an <img>
   that fails leaves a broken icon. Failing back to a finished monogram means the
   miss is invisible. */
.co-mark {
  position: relative;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: var(--co-size, 30px);
  height: var(--co-size, 30px);
  flex: none;
  border-radius: var(--r-sm);
  overflow: hidden;
  background: hsl(var(--co-hue, 210) 42% 26%);
}
.co-mono {
  font-size: var(--t-micro);
  font-weight: 700;
  letter-spacing: -0.02em;
  color: hsl(var(--co-hue, 210) 65% 88%);
  user-select: none;
}
.co-mark img {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  object-fit: contain;
  /* Most brand marks ship on white, so a white plate stops a dark logo
     disappearing into the dark panel behind it. No padding: these sources are
     32-48px and every pixel spent on inset is a pixel of upscaling. */
  background: var(--logo-plate);
  /* Most sources are now vector or 192px, so the box can be larger without
     upscaling. A couple of companies genuinely have nothing above 32px anywhere —
     HP is one — and those stay soft because that is the ceiling, not the setting. */
  image-rendering: auto;
}

/* The row's three text lines span the second column together. */
.ew-row > .ew-tick,
.ew-row > .ew-name,
.ew-row > .ew-eps,
.ew-row > .ew-unconf { grid-column: 2; }


/* ---- the stop-to-target gauge in Optic's open positions ------------------
 *
 * Four prices and a direction are enough to work out whether a trade is nearly
 * over, but nobody does that arithmetic eleven times down a table. The gauge
 * puts the answer where the eye already is.
 *
 * Built out of divs rather than an SVG on purpose: there is one per row, they
 * re-render on every mark refresh, and percentage-positioned blocks cost the
 * layout engine nothing next to eleven inline SVG documents.
 */
/* A fixed width, not a minimum. Table cells size to their content's widest
 * unwrapped line, so the caption underneath — a full sentence on the rows where
 * the target is unreachable — dragged this column out to 758px and pushed five
 * columns off the right edge. An explicit width makes the caption wrap instead. */
.pos-gauge {
  width: 150px;
  /* `table.data td` is nowrap, which is right for a column of figures and wrong
     for a sentence. Inherited, it made the caption ignore the 150px above and
     run out across the P&L and risk columns, printing one row's explanation on
     top of another row's numbers. */
  white-space: normal;
}

.pos-ends {
  display: flex;
  justify-content: space-between;
  gap: var(--space-2);
  font-size: var(--t-caption);
  color: var(--ink-muted);
  margin-bottom: var(--space-1);
  white-space: nowrap;
}

.pos-track {
  position: relative;
  height: 6px;
  border-radius: 3px;
  /* The rail is the whole span from stop to target, so it has to read as a
   * distance rather than as an empty bar waiting to be filled. */
  background: var(--surface-2);
  box-shadow: inset 0 0 0 1px var(--border);
}

.pos-fill {
  position: absolute;
  top: 0;
  height: 100%;
  border-radius: 3px;
  background: var(--ink-muted);
  opacity: 0.55;
}
.pos-fill.up { background: var(--pos); opacity: 0.75; }
.pos-fill.down { background: var(--neg); opacity: 0.75; }

/* Where the trade opened. A thin upright mark, deliberately unlike the dot, so
 * the pair reads as "from here" and "to here" and not as two data points. */
.pos-entry {
  position: absolute;
  top: -3px;
  width: 2px;
  height: 12px;
  margin-left: -1px;
  border-radius: 1px;
  background: var(--ink-muted);
}

/* Where the stock is now. Ringed in the panel background so it stays legible
 * sitting on top of its own fill. */
.pos-now {
  position: absolute;
  top: 50%;
  width: 9px;
  height: 9px;
  /* One transform for both axes. The -4.5px margin was half the width and had
     to be maintained alongside it. */
  transform: translate(-50%, -50%);
  border-radius: 50%;
  background: var(--ink);
  box-shadow: 0 0 0 2px var(--surface);
}
.pos-now.up { background: var(--pos); }
.pos-now.down { background: var(--neg); }

.pos-read {
  margin-top: var(--space-1);
  font-size: var(--t-caption);
  color: var(--ink-muted);
  line-height: 1.5;
}

/* ---- "last updated" on Optic's Read ------------------------------------
 *
 * The page is rebuilt on a schedule rather than on load, so how old it is
 * is part of what it says. Given its own line under the title, with the exact
 * stamp carrying the weight and the relative form ("3h ago") as the quick read.
 */
.read-updated {
  display: flex;
  align-items: center;
  gap: var(--space-2);
  margin: var(--space-2) 0 var(--space-0);
  font-size: var(--t-small);
  color: var(--ink-2);
  flex-wrap: wrap;
}
.read-updated strong { color: var(--ink); font-weight: 600; }
.read-updated-ago { color: var(--ink-muted); }

/* A quiet live dot. It is a freshness cue, not a status light, so it does not
   pulse — an animation here would keep pulling the eye back to a line that is
   read once. */
.read-updated-dot {
  width: 7px;
  height: 7px;
  border-radius: 50%;
  background: var(--accent);
  flex: 0 0 auto;
}

.read-next {
  margin: 0 0 var(--space-1);
  font-size: var(--t-caption);
  color: var(--ink-muted);
  line-height: 1.5;
  max-width: 68ch;
}
.read-next strong { color: var(--ink-2); font-weight: 600; }

/* ---- text utilities -----------------------------------------------------
 *
 * These exist to close the hole that let nine off-scale font sizes onto the
 * page. 240 elements carried an inline style attribute, and the two commonest
 * properties in them were `color` (83) and `font-size` (47) — precisely the two
 * that should only ever come from a token. An inline declaration wins over every
 * stylesheet rule, so each one is a small permanent exception to the design
 * system, and they are invisible until you go looking.
 *
 * Naming them makes the intent readable at the call site too: `class="subnote"`
 * says what the text IS, where `style="color:var(--ink-muted);font-size:
 * var(--t-caption)"` only says how it is painted.
 */
.muted { color: var(--ink-muted); }
.dim { color: var(--ink-2); }
.caption { font-size: var(--t-caption); }
.small { font-size: var(--t-small); }

/* The single commonest pattern in the app: a quiet second line under a figure —
 * the notional under a share count, the mark under an entry price. */
.subnote {
  font-size: var(--t-caption);
  color: var(--ink-muted);
  line-height: 1.5;
}
.subnote.sm { font-size: var(--t-small); }

/* A table header that is a label rather than a column name — used where a th
 * spans a row of prose and the uppercase tracking treatment would shout. */
.th-plain {
  color: var(--ink-muted);
  font-weight: 400;
  text-transform: none;
  letter-spacing: 0;
}

/* ---- block rhythm reset -------------------------------------------------
 *
 * The last source of off-grid spacing, and the least visible one: there was no
 * reset, so every block element still carried the browser's own margin. Those
 * defaults are em-based, which means they scale with whatever font size the
 * element happens to have — a `.caveat` at 11.5px contributed an 11.5px margin,
 * a `.sub` at 14px contributed 14px, an `h2` contributed 17.43px. None of those
 * numbers was chosen by anyone, none is on the grid, and no amount of tidying
 * the authored CSS removes them.
 *
 * Normalised rather than zeroed. Zeroing would collapse every prose block that
 * has been relying on the default all along and turn a spacing pass into a
 * layout rewrite; giving them one on-grid value keeps the rhythm they had and
 * puts it on the scale. Anything with its own rule still wins on specificity.
 */
main p,
main dl,
main dd,
main ul,
main ol,
main figure {
  margin: 0 0 var(--space-3);
}

main h2, main h3, main h4, main h5 {
  margin: 0 0 var(--space-2);
}

/* Headings had no size of their own outside `.panel > h2`, so any heading that
   was not a direct child of a panel fell through to the browser's `1.5em` —
   22.5px once the base moved to 15, a size that is on no scale and changes
   whenever the base does. */
main h2 { font-size: var(--t-d3); line-height: 1.3; }
main h3 { font-size: var(--t-heading); line-height: 1.3; }
main h4, main h5 { font-size: var(--t-base); line-height: 1.3; }

/* A trailing margin inside a panel fights the panel's own padding and reads as
   a lopsided box — the gap under the last paragraph ends up larger than the gap
   above the first. */
main p:last-child,
main dl:last-child,
main ul:last-child,
main ol:last-child {
  margin-bottom: 0;
}

/* `b` and `strong` default to `bolder`, which against a variable font resolves
   to 900 — a weight nothing else in the app uses and which Inter draws heavy
   enough to look like a different family. Pinned to the scale's top UI weight. */
b, strong { font-weight: 600; }

/* ---- seasonality ---------------------------------------------------------
 *
 * The verdict chip is the point of the panel, so it is the only coloured thing
 * in the row apart from the numbers themselves. "Noise" is deliberately grey and
 * quiet rather than red: it is the expected, correct answer, not a failure.
 */
.seas-verdict {
  display: inline-block;
  padding: var(--space-0) var(--space-2);
  border-radius: var(--r-pill);
  font-size: var(--t-micro);
  font-weight: 600;
  text-transform: uppercase;
  letter-spacing: 0.04em;
  white-space: nowrap;
  border: 1px solid currentColor;
}
.seas-verdict.up { color: var(--pos); }
.seas-verdict.warn { color: var(--warn); }
.seas-verdict.muted { color: var(--ink-muted); border-color: var(--border-strong); }

/* Bars read from a shared centre line, so the rows compare against each other
   rather than each against its own maximum — the usual way a seasonality chart
   makes a 0.2% difference look decisive. */
.seas-bar {
  position: relative;
  width: 92px;
  height: 8px;
  border-radius: var(--r-xs);
  background: var(--surface-2);
  box-shadow: inset 0 0 0 1px var(--border);
}
.seas-bar::before {
  content: '';
  position: absolute;
  left: 50%;
  top: -1px;
  bottom: -1px;
  width: 1px;
  background: var(--border-strong);
}
.seas-bar i {
  position: absolute;
  top: 1px;
  bottom: 1px;
  border-radius: 1px;
}
.seas-bar i.up { background: var(--pos); }
.seas-bar i.down { background: var(--neg); }

.seas-rpt {
  font-size: var(--t-micro);
  color: var(--ink-muted);
  text-transform: uppercase;
  letter-spacing: 0.04em;
  border-bottom: 1px dotted var(--border-strong);
  cursor: help;
}

/* ---- sector relative rotation -------------------------------------------
 *
 * The quadrant chip carries the same four colours as the chart's washes, so a
 * row in the table and a dot in the chart can be matched without a legend.
 */
.rot-quad {
  display: inline-block;
  padding: var(--space-0) var(--space-2);
  border-radius: var(--r-pill);
  font-size: var(--t-micro);
  font-weight: 600;
  text-transform: uppercase;
  letter-spacing: 0.04em;
  white-space: nowrap;
  border: 1px solid currentColor;
}
.rot-quad.up { color: var(--pos); }
.rot-quad.warn { color: var(--warn); }
.rot-quad.down { color: var(--neg); }
.rot-quad.accent { color: var(--s1); }

.rot-tallies {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(180px, 1fr));
  gap: var(--space-2);
  margin: var(--space-3) 0;
}
.rot-tally {
  display: flex;
  align-items: baseline;
  gap: var(--space-2);
  padding: var(--space-2) var(--space-3);
  background: var(--surface-2);
  border: 1px solid var(--border);
  border-radius: var(--r-md);
  min-width: 0;
}
.rot-tally strong {
  font-size: var(--t-heading);
  font-variant-numeric: tabular-nums;
}
/* The symbol list is the part that overflows, so it is the part that truncates
   — not the count, which is the number the row exists to show. */
.rot-tally span {
  font-size: var(--t-caption);
  color: var(--ink-muted);
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
  min-width: 0;
}


/* The logo in a typeahead row. Fixed size so ten rows of different logos do not
   each set their own row height, and the list does not reflow as they arrive. */
.combo-list .co-mark { flex: 0 0 auto; }

/* ==================================================== chart workspace ====
 *
 * Its own tab, and its own layout rules: the rest of the app is a column of
 * panels inside a 1120px measure, and this is a single surface that wants the
 * whole window. Hence the escape from `main`'s max-width and the fixed-height
 * grid — a chart that scrolls is a chart you cannot read against its own axis.
 */
/* The session bar is off on Charting, and this is de-duplication rather than
   a space grab -- though it is worth 115px on the one view sized against the
   viewport instead of scrolling.

   The chart's own header already states the phase: "AAPL 336.72 338.91 334.30
   335.92 V 24,696,400 -0.33% Overnight". So the bar above it said "OVERNIGHT"
   a second time, with a phase strip and a timetable, directly over a chart
   that draws its own dates along the bottom and a status line already reading
   "Chart: AAPL 1D 6m".

   Measured at 1440x900: the plot goes 397px to 512px, 44% of the window to
   57%. Scoped to this view rather than removed, because everywhere else the
   session bar is the only thing saying what the market is doing. */
body[data-view="chart"] .sessionbar { display: none; }

.view-workspace.active {
  /* Flex column, not a grid with named rows.
   *
   * This was `grid-template-rows: auto 1fr`, written when the workspace had two
   * children: the toolbar and the body. The Dossier section strip was added
   * later, making three, so `1fr` landed on the TOOLBAR and the body fell into
   * an implicit auto row that took its content height. Measured with a symbol
   * loaded and Pulse open: rows resolved to 48.39px / 10.19px / 595.42px, so
   * the toolbar was 10px tall against 27px of content and its labels were
   * sliced through the middle. That is what was reported as the charting UI
   * being broken.
   *
   * Adding a third row would not fix it either: securityHeader returns '' when
   * no symbol is charted, so the child count is 2 or 3 depending on state, and
   * a row sized by POSITION cannot follow that. Flex attaches the flexing to
   * the element, which is the same correction `.ws-canvas` needed for the same
   * reason.
   */
  display: flex;
  flex-direction: column;
  /* The viewport minus the chrome above it. dvh rather than vh so a mobile
     browser's collapsing address bar does not leave the chart overflowing. */
  height: calc(100dvh - var(--chrome-h, 132px));
  /* Break out of the reading measure. The workspace is the one view where more
     width is strictly better. */
  width: 100%;
  max-width: none;
  margin: 0;
}
/* Chrome keeps its natural height; the body takes what is left. min-height: 0
   so it can shrink below its content, which is what lets the chart column
   scroll its own dock instead of pushing the toolbar off the top. */
.view-workspace.active > * { flex: 0 0 auto; }
.view-workspace.active > .ws-body { flex: 1 1 auto; min-height: 0; }

.ws-toolbar {
  display: flex;
  align-items: center;
  gap: var(--space-2);
  padding: var(--space-2) var(--space-3);
  border-bottom: 1px solid var(--border);
  background: var(--surface);
  flex-wrap: wrap;
  /* The menus pop downward out of this bar, so it cannot clip them. */
  position: relative;
  z-index: 5;
}
.ws-toolbar-gap { flex: 1 1 auto; }

/* The tool drawer. Nothing at all above the phone breakpoint.
 *
 * `display: contents` is what makes this free on a desktop: the wrapper stops
 * generating a box and its children become flex items of .ws-toolbar again, so
 * the row is laid out exactly as it was before the wrapper existed. Both
 * wrappers carry this class -- the tool groups are not contiguous in the
 * toolbar, and reordering them to make one wrapper would have changed the
 * desktop to fix the phone.
 *
 * The button is hidden rather than not rendered, so the markup is the same at
 * every width and only CSS decides. */
.ws-tools { display: contents; }
.ws-tools-btn { display: none; }

@media (max-width: 559px) {
  /* Measured at 375x812 before this: nineteen buttons over six rows, 227px of
     toolbar above a 443px chart. Inline now: the range pills, the interval,
     Line/Candles and Reset zoom -- what you reach for constantly. In the
     drawer: Levels, Stages, Indicators, the study menus, Panes and Colours. */
  .ws-tools-btn { display: inline-flex; }
  .ws-tools { display: none; }
  .ws-toolbar.tools-open .ws-tools { display: contents; }

  /* A flexible spacer on a wrapping row is a row of its own, and it was:
     measured as one of the six. It has nothing to push apart here. */
  .ws-toolbar-gap { display: none; }
}

.ws-menu { position: relative; }
.ws-menu-btn {
  padding: var(--space-1) var(--space-3);
  border: 1px solid var(--border-strong);
  border-radius: var(--r-md);
  background: var(--surface-2);
  color: var(--ink-2);
  font-size: var(--t-small);
  cursor: pointer;
  white-space: nowrap;
}
.ws-menu-btn:hover { color: var(--ink); border-color: var(--ink-muted); }
.ws-menu-btn.on {
  color: var(--accent);
  border-color: color-mix(in srgb, var(--accent) 55%, transparent);
  background: color-mix(in srgb, var(--accent) 10%, var(--surface-2));
}
/* The interval menu: its button names the size on screen, and the options
   open as a grid that fills the menu, four to a line.

   Asked for as "remove the circular surrounding of the time frames and have
   it cover the whole box in grids". The options were the row's pills,
   capsule and all, and the capsule kept around a two-line grid drew a
   stadium inside the box. Now the grid runs to the menu's edges, one cell
   per size with a hairline between cells. The lines are cell borders rather
   than a gap over a filled grid, so the cells stay clear and the menu's
   glass shows through them as it does in every other menu. The menu clips
   to its own corners, which a corner cell's fill would otherwise square off.

   Two classes on the menu: `.ws-menu-pop` sets its padding and 210px floor
   further down at the same weight, and on one class this rule lost to it,
   as the `min-width: 0` it carried always had. */
.ws-caret { margin-left: var(--space-1); font-size: 0.8em; opacity: 0.7; }
.ws-menu-pop.ws-interval-pop { min-width: 0; padding: 0; overflow: hidden; }
.ws-interval-pop .pills {
  display: grid;
  /* 52px a cell keeps the menu the 210px it was. */
  grid-template-columns: repeat(4, minmax(52px, 1fr));
  gap: 0;
  padding: 0;
  background: none;
  border: 0;
  border-radius: 0;
}
.ws-interval-pop .pill {
  min-height: 40px;
  padding: var(--space-2) var(--space-3);
  border-radius: 0;
  border-right: 1px solid var(--border);
  border-bottom: 1px solid var(--border);
  /* The pill's own, restated: a cell's hover is a fill, and it eases in as
     every other hover does. */
  transition: background 120ms ease, color 120ms ease;
}
/* No line along the menu's own edges, where its border already is: not after
   the last cell of a line, and not under the last line, however many sizes
   it holds. */
.ws-interval-pop .pill:nth-child(4n) { border-right: 0; }
.ws-interval-pop .pill:nth-last-child(-n+4):nth-child(4n+1),
.ws-interval-pop .pill:nth-last-child(-n+4):nth-child(4n+1) ~ .pill { border-bottom: 0; }
.ws-interval-pop .pill:hover { background: var(--surface-2); }
.ws-interval-pop .pill.on { background: var(--btn-primary); color: var(--btn-primary-ink); }
/* Inside the cell: the menu's clip would cut an outline drawn outside it. */
.ws-interval-pop .pill:focus-visible { outline-offset: -2px; }
@media (prefers-reduced-motion: reduce) {
  /* At this weight the pill's own reduced-motion rule would lose to the
     transition above. */
  .ws-interval-pop .pill { transition: none; }
}
/* The trading-hours menu: the interval menu's cells, one to a line, since each
   option is a phrase rather than a size. On 1D and 1W the button is disabled,
   and reads so, as a Line / Candles button does where it cannot apply. */
/* Outranks the menus' own :hover, so a pointer over it changes nothing. */
.ws-session .ws-menu-btn:disabled {
  color: var(--ink-muted);
  border-color: var(--border-strong);
  cursor: not-allowed;
}
/* The 210px floor every menu has, kept: the options are phrases, and a menu
   the width of its button cut "Regular trading hours" off at "hou". */
.ws-menu-pop.ws-session-pop { padding: 0; overflow: hidden; }
.ws-session-pop .pills {
  display: grid;
  grid-template-columns: 1fr;
  gap: 0;
  padding: 0;
  background: none;
  border: 0;
  border-radius: 0;
}
.ws-session-pop .pill {
  min-height: 40px;
  padding: var(--space-2) var(--space-3);
  border-radius: 0;
  border-bottom: 1px solid var(--border);
  white-space: nowrap;
  transition: background 120ms ease, color 120ms ease;
}
.ws-session-pop .pill:last-child { border-bottom: 0; }
.ws-session-pop .pill:hover { background: var(--surface-2); }
.ws-session-pop .pill.on { background: var(--btn-primary); color: var(--btn-primary-ink); }
.ws-session-pop .pill:focus-visible { outline-offset: -2px; }
@media (prefers-reduced-motion: reduce) {
  .ws-session-pop .pill { transition: none; }
}

.ws-count {
  display: inline-block;
  min-width: 15px;
  padding: 0 var(--space-0);
  border-radius: var(--r-pill);
  background: var(--accent);
  color: var(--accent-ink);
  font-size: var(--t-micro);
  text-align: center;
}

.ws-menu-pop {
  position: absolute;
  top: calc(100% + var(--space-1));
  left: 0;
  min-width: 210px;
  padding: var(--space-2);
  background: var(--surface);
  border: 1px solid var(--border-strong);
  border-radius: var(--r-md);
  box-shadow: var(--elev-2);
  z-index: 20;
}

/* Chart read: the trend and its facts, then the levels and the two momentum
   readings as label and value, in the menus' own surface. */
.ws-read-pop {
  width: min(360px, calc(100vw - 2 * var(--space-4)));
  padding: var(--space-3);
}
.cr-head {
  font-size: var(--t-micro);
  font-weight: 600;
  letter-spacing: 0.06em;
  text-transform: uppercase;
  color: var(--ink-muted);
}
.cr-trend { margin: var(--space-2) 0 var(--space-1); font-size: var(--t-lead); font-weight: 600; color: var(--ink); }
.cr-trend.up { color: var(--pos); }
.cr-trend.down { color: var(--neg); }
.cr-facts {
  margin: 0 0 var(--space-2);
  padding-left: var(--space-4);
  font-size: var(--t-small);
  color: var(--ink-2);
  line-height: 1.5;
}
.cr-rows {
  display: grid;
  grid-template-columns: auto 1fr;
  gap: var(--space-1) var(--space-3);
  margin: 0;
  font-size: var(--t-small);
}
.cr-rows dt { color: var(--ink-muted); }
.cr-rows dd { margin: 0; text-align: right; color: var(--ink); font-variant-numeric: tabular-nums; }
.cr-note { margin: var(--space-2) 0; font-size: var(--t-caption); color: var(--ink-muted); line-height: 1.5; }
.cr-explain { width: 100%; justify-content: center; }

/* The quick set at the top of Indicators, ruled off from the single lines. */
.ws-presets {
  padding-bottom: var(--space-1);
  margin-bottom: var(--space-1);
  border-bottom: 1px solid var(--border);
}
.ws-presets-h { margin: 0 0 var(--space-1); padding: 0 var(--space-2); }

@media (max-width: 559px) {
  /* The toolbar menus are anchored to the TOOLBAR here, not to their button.
   *
   * `.ws-menu-pop` has a 210px minimum and opens from `left: 0` of the button
   * it belongs to, which is fine in a wide bar and runs off a narrow one:
   * measured at 375x812, Trends opened at x=172 and ended at 382, seven
   * pixels past the screen, with an ancestor clipping the rest. Events and
   * Panes were off the edge too.
   *
   * Making `.ws-menu` static hands the pop's containing block to the toolbar,
   * which is already `position: relative` for exactly this reason -- so
   * `top: 100%` becomes "under the whole bar", which on a wrapping multi-row
   * toolbar is where it belongs anyway.
   *
   * THIS BLOCK HAS TO SIT HERE, below the two rules it overrides. It was a
   * hundred lines up with the rest of the phone toolbar rules, where
   * `.ws-menu { position: relative }` and this `.ws-menu-pop` both followed
   * it unmediated at equal specificity and discarded the lot. That is the
   * fourth rule today to lose a cascade this way. */
  .ws-menu { position: static; }
  .ws-menu-pop {
    left: var(--space-3);
    right: var(--space-3);
    min-width: 0;
  }
}
@supports ((backdrop-filter: blur(1px)) or (-webkit-backdrop-filter: blur(1px))) {
  .ws-menu-pop {
    background: var(--glass-strong);
    border-color: var(--glass-edge);
    -webkit-backdrop-filter: blur(var(--glass-blur)) saturate(var(--glass-sat));
    backdrop-filter: blur(var(--glass-blur)) saturate(var(--glass-sat));
  }
}
.ws-opt {
  display: flex;
  align-items: center;
  gap: var(--space-2);
  padding: var(--space-1) var(--space-1);
  border-radius: var(--r-xs);
  font-size: var(--t-small);
  cursor: pointer;
}
.ws-opt:hover { background: var(--surface-2); }
.ws-swatch {
  width: 12px;
  height: 3px;
  border-radius: 2px;
  flex: 0 0 auto;
}
.ws-manage {
  width: 100%;
  margin-top: var(--space-2);
  padding: var(--space-1);
  border: 1px solid var(--border);
  border-radius: var(--r-xs);
  background: none;
  color: var(--ink-muted);
  font-size: var(--t-caption);
  cursor: pointer;
}
.ws-manage:hover { color: var(--ink); border-color: var(--ink-muted); }

/* The studies menu. Wider than the overlay menus because these names are words
   rather than `SMA 20`, and `Regression channel` wrapped to two lines in 210px,
   which made a five-item list look like ten. */
.ws-studies { min-width: 236px; }

/* Same treatment as .lvl-note, which is the equivalent line on the Analysis
   tab's indicator dropdown. Both classes shipped as markup with no rule at all
   for one revision: the note rendered at body size and was the tallest thing in
   the popover. */
.ws-studies-note {
  margin: var(--space-2) var(--space-1) 0;
  font-size: var(--t-micro);
  line-height: 1.5;
  color: var(--ink-muted);
}

/* Colour picker for the price mark. Wider than the overlay menus because a row
   of twelve swatches plus the wheel does not fit 210px, and wrapping them to
   three ragged lines made the palette read as a list rather than a strip. */
.ws-colors { min-width: 268px; }

.wc-row + .wc-row {
  margin-top: var(--space-2);
  padding-top: var(--space-2);
  border-top: 1px solid var(--border);
}

.wc-head {
  display: flex;
  align-items: center;
  gap: var(--space-1);
  margin-bottom: var(--space-1);
}

.wc-label { font-size: var(--t-caption); color: var(--ink-2); }

/* The current colour, shown as a square rather than named. A hex string is not
   something anyone recognises at a glance, and the token names are internal. */
.wc-now {
  width: 12px;
  height: 12px;
  border-radius: var(--r-xs);
  border: 1px solid var(--border-strong);
}

/* Pushed to the right so it does not sit against the preview square and read as
   a caption for it. */
.wc-clear {
  margin-left: auto;
  padding: 0 var(--space-1);
  border: 1px solid var(--border);
  border-radius: var(--r-xs);
  background: none;
  color: var(--ink-muted);
  font-size: var(--t-micro);
  cursor: pointer;
  transition: color var(--dur-ui) var(--ease-ui),
              border-color var(--dur-ui) var(--ease-ui);
}
.wc-clear:hover { color: var(--ink); border-color: var(--ink-muted); }

.wc-swatches {
  display: flex;
  flex-wrap: wrap;
  gap: var(--space-1);
}

/* 18px, not smaller: this is a touch target as well as a colour sample, and the
   selected ring is drawn outside the box so it cannot shrink the colour. */
.wc-swatch {
  width: 18px;
  height: 18px;
  padding: 0;
  border: 1px solid var(--border-strong);
  border-radius: var(--r-xs);
  cursor: pointer;
  transition: transform var(--dur-ui) var(--ease-ui),
              box-shadow var(--dur-ui) var(--ease-ui);
}
.wc-swatch:hover { transform: scale(1.12); }

/* box-shadow rather than an outline or a border: a border would eat 2px of the
   swatch and change the colour area, and the ring needs a gap so it reads
   against a swatch of any colour including one close to --ink. */
.wc-swatch.on {
  box-shadow: 0 0 0 1px var(--surface), 0 0 0 3px var(--ink);
}

/* The native picker. The input itself is the target, sized to match a swatch;
   the "+" underneath is what the reader sees, because a bare colour input
   renders as a filled box that looks like a thirteenth preset. */
.wc-wheel {
  position: relative;
  width: 18px;
  height: 18px;
  border: 1px dashed var(--ink-muted);
  border-radius: var(--r-xs);
  display: grid;
  place-items: center;
  cursor: pointer;
  transition: border-color var(--dur-ui) var(--ease-ui);
}
.wc-wheel input {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  opacity: 0;
  cursor: pointer;
}
.wc-wheel span {
  font-size: var(--t-micro);
  line-height: 1;
  color: var(--ink-muted);
  pointer-events: none;
  transition: color var(--dur-ui) var(--ease-ui);
}
.wc-wheel:hover { border-color: var(--ink); }
.wc-wheel:hover span { color: var(--ink); }

/* The [hidden] pair, per the house rule: an author `display` beats the UA
   stylesheet's [hidden] whatever the specificity, and this element is toggled
   by the `hidden` attribute from the colour wheel's handler. There is no
   `display` below today, so this is belt and braces rather than a fix. */
.wc-warn[hidden] { display: none; }

.wc-warn {
  margin: var(--space-1) 0 0;
  font-size: var(--t-micro);
  line-height: 1.4;
  color: var(--warn);
}

.ws-body {
  display: grid;
  /* rail · chart · dock · widget rail */
  grid-template-columns: auto 1fr auto auto;
  min-height: 0;          /* let the chart column actually shrink */
}

/* The tool rail. Icon-only on purpose — nine labelled buttons would take the
   width the chart needs, and each carries its name in a title attribute. */
.ws-rail {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: var(--space-1);
  /* Room either side of the icons. At --space-1 a side the 30px buttons met
     the rail's edge, and the scrollbar a short window gives it took its width
     from them: asked for as "have a bit more space for this section". */
  padding: var(--space-2) var(--space-2);
  min-width: 48px;
  border-right: 1px solid var(--border);
  background: var(--surface);
  overflow-y: auto;
  /* Still scrolls by wheel and touch when the tools outrun the height; the
     bar itself took width the icons need. */
  scrollbar-width: none;
}
.ws-rail::-webkit-scrollbar { display: none; }
.ws-rail-gap { flex: 1 1 auto; min-height: var(--s4); }

/* A widget button that cannot act says so.
   Not hidden: the rail is how a reader learns these panels exist, and removing
   it with Pulse open would make the feature look absent rather than
   unavailable. The cursor and the opacity carry it, and the title gives the
   reason. */
.ws-wrail-btn:disabled {
  opacity: 0.4;
  cursor: not-allowed;
}
.ws-tool {
  flex: none;
  width: 32px;
  height: 32px;
  display: grid;
  place-items: center;
  border: 1px solid transparent;
  border-radius: var(--r-xs);
  background: none;
  color: var(--ink-muted);
  font-size: var(--t-base);
  cursor: pointer;
}
.ws-tool:hover { color: var(--ink); background: var(--surface-2); }
.ws-tool.on {
  color: var(--accent);
  border-color: color-mix(in srgb, var(--accent) 45%, transparent);
  background: color-mix(in srgb, var(--accent) 12%, transparent);
}

.ws-canvas {
  position: relative;
  min-width: 0;
  min-height: 0;
  /* Extra panes scroll together rather than painting over the navigator when
     their combined minimum height exceeds the available workspace. */
  overflow-y: auto;
  /* Flex, not grid, and that is a fix rather than a preference.
     head, legend, plot, then any oscillator panes, then the navigator. The plot
     is the only child that flexes, so the panes and the navigator keep their
     heights as the window resizes and never squeeze the chart to nothing.

     A grid sized the rows by POSITION: `auto auto 1fr auto` meant "the third
     child flexes". The legend renders empty on some configurations, so every
     child shifted up one slot and the RSI pane inherited the 1fr row with no
     free space left in it, collapsing to 1px while the MACD pane below it was
     fine. Measured `grid-template-rows: 51px 680px 1px 157px 46px` against six
     children. Flex attaches the flexing to the element instead of to its
     index, so it cannot come apart when a sibling renders nothing. */
  display: flex;
  flex-direction: column;
}
.ws-canvas > * { flex: 0 0 auto; }
.ws-plot { flex: 1 1 auto; }

/* ---- oscillator panes -----------------------------------------------------
   Stacked under the price plot, sharing its x-axis, the way a charting package
   does it. Each carries its own legend because each has its own y-scale: "RSI
   43.9" belongs to the RSI pane the way the OHLC row belongs to the price plot.

   A top border and no card: they are part of the chart above them rather than
   panels of their own, and a bordered box around each would read as three
   unrelated charts. */
.ws-pane {
  border-top: 1px solid var(--grid);
  min-width: 0;
  min-height: 0;
}
.ws-pane-head {
  display: flex;
  align-items: baseline;
  gap: var(--space-2);
  padding: var(--space-1) var(--space-3) 0;
  font-size: var(--t-micro);
  color: var(--ink-muted);
}
.ws-pane-legend {
  display: flex;
  align-items: baseline;
  gap: var(--space-1);
  flex-wrap: wrap;
  min-width: 0;
}
.ws-pane-legend strong { color: var(--ink-2); font-weight: 600; }
.ws-pane-val {
  color: var(--ink);
  font-variant-numeric: tabular-nums;
  margin-right: var(--space-1);
}
/* Inline swatches, sized to sit on a text baseline rather than as list bullets:
   this row is read as a sentence, not as a key. */
.ws-pane-key {
  display: inline-block;
  width: 8px;
  height: 8px;
  border-radius: 2px;
  margin: 0 var(--space-0) 0 var(--space-1);
}
.ws-pane-cross { color: var(--ink-2); }
.ws-pane-cross.up { color: var(--pos); }
.ws-pane-cross.down { color: var(--neg); }
/* Remove, right after the reading, shown while the pane is hovered or the
   button has focus: the price legend's rows do the same with theirs. With no
   hover to reveal it, on a touch screen, it is always shown. */
.ws-pane-close {
  flex: 0 0 auto;
  background: none;
  border: 0;
  color: var(--ink-muted);
  cursor: pointer;
  font-size: var(--t-small);
  line-height: 1;
  padding: var(--space-0) var(--space-1);
  border-radius: var(--r-sm);
  opacity: 0;
  transition: color var(--dur-ui) var(--ease-ui), opacity 120ms ease;
}
.ws-pane:hover .ws-pane-close,
.ws-pane-close:focus-visible { opacity: 1; }
.ws-pane-close:hover,
.ws-pane-close:focus-visible { color: var(--ink); }
@media (hover: none) {
  .ws-pane-close { opacity: 1; }
}
.ws-pane-plot { min-width: 0; min-height: 0; }

.ws-menu-note {
  margin: var(--space-2) 0 0;
  padding: 0 var(--space-2) var(--space-1);
  font-size: var(--t-micro);
  color: var(--ink-muted);
  max-width: 230px;
  line-height: 1.45;
}
.ws-head {
  display: flex;
  align-items: baseline;
  gap: var(--space-3);
  padding: var(--space-2) var(--space-3);
  /* Ends short of the full-screen button in the panel's corner. A margin and
     not padding, because the narrow header clips its overflow at its padding
     edge: padding left room the text still ran into, under the button. */
  margin-right: calc(28px + var(--space-3));
  font-size: var(--t-small);
  color: var(--ink-2);
  flex-wrap: wrap;
}
.ws-head strong { font-size: var(--t-heading); color: var(--ink); }
/* The symbol, which opens the search. A button that still reads as the
   heading it replaced: no chrome of its own, the logo beside the ticker, and an
   underline on hover and on keyboard focus to say it can be pressed. */
.ws-sym-btn {
  display: inline-flex;
  align-items: center;
  gap: var(--space-2);
  padding: 0;
  border: 0;
  border-radius: var(--r-sm);
  background: none;
  font: inherit;
  color: var(--ink);
  cursor: pointer;
  transition: color var(--dur-ui) var(--ease-ui);
}
.ws-sym-btn strong {
  text-decoration: underline 1px transparent;
  text-underline-offset: 4px;
  transition: text-decoration-color var(--dur-ui) var(--ease-ui);
}
.ws-sym-btn:hover strong, .ws-sym-btn:focus-visible strong { text-decoration-color: currentColor; }
.ws-sym-btn:focus-visible { outline: 2px solid var(--focus); outline-offset: 3px; }
.ws-head-name { color: var(--ink-muted); }
.ws-head-state { color: var(--ink-muted); margin-left: auto; }
/* Full screen, as an icon in the chart panel's top-right corner, beside the
   header. See wsMaxButton. */
.ws-full-btn {
  position: absolute;
  top: var(--space-1);
  right: var(--space-2);
  z-index: 4;
  display: grid;
  place-items: center;
  width: 28px;
  height: 28px;
  padding: 0;
  border: 1px solid var(--border-strong);
  border-radius: var(--r-md);
  background: var(--surface-2);
  color: var(--ink-2);
  cursor: pointer;
}
.ws-full-btn:hover { color: var(--ink); border-color: var(--ink-muted); }
.ws-full-btn.on {
  color: var(--accent);
  border-color: color-mix(in srgb, var(--accent) 55%, transparent);
}
.ws-ohlc { display: flex; gap: var(--space-2); font-variant-numeric: tabular-nums; }
.ws-ohlc b { color: var(--ink); font-weight: 500; }

/* The legend sits over the chart rather than above it, the way the reference
   does — the top-left of a price plot is almost always empty space, and putting
   the readings there keeps them next to the lines they describe. */
.ws-legend {
  position: absolute;
  /* The header's measured height, published by wsWatchHead. This was the
     literal 44px -- the height of a header that fits on one line -- and the
     header wraps to two at workspace widths around 900px, where the legend was
     then drawn through it. The fallback is the old number, which is right for
     the one-line case and is what applies before the first measurement. */
  top: var(--ws-head-h, 44px);
  left: var(--space-3);
  z-index: 3;
  pointer-events: auto;
  font-size: var(--t-caption);
  line-height: 1.5;
  max-width: 340px;
}
/* The header is the collapse toggle, so it is a button — but it has to keep
   looking like a heading, not like a control. */
.ws-leg-title {
  display: flex;
  align-items: center;
  gap: var(--space-1);
  background: none;
  border: none;
  padding: 0;
  font: inherit;
  font-size: var(--t-small);
  font-weight: 600;
  color: var(--ink);
  margin-bottom: var(--space-0);
  cursor: pointer;
  text-align: left;
}
.ws-leg-title:hover { color: var(--accent); }
.ws-leg-caret {
  color: var(--ink-muted);
  font-size: var(--t-micro);
  flex: 0 0 auto;
}
.ws-leg-count {
  color: var(--ink-muted);
  font-weight: 400;
  font-size: var(--t-caption);
}
/* Collapsed, the legend is a single line sitting on the chart, so it gets a
   background to stay readable over candles. Expanded it does not need one —
   the rows are short and the chart's top-left is mostly empty by then. */
.ws-legend.collapsed .ws-leg-title {
  background: color-mix(in srgb, var(--surface) 82%, transparent);
  padding: var(--space-0) var(--space-2);
  border-radius: var(--r-xs);
}
.ws-leg-row {
  display: flex;
  align-items: center;
  gap: var(--space-1);
}
.ws-leg-row:hover .ws-leg-btn { opacity: 1; }
.ws-leg-name { font-weight: 500; }
.ws-leg-args { color: var(--ink-muted); font-weight: 400; }
.ws-leg-val { color: var(--ink-2); font-variant-numeric: tabular-nums; }
.ws-leg-static { color: var(--ink-muted); }
/* The stage key: one square per stage on screen, named in its title. */
.ws-leg-key { display: inline-flex; align-items: center; gap: var(--space-0); }
.ws-leg-sw { width: 8px; height: 8px; border-radius: var(--space-0); }
.ws-leg-empty { color: var(--ink-muted); }
.ws-leg-btn {
  border: none;
  background: none;
  color: var(--ink-muted);
  cursor: pointer;
  padding: 0 var(--space-0);
  font-size: var(--t-caption);
  /* Hidden until the row is hovered: eleven rows each carrying two always-visible
     controls is more chrome than data. */
  opacity: 0;
  transition: opacity 120ms ease;
}
.ws-leg-btn:hover { color: var(--ink); }

.ws-chart { min-height: 0; min-width: 0; }

/* The dock. Fixed width so the chart's width does not change as widgets are
   added and removed — a chart that reflows every time you open a panel makes
   comparing two moments impossible. */
/* Collapsed when it holds nothing, so the chart's 1fr column takes the space.
   :empty rather than a class, because the one thing that decides this is
   whether wsDock() produced any panels, and a class would be a second record of
   that which can disagree with the first. */
.ws-dock:empty { display: none; }

.ws-dock {
  width: 260px;
  border-left: 1px solid var(--border);
  background: var(--surface);
  overflow-y: auto;
  padding: var(--space-2);
  display: flex;
  flex-direction: column;
  gap: var(--space-2);
}
.ws-widget {
  border: 1px solid var(--border);
  border-radius: var(--r-md);
  background: var(--surface-2);
  overflow: hidden;
}
.ws-widget-head {
  display: flex;
  align-items: center;
  justify-content: space-between;
  padding: var(--space-1) var(--space-2);
  border-bottom: 1px solid var(--border);
  font-size: var(--t-caption);
  letter-spacing: 0.01em;
  color: var(--ink-muted);
}
.ws-widget:hover .ws-leg-btn { opacity: 1; }
.ws-widget-body { padding: var(--space-2); font-size: var(--t-caption); }
.ws-list { list-style: none; margin: 0; padding: 0; }
.ws-list li { padding: var(--space-0) 0; }

/* Insider filings: direction, who, how much, when.
   A grid rather than inline spans so the amounts line up down the column. Read
   left to right the row is a sentence; read down, the numbers are comparable,
   which is the whole reason for showing them. `auto` on the name lets a long
   one take the space it needs and truncate rather than push the amount off. */
.ws-ins li {
  display: grid;
  grid-template-columns: auto minmax(0, 1fr) auto auto;
  align-items: baseline;
  gap: 0 var(--space-1);
}

.ws-ins-act { font-weight: 600; }

.ws-ins-who {
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

/* Tabular figures so the share counts and values align on the digit, and the
   sans stack because there is no mono token in this design system. */
.ws-ins-amt {
  font-variant-numeric: tabular-nums;
  color: var(--ink-2);
}

/* Narrow dock: the date drops to a second line rather than squeezing the
   amount, because the amount is what was asked for and the date is already
   implied by the ordering. */
@media (max-width: 860px) {
  .ws-ins li { grid-template-columns: auto minmax(0, 1fr) auto; }
  .ws-ins li .ws-leg-args { grid-column: 2 / -1; }
}

/* The dock's compact seasonality grid. */
.ws-seas {
  display: flex;
  align-items: flex-end;
  gap: 2px;
  height: 62px;
  margin-bottom: var(--space-1);
}
.ws-seas-col {
  flex: 1 1 0;
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: flex-end;
  gap: 1px;
  min-width: 0;
}
.ws-seas-pct { font-size: 8px; color: var(--ink-muted); }
.ws-seas-bar { width: 100%; border-radius: 2px 2px 0 0; }
.ws-seas-bar.up { background: var(--pos); }
.ws-seas-bar.down { background: var(--neg); }
.ws-seas-lab { font-size: 8px; color: var(--ink-muted); }

.ws-empty {
  padding: var(--space-7) var(--space-4);
  max-width: 620px;
  margin: 0 auto;
}
.ws-load { display: flex; gap: var(--space-2); margin: var(--space-4) 0 var(--space-3); }
.ws-load input {
  flex: 1 1 auto;
  padding: var(--space-2) var(--space-3);
  background: var(--surface-2);
  border: 1px solid var(--border-strong);
  border-radius: var(--r-md);
  color: var(--ink);
  text-transform: uppercase;
}
.ws-quick { display: flex; gap: var(--space-2); flex-wrap: wrap; }

@media (max-width: 900px) {
  /* The dock goes below the chart rather than beside it: at this width a 260px
     column leaves the chart too narrow to read. */
  .ws-body { grid-template-columns: auto 1fr; }
  .ws-dock {
    grid-column: 1 / -1;
    width: auto;
    border-left: none;
    border-top: 1px solid var(--border);
    flex-direction: row;
    overflow-x: auto;
  }
  .ws-widget { min-width: 220px; }
  .view-workspace.active { height: auto; }
}


/* ---- the widget rail ---------------------------------------------------
 *
 * Fourteen widgets, each one click away with its state visible. A dropdown of
 * fourteen names would hide which are open, and fourteen unlabelled glyphs is a
 * memory test — so both icon and label, stacked, at the smallest size that
 * stays legible.
 */
.ws-wrail {
  display: flex;
  flex-direction: column;
  gap: 1px;
  padding: var(--space-2) var(--space-1);
  border-left: 1px solid var(--border);
  background: var(--surface);
  overflow-y: auto;
  width: 62px;
  flex: 0 0 auto;
}
.ws-wrail-btn {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 1px;
  padding: var(--space-1) 0;
  border: 1px solid transparent;
  border-radius: var(--r-xs);
  background: none;
  color: var(--ink-muted);
  cursor: pointer;
  line-height: 1.1;
}
.ws-wrail-btn:not(:disabled):hover { color: var(--ink); background: var(--surface-2); }
.ws-wrail-btn.on {
  color: var(--accent);
  background: color-mix(in srgb, var(--accent) 12%, transparent);
}
.ws-wrail-ico { font-size: var(--t-body); }
.ws-wrail-lab { font-size: 9px; text-align: center; }

.ws-dock-panels { display: flex; flex-direction: column; gap: var(--space-2); }
.ws-none { margin: 0; }

/* Widget internals. Kept tight — a 260px column has no room for the panel
   spacing the main views use. */
.ws-widget-body .data.narrow td { padding: var(--space-0) var(--space-1); }
.ws-widget-body .data.narrow td.name { color: var(--ink-2); }
.ws-news-link {
  color: var(--ink-2);
  text-decoration: none;
  display: block;
  line-height: 1.4;
}
.ws-news-link:hover { color: var(--ink); text-decoration: underline; }
.ws-watch-row { display: flex; align-items: center; justify-content: space-between; }
.ws-watch-row:hover .ws-leg-btn { opacity: 1; }
.ws-watch-row .tkr.on { color: var(--accent); font-weight: 600; }
.ws-add { margin-top: var(--space-1); }
.ws-add input,
.ws-note {
  width: 100%;
  padding: var(--space-1) var(--space-2);
  background: var(--plane);
  border: 1px solid var(--border);
  border-radius: var(--r-xs);
  color: var(--ink);
  font: inherit;
  font-size: var(--t-caption);
}
.ws-note { resize: vertical; line-height: 1.5; }
.ws-checks { list-style: none; margin: 0; padding: 0; }
.ws-checks li { padding: var(--space-0) 0; }
.ws-checks label {
  display: flex;
  gap: var(--space-2);
  align-items: flex-start;
  cursor: pointer;
  line-height: 1.4;
}
.ws-checks input { margin-top: 2px; flex: 0 0 auto; }

@media (max-width: 900px) {
  .ws-body { grid-template-columns: auto 1fr; }
  .ws-wrail {
    grid-column: 1 / -1;
    flex-direction: row;
    width: auto;
    border-left: none;
    border-top: 1px solid var(--border);
    overflow-x: auto;
  }
  .ws-wrail-btn { flex: 0 0 auto; min-width: 54px; }
}

/* ---- the indicator manager --------------------------------------------- */
.ws-modal {
  position: fixed;
  inset: 0;
  z-index: 60;
  background: var(--scrim);
  display: grid;
  place-items: center;
  padding: var(--space-4);
}
.ws-modal-box {
  width: min(760px, 100%);
  max-height: 86vh;
  display: flex;
  flex-direction: column;
  background: var(--surface);
  border: 1px solid var(--border-strong);
  border-radius: var(--r-lg);
  box-shadow: var(--elev-3);
  overflow: hidden;
}
.ws-modal-head {
  display: flex;
  align-items: center;
  justify-content: space-between;
  padding: var(--space-3) var(--space-4);
  border-bottom: 1px solid var(--border);
}
.ws-modal-head .ws-leg-btn { opacity: 1; font-size: var(--t-heading); }
.ws-modal-bar {
  display: flex;
  gap: var(--space-2);
  padding: var(--space-3) var(--space-4);
  border-bottom: 1px solid var(--border);
}
.ws-modal-bar input {
  flex: 1 1 auto;
  padding: var(--space-2) var(--space-3);
  background: var(--surface-2);
  border: 1px solid var(--border-strong);
  border-radius: var(--r-md);
  color: var(--ink);
}
.ws-modal-body {
  padding: var(--space-3) var(--space-4);
  overflow-y: auto;
  min-height: 0;
}
.ws-modal-foot {
  margin: 0;
  padding: var(--space-3) var(--space-4);
  border-top: 1px solid var(--border);
  font-size: var(--t-caption);
  color: var(--ink-muted);
  line-height: 1.5;
}
.ws-mgroup { margin-bottom: var(--space-4); }
.ws-mgroup h4 {
  margin: 0 0 var(--space-2);
  font-size: var(--t-caption);
  letter-spacing: 0.01em;
  color: var(--ink-muted);
}
.ws-mrow {
  padding: var(--space-2) 0;
  border-bottom: 1px solid var(--grid);
}
.ws-mrow:last-child { border-bottom: none; }
.ws-mrow-top {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--space-2);
}
.ws-mrow-name { font-weight: 600; font-size: var(--t-small); }
.ws-mrow-controls {
  display: flex;
  flex-wrap: wrap;
  gap: var(--space-3);
  margin-top: var(--space-2);
  padding-left: var(--space-5);
}
.ws-mfield { display: flex; flex-direction: column; gap: 2px; }
.ws-mfield > label {
  font-size: var(--t-micro);
  text-transform: uppercase;
  letter-spacing: 0.04em;
  color: var(--ink-muted);
}
.ws-mfield select,
.ws-mfield input {
  padding: var(--space-0) var(--space-1);
  background: var(--surface-2);
  border: 1px solid var(--border);
  border-radius: var(--r-xs);
  color: var(--ink);
  font: inherit;
  font-size: var(--t-caption);
  min-width: 68px;
}
.ws-swatches { display: flex; gap: var(--space-1); flex-wrap: wrap; max-width: 208px; }
.ws-cswatch {
  width: 16px;
  height: 16px;
  border-radius: var(--r-xs);
  border: 1px solid transparent;
  cursor: pointer;
  padding: 0;
}
/* The chosen colour gets a ring in the surface colour plus an outline, so it
   reads as selected against any hue including the near-white one. */
.ws-cswatch.on {
  box-shadow: 0 0 0 2px var(--surface), 0 0 0 3px var(--ink);
}
.ws-mnote {
  margin: var(--space-1) 0 0 var(--space-5);
  font-size: var(--t-caption);
  color: var(--warn);
  line-height: 1.45;
}

/* ---- the drawing layer ------------------------------------------------- */
.ws-plot { position: relative; min-height: 180px; min-width: 0; }

/* The charting workspace on a phone.
 *
 * Nothing here had a narrow rule at all: the desktop grid just did whatever a
 * 375px viewport made it do. Measured on AAPL at 375x812 before any of this,
 * with the plot at y=752 of an 812px screen -- the chart was below the fold
 * on the tab whose job is charting.
 *
 *   drawing rail   40 x 649   a column beside the chart, fine
 *   canvas        298 x 649   of which the PLOT was 180 and two
 *                             oscillator panes were 179 each
 *   widget rail   338 x 611   fifteen buttons stacked full-width, and
 *                             because the grid has three columns and four
 *                             children it wrapped to its own row under
 *                             everything
 *
 * So the price chart was the third-largest thing in its own workspace. */
@media (max-width: 559px) {
  /* The rail becoming a row is the 900px block's job and it already does it.
     Only the scrollbar is added here: a visible one across a 44px strip is
     most of the strip. */
  .ws-wrail { overflow-y: hidden; scrollbar-width: none; }
  .ws-wrail::-webkit-scrollbar { display: none; }

  /* The chart gets the room. 180px was the floor and it was also the whole
     allowance, because the two panes below it take their height from content
     and there was nothing left to flex into. */
  .ws-plot { min-height: 300px; }
  .ws-pane { max-height: 110px; }

  /* Every pill row here can be wider than the screen -- the interval ladder
     is eight pills totalling 412px in a 338px toolbar, so `1W` sat at x=359
     to 408 and could not be tapped at all. They scroll rather than overflow.
     `overflow-x: visible` was the default and is what hid it. */
  .ws-toolbar .pills,
  .ws-toolbar .seg {
    overflow-x: auto;
    scrollbar-width: none;
    max-width: 100%;
  }
  .ws-toolbar .pills::-webkit-scrollbar,
  .ws-toolbar .seg::-webkit-scrollbar { display: none; }
  .ws-toolbar .pill { flex: none; }
}
.ws-chart { min-height: 0; min-width: 0; }
/* Over the chart, same box. `pointer-events: none` on the wrapper and `auto` on
   the SVG inside it, so the chart's own crosshair still works everywhere the
   drawing layer has nothing — otherwise a transparent sheet would swallow every
   hover the chart depends on. */
.ws-draw {
  position: absolute;
  inset: 0;
  pointer-events: none;
}
/* The drawing canvas must NOT intercept pointer events.
 *
 * This was `auto`, which made the whole overlay SVG hit-testable across the
 * entire chart. The hit stack under the cursor read: ws-draw-svg on top, then
 * the chart's own transparent hover rect buried beneath it. So every pointermove
 * was swallowed before the chart saw it, and the crosshair, the tooltip and the
 * OHLC header readout all did nothing. The header still showed figures, which
 * made it look like a formatting problem rather than a dead hover: they were
 * the last bar, painted once at mount and never updated because onHover never
 * fired.
 *
 * `none` here costs nothing, because the parts that DO need clicking declare it
 * themselves in the rules below: a drawn line takes pointer-events on its
 * stroke, a rectangle and a handle take all. Clicking a drawing therefore still
 * targets that drawing. And when a tool is armed, .ws-canvas.arming turns this
 * layer back on so a new drawing can be started anywhere on the plot. */
.ws-draw-svg { pointer-events: none; }
.ws-draw-svg .ws-dr { cursor: move; }
.ws-draw-svg .ws-dr line,
.ws-draw-svg .ws-dr rect { pointer-events: stroke; }
.ws-draw-svg .ws-dr rect { pointer-events: all; }
.ws-draw-svg .ws-dr.on line,
.ws-draw-svg .ws-dr.on rect { filter: drop-shadow(0 0 3px rgba(255, 255, 255, 0.35)); }
.ws-draw-svg .ws-dr-handle { cursor: grab; pointer-events: all; }
.ws-draw-svg .ws-dr-pending,
.ws-draw-svg .ws-dr-pending * { pointer-events: none; }
/* The invisible grab target. `stroke` rather than `visiblePainted`, because the
   twin is transparent and visiblePainted would refuse to hit an unpainted
   stroke. Cursor matches the visible line so the widened area does not feel
   like a separate object. */
.ws-draw-svg .ws-dr-hit { pointer-events: stroke; cursor: move; }
/* A drawing tool armed means the next click on the plot places a point, so the
   cursor should say so. */
.ws-canvas.arming .ws-draw-svg,
.ws-canvas.arming .ws-draw { cursor: crosshair; pointer-events: auto; }
/* And a finger dragged across the plot draws rather than scrolls the page,
   while a tool is armed and only then. */
.ws-canvas.arming .ws-draw { touch-action: none; }

/* The plot's cursor is the chart's own crosshair, set by its overlay, rather
 * than an open hand: it points at a bar, which hovering, a held press that
 * measures and every drawing tool all need. The closed hand, once a drag is
 * panning, is `body.ws-panning`, in the chart-navigator section. */
.ws-canvas.arming .ws-chart { cursor: crosshair; }
.ws-tool:disabled { opacity: 0.3; cursor: default; }

/* The Macro tab's right column: breadth, then equal-weight vs cap-weight,
   stacked so the column runs as long as the regime panel beside it. */
.macro-side { display: flex; flex-direction: column; gap: var(--space-5); min-width: 0; }
.macro-side > .panel { margin: 0; }
/* And the two columns end on one line. `.grid.c2` sizes panels to their own
   content, which is right where one panel has far less to say than the other;
   here they are within a tile's height of each other at most widths, and the
   difference was the gap. The shorter side's last panel takes it up. */
.grid.c2.macro-row { align-items: stretch; }
.macro-side > .panel.is-open:last-child { flex: 1 0 auto; }
/* Only while it is open. Closed, a panel is its heading and nothing else, and
   stretching it to the regime panel's height drew a column of empty box under a
   one-line heading: reported with the 12-term breakdown, a tall regime panel
   and equal-weight vs cap-weight shut beside it, as "fix this UI issue in the
   macro tab". A shut panel keeps the height of its heading. */
.macro-side > .panel.is-closed { flex: 0 0 auto; }
.macro-why { color: var(--ink-2); font-size: var(--t-small); margin: 0 0 var(--space-3); }
.macro-why strong { color: var(--ink); }

/* ---- currencies -------------------------------------------------------- */
.fx-sub { color: var(--ink-muted); font-size: var(--t-caption); }
.fx-driver {
  display: inline-block;
  padding: 1px var(--space-2);
  border-radius: var(--r-pill);
  border: 1px solid var(--border-strong);
  font-size: var(--t-micro);
  text-transform: uppercase;
  letter-spacing: 0.03em;
  color: var(--ink-2);
  white-space: nowrap;
}
.fx-means { color: var(--ink-muted); white-space: normal; text-align: left; }
/* The explanation row. Its own row rather than a column, because three
   paragraphs squeezed into a cell off the right edge of a scrolling table is
   exactly where a reader stops reading. */
/* Specificity, not inheritance, is the trap here.
 *
 * `table.data td` sets white-space: nowrap and scores (0,1,2). A plain
 * `.fx-detail td` scores (0,1,1) and loses, so this row stayed nowrap: each of
 * the three explanation paragraphs rendered as one unbroken line and painted
 * straight over the column beside it. The labels looked fine because they sit
 * in their own span, which made it read as a grid bug rather than a wrapping
 * one. Same defect as the positions gauge caption.
 *
 * Fixed on two fronts. The selector below now outscores table.data td, and the
 * prose elements each carry their own declaration, because a declaration on the
 * element always beats a value inherited from an ancestor and so cannot be lost
 * to a future specificity change. */
table.data tr.fx-detail td,
.fx-read,
.fx-grid,
.fx-grid > div { white-space: normal; }

.fx-detail td {
  white-space: normal;
  text-align: left;
  padding-top: 0;
  padding-bottom: var(--space-3);
  border-bottom: 1px solid var(--baseline);
}
.fx-read { color: var(--ink); margin-bottom: var(--space-2); }
.fx-grid {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(240px, 1fr));
  gap: var(--space-3);
  color: var(--ink-2);
  font-size: var(--t-caption);
  line-height: 1.55;
}
.fx-lab {
  display: block;
  color: var(--ink-muted);
  font-size: var(--t-micro);
  letter-spacing: 0.01em;
  margin-bottom: 2px;
}

/* ---- Pulse: persona picker and starter cards ---------------------------- */
.pulse-persona {
  display: flex;
  align-items: center;
  gap: var(--space-2);
  padding: var(--space-2) var(--space-3);
  border-bottom: 1px solid var(--border);
  flex-wrap: wrap;
}
/* `.pulse-persona-lab` lived here. The label moved inside the box so both
   controls wear the same `.oc-field`, and the class went with it. */
/* The two controls share the row, and their menus stay inside the panel.
 *
 * Asked for as "fix how the dynamic sizing works for optic persona when the
 * panel becomes smaller". Each box was as wide as its label, so the pair
 * measured 177px and 144px and changed size with the choice. Each menu was a
 * fixed 320px opening from its own box's left edge: with Pulse at its 382px
 * default the persona menu ended 139px past the window, at 460px 61px, and at
 * 320px, with the boxes stacked, 15px. The rule this replaces sized
 * `.pp-field`, which is not a child of this row, so it never applied.
 *
 * Now the boxes split the row evenly and each takes the whole line once the
 * panel is too narrow for two; a long label ends in an ellipsis rather than
 * widening its box; the mode menu opens from the row's left edge and the
 * persona menu from its right, and neither is wider than the panel. */
.pulse-persona > .km,
.pulse-persona > .pp { flex: 1 1 150px; min-width: 0; }
.pulse-persona .km-btn,
.pulse-persona .pp-field { width: 100%; min-width: 0; }
.pulse-persona .km-now,
.pulse-persona .pp-now {
  max-width: 100%;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}
.pulse-persona .km-menu,
.pulse-persona .oc-menu {
  max-width: min(calc(var(--chat-w) - 2 * var(--space-3)), calc(100vw - 2 * var(--space-3)));
}
.pulse-persona .pp .oc-menu { left: auto; right: 0; }
/* The blurb takes the whole next line rather than competing for width: at
   200px it truncated to three words and stopped explaining anything. */
.pulse-persona-blurb {
  flex: 1 1 100%;
  font-size: var(--t-caption);
  color: var(--ink-muted);
  line-height: 1.45;
}

/* The empty state is a greeting, so it is centred on the panel.
   The mascot was flush left against the panel padding with the heading and
   the text ranged beside it, which reads as a stray illustration rather than
   as the panel introducing itself. The starter cards keep their own grid and
   their text stays left-aligned -- those are things to read, not a greeting. */
.pulse-empty { padding: var(--space-4) var(--space-3); text-align: center; }
.pulse-empty .pulse-logo { display: block; margin: 0 auto var(--space-3); }
.pulse-empty .pulse-empty-sub { margin-inline: auto; max-width: 46ch; }
.pulse-empty .pulse-cards { text-align: left; }
.pulse-empty h3 { margin: 0 0 var(--space-2); font-size: var(--t-heading); }
.pulse-empty-sub {
  margin: 0 0 var(--space-4);
  color: var(--ink-2);
  font-size: var(--t-small);
  line-height: 1.55;
}
.pulse-cards {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(210px, 1fr));
  gap: var(--space-2);
}
.pulse-card {
  display: flex;
  gap: var(--space-2);
  align-items: flex-start;
  text-align: left;
  padding: var(--space-3);
  background: var(--surface-2);
  border: 1px solid var(--border);
  border-radius: var(--r-md);
  color: var(--ink-2);
  font: inherit;
  font-size: var(--t-caption);
  line-height: 1.5;
  cursor: pointer;
}
.pulse-card:hover {
  color: var(--ink);
  border-color: var(--ink-muted);
  background: var(--raised);
}
.pulse-card-n {
  flex: 0 0 auto;
  color: var(--accent);
  font-weight: 600;
  font-variant-numeric: tabular-nums;
}
.pulse-more {
  margin-top: var(--space-3);
  background: none;
  border: none;
  padding: 0;
  color: var(--ink-muted);
  font: inherit;
  font-size: var(--t-caption);
  text-decoration: underline;
  text-decoration-style: dotted;
  cursor: pointer;
}
.pulse-more:hover { color: var(--ink); }
.pulse-empty-note {
  margin: var(--space-3) 0 0;
  font-size: var(--t-caption);
  color: var(--ink-muted);
  line-height: 1.5;
}

/* ---- stock maps -------------------------------------------------------- */
.map-picker { display: flex; flex-wrap: wrap; gap: var(--space-1); margin: var(--space-3) 0; }
.map-universe {
  margin: 0 0 var(--space-2);
  font-size: var(--t-caption);
  color: var(--ink-muted);
}
/* The treemap. Absolute tiles inside a fixed-aspect box, because the server's
   layout is in 0-100 space and percentage positioning scales it for free —
   a resize is a re-scale, not a refetch. */
.map-tiles {
  position: relative;
  width: 100%;
  aspect-ratio: 16 / 9;
  min-height: 260px;
  border: 1px solid var(--border);
  border-radius: var(--r-md);
  overflow: hidden;
  background: var(--surface-2);
}
.map-tile {
  position: absolute;
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  gap: 1px;
  /* A hairline gap so adjacent tiles of similar colour stay distinguishable —
     without it a run of flat sectors reads as one block. */
  outline: 1px solid var(--plane);
  outline-offset: -1px;
  cursor: pointer;
  overflow: hidden;
  transition: filter 90ms ease;
}
.map-tile:hover { filter: brightness(1.25); }
.map-tile-sym {
  font-size: var(--t-caption);
  font-weight: 600;
  color: var(--ink);
  line-height: 1.1;
}
.map-tile-val {
  font-size: var(--t-micro);
  color: var(--ink-2);
  font-variant-numeric: tabular-nums;
}
.map-legend {
  margin: var(--space-2) 0 0;
  font-size: var(--t-caption);
  color: var(--ink-muted);
  line-height: 1.5;
}
@media (max-width: 640px) {
  /* 16:9 leaves tiles too small to label on a phone; taller gives each row more
     height at the cost of width, which is the better trade for a treemap. */
  .map-tiles { aspect-ratio: 4 / 5; }
}

/* Five tiles across, for the relative-performance horizons. `c4` wraps 5 items
   into 4+1 and leaves an orphan; this row is naturally five. */
.grid.c5 { display: grid; grid-template-columns: repeat(auto-fit, minmax(120px, 1fr)); gap: var(--space-2); }

/* The unreachable-origin error. More than one line, because "HTTP 530" is not
   an explanation and the reader needs to know the address itself may be dead. */
.error-note {
  margin-top: var(--space-2);
  font-size: var(--t-caption);
  color: var(--ink-muted);
  line-height: 1.55;
}
.error-acts { margin-top: var(--space-3); }

/* The empty state borrows the error state's action spacing rather than
   choosing its own: they are the same gesture -- a sentence, then the control
   that answers it -- and two panels a reader sees in the same session should
   not sit their buttons at different distances. */
.empty-acts { margin-top: var(--space-3); }
/* Room to breathe, because this panel is usually alone on the page rather than
   one card in a grid, and centred for the same reason. */
.empty-state {
  padding: var(--space-7) var(--space-6);
  text-align: center;
}
.empty-state > h2 { margin-bottom: var(--space-2); }
.empty-state > .sub { max-width: 46ch; margin-inline: auto; }

/* ---- economic series picker -------------------------------------------- */
.econ-groups { display: flex; flex-direction: column; gap: var(--space-2); margin: var(--space-3) 0; }
.econ-group { display: flex; align-items: baseline; gap: var(--space-2); flex-wrap: wrap; }
.econ-group-lab {
  flex: 0 0 130px;
  font-size: var(--t-micro);
  letter-spacing: 0.01em;
  color: var(--ink-muted);
}
.econ-pills { display: flex; flex-wrap: wrap; gap: var(--space-1); flex: 1 1 auto; }
@media (max-width: 640px) {
  .econ-group-lab { flex: 1 1 100%; }
}

/* ---- alerts ------------------------------------------------------------- */
.ws-wrail-btn { position: relative; }
.ws-wrail-badge {
  position: absolute;
  top: 2px;
  right: 6px;
  min-width: 14px;
  padding: 0 var(--space-1);
  border-radius: var(--r-pill);
  background: var(--neg);
  color: var(--ink-on-solid);
  font-size: 9px;
  font-weight: 700;
  line-height: 14px;
  text-align: center;
}
.alert-list { display: flex; flex-direction: column; gap: var(--space-2); }
.alert-row {
  padding: var(--space-2);
  border-radius: var(--r-xs);
  background: var(--surface-2);
  border-left: 2px solid transparent;
}
/* Unread gets the accent edge rather than a bolder weight: the titles are
   already short, and bolding half a list makes neither half readable. */
.alert-row.unseen { border-left-color: var(--accent); }
.alert-top { display: flex; align-items: center; gap: var(--space-1); flex-wrap: wrap; }
.alert-kind {
  font-size: var(--t-micro);
  text-transform: uppercase;
  letter-spacing: 0.04em;
  padding: 0 var(--space-1);
  border-radius: var(--r-xs);
  border: 1px solid currentColor;
}
.alert-kind.high { color: var(--neg); }
.alert-kind.normal { color: var(--accent); }
.alert-kind.low { color: var(--ink-muted); }
.alert-title { color: var(--ink); margin-top: 2px; line-height: 1.4; }
.alert-body { color: var(--ink-muted); font-size: var(--t-caption); line-height: 1.45; }
.alert-acts { display: flex; gap: var(--space-1); margin-top: var(--space-2); }
.alert-delivery {
  margin-top: var(--space-3);
  padding-top: var(--space-2);
  border-top: 1px solid var(--border);
  font-size: var(--t-caption);
  color: var(--ink-muted);
  line-height: 1.5;
}
.alert-blockers { margin: var(--space-1) 0 0; padding-left: var(--space-4); }
.alert-blockers li { margin-bottom: var(--space-1); }

/* ---- the workspace in a narrow column ----------------------------------
 *
 * Triggered by the workspace's own width, not the viewport's — the two differ
 * by 400px exactly when Pulse is open, which is the case this exists for.
 *
 * The dock is dropped rather than shrunk. A 120px news widget is not a smaller
 * useful widget, it is an unreadable one, and leaving it in place is what
 * squeezed the chart until its header and its legend drew on top of each other.
 */
/* Driven by `body.chat-open` as well as the measured class.
 *
 * The JS observer alone did not work, and the reason is the same one that broke
 * the drawing handlers: renderChartWorkspace replaces .ws-body, so an observer
 * bound to the old element is watching a node that is no longer on the page.
 * `body.chat-open` is set by the Pulse toggle and never replaced, so keying off
 * it is deterministic — the layout reflows the instant the panel opens, with no
 * observer, no debounce and nothing to re-attach.
 *
 * The measured `.narrow` class is kept for the other case: a genuinely small
 * window with Pulse closed. */
.ws-body.narrow,
body.chat-open .ws-body { grid-template-columns: auto 1fr auto; }
.ws-body.narrow .ws-dock,
body.chat-open .ws-body .ws-dock { display: none; }

/* …unless the reader picked one from the rail, and Pulse is closed.
 *
 * The blanket hide above is right for the DEFAULT state: four widgets opened on
 * a previous visit should not eat a narrow chart. It is wrong for an explicit
 * click, because then the rail buttons change state, the widget renders, and it
 * sits behind `display: none` — from the outside every button in the rail looks
 * dead.
 *
 * The rule that re-showed it is BELOW, scoped to `body:not(.chat-open)`. This
 * spot used to hold a near-identical copy that also matched `body.chat-open`,
 * and the two contradicted each other: the comment on the scoped version
 * records that floating the dock with Pulse open "reintroduced exactly the
 * fault being fixed", and the unscoped copy kept doing it. Measured with Pulse
 * at 620px on a 1142px window: the dock rendered at 300px on top of a 382px
 * chart, covering 78% of it — three grid tracks, four visible children, and the
 * dock auto-placed straight onto the canvas.
 *
 * So there is one float rule now and it lives below. With Pulse open the dock
 * stays hidden and the widget rail is still there; the chart is what the tab is
 * for. */

/* Opening a widget while narrow floats it over the chart instead of taking the
   chart's width back — you asked to see one widget, not to lose the chart. */
/* Floating the dock is for the small-window case only.
 *
 * It was applied with Pulse open too, and that reintroduced exactly the fault
 * being fixed: the dock stopped taking grid width but started drawing on top of
 * a 371px chart, which from the reader's side is the same overlap. With Pulse
 * open the dock is simply hidden — the widget rail is still there, and the
 * chart is what the tab is for.
 */
/* Scoped to `body:not(.chat-open)` deliberately.
 *
 * Opening Pulse makes the workspace narrow too, so without this the float rule
 * matched and re-showed the dock on top of the chart — beating the hide rule
 * above on specificity and undoing the fix. The float is for a small window with
 * Pulse closed; with Pulse open the dock stays hidden.
 */
body:not(.chat-open) .ws-body.narrow.dock-open { grid-template-columns: auto 1fr auto; }
body:not(.chat-open) .ws-body.narrow.dock-open .ws-dock {
  display: block;
  position: absolute;
  top: 0;
  right: 62px;                 /* clear of the widget rail */
  bottom: 0;
  width: min(300px, 70%);
  z-index: 6;
  background: var(--surface);
  border-left: 1px solid var(--border-strong);
  /* Directional: this drawer slides in from the right, so the cast goes
   left rather than down and the elevation scale does not fit. The
   colour is still the theme's. */
  box-shadow: -12px 0 28px -12px var(--shadow-cast);
  overflow-y: auto;
}
.ws-body { position: relative; }

/* Below 900px the widget rail turns into a horizontal strip along the bottom,
   so the overlay clears the bottom rather than the right.

   AFTER the rule it adjusts, not before. The selector is identical, so source
   order is the only thing deciding — placed above, its `right: 0` lost to the
   base rule's `right: 62px` and the overlay stayed clear of a rail that was no
   longer on that edge. */
@media (max-width: 900px) {
  body:not(.chat-open) .ws-body.narrow.dock-open .ws-dock {
    right: 0;
    bottom: 66px;
    width: min(320px, 92%);
  }
}

/* The tool rail loses its labels-by-title breathing room too.
 *
 * Guarded to the widths where the rail is still VERTICAL. Below 900px the layout
 * turns it into a horizontal strip along the bottom, and pinning that to 54px
 * squeezed fourteen buttons into one visible slot — it looked like a stray green
 * box in the corner rather than a rail. The two rules were fighting and this one
 * was winning on specificity. */
@media (min-width: 901px) {
  .ws-body.narrow .ws-wrail,
  body.chat-open .ws-body .ws-wrail { width: 54px; }
}
@media (min-width: 901px) {
  .ws-body.narrow .ws-wrail-lab,
  body.chat-open .ws-body .ws-wrail-lab { display: none; }
}

/* And the legend, which sits ON the chart, has to stop competing with the
   header when there is barely room for either. */
/* The legend stops overlaying and takes its own line.
 *
 * Absolutely positioned over the chart's top-left corner is right when there is
 * room — that corner is usually empty and the values sit next to the price they
 * describe. In a narrow column it lands squarely on the header instead, which is
 * the overlap in the report. Below the threshold it becomes a normal block above
 * the chart: it costs a row of height and collides with nothing. */
.ws-body.narrow .ws-legend,
body.chat-open .ws-body .ws-legend {
  position: static;
  max-width: none;
  margin: 0 0 var(--space-1);
  font-size: var(--t-micro);
  background: none;
}
.ws-body.narrow .ws-leg-args,
body.chat-open .ws-body .ws-leg-args { display: none; }

/* ---- the workspace in a narrow column, take two -------------------------
 *
 * Measured with Pulse open at 1280px: the toolbar had wrapped to four rows
 * (123px), the price header to two (75px) and the legend was a six-row stack
 * (119px). That is 317px of chrome above a chart that had 160px left — the
 * inverse of what the tab is for. TradingView and TrendSpider both hold their
 * chrome under about 70px at any width, and they do it by scrolling the toolbar
 * sideways rather than wrapping it.
 */

/* One row, scrolled sideways. Wrapping is what turned five buttons into four
   rows; a horizontal scroll keeps every control reachable at a fixed height. */
body.chat-open .ws-toolbar,
.ws-body.narrow .ws-toolbar {
  flex-wrap: nowrap;
  overflow-x: auto;
  overflow-y: hidden;
  gap: var(--space-1);
  padding: var(--space-1) var(--space-2);
  scrollbar-width: none;
}
body.chat-open .ws-toolbar::-webkit-scrollbar,
.ws-body.narrow .ws-toolbar::-webkit-scrollbar { display: none; }
body.chat-open .ws-toolbar > *,
.ws-body.narrow .ws-toolbar > * { flex: 0 0 auto; }
/* The range and interval pills are the widest group and the least often
   changed once set, so they are the ones that scroll off. */
body.chat-open .ws-toolbar .pills,
.ws-body.narrow .ws-toolbar .pills { flex-wrap: nowrap; }

/* The price header on one line. The OHLC set is four labelled numbers and the
   first thing worth dropping — the candle under the cursor gives them back on
   hover, and the last price is already on the axis. */
body.chat-open .ws-head,
.ws-body.narrow .ws-head {
  flex-wrap: nowrap;
  overflow: hidden;
  gap: var(--space-2);
  padding: var(--space-1) var(--space-2);
  white-space: nowrap;
}
body.chat-open .ws-head .ws-ohlc,
.ws-body.narrow .ws-head .ws-ohlc { display: none; }
/* The company name and the session word go too. The symbol is already in the
   legend and the session is on the status bar; at this width they are the two
   things costing space and saying least. */
body.chat-open .ws-head .ws-head-name,
.ws-body.narrow .ws-head .ws-head-name,
body.chat-open .ws-head .ws-head-state,
.ws-body.narrow .ws-head .ws-head-state { display: none; }
/* The stage chip shortens rather than vanishes: "Stage 2" survives, the name
   and the "new" tag go. Measured at 561px, the header 263px wide and nowrap
   with overflow hidden, the full chip ran 41px past its edge and was cut off
   mid-word. The number is the reading; the name repeats it in words and the
   tag is in the title, which is where the rest of the reasoning already is. */
body.chat-open .ws-head .stage-name,
.ws-body.narrow .ws-head .stage-name,
body.chat-open .ws-head .stage-tag,
.ws-body.narrow .ws-head .stage-tag { display: none; }
/* And Explain chart becomes the Pulse mark, since on this one clipped line
   its words were cut to "Ex". The button keeps its name. See chartPulse. */
body.chat-open .ws-head .chart-pulse,
.ws-body.narrow .ws-head .chart-pulse { padding: var(--space-1); }
body.chat-open .ws-head .chart-pulse-lab,
.ws-body.narrow .ws-head .chart-pulse-lab { display: none; }
body.chat-open .ws-head .chart-pulse-ico,
.ws-body.narrow .ws-head .chart-pulse-ico { display: block; }

/* The legend as a single scrolling line, the way both reference products do it:
   symbol, then each overlay with its value, all on one row. */
body.chat-open .ws-legend,
.ws-body.narrow .ws-legend {
  display: flex;
  align-items: center;
  gap: var(--space-2);
  overflow-x: auto;
  scrollbar-width: none;
  padding: 0 var(--space-2);
  margin: 0;
}
body.chat-open .ws-legend::-webkit-scrollbar,
.ws-body.narrow .ws-legend::-webkit-scrollbar { display: none; }
body.chat-open .ws-leg-body,
.ws-body.narrow .ws-leg-body {
  display: flex;
  align-items: center;
  gap: var(--space-3);
}
body.chat-open .ws-leg-row,
.ws-body.narrow .ws-leg-row { flex: 0 0 auto; }
body.chat-open .ws-leg-title,
.ws-body.narrow .ws-leg-title { flex: 0 0 auto; margin: 0; }
/* The per-row hide and remove buttons need a hover to find at this size and
   there is no hover on a laptop trackpad worth relying on — the toolbar's
   checkboxes do the same job with room to hit them. */
body.chat-open .ws-leg-btn,
.ws-body.narrow .ws-leg-btn { display: none; }
body.chat-open .ws-leg-static,
.ws-body.narrow .ws-leg-static { display: none; }

/* Force text presentation on the rail glyphs.
   Belt and braces alongside choosing non-emoji codepoints: a future glyph that
   does have an emoji form will still render flat. */
.ws-wrail-ico {
  font-variant-emoji: text;
  font-family: var(--sans);
}

/* Scroll affordance on the compacted toolbar and legend.
   A row that silently cuts off looks broken; a fade says "there is more". Local
   attachment so it moves with the content and disappears at the end. */
body.chat-open .ws-toolbar,
.ws-body.narrow .ws-toolbar,
body.chat-open .ws-legend,
.ws-body.narrow .ws-legend {
  background-image: linear-gradient(to left, var(--surface) 30%, transparent);
  background-repeat: no-repeat;
  background-size: 28px 100%;
  background-position: 100% 0;
  background-attachment: local;
}

/* Deliberately NOT flexed.
 *
 * I tried making the host a flex child that fills .ws-plot, to stop it inheriting
 * the height of a stale chart. It made things worse in two steps, and both are
 * worth recording so the next attempt does not repeat them:
 *
 *   - `min-height: 0` lost to the INLINE 160px that mount() sets as a
 *     placeholder while a chart builds, so the host was pinned at 160px.
 *   - `min-height: 0 !important` beat the inline value and then collapsed the
 *     empty host to ZERO height, so a build that had not landed yet had nothing
 *     to measure and never landed at all.
 *
 * The host sizing to its SVG is fine. The height comes from the builder, which
 * measures the available space at build time; the fix for a stale height is to
 * rebuild, not to restyle the box. */

/* Sector drill-down: the breadcrumb, and the cue that a tile is clickable. */
.map-crumb {
  background: none;
  border: 0;
  padding: 0;
  color: var(--s1);
  font: inherit;
  font-size: inherit;
  cursor: pointer;
  text-decoration: underline;
}
.map-crumb:hover { color: var(--ink); }
.map-crumb-sep { color: var(--ink-muted); margin: 0 var(--space-0); }
.map-hint { color: var(--ink-muted); font-style: italic; }
/* Only drillable tiles advertise it. An individual stock opens its chart, which
   is a different gesture and should not borrow the same affordance. */
.map-tile[data-map-sector] { cursor: zoom-in; }
.map-tile[data-map-sector]:hover { outline: 2px solid var(--ink); outline-offset: -2px; }

/* While panning the chart. `grabbing` on the body rather than the chart,
   because the pointer routinely leaves the plot mid-drag and a cursor that
   reverts halfway through reads as the gesture having been dropped.
   
   The universal selector and the !important are the one place in this file
   they are earned: every element the pointer crosses declares its own cursor —
   text over a label, grab over the plot, pointer over a button — and during a
   drag all of them are wrong. The rule only applies while the class is set,
   which is only from a pan's first move to its release. */
body.ws-panning, body.ws-panning * { cursor: grabbing !important; }
body.ws-panning { user-select: none; }
.ws-zoom-reset { color: var(--s1); }

/* ------------------------------------------------------- chart navigator
 *
 * A compressed view of the whole series with the visible window drawn on it.
 * It exists so panning has somewhere to live that is not the plot: with a
 * dedicated control, a plain drag on the chart can go back to measuring, and
 * neither gesture needs a modifier key to disambiguate it.
 *
 * It also answers a question zooming otherwise leaves open, which is where in
 * the history you currently are. */
.ws-nav {
  height: 46px;
  border-top: 1px solid var(--border);
  background: var(--surface);
  cursor: grab;
  user-select: none;
  touch-action: none;
  position: relative;
}
.ws-nav.dragging { cursor: grabbing; }
.ws-nav svg { display: block; width: 100%; height: 100%; }
/* The window is the chart's own colour, like its line: Optic's gold. */
.ws-nav-window { fill: var(--brand); opacity: 0.16; }
.ws-nav-edge { fill: var(--brand); opacity: 0.85; cursor: ew-resize; }
.ws-nav-mask { fill: var(--plane); opacity: 0.55; }
.ws-nav-line { fill: none; stroke: var(--ink-muted); stroke-width: 1; }
.ws-nav-area { fill: var(--ink-muted); opacity: 0.16; }
@media (max-height: 700px) { .ws-nav { height: 34px; } }

/* ==================================================== texture and glass ==== */

/* Background grain.
 *
 * One fixed overlay for the whole document rather than a texture on each
 * surface: a tile that scrolls with the content reads as a pattern, and the
 * point of grain is that it belongs to the screen, not to the page.
 *
 * Generated by feTurbulence in a data URI, so there is no image to fetch and
 * nothing to go stale. fractalNoise at a high baseFrequency gives fine grain;
 * lower frequencies produce clouds, which look like a rendering fault.
 *
 * pointer-events: none is load-bearing. A fixed full-viewport layer without it
 * swallows every click on the page.
 */
body::after {
  content: '';
  position: fixed;
  inset: 0;
  z-index: 9999;
  pointer-events: none;
  opacity: var(--grain-opacity);
  background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='160' height='160'%3E%3Cfilter id='n'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='0.85' numOctaves='3' stitchTiles='stitch'/%3E%3C/filter%3E%3Crect width='160' height='160' filter='url(%23n)'/%3E%3C/svg%3E");
  background-repeat: repeat;
  background-size: 160px 160px;
}
/* Grain is decoration and it is the kind of static texture that some readers
   find uncomfortable, so it goes with the rest of the motion preferences. */
@media (prefers-reduced-motion: reduce) {
  body::after { opacity: 0; }
}
@supports ((backdrop-filter: blur(1px)) or (-webkit-backdrop-filter: blur(1px))) {
}
@supports not ((backdrop-filter: blur(1px)) or (-webkit-backdrop-filter: blur(1px))) {
}

/* A reader who asked for less motion gets instant jumps. Overriding on both
   elements because either can own the scroll depending on the browser. */
@media (prefers-reduced-motion: reduce) {
  html, body { scroll-behavior: auto; }
}

/* Horizontal clipping lives here, not on html or body.
 *
 * On the root it either breaks position: sticky (as `hidden`, by making the
 * root a scroll container) or breaks IntersectionObserver (as `clip`, by
 * degenerating its implicit root). On main it does neither: the sticky header
 * is a sibling and still measures against the viewport, and observers inside
 * main still intersect the viewport normally.
 *
 * Measured at 942px wide, nothing currently overflows, so this is a guard for
 * narrow widths rather than a fix for a live problem: a wide table or a chart
 * at a small viewport should clip rather than add a horizontal scrollbar to
 * the whole document.
 */
main { overflow-x: clip; }

/* ============================================ retro-futuristic display ==== */

/* Headings and the wordmark take the display face; everything else does not.
 *
 * Space Grotesk's character sits in its widths and its squared terminals, which
 * is exactly what a heading wants and exactly what a column of prices does not.
 * Numbers stay in Plus Jakarta Sans throughout, including inside these
 * headings, so a panel title reading "SPY 612.40" does not change face
 * mid-sentence.
 *
 * -0.01em because the face is wide by design; at display sizes the default
 * tracking reads loose next to the tighter body text under it.
 */
/* .ws-head-sym was in this list and is in no markup: the chart header draws its
   symbol as a bare <strong>. Naming it here made the rule look like it covered
   the chart header when it did not, so the header is added by the element that
   actually exists. */
h1, h2, h3,
.home-title, .brand-text, .hero, .value, .ws-head > strong {
  font-family: var(--display);
  letter-spacing: -0.01em;
}
/* Figures inside a display-face heading fall back to the reading face, which
   has the tabular numerals the rest of the app relies on. */
h1 .num, h2 .num, h3 .num,
.hero .num, .value .num { font-family: var(--sans); }

/* ------------------------------------------------ organic, then precise ---
 *
 * The chrome gets softer, less uniform corners; the data does not.
 *
 * A treemap tile, a table cell and a candle are measurements — rounding them
 * makes a boundary ambiguous, and on a treemap it literally misreports area.
 * So the curve lives on the things you touch (panels, buttons, pills, the
 * floating chrome) and stops at the things you read.
 *
 * The asymmetric panel radius is the "organic" part: equal corners read as a
 * box, a slightly longer top-left reads as a card that was placed rather than
 * generated. Two pixels of difference, not ten.
 */
/* Not `.pill`. It was swept in here with the buttons and landed on 10px,
 * which is the one radius a pill must not have: the rule above this block
 * says the curve belongs on "panels, buttons, pills", and 10px on a 29px-tall
 * control is a rounded rectangle. `.pill` carries --r-pill in its own rule and
 * now keeps it, which is also what `.chip` and `.wd-chip` beside it already
 * render at. */
.btn, .ws-menu-btn {
  border-radius: var(--r-ctl);
}
.btn.primary {
  border-radius: var(--r-ctl);
}
/* Precision stays where a shape carries a number. */
.map-tile,
table.data td,
table.data th,
.candle, .bar { border-radius: 0; }

/* ================================================== phone layout (<560px) ==
 *
 * The app had no breakpoint below 620px, so a 375px phone was being served
 * rules written for a laptop. Measured on an iPhone-sized viewport before this
 * block existed: the top bar alone was 211px tall and the status strip under it
 * another 78px, so 289px — 36% of an 812px screen — was chrome before a single
 * figure appeared. The ticker input had collapsed to 88px and was clipping its
 * own placeholder to "TICKER (", and the section dropdowns overhung the right
 * edge by 5px, which is what put a horizontal scrollbar on the document.
 *
 * The fix is to give each row of the bar one job instead of letting six items
 * wrap against each other: identity, then search, then navigation.
 */
@media (max-width: 559px) {
  /* flex-wrap and row-gap are NOT repeated here.
   *
   * The 1280px block above already wraps the bar and gives the tab strip its
   * own full-width row, and it matches at this width too — so declaring them
   * again would be two rules for one layout, which is the thing this pass spent
   * its time removing elsewhere in the file. What is genuinely different on a
   * phone is only how much room the chrome is allowed to take. */
  header.topbar {
    gap: var(--space-2);
    padding-left: var(--space-3);
    padding-right: var(--space-3);
  }

  /* Row one: who this is, and the two controls that are always available.
     The brand keeps its mark and drops nothing — it is already the narrowest
     thing in the bar. */
  .brand { margin-left: calc(var(--space-1) * -1); }

  /* Row two. order:1 puts it after the brand and the buttons regardless of
     source order, and the 100% basis is what forces the break — without it the
     input keeps trying to share a row and loses the negotiation, which is how
     it ended up at 88px. */
  .search {
    order: 1;
    flex: 1 0 100%;
    min-width: 0;
  }
  /* The input is wrapped in .combo — the autocomplete positions its list
     against it — so growing the input alone changes nothing: the wrapper is
     `flex: 0 1 auto` and stays at its content width, which is what left 96px of
     the row empty while the placeholder truncated inside it. The wrapper has to
     grow, and min-width:0 is what lets it, since a flex item will not shrink
     below its content's intrinsic width without it. */
  .search .combo {
    flex: 1 1 auto;
    min-width: 0;
  }
  .search input {
    width: 100%;
    min-width: 0;
  }
  .search .btn { flex: none; }

  /* Uppercase at 0.06em is roughly two extra characters of width on a string
     this long. On a phone that is the difference between the placeholder fitting
     and advertising a capability it visibly cannot show. */
  .search input,
  .home-search input { letter-spacing: 0.02em; }

  /* Row three, and one row only.
   *
   * This is the one thing the 1280px block does not do: it lets the strip wrap,
   * which put six tabs on two lines separated by a 2px gap — one broken row
   * rather than two deliberate ones. A scrolling strip is honest about there
   * being more to the right, and the dropdowns escape its clipping because they
   * are fixed rather than absolute below. Order, width and overflow-x all come
   * from that block; only the wrap behaviour changes here. */
  nav.tabs,
  nav.tabs-group {
    /* The desktop strip's flex:none/min-width:auto made this 732px wide on a
       390px phone, so overflow:auto had nothing inside its own box to scroll. */
    flex: 1 1 100%;
    min-width: 0;
    flex-wrap: nowrap;
    /* overflow-x IS needed here, and is not the duplicate it looks like.
     *
     * The 1280px block sets `nav.tabs { overflow-x: auto }`, but further down
     * the file `nav.tabs-group { overflow: visible }` fires unconditionally to
     * let the section dropdowns escape the strip — and that is a later rule on
     * the same element, so it wins. Dropping this line on the grounds that the
     * value already appeared once put the two right-hand tabs 205px outside the
     * document and the horizontal scrollbar straight back. Measured: 375px
     * viewport, 580px scrollWidth.
     *
     * The dropdowns still escape, because on a phone they are position:fixed. */
    overflow-x: auto;
    overflow-y: visible;
    scrollbar-width: none;
    -webkit-overflow-scrolling: touch;
  }
  nav.tabs::-webkit-scrollbar,
  nav.tabs-group::-webkit-scrollbar { display: none; }
  nav.tabs button { padding: var(--space-2) var(--space-3); }

  /* Chrome takes less, content takes more. */
  .statusline { padding: var(--space-2) var(--space-3); gap: var(--space-3); }
  main { padding: var(--space-5) var(--space-4) var(--space-8); }

  /* One column. auto-fit gives two cramped columns at 375 minus padding, which
     is 343 — enough for the grid to try and not enough for either side to hold
     a sentence. */
  .grid,
  .grid.c2, .grid.c3, .grid.c4 { grid-template-columns: 1fr; }

  /* Momentum scrolling for the wide tables. .table-scroll already carries
     overflow-x: auto — this adds the flick the horizontal gesture wants on a
     touch screen, and nothing else, because a second overflow declaration
     would just be a copy of the first one. */
  .table-scroll { -webkit-overflow-scrolling: touch; }

  /* The landing form stacks. Side by side, the button took 110px of a 343px row
     and left the field too narrow for a ticker and a company name to both look
     plausible in it — which is the one thing that field has to communicate. */
  .home-search { flex-direction: column; align-items: stretch; }
  .home-search .combo { flex: 1 1 auto; min-width: 0; }
  .home-search button { width: 100%; }
}

/* Between the phone and the inline row, the strip may wrap.
 *
 * Three arrangements, and this is the middle one. Inline (above 1410) it must
 * not wrap: `nav.tabs-group { flex-wrap: nowrap }` keeps the strip honest
 * about its width, because a shrinkable box whose content can wrap always
 * wraps before anything else on the row gives up a pixel. On a phone (below
 * 560) it must not wrap either: there it is a one-row horizontal scroller,
 * and the phone block sets `overflow-x: auto` on nav.tabs for exactly that.
 *
 * In between it has a full row of its own and no escape valve. It cannot
 * scroll, because the unconditional `nav.tabs-group { overflow: visible }`
 * beats the phone block's `overflow-x: auto` on source order and applies at
 * every width; and until this rule it could not wrap either. Measured at
 * 771px once Watchlist made eight groups: the last tab ran 784 to 876
 * against a 771px window, off the screen and unreachable. Seven groups ended
 * at 761 and fitted by 10px, which is why it appeared only now.
 *
 * **Both bounds matter.** Written without the lower one -- inside the 1410
 * block, where it started -- it also applied at 375 and beat the phone's
 * nowrap on specificity, turning the scroller into three wrapped rows and a
 * 288px header on an 812px screen. Measured before it was scoped. */
@media (min-width: 560px) and (max-width: 1410px) {
  nav.tabs.tabs-group { flex-wrap: wrap; row-gap: var(--space-2); }
}

/* The same overflow guard for every width: a menu may not be wider than the
   window it opens in, whatever its min-width asks for. */
.nav-menu { max-width: calc(100vw - 2 * var(--space-4)); }

/* A menu near the right edge opens leftwards, or it opens off the screen.
   `left: 0` anchors a 190px menu to the left edge of its tab, which is fine
   until that tab is near the right edge of the window. Measured at 900px once
   Watchlist became the eighth group: the strip ended at 873 and its menu ran
   781 to 971, 71px past the window, and nothing could scroll to it -- the page
   does not scroll horizontally and `nav.tabs-group { overflow: visible }`
   means the strip does not either.
   The class is set by `navMenuSides`, which measures. It was `:last-child`
   first, and position in the list turned out not to be the question: the strip
   wraps everywhere from 560 to 1410, so the last group can be the FIRST item
   on the second row. Measured at 900 the moment the Positions group gained a
   caret -- 14px, enough to tip the row -- Watchlist moved to x=18 and the
   right-anchored menu ran -79 to 111, 98px off the LEFT edge instead.
   The max-width above guards the width; this guards the side. */
nav.tabs-group > .nav-item.menu-right .nav-menu { left: auto; right: 0; }


/* ============================================== interaction, everywhere ====
 *
 * Fifty-eight of the eighty-three hover states in this file changed colour with
 * no transition on them, so most of the app snapped between two states while a
 * handful of buttons eased. That reads as unfinished rather than as fast: the
 * eye registers the inconsistency between two adjacent controls long before it
 * registers either one's speed.
 *
 * The timing is the same 120ms ease the thirty-seven rules that DID have a
 * transition were already using, held in a token now so the next one added
 * cannot pick a different number by accident.
 *
 * Only paint-level properties are listed. Animating a layout property forces a
 * full-document reflow on every frame, which is why padding and width are
 * deliberately absent — the same reasoning already written above `main`.
 * `text-decoration-color` rather than `text-decoration`: the shorthand is not
 * animatable, so a rule that underlines on hover fades the rule in instead of
 * doing nothing.
 */
:root {
  --dur-ui: 120ms;
  --ease-ui: ease;
}

.att-x, .brief-head a, .cal-pill, .cat-card > summary, .chart-pulse, .chart-pulse.is-explain, .combo-list li,
.hm-more, .wl-open, .wl-x,
.al-sym, .mtab, .msg-save, .rh-open, .rh-route,
.cp-row,
.pl-reason > details > summary,
.eval-h > summary, .ew-row, .ex-close, .ex-got, .ex-more,
.factor-defs-wrap > summary, .gloss-term, .home-quick button, .inst-clickable,
.lvl-clear, .lvl-menu > summary, .lvl-opt, .map-crumb, .map-tile[data-map-sector],
.msg .sources a, .nav-page, .panel, .panel > h2.is-toggle, .ph-del, .ph-item,
.pulse-card, .pulse-more, .read-cal-what a, .read-lead-title a, .read-nav a,
.sector-read-btn, .seg button, .seg-opt, .ses-co-link, .ses-co-more, .sub-close,
.suggest button, .tabs-sub button, .tkr, .ws-leg-row, .ws-leg-title, .ws-manage, .ws-pane,
.ws-menu-btn, .ws-news-link, .ws-opt, .ws-tool, .ws-watch-row, .ws-widget,
#chat-history-btn, .sec-bulk-btn, .sec-chip, .ws-wrail-btn, .ws-wrail-pulse,
.ws-full-btn,
details.ind-explain > summary,
details.read-cal-row > summary,
.cb-tkr,
.wd-btn, .wd-check,
.acct-btn, .acct-btn .acct-face, .acct-items button, .acct-out, .acct-warn button,
.auth-alt button, .auth-close, .auth-link, .auth-peek, .auth-provider,
.auth-toast-act, .chat-allow button, .set-danger .btn,
table.data tr, *::-webkit-scrollbar-thumb {
  transition:
    color var(--dur-ui) var(--ease-ui),
    background-color var(--dur-ui) var(--ease-ui),
    border-color var(--dur-ui) var(--ease-ui),
    text-decoration-color var(--dur-ui) var(--ease-ui),
    outline-color var(--dur-ui) var(--ease-ui),
    opacity var(--dur-ui) var(--ease-ui),
    filter var(--dur-ui) var(--ease-ui),
    box-shadow var(--dur-ui) var(--ease-ui);
}

/* Honoured here as everywhere else in this file. A vestibular trigger does not
   care that the movement was only a colour fade. */
@media (prefers-reduced-motion: reduce) {
  .att-x, .brief-head a, .cal-pill, .cat-card > summary, .chart-pulse, .chart-pulse.is-explain, .combo-list li,
.hm-more, .wl-open, .wl-x,
.al-sym, .mtab, .msg-save, .rh-open, .rh-route,
.cp-row,
.pl-reason > details > summary,
.eval-h > summary, .ew-row, .ex-close, .ex-got, .ex-more,
.factor-defs-wrap > summary, .gloss-term, .home-quick button, .inst-clickable,
.lvl-clear, .lvl-menu > summary, .lvl-opt, .map-crumb, .map-tile[data-map-sector],
.msg .sources a, .nav-page, .panel, .panel > h2.is-toggle, .ph-del, .ph-item,
.pulse-card, .pulse-more, .read-cal-what a, .read-lead-title a, .read-nav a,
.sector-read-btn, .seg button, .seg-opt, .ses-co-link, .ses-co-more, .sub-close,
.suggest button, .tabs-sub button, .tkr, .ws-leg-row, .ws-leg-title, .ws-manage, .ws-pane,
.ws-menu-btn, .ws-news-link, .ws-opt, .ws-tool, .ws-watch-row, .ws-widget,
#chat-history-btn, .sec-bulk-btn, .sec-chip, .ws-wrail-btn, .ws-wrail-pulse,
.ws-full-btn,
details.ind-explain > summary,
details.read-cal-row > summary,
table.data tr, *::-webkit-scrollbar-thumb { transition: none; }
}

/* ================================================== pointer target sizes ==
 *
 * WCAG 2.5.8 asks for 24x24 CSS pixels on anything you have to hit. Measured
 * across the Analysis view, three controls were under it: the panel collapse
 * chevron at 17x13, the "ask Pulse" pills at 63x16, and a pair of inline links
 * in the company line at roughly 20x17.
 *
 * The fix grows the HIT area and leaves the drawing alone. Padding would have
 * worked too and would also have moved every panel heading and pill a few
 * pixels, which is a visual change made for a reason that has nothing to do
 * with how it looks. An inset ::after is invisible, costs no layout, and is
 * part of the button so it forwards the click for free.
 *
 * The ~20 checkboxes that also measure 13x13 are deliberately NOT here. Every
 * one is inside its <label>, and a label toggles its control natively, so the
 * real target is the 206x36 row — already twice the minimum. Padding the box
 * itself would only push the caption away from it.
 */
.panel-toggle,
.ask-pulse { position: relative; }

.panel-toggle::after,
.ask-pulse::after {
  content: '';
  position: absolute;
  /* Enough to clear 24px in the short axis from the smallest of them (13px),
     and centred, so the growth is symmetrical about the glyph. */
  inset: -6px;
  /* Not a click shield for anything underneath: it belongs to the button. */
  border-radius: inherit;
}

/* These two sit in a line of company detail rather than a paragraph, so the
   "target is in a sentence" exemption is a stretch. Vertical padding is safe
   here — the line already has room and nothing is aligned to their box. */
.ses-co-link,
.ses-co-more {
  display: inline-block;
  padding: var(--space-1) var(--space-0);
}

/* ================================================= the chart Pulse button ==
 *
 * Top-right of the chart header, where the reference platforms put theirs.
 * Distinct from .ask-pulse — that one sits beside a panel heading and explains
 * the panel; this one reads the chart and is the primary action in its corner,
 * so it carries the accent rather than a quiet outline.
 */
.chart-pulse {
  margin-left: auto;
  display: inline-flex;
  align-items: center;
  gap: var(--space-2);
  flex: none;
  font: inherit;
  font-size: var(--t-caption);
  font-weight: 600;
  letter-spacing: 0.02em;
  padding: var(--space-1) var(--space-3);
  border-radius: var(--r-pill);
  border: 1px solid var(--btn-primary);
  background: color-mix(in srgb, var(--btn-primary) 16%, transparent);
  color: var(--ink);
  cursor: pointer;
  white-space: nowrap;
}
.chart-pulse:hover {
  background: var(--btn-primary);
  color: var(--btn-primary-ink);
}
.chart-pulse:focus-visible { outline: 2px solid var(--focus); outline-offset: 2px; }
.chart-pulse-ico { display: none; width: 18px; height: 18px; flex: none; }
/* The same live dot the top-bar Pulse button uses, so the two read as one
   feature in two places rather than as two different buttons. */

/* Beside a panel heading it is one control among several, so it drops the
   auto margin that pushes it to the far corner on the chart itself. */
.panel > h2 .chart-pulse { margin-left: var(--space-3); }

/* Pulse at the foot of the widget rail.
 *
 * The rail is a column of panel toggles; this one is an action, so it gets the
 * accent the toggles deliberately do not have and sits apart from them with a
 * divider. margin-top: auto pins it to the bottom whatever the widget count.
 *
 * There was a `.ws-wrail { display: flex; flex-direction: column; }` here,
 * restating what the base rule nine hundred lines up already says, as setup
 * for that auto margin. Unmediated and last, it silently undid both
 * responsive blocks: the rail is meant to become a scrolling ROW below 900px
 * and it stayed a column, so on a 375px phone fifteen buttons rendered as
 * 338x611 -- a wall of them under the chart, taller than the viewport. */
.ws-wrail-pulse {
  margin-top: auto;
  color: var(--ink);
  border-top: 1px solid var(--border);
}
@media (max-width: 900px) {
  /* `margin-top: auto` pins to the bottom of a column and means nothing in a
     row, so the divider and the gap move to the inline end.
     Below the rule above, not up with the other 900px declarations: that is
     where this lived and the unmediated block immediately above discarded it.
     Measured, the computed margin-top on a tablet was 0 either way, which is
     how a dead override looks when the property it sets happens to resolve to
     the same value. */
  .ws-wrail-pulse {
    margin-top: 0;
    margin-left: auto;
    border-top: 0;
    border-left: 1px solid var(--border);
  }
}

.ws-wrail-pulse .ws-wrail-ico { color: var(--btn-primary); }
.ws-wrail-pulse:hover {
  background: color-mix(in srgb, var(--btn-primary) 18%, transparent);
  color: var(--ink);
}
.ws-wrail-pulse:hover .ws-wrail-ico { color: var(--ink); }

/* The saved-conversations button in the Pulse header.
 *
 * .icon-btn gives it a 38px box, but the glyph inside inherits the header's
 * font size and lands at about 15px in the middle of it — so the control read
 * as a faint mark rather than something to press, and the 38px hit area was
 * invisible. Scoped rather than applied to .icon-btn, because the same class
 * carries the top-bar gear and that one sits in a tighter row.
 */
#chat-history-btn {
  width: 44px;
  height: 44px;
  font-size: 22px;
  line-height: 1;
  color: var(--ink-2);
  /* A resting border, so the boundary of the thing you are aiming at is
     visible before the pointer arrives rather than only on hover. */
  border-color: var(--border);
}
#chat-history-btn:hover {
  background: var(--surface-2);
  border-color: var(--border-strong);
  color: var(--ink);
}
#chat-history-btn[aria-expanded="true"] {
  background: var(--surface-2);
  border-color: var(--btn-primary);
  color: var(--ink);
}

/* ================================================= the disclaimer footer ==
 *
 * One column, one measure, no rule. It was two columns with a hairline
 * between them and that is what came back as "why is there a breaker here?".
 *
 * The columns were there for a real reason: capped at 110ch the notice left
 * about 700px of a 1900px bar empty, and uncapping it ran to 165 characters a
 * line, which is past where the eye reliably finds the start of the next one.
 * Columns gave both a readable measure and a filled bar.
 *
 * They also produced columns **two lines deep**, measured at 1900px. That is
 * the same fault this block already recorded talking itself out of a
 * `column-width` of 46ch -- "four columns of one and a half lines each... the
 * sentence had been chopped into slivers" -- just less extreme, and it stopped
 * at two slivers instead of four. Multi-column setting needs columns deep
 * enough that reading down one and returning to the top of the next is worth
 * the journey. For a single 557-character paragraph it never was, and the
 * hairline made the split look intentional rather than fixing it.
 *
 * So the cap in the base rule stands at every width. The cost is height: on a
 * wide screen this goes from two lines to five, because the text is the length
 * it is. Filling the bar was never worth cutting a sentence in half to do it,
 * and if the footer needs to be shorter the honest fix is shorter copy, which
 * is a decision for whoever owns the liability wording and not for a
 * stylesheet.
 */

/* ==================================================== the section index ===
 *
 * A sticky row of jump chips above a long view. It exists because collapsing
 * panels solved the reading cost and not the travel cost: the Swing view runs
 * to nine screens with 21 of its 29 sections already closed, so two of those
 * screens are nothing but section titles a reader scrolls past to reach one.
 *
 * Sticky under the top bar and the status strip, whose combined height is
 * published as --topbar-h by the same observer the phone dropdowns use.
 */
.sec-index {
  position: sticky;
  /* Below the ticker workspace header where a view has one. --sechead-h is
     published by trackSecurityHeader and is 0 on the views that do not. */
  top: calc(var(--topbar-h, 106px) + var(--sechead-h, 0px) + var(--space-1));
  z-index: 20;
  display: flex;
  flex-wrap: nowrap;
  gap: var(--space-1);
  margin-bottom: var(--space-2);
  padding: var(--space-2) var(--space-1);
  /* Scrolls rather than wraps. Twenty-nine chips wrapped to four rows is a
     third screen of chrome added to fix two screens of it. */
  overflow-x: auto;
  overflow-y: hidden;
  scrollbar-width: none;
  background: var(--glass);
  border: 1px solid var(--glass-edge);
  border-radius: var(--r-chrome);
  /* Sticky, with the page scrolling underneath it. A 1px edge is not enough
     to say which one is on top once anything is moving. */
  box-shadow: var(--elev-1);
}
/* Hidden, because a 10px bar under a 26px chip is most of the row's height.
   The edge fades below carry the "there is more" signal instead, and the wheel
   and drag handlers in armSectionIndex carry the driving. */
.sec-index::-webkit-scrollbar { display: none; }

/* Where the row continues. Set from the real scroll offset by armSectionIndex,
   so a fade never appears at an end that has nothing past it. */
.sec-index::before,
.sec-index::after {
  content: '';
  position: sticky;
  flex: none;
  align-self: stretch;
  width: 26px;
  margin: calc(var(--space-2) * -1) 0;
  pointer-events: none;
  opacity: 0;
  transition: opacity var(--dur-ui, 120ms) var(--ease-ui, ease);
}
.sec-index::before {
  left: 0;
  order: -1;
  margin-right: -26px;
  background: linear-gradient(to right, var(--surface), transparent);
}
.sec-index::after {
  right: 0;
  order: 1;
  margin-left: -26px;
  background: linear-gradient(to left, var(--surface), transparent);
}
.sec-index.more-left::before,
.sec-index.more-right::after { opacity: 1; }

/* A grab cursor on the strip itself, not on the chips — the chips are buttons
   and should keep saying so. */
.sec-index { cursor: grab; }
.sec-index:active { cursor: grabbing; }
.sec-chip { cursor: pointer; }
@supports ((backdrop-filter: blur(1px)) or (-webkit-backdrop-filter: blur(1px))) {
  .sec-index {
    -webkit-backdrop-filter: blur(var(--glass-blur)) saturate(var(--glass-sat));
    backdrop-filter: blur(var(--glass-blur)) saturate(var(--glass-sat));
  }
}
@supports not ((backdrop-filter: blur(1px)) or (-webkit-backdrop-filter: blur(1px))) {
  .sec-index { background: var(--surface); }
}

.sec-chip {
  flex: none;
  font: inherit;
  font-size: var(--t-caption);
  font-weight: 500;
  white-space: nowrap;
  padding: var(--space-1) var(--space-3);
  /* 24px minimum in the short axis, per WCAG 2.5.8, without a hit-area hack:
     there is room in this row to just be big enough. */
  min-height: 26px;
  background: transparent;
  border: 1px solid transparent;
  /* --r-pill, not --r-sm: this was the only one of the four chips drawn
     square, and it sits in a row beside `.chip`. Two controls with the same
     job and the same name reading as different components is the whole of
     what "incohesive" meant. */
  border-radius: var(--r-pill);
  color: var(--ink-muted);
  cursor: pointer;
}
.sec-chip:hover { color: var(--ink); background: var(--surface-2); }
.sec-chip:focus-visible { outline: 2px solid var(--focus); outline-offset: 1px; }
/* The section currently on screen. Border rather than fill, so the marker does
   not compete with whichever chip the pointer is over. */
.sec-chip.on {
  color: var(--ink);
  border-color: var(--btn-primary);
}

/* On a phone the row would be most of the screen. The chips still scroll, but
   the index gives up its sticky slot — three rows of chrome were already the
   problem there. */
@media (max-width: 559px) {
  .sec-index { position: static; }
}

/* Anchor offset for the sticky chrome.
 *
 * A jump landed the section's top edge at viewport y=0, which is behind the top
 * bar and the index itself — so the heading a reader had just clicked was the
 * one thing they could not see. scroll-margin-top is what the property exists
 * for: it moves the resting place, not the element. --topbar-h is measured, the
 * rest is the index's own height plus a gap. */
.panel[data-collapsible="1"] {
  scroll-margin-top: calc(var(--topbar-h, 106px) + var(--sechead-h, 0px) + 60px);
}

/* Expand all / Collapse all, riding in the section index.
 *
 * They used to be their own bar directly beneath it. The index is sticky and
 * that bar was not, so scrolling drove the two buttons up through the chips —
 * the collision in the screenshot. One row, with the buttons pinned right while
 * the chips scroll beneath them. */
.sec-bulk {
  position: sticky;
  right: 0;
  order: 2;
  flex: none;
  display: flex;
  gap: var(--space-1);
  align-items: center;
  padding-left: var(--space-3);
  margin-left: auto;
  /* An opaque backing, so a chip scrolling under the buttons disappears behind
     them rather than showing through the text. */
  background: var(--surface);
  box-shadow: -10px 0 10px -6px var(--surface);
}
.sec-bulk-btn {
  font: inherit;
  font-size: var(--t-caption);
  white-space: nowrap;
  min-height: 26px;
  padding: var(--space-1) var(--space-3);
  background: transparent;
  border: 1px solid var(--border);
  border-radius: var(--r-sm);
  color: var(--ink-muted);
  cursor: pointer;
}
.sec-bulk-btn:hover { color: var(--ink); border-color: var(--border-strong); }
.sec-bulk-btn:focus-visible { outline: 2px solid var(--focus); outline-offset: 1px; }

/* ========================================================= the Optic loop ===
 *
 * The three panels above everything else on the asset page. Deliberately NOT
 * `.panel` cards: the brief's own principle is that not every element should be
 * a card, and three stacked cards at the top of a page that already has 29 of
 * them buries the conclusion in the same chrome as the evidence. These are page
 * sections with a rule under the heading.
 */

.pl-hero {
  padding: var(--space-6) 0 var(--space-5);
  border-bottom: 1px solid var(--border);
  margin-bottom: var(--space-5);
}

.pl-top {
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  gap: var(--space-3);
  margin-bottom: var(--space-3);
}
.pl-eyebrow {
  font-size: var(--t-caption);
  font-weight: 600;
  letter-spacing: 0.1em;
  color: var(--ink-muted);
}
.pl-fresh { font-size: var(--t-caption); color: var(--ink-muted); }

/* The sentence is the largest thing here, which is the point: it is the answer.
   A 32px stance word was the largest thing before it, and a word is not an
   answer — the reader still had to assemble "down today but read bullish,
   carried by positioning, with macro against" out of it and five bars.

   Direction is the rule down the left, not a second copy of the stance word.
   The sentence already contains it, and the app puts direction in colour
   everywhere else. 46ch because a display-size line longer than that stops
   being a sentence and becomes a paragraph. */
.pl-lede {
  margin: 0 0 var(--space-4);
  padding-left: var(--space-4);
  border-left: 3px solid var(--ink-muted);
  font: 600 var(--t-d3)/1.35 var(--display);
  letter-spacing: -0.01em;
  color: var(--ink);
  max-width: 46ch;
  text-wrap: pretty;
}
.pl-lede.is-bullish { border-left-color: var(--pos); }
.pl-lede.is-bearish { border-left-color: var(--neg); }

/* "Read more": the prose between the one-sentence lede and the five bars.

   Indented to the lede's text column rather than the panel's, so it reads as a
   continuation of that sentence and not as a new section. Same summary
   treatment .pl-bars used to carry before its summary was removed: a small
   underlined word with no disclosure triangle, which is also how .pl-src
   renders, so the two remaining disclosures in this panel match. */
.pl-more { margin: calc(var(--space-4) * -1) 0 var(--space-4); }
.pl-more > summary {
  cursor: pointer;
  list-style: none;
  padding-left: var(--space-4);
  font-size: var(--t-small);
  font-weight: 600;
  color: var(--ink-2);
  text-decoration: underline;
  text-underline-offset: 3px;
  transition: color var(--dur-ui) var(--ease-ui);
}
.pl-more > summary::-webkit-details-marker { display: none; }
.pl-more > summary:hover { color: var(--ink); }
.pl-more-body {
  margin-top: var(--space-3);
  padding-left: var(--space-4);
  /* 68ch, wider than the lede's 46ch: this is a paragraph and the lede is a
     headline, and a display-width measure on body copy is hard to track. */
  max-width: 68ch;
}
.pl-more-body p {
  margin: 0 0 var(--space-3);
  font-size: var(--t-small);
  line-height: 1.6;
  color: var(--ink-2);
  text-wrap: pretty;
}
.pl-more-body p:last-child { margin-bottom: 0; }
.pl-more-body strong { color: var(--ink); font-weight: 600; }
/* The limits paragraph is quieter than the reasoning above it, the same way
   .pl-caveat sits under the bars. It is not less important; it is the part a
   reader returns to rather than starts with. */
.pl-more-limits {
  font-size: var(--t-caption) !important;
  color: var(--ink-muted) !important;
  padding-top: var(--space-3);
  border-top: 1px solid var(--border);
}

/* The freshest company story, as a link. Flat: a bordered card here would
   out-shout the sentence above it, which is the wrong priority. */
.pl-story {
  display: grid;
  gap: var(--space-0);
  margin-bottom: var(--space-4);
  padding-left: var(--space-4);
  border-left: 3px solid var(--border);
  text-decoration: none;
  transition: border-color var(--dur-ui) var(--ease-ui);
}
.pl-story:hover { border-left-color: var(--ink-muted); }
/* The tier badge is inline-block everywhere else, but as a grid item it takes
   the column: measured at 660px in a 681px row, so "BREAKING" rendered as a
   full-width outlined bar above the headline. Only the badge shrink-wraps —
   `justify-items: start` on the row would also pull the title and the byline
   off the grid's width for no reason. */
.pl-story > .nw-tier { justify-self: start; }
.pl-story-t {
  font-size: var(--t-base);
  font-weight: 500;
  color: var(--ink);
  max-width: 62ch;
}
.pl-story:hover .pl-story-t { text-decoration: underline; }
.pl-story-m { font-size: var(--t-caption); color: var(--ink-muted); }

.pl-meta {
  display: flex;
  align-items: baseline;
  flex-wrap: wrap;
  gap: var(--space-3);
  font-size: var(--t-small);
  color: var(--ink-muted);
}
.pl-src > summary {
  cursor: pointer;
  list-style: none;
  text-decoration: underline;
  text-underline-offset: 2px;
  transition: color var(--dur-ui) var(--ease-ui);
}
.pl-src > summary::-webkit-details-marker { display: none; }
.pl-src > summary:hover { color: var(--ink); }
.pl-src-list {
  margin: var(--space-1) 0 0;
  font-size: var(--t-caption);
  line-height: 1.5;
  color: var(--ink-muted);
  max-width: 64ch;
}

/* The workings, always shown. They sat behind a "How the N inputs scored"
   summary and no longer do: a toggle over the one section that qualifies
   everything above it makes the default view of this panel the confident half
   with the evidence hidden.

   The rules for that summary are gone rather than left dark, and the spacing
   it used to contribute (space-3 above it, space-4 below it when open) is
   carried here instead — without it the first bar butts against the meta row
   and the two read as one block. */
.pl-bars { margin-top: var(--space-5); }

/* One row per input. A grid rather than flex so the labels, bars and values
   line up in three columns however long a label is. */
.pl-factors {
  display: grid;
  grid-template-columns: auto 1fr auto;
  gap: var(--space-2) var(--space-4);
  align-items: center;
  max-width: 520px;
}
.pl-flabel { font-size: var(--t-small); color: var(--ink-2); cursor: help; }
.pl-fval {
  font-size: var(--t-small);
  font-variant-numeric: tabular-nums;
  color: var(--ink-muted);
  text-align: right;
  min-width: 4ch;
}
.pl-factor.is-none .pl-flabel,
.pl-factor.is-none .pl-fval { opacity: 0.55; }
.pl-factor { display: contents; }

/* Ten discrete cells, not a continuous fill. A gradient bar invites reading a
   precision off it that the underlying score does not have. */
.pl-bar { display: flex; gap: 2px; }
.pl-cell {
  flex: 1 1 auto;
  height: 8px;
  border-radius: 1px;
  background: var(--raised);
  border: 1px solid var(--border);
}
.pl-bar.tone-up .pl-cell.on { background: var(--pos); border-color: var(--pos); }
.pl-bar.tone-down .pl-cell.on { background: var(--neg); border-color: var(--neg); }
.pl-bar.tone-flat .pl-cell.on { background: var(--ink-muted); border-color: var(--ink-muted); }
/* An input with no data gets an empty track, never a half-full bar — drawing
   50% for "we could not price this" is the failure this avoids. */
.pl-bar.is-none {
  display: block;
  height: 8px;
  border-radius: 1px;
  border: 1px dashed var(--border-strong);
}

.pl-skill {
  margin: var(--space-5) 0 0;
  font-size: var(--t-small);
  color: var(--ink-2);
  max-width: 62ch;
}
.pl-caveat, .pl-method {
  margin: var(--space-2) 0 0;
  font-size: var(--t-caption);
  color: var(--ink-muted);
  max-width: 72ch;
}

/* ---------------------------------------------- why, and what matters next */

.pl-block {
  padding: 0 0 var(--space-5);
  margin-bottom: var(--space-5);
  border-bottom: 1px solid var(--border);
}
.pl-h {
  font-family: var(--display);
  font-size: var(--t-heading);
  font-weight: 600;
  letter-spacing: -0.01em;
  margin: 0 0 var(--space-4);
}
.pl-h-chg { font-size: var(--t-lead); font-weight: 500; margin-left: var(--space-2); }
.pl-empty { margin: 0; font-size: var(--t-small); color: var(--ink-2); max-width: 66ch; }

.pl-why { list-style: none; margin: 0; padding: 0; display: grid; gap: var(--space-1); }
.pl-reason > details > summary {
  display: flex;
  align-items: baseline;
  gap: var(--space-3);
  padding: var(--space-3) var(--space-3) var(--space-3) 0;
  cursor: pointer;
  border-radius: var(--r-sm);
  list-style: none;
}
.pl-reason > details > summary::-webkit-details-marker { display: none; }
.pl-reason > details > summary:hover { background: var(--surface-2); }
.pl-reason > details > summary:focus-visible { outline: 2px solid var(--focus); outline-offset: 1px; }
.pl-rank {
  flex: none;
  width: 20px;
  font-size: var(--t-caption);
  font-variant-numeric: tabular-nums;
  color: var(--ink-muted);
}
.pl-arrow { flex: none; font-size: 10px; }
.pl-arrow.is-up { color: var(--pos); }
.pl-arrow.is-down { color: var(--neg); }
.pl-rtext { flex: 1 1 auto; font-size: var(--t-lead); color: var(--ink); }
.pl-rtag {
  flex: none;
  font-size: var(--t-caption);
  color: var(--ink-muted);
  text-transform: uppercase;
  letter-spacing: 0.06em;
}
.pl-evidence {
  margin: 0 0 var(--space-3);
  padding: 0 0 0 calc(20px + var(--space-3));
  list-style: none;
  display: grid;
  gap: var(--space-1);
}
.pl-evidence li {
  font-size: var(--t-small);
  color: var(--ink-2);
  padding-left: var(--space-3);
  border-left: 2px solid var(--border-strong);
}

.pl-next { display: grid; grid-template-columns: 1fr 1fr; gap: var(--space-6); }
.pl-next-h {
  font-size: var(--t-caption);
  font-weight: 600;
  letter-spacing: 0.01em;
  color: var(--ink-muted);
  margin: 0 0 var(--space-3);
}
.pl-next-list { list-style: none; margin: 0; padding: 0; display: grid; gap: var(--space-3); }
.pl-next-item { display: grid; gap: 2px; }
.pl-next-label { font-size: var(--t-body); color: var(--ink); }
.pl-next-detail { font-size: var(--t-caption); color: var(--ink-muted); }

@media (max-width: 720px) {
  .pl-next { grid-template-columns: 1fr; gap: var(--space-5); }
  .pl-factors { max-width: none; }
  /* The lede drops a step on a phone: --t-d3 over 46ch at 375px runs to six
     lines and stops reading as one sentence. */
  .pl-lede { font-size: var(--t-heading); padding-left: var(--space-3); }
  .pl-story { padding-left: var(--space-3); }
}

/* "Explain chart" sits beside the open-ended Pulse button on the chart header.
   The primary of the two: a structured read is what someone wants nine times
   out of ten, and the free-form one is there for the tenth. */
.chart-pulse.is-explain {
  border-color: var(--btn-primary);
  color: var(--ink);
  margin-right: var(--space-1);
}
.chart-pulse.is-explain:hover {
  background: color-mix(in srgb, var(--btn-primary) 20%, transparent);
}

/* ==================================================== the command palette ===
 *
 * Cmd/Ctrl+K. Deliberately not a modal in the middle of the screen: it sits
 * near the top, where the search box it replaces already was, so the gesture
 * lands somewhere the eye is already looking.
 */
.cp-scrim {
  position: fixed;
  inset: 0;
  z-index: 90;
  background: var(--scrim);
  animation: cp-fade 120ms ease both;
}
.cp {
  position: fixed;
  z-index: 91;
  top: 12vh;
  left: 50%;
  transform: translateX(-50%);
  width: min(640px, calc(100vw - 2 * var(--space-5)));
  display: flex;
  flex-direction: column;
  background: var(--surface);
  border: 1px solid var(--border-strong);
  border-radius: var(--r-lg);
  box-shadow: var(--elev-3);
  overflow: hidden;
  animation: cp-rise 140ms cubic-bezier(0.22, 0.61, 0.36, 1) both;
}
@keyframes cp-fade { from { opacity: 0; } to { opacity: 1; } }
@keyframes cp-rise {
  from { opacity: 0; transform: translateX(-50%) translateY(-6px); }
  to { opacity: 1; transform: translateX(-50%) translateY(0); }
}
@media (prefers-reduced-motion: reduce) {
  .cp-scrim, .cp { animation: none; }
}

.cp-field {
  display: flex;
  align-items: center;
  gap: var(--space-3);
  padding: var(--space-4) var(--space-4);
  border-bottom: 1px solid var(--border);
  transition: border-color var(--dur-ui) ease, box-shadow var(--dur-ui) ease;
}
/* The field lights up while it holds the caret, so the one place you type reads
   as the active surface rather than as a plain header. Same accent the rail
   uses for "you are here", carried to the palette so the two feel like one
   product. */
.cp-field:focus-within {
  border-bottom-color: color-mix(in srgb, var(--btn-primary) 55%, var(--border-strong));
  box-shadow: inset 0 -1px 0 color-mix(in srgb, var(--btn-primary) 45%, transparent);
}
.cp-field:focus-within .cp-icon { color: var(--ink-2); }
.cp-icon {
  color: var(--ink-muted);
  font-size: 16px;
  flex: none;
  transition: color var(--dur-ui) ease;
}
#cp-input {
  flex: 1 1 auto;
  min-width: 0;
  font: inherit;
  font-size: var(--t-lead);
  background: none;
  border: 0;
  color: var(--ink);
  outline: none;
}
#cp-input::placeholder { color: var(--ink-muted); }
.cp-kbd, .cp-foot kbd {
  flex: none;
  font: inherit;
  font-size: var(--t-micro);
  padding: 2px var(--space-1);
  border: 1px solid var(--border-strong);
  border-radius: var(--r-xs);
  color: var(--ink-muted);
  background: var(--raised);
}

.cp-list { max-height: min(48vh, 420px); overflow-y: auto; padding: var(--space-2) 0; }
.cp-group {
  padding: var(--space-2) var(--space-4) var(--space-1);
  font-size: var(--t-micro);
  font-weight: 600;
  letter-spacing: 0.1em;
  color: var(--ink-muted);
}
.cp-row {
  display: flex;
  align-items: baseline;
  gap: var(--space-3);
  padding: var(--space-2) var(--space-4);
  cursor: pointer;
  /* 26px so a pointer target clears WCAG 2.5.8 without a hit-area hack. */
  min-height: 26px;
  transition: background-color var(--dur-ui) ease, box-shadow var(--dur-ui) ease;
}
/* Highlight follows the keyboard, not the pointer. Two competing highlights in
   a list you drive with arrow keys is worse than one that is occasionally in a
   different place from the cursor. The inset bar is the rail's own "you are
   here" mark, so the selected row is read the same way in both places. */
.cp-row.on {
  background: color-mix(in srgb, var(--btn-primary) 22%, transparent);
  box-shadow: inset 2px 0 0 var(--btn-primary);
}
.cp-row-lead {
  flex: none;
  width: 26px;
  font-size: var(--t-caption);
  color: var(--ink-muted);
  text-align: center;
  transition: color var(--dur-ui) ease;
}
.cp-row.on .cp-row-lead { color: var(--ink-2); }
.cp-row-main { flex: none; font-size: var(--t-body); color: var(--ink); font-weight: 500; }
.cp-row-detail {
  flex: 1 1 auto;
  min-width: 0;
  font-size: var(--t-small);
  color: var(--ink-muted);
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}
.cp-row-tag {
  flex: none;
  font-size: var(--t-micro);
  color: var(--ink-muted);
  text-transform: uppercase;
  letter-spacing: 0.06em;
}
.cp-hint {
  margin: 0;
  padding: var(--space-3) var(--space-4) var(--space-4);
  font-size: var(--t-small);
  color: var(--ink-muted);
}
.cp-foot {
  display: flex;
  gap: var(--space-4);
  padding: var(--space-2) var(--space-4);
  border-top: 1px solid var(--border);
  font-size: var(--t-micro);
  color: var(--ink-muted);
}
.cp-foot span { display: inline-flex; align-items: center; gap: var(--space-1); }

/* The page must not scroll behind the palette. */
body.cp-open { overflow: hidden; }

/* On a phone it takes the full width and sits higher — the keyboard claims the
   bottom half of the screen the moment the field focuses. */
@media (max-width: 559px) {
  .cp {
    top: var(--space-3);
    width: calc(100vw - 2 * var(--space-3));
    border-radius: var(--r-chrome);
  }
  .cp-list { max-height: 42vh; }
  .cp-foot { display: none; }
}

/* ============================================================ home market ===
 *
 * The landing page's market screen. Sections with a rule, not a wall of cards —
 * the page already had six navigation cards and adding four more would make the
 * market look like more product tour.
 */
/* A dashboard, not a stack.
 *
 * Every block here was a full-width row, which was survivable inside a 1000px
 * column and fell apart the moment the page filled the window: a watchlist row
 * put its symbol and its verdict two feet apart, and "What matters now" used
 * the left fifth of a 1900px page with nothing in the rest. Width was being
 * spent stretching five-item lists instead of showing more at once.
 *
 * Columns are the fix, and the count is set by how narrow a block can get
 * before its own contents break rather than by the screen: these hold a symbol,
 * a price, a change and a short verdict, which needs about 420px. So the
 * breakpoints are 2 x 420 plus gutters, then 3. */
.hm-market { margin: var(--space-7) 0 var(--space-6); }

/* ---- the board ------------------------------------------------------------
 *
 * Two containers, each packing its own cards, rather than one grid of equal
 * cells. A grid row is as tall as its tallest item, so "What matters now" at
 * 631px forced 631px rows and the watchlist beside it at 306px left 325px of
 * empty card, with 377px under Ask Pulse. `align-items: start` is what made
 * that visible rather than stretching the short cards, and stretching would
 * only have moved the emptiness inside them.
 *
 * No ordering fixed it and `grid-auto-flow: dense` could not: the row was
 * fully occupied and it was the unused *height* that showed. Measured, the
 * four single-column blocks came to 1410px of content against 1893px of
 * capacity at three columns of 631, so 483px was empty whatever went where.
 *
 * A flex column is as tall as its contents, so each side packs with no holes,
 * and the only remaining imbalance is between the two sides' totals. The
 * movers table moved from full width into the main column to balance them:
 * main 945, rail 825.
 */
.hm-board {
  display: grid;
  grid-template-columns: minmax(0, 1fr);
  gap: var(--space-5);
  align-items: start;
}
.hm-main,
.hm-rail {
  display: flex;
  flex-direction: column;
  gap: var(--space-5);
  min-width: 0;                 /* or the movers table's width sets the column */
}

/* One column until there is room for a rail. 940px was the old two-column
   breakpoint and is where a 1fr/1fr split of this content stops being cramped;
   the rail only earns its narrower third above 1280.

   Those are window widths, and the board is not the window: with Pulse open
   the window stays 1280 wide while the board loses the panel's width, and the
   rail came out 219px, too narrow for a watchlist row. So the split follows
   the board's own width, at the widths the board measured at those two window
   widths with Pulse shut (672.8px at 940, 1012.8px at 1280), which leaves every
   layout without Pulse exactly as it was. The window rules stay for a browser
   without container queries. */
#hm-market { container-type: inline-size; container-name: hm-market; }
@supports not (container-type: inline-size) {
  @media (min-width: 940px) {
    .hm-board { grid-template-columns: minmax(0, 1fr) minmax(0, 1fr); }
  }
  @media (min-width: 1280px) {
    .hm-board { grid-template-columns: minmax(0, 2fr) minmax(0, 1fr); }
  }
}
@container hm-market (min-width: 672px) {
  .hm-board { grid-template-columns: minmax(0, 1fr) minmax(0, 1fr); }
}
@container hm-market (min-width: 1012px) {
  .hm-board { grid-template-columns: minmax(0, 2fr) minmax(0, 1fr); }
}

.hm-greet,
.hm-degraded { grid-column: 1 / -1; }

.hm-skel {
  height: 180px;
  border-radius: var(--r-chrome);
  background: var(--raised);
  animation: skel-pulse 1.5s ease-in-out infinite both;
}
@media (prefers-reduced-motion: reduce) { .hm-skel { animation: none; } }
/* The empty-state voice, in one declaration.
 *
 * `.hm-none`, `.wl-none`, `.ws-none`, `.cmp-hint` and `.ht-rest` each said
 * "muted, small" separately, so the treatment for "there is nothing here and
 * that is not an error" lived in five places and had already drifted in one
 * of them -- `.ws-none` set no size at all and inherited whatever its host
 * used. Margins are still local, because how far a note sits from the thing
 * it is about depends on the thing. */
.hm-none, .hm-degraded, .wl-none, .wl-loading, .wl-more,
.ws-none, .cmp-hint, .ht-rest {
  font-size: var(--t-small);
  color: var(--ink-muted);
}
.hm-none, .hm-degraded {
  margin: 0;
}
.hm-degraded { margin-top: var(--space-4); }

.hm-greet {
  display: flex;
  align-items: baseline;
  flex-wrap: wrap;
  gap: var(--space-3);
  margin-bottom: var(--space-5);
}
.hm-hello {
  font-family: var(--display);
  font-size: var(--t-d3, 28px);
  font-weight: 600;
  letter-spacing: -0.02em;
  margin: 0;
}
.hm-session { font-size: var(--t-small); color: var(--ink-muted); }
.hm-session.is-open { color: var(--pos); }

/* Four numbers, not eight. A strip anyone can read in one pass. */

/* ------------------------------------------------------------- explore */

.ex-wrap { display: grid; gap: var(--space-5); }

/* The screens, grouped. auto-fill rather than a fixed column count so six
   groups become three columns on a laptop and one on a phone without a
   breakpoint for each width. */
.ex-groups {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(260px, 1fr));
  gap: var(--space-4);
  margin-top: var(--space-3);
}

/* The name of a group of scans, and it is a label rather than a heading.
 *
 * It was written as one -- `font: 600 var(--t-micro)/1 var(--sans)` -- and
 * rendered as neither. `.panel h3` is (0,1,1) and sets `font-size:
 * var(--t-base)`, so it beat this (0,1,0) and the label drew at 17.25px, a
 * size nobody chose. The `font` shorthand did win for the family, and it names
 * `--sans`, so this was the only heading in the product not in the display
 * face: six of them, on Explore, all at a sub-heading size in Plus Jakarta
 * Sans next to twenty-four panel headings in Host Grotesk.
 *
 * Kept as a label rather than promoted to a heading, because that is what it
 * is, and given the uppercase treatment every other `--t-micro` label in the
 * file already has. `.panel .ex-group-h` is (0,2,0) and beats `.panel h3`. */
.panel .ex-group-h,
.ex-group-h {
  margin: 0 0 var(--space-1);
  font-family: var(--sans);
  font-size: var(--t-micro);
  font-weight: 600;
  line-height: 1;
  text-transform: uppercase;
  letter-spacing: 0.06em;
  color: var(--ink-muted);
}

/* A screen. Text-first, no card: six groups of three cards each is eighteen
   boxes on one page, which is the clutter this whole redesign is against. */
.ex-scan {
  display: block;
  width: 100%;
  margin-bottom: var(--space-1);
  padding: var(--space-1) var(--space-2);
  border: 0;
  border-left: 2px solid var(--border);
  border-radius: 0;
  background: none;
  text-align: left;
  cursor: pointer;
  transition: border-color var(--dur-ui) var(--ease-ui),
              background var(--dur-ui) var(--ease-ui);
}
.ex-scan:hover { border-left-color: var(--btn-primary); background: var(--raised); }

.ex-scan-name { display: block; font: 500 var(--t-small)/1.3 var(--sans); color: var(--ink); }
/* Two lines, clamped. The description is one long sentence per screen and the
   card is a preview, not the documentation: clicking through is where the full
   text belongs. Clamped rather than sliced in JS for the reason `.ses-co-sum`
   gives: a character cut lands mid-word and reads as broken. */
.ex-scan-note {
  display: -webkit-box;
  -webkit-line-clamp: 2;
  -webkit-box-orient: vertical;
  overflow: hidden;
  margin-top: 2px;
  font-size: var(--t-micro);
  line-height: 1.45;
  color: var(--ink-muted);
}

.ex-row { cursor: pointer; transition: background var(--dur-ui) var(--ease-ui); }
.ex-row:hover { background: var(--raised); }

/* Cross-asset: a dense grid, because nineteen instruments as rows is a long
   page and the whole value is seeing them together. */
.ex-cross {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(150px, 1fr));
  gap: var(--space-1);
  margin-top: var(--space-3);
}

.ex-inst {
  display: grid;
  grid-template-columns: minmax(0, 1fr) auto;
  align-items: baseline;
  gap: 0 var(--space-1);
  padding: var(--space-1) var(--space-2);
  border: 1px solid var(--border);
  border-radius: var(--r-sm);
  background: none;
  text-align: left;
  cursor: pointer;
  transition: border-color var(--dur-ui) var(--ease-ui);
}
.ex-inst:hover { border-color: var(--ink-muted); }

.ex-inst-name {
  font-size: var(--t-caption);
  color: var(--ink-2);
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}
.ex-inst-chg { font-variant-numeric: tabular-nums; font-size: var(--t-small); }
/* The multiple, on its own line under the pair, in the muted register: it
   explains the ordering rather than competing with the change. */
.ex-inst-rel {
  grid-column: 1 / -1;
  font-size: var(--t-micro);
  font-variant-numeric: tabular-nums;
  color: var(--ink-muted);
}

/* ------------------------------------------------ market command centre */

/* The strip. A band, not a row of cards.
   Scrolls horizontally rather than wrapping: eight cells do not fit a phone and
   a strip that becomes three rows is no longer a strip. The order puts the
   indices first so those are what is on screen before anyone scrolls.
   `scrollbar-width: none` because the band is short enough that a visible bar
   doubles its height on the platforms that reserve space for one. */
/* The box, and the track that scrolls inside it. The box holds the border and
   clips the track to its rounded corners, so the border stays whole while the
   cells move. */
.ms-strip {
  margin: 0 0 var(--space-5);
  border: 1px solid var(--border);
  border-radius: var(--r-md);
  background: var(--raised);
  overflow: hidden;
}
.ms-track {
  display: flex;
  gap: 0;
  overflow-x: auto;
  scrollbar-width: none;
}
.ms-track::-webkit-scrollbar { display: none; }

/* The right edge fades while there is more to scroll to, because the
   scrollbar is hidden and nothing else says so: a last cell cut off with a
   hard edge reads as a layout bug rather than as more content.

   Only then, and only on the track. This mask was on the box itself and always
   on, on the grounds that it cost nothing when the strip fitted, and it took
   the border and the rounded corner with it at every width: "it looks like the
   box is fading away". `.ms-more` is set by `stripFade` in app.js while the
   track has more to the right than it shows.

   Guarded on `mask-image` support so browsers without it get the plain
   overflow rather than an invisible strip.

   The `#000` is not a colour and is not a missing token: a mask reads only the
   alpha channel, so this says "fully opaque here, transparent there". A theme
   token would be wrong, because the strip must fade the same way in both
   themes. It is the one place in this file where a literal is the correct
   thing. */
@supports (mask-image: linear-gradient(#000, #000)) or
          (-webkit-mask-image: linear-gradient(#000, #000)) {
  .ms-strip.ms-more .ms-track {
    -webkit-mask-image: linear-gradient(to right, #000 calc(100% - 28px), transparent);
    mask-image: linear-gradient(to right, #000 calc(100% - 28px), transparent);
  }
}

.ms-cell {
  display: grid;
  /* max-content, not auto, and packed to the start.
     An `auto` grid column absorbs free space, so once the cells were allowed to
     grow the label and the figure were pushed to opposite ends of a 240px box:
     "S&P 500" hard left and "+0.86%" hard right, reading as two unrelated
     numbers rather than one quote. The cell still grows, the extra width is now
     trailing space inside it, and the reading stays together. */
  grid-template-columns: max-content max-content;
  grid-template-rows: auto auto;
  /* Centred in the cell, not packed to its left edge.
     The columns stay `max-content`, which is what stops the label and the
     figure drifting to opposite ends of a grown cell: this moves the pair as a
     unit and leaves the trailing space split either side instead of all after
     it. Same decision as `.tile`, and for the same reason: a row of equal boxes
     with ragged-left contents reads as a column of accidents. */
  justify-content: center;
  align-items: baseline;
  gap: 0 var(--space-1);
  /* Grow to fill, never shrink below the reading.
     `0 0 auto` left the eight cells at their natural width in a container that
     is now much wider than they are: measured at 1900, the last cell ended at
     1249 inside a box running to 1722, so 473px of bordered empty strip sat
     after Bitcoin. Growing distributes that space between them.
     flex-shrink stays 0, which is what keeps the horizontal scroll honest on a
     phone: the cells hold their width and the strip scrolls, rather than eight
     figures crushing into 375px. */
  flex: 1 0 auto;
  /* A little smaller than it was (132px, the figure at --t-base, and 9px by
     14px of padding), asked for with the strip in a screenshot: eight cells
     now need about 930px where they needed about 1,070, so the strip fits
     the column on a 1280px screen instead of scrolling. */
  min-width: 112px;
  padding: calc(var(--space-1) + var(--space-0)) var(--space-2);
  border: 0;
  border-right: 1px solid var(--border);
  background: none;
  text-align: center;
  cursor: pointer;
  transition: background var(--dur-ui) var(--ease-ui);
}
.ms-cell:last-child { border-right: 0; }
.ms-cell:hover { background: var(--surface); }

.ms-label {
  grid-column: 1 / -1;
  font-size: var(--t-micro);
  letter-spacing: 0.01em;
  color: var(--ink-muted);
}
/* Tabular figures so the strip does not jitter as quotes update, and so the
   values line up against each other when scanned. */
.ms-last {
  font: 600 var(--t-body)/1.2 var(--sans);
  font-variant-numeric: tabular-nums;
  color: var(--ink);
}
.ms-chg { font-size: var(--t-caption); font-variant-numeric: tabular-nums; }
.ms-word {
  grid-column: 1 / -1;
  font-size: var(--t-micro);
  color: var(--ink-muted);
}

/* What matters now. The first substantial block on the page, and the one the
   whole redesign exists to put here, so its heading carries a little more
   weight than the blocks below it. Do not make secondary information visually
   louder than the primary information. */
.cc-now .hm-h { font-size: var(--t-lead); }

.cc-headline {
  margin: 0 0 var(--space-3);
  font: 500 var(--t-lead)/1.35 var(--sans);
  color: var(--ink);
  max-width: 60ch;
}

.cc-moves { list-style: none; margin: 0; padding: 0; display: grid; gap: var(--space-0); }

/* Three columns so the percentages form a column of their own: the whole point
   of ranking these is that they are compared against each other. */
.cc-move {
  display: grid;
  grid-template-columns: minmax(0, 12ch) auto auto minmax(0, 1fr);
  align-items: baseline;
  gap: var(--space-2);
  width: 100%;
  padding: var(--space-1) var(--space-1);
  border: 0;
  border-radius: var(--r-xs);
  background: none;
  text-align: left;
  cursor: pointer;
  transition: background var(--dur-ui) var(--ease-ui),
              box-shadow var(--dur-ui) var(--ease-ui);
}
/* Robinhood and Coinbase both mark the row you are on with a single accent edge
   rather than a full fill. The same gold the rail and palette use, so hovering
   a move here reads the same way as selecting a section or a palette row. */
.cc-move:hover { background: var(--raised); box-shadow: inset 2px 0 0 var(--btn-primary); }
.cc-move-name { font-weight: 600; color: var(--ink); }
.cc-move-chg { font-variant-numeric: tabular-nums; }
/* The multiple, in the same muted register as the note: it explains the
   ordering rather than being a headline number of its own. */
.cc-move-rel {
  font-size: var(--t-caption);
  font-variant-numeric: tabular-nums;
  color: var(--ink-muted);
}
.cc-move-note {
  font-size: var(--t-caption);
  color: var(--ink-muted);
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

/* The line that says what a block did and did not measure. Same register as
   .pl-method and .hm-none, which is deliberate: every panel here carries one. */
.cc-method {
  margin: var(--space-2) 0 0;
  font-size: var(--t-micro);
  line-height: 1.5;
  color: var(--ink-muted);
  max-width: 72ch;
}

/* Market questions. Text buttons, not cards: five cards would out-shout the
   market data above them, which is the wrong priority for this page. */
.cc-qs { list-style: none; margin: 0; padding: 0; display: grid; gap: var(--space-0); }
.cc-q {
  display: flex;
  align-items: center;
  gap: var(--space-2);
  width: 100%;
  padding: var(--space-1) var(--space-2);
  border: 1px solid var(--border);
  border-radius: var(--r-sm);
  background: none;
  color: var(--ink-2);
  font: 400 var(--t-small)/1.4 var(--sans);
  text-align: left;
  cursor: pointer;
  transition: color var(--dur-ui) var(--ease-ui),
              border-color var(--dur-ui) var(--ease-ui),
              background-color var(--dur-ui) var(--ease-ui);
}
/* A quiet "ask" caret that only firms up on hover or focus. Kalshi frames a
   question as a thing you can act on and Robinhood makes that affordance feel
   tactile; the twist is to keep both without turning five prompts into five
   loud cards -- the priority note above still holds, so the caret rests at 0.35
   until the row is reached. currentColor, so it follows the row's own ink. */
.cc-q::after {
  content: "";
  flex: none;
  margin-left: auto;
  width: 6px;
  height: 6px;
  border-right: 1.5px solid currentColor;
  border-top: 1.5px solid currentColor;
  transform: rotate(45deg);
  opacity: 0.35;
  transition: opacity var(--dur-ui) var(--ease-ui),
              transform var(--dur-ui) var(--ease-ui);
}
.cc-q:hover,
.cc-q:focus-visible {
  color: var(--ink);
  border-color: color-mix(in srgb, var(--btn-primary) 45%, var(--border-strong));
  background: var(--bg-hover);
}
.cc-q:focus-visible { outline: 2px solid var(--focus); outline-offset: 1px; }
.cc-q:hover::after,
.cc-q:focus-visible::after { opacity: 0.75; transform: translateX(2px) rotate(45deg); }

.cc-movers-t { width: 100%; }
/* Paint-level only, per the design system: animating a layout property forces a
   full-document reflow per frame. */
.cc-mover {
  cursor: pointer;
  transition: background var(--dur-ui) var(--ease-ui);
}
.cc-mover:hover { background: var(--raised); }

/* Phone: the strip keeps its own scroll, but the move rows drop the note so the
   name and the number stay on one line rather than truncating both. */
/* Phone: keep the name, the change and the multiple. The note is the first
   thing to go, because the multiple is what justifies the row's position and
   the note only names the asset class. */
@media (max-width: 559px) {
  .cc-move { grid-template-columns: minmax(0, 1fr) auto auto; }
  .cc-move-note { display: none; }
}

/* Cards, because they sit side by side now. A hairline between stacked rows
   reads as a divider; the same hairline between two columns reads as nothing at
   all, and the blocks ran into each other. */
.hm-block {
  padding: var(--space-4) var(--space-5) var(--space-5);
  border: 1px solid var(--border);
  border-radius: var(--r-md);
  background: var(--surface);
  min-width: 0;
}
.hm-block-head {
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  gap: var(--space-3);
  margin-bottom: var(--space-3);
}
.hm-h {
  font-family: var(--display);
  font-size: var(--t-heading);
  font-weight: 600;
  letter-spacing: -0.01em;
  margin: 0;
}
.hm-more {
  font: inherit;
  font-size: var(--t-small);
  background: none;
  border: 0;
  padding: var(--space-1) 0;
  color: var(--s1);
  cursor: pointer;
}
.hm-more:hover { color: var(--ink); }
.hm-more:focus-visible { outline: 2px solid var(--focus); outline-offset: 2px; }
.hm-read-head { margin: 0 0 var(--space-2); font-size: var(--t-lead); color: var(--ink); }
.hm-read-body { margin: 0; font-size: var(--t-small); color: var(--ink-2); max-width: 76ch; line-height: 1.55; }
.hm-alerts { list-style: none; margin: 0; padding: 0; display: grid; gap: var(--space-2); }
.hm-alerts li { display: flex; gap: var(--space-3); align-items: baseline; }
.hm-alert-sym { flex: none; width: 5ch; font-weight: 600; font-size: var(--t-small); }
.hm-alert-text { font-size: var(--t-small); color: var(--ink-2); }

/* ============================================================== watchlist ===
 *
 * Rows, not cards. A card per symbol would put ten borders on screen for ten
 * facts, and the whole reason to look at a watchlist is to compare across rows.
 */
.wl-list { list-style: none; margin: 0; padding: 0; }
.wl-row {
  display: flex;
  align-items: stretch;
  border-bottom: 1px solid var(--border);
}
.wl-row:last-child { border-bottom: 0; }
/* symbol · price · change · what changed · signal.
 *
 * In rem, not ch: rem is relative to the root font size, so the header row and
 * the data rows resolve the tracks identically despite running at 10px and
 * 15px. The last track is fixed rather than `auto` for the same reason —
 * `auto` sized itself to "SIGNAL" in the header and "BULLISH" in the rows,
 * which are different widths. */
:root { --wl-cols: 4.25rem 6rem 5.5rem 1fr 5.5rem 5.25rem; }

.wl-open {
  flex: 1 1 auto;
  min-width: 0;
  display: grid;
  grid-template-columns: var(--wl-cols);
  gap: var(--space-3);
  align-items: baseline;
  padding: var(--space-3) var(--space-2);
  font: inherit;
  text-align: left;
  background: none;
  border: 0;
  cursor: pointer;
  border-radius: var(--r-sm);
}
.wl-open:hover { background: var(--surface-2); }
.wl-open:focus-visible { outline: 2px solid var(--focus); outline-offset: -2px; }
.wl-sym { font-weight: 600; font-size: var(--t-body); color: var(--ink); }
.wl-price, .wl-chg { font-variant-numeric: tabular-nums; font-size: var(--t-small); }
.wl-price { color: var(--ink-2); }
.wl-changed { min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
/* The what-changed line is the reason this is a feed and not a quote table, so
   it gets the readable colour and the price gets the quiet one. */
.wl-changed-text { font-size: var(--t-small); color: var(--ink); cursor: help; }
.wl-quiet { font-size: var(--t-small); color: var(--ink-muted); }
.wl-signal {
  text-align: right;
  font-size: var(--t-micro);
  font-weight: 600;
  letter-spacing: 0.06em;
  text-transform: uppercase;
  cursor: help;
}
.wl-signal.is-bullish { color: var(--pos); }
.wl-signal.is-bearish { color: var(--neg); }
.wl-signal.is-neutral, .wl-signal.is-unknown { color: var(--ink-muted); }
/* The colour of "All of it" above the list, the home page's other way in. */
.wl-go { text-align: right; font-size: var(--t-small); font-weight: 500; color: var(--s1); white-space: nowrap; }
.wl-open:hover .wl-go-word, .wl-open:focus-visible .wl-go-word { text-decoration: underline; }
/* The signal column's header is no longer the last span, so it is aligned by
   position; the new last one is the Analyze column's, and empty. */
.wv-cols span:nth-child(5) { text-align: right; }

/* The layout follows the list's own width, not the window's. With Pulse open
   the window is still desktop-wide while the home page's card is a third of
   it, and the five fixed columns (340px of them) ran off the card's right edge
   and under the Pulse panel: BULLISH read as BU. A phone is the same case
   from the other side, so the two-line layout is one rule for both. */
#hm-watch, .wv-panel { container-type: inline-size; }
@container (max-width: 559px) {
  .wv-cols { display: none; }
  /* 3.75rem and the smaller gap because the second line is where "what
     changed" sits: at 4.25rem and 12px gaps a 328px card (Pulse open, 1280px
     window) left it 62px and "No change" needs 75, so it read "No cha...". A
     five-letter symbol is about 3.4rem in the row's bold face. */
  .wl-open {
    grid-template-columns: 3.75rem minmax(0, 1fr) auto auto;
    grid-template-areas: 'sym price chg go' 'sym changed signal go';
    column-gap: var(--space-2);
    row-gap: 2px;
  }
  .wl-sym { grid-area: sym; align-self: center; }
  .wl-price { grid-area: price; }
  .wl-chg { grid-area: chg; text-align: right; }
  .wl-changed { grid-area: changed; }
  .wl-signal { grid-area: signal; text-align: right; }
  .wl-go { grid-area: go; align-self: center; }
}
/* The word goes last, and only where it would squeeze the numbers. On a 375px
   phone the list is 285px inside its card, where the word would leave "No
   change" about 52px of the 75 it needs, so a phone gets the chevron alone. */
@container (max-width: 329px) {
  .wl-go-word { display: none; }
}
/* The home card's four columns, spread evenly across it.
 *
 * Asked for from a screenshot as "center these out evenly". The price and
 * what-changed column above is a 1fr track, so it took every spare pixel: its
 * short text sat against the symbol, the change sat against Analyze, and the
 * gap between them was wider than both. Each row was its own grid, so a
 * column could only line up down the list by being a fixed or fr size.
 *
 * Here the rows share the list's columns (subgrid), each as wide as its widest
 * entry, with a spacer track between each pair taking an equal share of what
 * is left. Measured on a 379px card at a 1512px window: 43px between every
 * pair, where it was 21, 86 and 21. A longer note narrows all three spacers
 * alike, and the note is still what gives way first, clipping with its
 * ellipsis once the spacers are down to their minimum. The home card only:
 * the full list has its column headers and its move and remove buttons. And
 * where subgrid is missing, the rows keep the layout above. */
@supports (grid-template-columns: subgrid) {
  @container (max-width: 559px) {
    .wl-list.is-compact {
      display: grid;
      grid-template-columns: auto minmax(var(--space-2), 1fr) auto minmax(var(--space-2), 1fr)
        auto minmax(var(--space-2), 1fr) auto;
    }
    .wl-list.is-compact > .wl-row {
      grid-column: 1 / -1;
      display: grid;
      grid-template-columns: subgrid;
      column-gap: normal;
    }
    .wl-list.is-compact .wl-open {
      grid-column: 1 / -1;
      grid-template-columns: subgrid;
      grid-template-areas: 'sym . price . chg . go' 'sym . changed . signal . go';
      column-gap: normal;
    }
    /* A symbol with no data: its reason runs from the price column to the end. */
    .wl-list.is-compact > .wl-row.is-none > .wl-sym { grid-column: 1; }
    .wl-list.is-compact > .wl-row.is-none > .wl-none { grid-column: 3 / -1; }
  }
}
.wl-x {
  flex: none;
  width: 28px;
  font: inherit;
  background: none;
  border: 0;
  color: var(--ink-muted);
  cursor: pointer;
  border-radius: var(--r-sm);
}
.wl-x:hover { color: var(--neg); background: var(--surface-2); }
.wl-row.is-none { padding: var(--space-3) var(--space-2); gap: var(--space-3); align-items: baseline; }
.wl-none, .wl-loading, .wl-more { margin: var(--space-2) 0 0; }

/* On a phone the change and signal columns drop under the symbol rather than
   compressing five columns into 343px. The container query beside
   `#hm-watch, .wv-panel` does this now, for a narrow card at any window width;
   this is the same layout for a browser without container queries, and it
   must not run beside that one, because it comes later and its grid has no
   `go` area: the Analyze chevron fell onto a third line of its own. */
@supports not (container-type: inline-size) {
  @media (max-width: 559px) {
    .wl-open {
      grid-template-columns: 3.75rem minmax(0, 1fr) auto auto;
      grid-template-areas: 'sym price chg go' 'sym changed signal go';
      column-gap: var(--space-2);
      row-gap: 2px;
    }
    .wl-sym { grid-area: sym; align-self: center; }
    .wl-price { grid-area: price; }
    .wl-chg { grid-area: chg; text-align: right; }
    .wl-changed { grid-area: changed; }
    .wl-signal { grid-area: signal; text-align: right; }
    .wl-go { grid-area: go; align-self: center; }
    .wl-go-word { display: none; }
  }
}

/* ===================================================== watchlist & alerts ===
 *
 * Full-page versions of the feeds the home page previews. Sections with rules,
 * not cards — the home page's six-row summary and this are the same rows, and
 * wrapping the long one in a card would make them look like different things.
 */
/* Watchlist and Alerts used to render onto the bare page: `section.wv` had a
 * padding and nothing else, no surface, no border, no radius. Every other
 * route in the product puts its content in a `.panel` -- Scan, Explore, the
 * Optic Portfolio, all seven security facets -- so these two were the only
 * places where a table of numbers sat directly on the page background with its
 * column headers floating above it. They are panels now, and `.wv` is gone.
 *
 * `.panel > h2` is a child combinator and the title here is not a child: the
 * header is a ROW (title and sub on the left, "Add a symbol" or "Clear all" on
 * the right), so the h2 sits inside `.wv-head`. Without this rule it fell
 * through to `main h2` at `--t-d3` -- a 27.6px display title on a card whose
 * neighbours all use 20.7px. Specificity (0,2,1), which is what it takes to
 * beat `main h2` (0,0,2) and the bare `h2` at 32px. */
.panel > .wv-head h2 {
  margin: 0;
  font-family: var(--display);
  font-size: var(--t-heading);
  font-weight: 600;
  letter-spacing: -0.01em;
  line-height: 1.2;
}
.wv-head {
  display: flex;
  align-items: flex-start;
  justify-content: space-between;
  gap: var(--space-5);
  flex-wrap: wrap;
  margin-bottom: var(--space-5);
}
.wv-sub, .wv-none, .wv-method {
  margin: var(--space-1) 0 0;
  font-size: var(--t-small);
  color: var(--ink-muted);
  max-width: 68ch;
}
.wv-method { margin-top: var(--space-5); font-size: var(--t-caption); }
.wv-add { display: flex; gap: var(--space-2); align-items: center; }
.wv-add input {
  /* 11ch truncated the placeholder to "ADD A SYMB" — uppercase at 0.04em
     tracking is wider than the character count suggests. */
  width: 15ch;
  padding: var(--space-2) var(--space-3);
  background: var(--surface);
  border: 1px solid var(--border-strong);
  border-radius: var(--r-md);
  color: var(--ink);
  font: inherit;
  text-transform: uppercase;
  letter-spacing: 0.04em;
}
.wv-add input:focus { outline: 2px solid var(--focus); outline-offset: 1px; }

.wv-sorts { display: flex; flex-wrap: wrap; gap: var(--space-1); margin-bottom: var(--space-4); }

/* A header row for the feed, matching the row grid exactly. Without it the
   Signal column is five uppercase words with no stated meaning.

   The tracks come from --wl-cols rather than being written twice. They were
   written twice, in `ch`, and `ch` is relative to the element's OWN font size:
   the header runs at 10px and the rows at 15px, so an identical `6ch 9ch 8ch`
   resolved to 42.95px here and 65.88px there. Every column was about 53% wider
   in the rows than in the header it was supposed to line up with. */
.wv-cols {
  display: grid;
  grid-template-columns: var(--wl-cols);
  gap: var(--space-3);
  padding: 0 calc(var(--space-2) + 28px) var(--space-2) var(--space-2);
  font-size: var(--t-micro);
  font-weight: 600;
  letter-spacing: 0.08em;
  text-transform: uppercase;
  color: var(--ink-muted);
  border-bottom: 1px solid var(--border-strong);
}
/* The signal column is right-aligned in the rows, so its header must be too or
   the word sits to the left of the values under it (by position, since the
   Analyze column's empty header now comes after it). */

.al-list { list-style: none; margin: 0; padding: 0; }
.al-row {
  display: grid;
  grid-template-columns: 8ch 6ch 1fr;
  gap: var(--space-3);
  align-items: baseline;
  padding: var(--space-3) var(--space-2);
  border-bottom: 1px solid var(--border);
}
.al-row:last-child { border-bottom: 0; }
/* An unread alert gets a rule, not a fill: a list where half the rows are
   tinted reads as two lists. */
.al-row.is-new { border-left: 2px solid var(--btn-primary); padding-left: var(--space-3); }
.al-when { font-size: var(--t-caption); color: var(--ink-muted); font-variant-numeric: tabular-nums; }
.al-sym {
  font: inherit;
  font-weight: 600;
  font-size: var(--t-small);
  background: none;
  border: 0;
  padding: 0;
  color: var(--s1);
  cursor: pointer;
  text-align: left;
}
.al-sym:disabled { color: var(--ink-muted); cursor: default; }
.al-sym:hover:not(:disabled) { color: var(--ink); }
.al-body { display: grid; gap: 2px; }
.al-text { font-size: var(--t-small); color: var(--ink); }
.al-kind {
  font-size: var(--t-micro);
  color: var(--ink-muted);
  text-transform: uppercase;
  letter-spacing: 0.06em;
}

@media (max-width: 559px) {
  /* The column header cannot describe a two-line row, so it goes. */
  .wv-cols { display: none; }
  .wv-head { gap: var(--space-3); }
  .al-row { grid-template-columns: 6ch 1fr; }
  .al-when { grid-column: 1 / -1; }
}

/* ================================================================ thesis ====
 *
 * Your own view, saved, and diffed against the data on every load. The change
 * block is the feature — the text boxes are just storage — so it sits above
 * the form and is the only part with a fill behind it.
 */
.th-panel {
  padding: var(--space-5) 0;
  border-top: 1px solid var(--border);
  border-bottom: 1px solid var(--border);
  margin-bottom: var(--space-5);
}
.th-when { font-size: var(--t-caption); color: var(--ink-muted); }

/* The only tinted block on the asset page. It has earned it: this is the app
   telling you something you asked it to watch for. */
.th-changed {
  padding: var(--space-4);
  margin-bottom: var(--space-5);
  border-left: 3px solid var(--warn);
  border-radius: 0 var(--r-ctl) var(--r-ctl) 0;
  background: color-mix(in srgb, var(--warn) 9%, transparent);
}
.th-changed strong { display: block; margin-bottom: var(--space-3); font-size: var(--t-body); }
.th-changed ul { list-style: none; margin: 0; padding: 0; display: grid; gap: var(--space-2); }
.th-changed li {
  display: flex;
  flex-wrap: wrap;
  align-items: baseline;
  gap: var(--space-2);
  font-size: var(--t-small);
}
.th-field { min-width: 12ch; color: var(--ink-muted); }
.th-was { color: var(--ink-muted); text-decoration: line-through; font-variant-numeric: tabular-nums; }
.th-arrow { color: var(--ink-muted); }
.th-is { color: var(--ink); font-weight: 600; font-variant-numeric: tabular-nums; }
.th-detail { color: var(--ink-muted); font-size: var(--t-caption); }
.th-note, .th-steady, .th-local {
  margin: var(--space-3) 0 0;
  font-size: var(--t-caption);
  color: var(--ink-muted);
  max-width: 72ch;
}
.th-steady { margin: 0 0 var(--space-5); font-size: var(--t-small); }

.th-form { display: grid; gap: var(--space-4); }
.th-field-wrap { display: grid; gap: var(--space-1); }
.th-field-wrap > span {
  font-size: var(--t-micro);
  font-weight: 600;
  letter-spacing: 0.08em;
  text-transform: uppercase;
  color: var(--ink-muted);
}
.th-field-wrap textarea {
  font: inherit;
  font-size: var(--t-small);
  padding: var(--space-3);
  background: var(--surface);
  border: 1px solid var(--border-strong);
  border-radius: var(--r-ctl);
  color: var(--ink);
  resize: vertical;
  min-height: 3.4em;
}
.th-field-wrap textarea:focus { outline: 2px solid var(--focus); outline-offset: 1px; }
.th-field-wrap textarea::placeholder { color: var(--ink-muted); }
.th-actions { display: flex; align-items: center; gap: var(--space-3); flex-wrap: wrap; }
.th-local { margin: 0; }

@media (max-width: 720px) {
  .th-form .grid.c2 { grid-template-columns: 1fr; }
}

/* ============================== options intelligence, and the setup ========
 *
 * Both sit above the eight options panels as summaries of them. Sections with
 * rules, matching the Optic loop above — a card here would make the summary
 * look like a ninth panel rather than the layer over them.
 */
.ob-block, .su-block {
  padding: var(--space-5) 0;
  border-top: 1px solid var(--border);
  margin-bottom: var(--space-5);
}
.ob-bias {
  font-size: var(--t-small);
  text-transform: lowercase;
}
.ob-bias.is-bullish { color: var(--pos); }
.ob-bias.is-bearish { color: var(--neg); }
.ob-bias.is-neutral { color: var(--ink-muted); }

.ob-grid {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(180px, 1fr));
  gap: var(--space-4) var(--space-5);
}
.ob-item { display: grid; gap: 2px; min-width: 0; }
.ob-label, .su-label {
  font-size: var(--t-micro);
  font-weight: 600;
  letter-spacing: 0.01em;
  color: var(--ink-muted);
}
.ob-value {
  font-family: var(--display);
  font-size: var(--t-lead);
  font-variant-numeric: tabular-nums;
  color: var(--ink);
}
.ob-value.is-up { color: var(--pos); }
.ob-value.is-down { color: var(--neg); }
.ob-note, .su-note { font-size: var(--t-caption); color: var(--ink-muted); }

/* The setup row. Five cells across, wrapping rather than scrolling. */
.su-row {
  display: flex;
  flex-wrap: wrap;
  gap: var(--space-5);
  padding-bottom: var(--space-4);
}
.su-cell { display: grid; gap: 2px; min-width: 9ch; }
.su-value {
  font-family: var(--display);
  font-size: var(--t-lead);
  font-variant-numeric: tabular-nums;
  color: var(--ink);
  text-transform: capitalize;
}
/* The tag is not decoration. It is the difference between a scenario and a
   recommendation, so it reads at the same weight as the heading beside it. */
.su-tag {
  font-size: var(--t-caption);
  color: var(--warn);
  border: 1px solid color-mix(in srgb, var(--warn) 45%, transparent);
  border-radius: var(--r-pill);
  padding: var(--space-0) var(--space-3);
}
.su-headline { margin: 0 0 var(--space-3); font-size: var(--t-small); color: var(--ink-2); max-width: 76ch; }
.su-none { margin: 0; font-size: var(--t-small); color: var(--ink-2); max-width: 76ch; }
.su-warn { margin: 0 0 var(--space-3); padding-left: var(--space-5); display: grid; gap: var(--space-1); }
.su-warn li { font-size: var(--t-caption); color: var(--warn); }

@media (max-width: 559px) {
  .ob-grid { grid-template-columns: repeat(2, minmax(0, 1fr)); gap: var(--space-3); }
  .su-row { gap: var(--space-4); }
}

/* The invalidation sentence, under the setup row rather than inside a cell.
   A paragraph in a five-across grid squeezes every other cell to nothing. */
.su-invalid {
  margin: 0 0 var(--space-3);
  font-size: var(--t-small);
  color: var(--ink-2);
  max-width: 76ch;
}
.su-invalid strong { color: var(--ink); }

/* ========================================================= mobile tab bar ===
 *
 * Phone-only primary navigation, pinned to the bottom where the thumb is. The
 * top strip stays and still scrolls; this is the fast path to the five things
 * anyone opens this on a phone to do.
 */
.mtabs { display: none; }

@media (max-width: 559px) {
  .mtabs {
    display: grid;
    /* One column per tab, counted by the grid rather than written down here.
       It was `repeat(5, 1fr)` and MOBILE_TABS grew to six, which put the sixth
       on a second row and took the bar from 53px tall to 106px -- a hardcoded
       count in CSS against a list in JS that nothing holds together. */
    grid-auto-flow: column;
    grid-auto-columns: 1fr;
    position: fixed;
    left: 0;
    right: 0;
    bottom: 0;
    z-index: 70;
    background: var(--glass);
    border-top: 1px solid var(--glass-edge);
    /* The home-indicator strip on a notched phone sits under the bar; without
       this the labels are behind it. */
    padding-bottom: env(safe-area-inset-bottom);
  }
  @supports ((backdrop-filter: blur(1px)) or (-webkit-backdrop-filter: blur(1px))) {
    .mtabs {
      -webkit-backdrop-filter: blur(var(--glass-blur)) saturate(var(--glass-sat));
      backdrop-filter: blur(var(--glass-blur)) saturate(var(--glass-sat));
    }
  }
  @supports not ((backdrop-filter: blur(1px)) or (-webkit-backdrop-filter: blur(1px))) {
    .mtabs { background: var(--plane); }
  }

  .mtab {
    display: grid;
    justify-items: center;
    gap: 2px;
    /* 375 / 6 = 62px per target. Still well over the 24px minimum, and 52px
       tall is the height a thumb hits without aiming. */
    min-height: 52px;
    /* A grid column is min-content wide by default and "Watchlist" does not
       shrink, so without this the six columns overflow the bar instead of
       sharing it. */
    min-width: 0;
    padding: var(--space-2) 0;
    font: inherit;
    background: none;
    border: 0;
    color: var(--ink-muted);
    cursor: pointer;
  }
  .mtab-ico { font-size: 17px; line-height: 1; }
  .mtab-lab { font-size: 10px; letter-spacing: 0.02em; }
  .mtab.on { color: var(--btn-primary); }
  .mtab:focus-visible { outline: 2px solid var(--focus); outline-offset: -2px; }

  /* The unseen count, pinned to the icon rather than laid out with it: the tab
     is a grid of two fixed rows and a third item would resize every tab in the
     bar the moment one alert fired. position:relative goes on the button above
     so this has something to hang from. */
  .mtab { position: relative; }
  .mtab-badge {
    position: absolute;
    top: 4px;
    left: 50%;
    margin-left: 4px;
    min-width: 16px;
    padding: 0 4px;
    border-radius: var(--r-pill);
    background: var(--btn-primary);
    color: var(--btn-primary-ink);
    font: 600 10px/16px var(--sans);
    text-align: center;
  }

  /* Content must clear the bar, or the last panel on every page is unreachable
     behind it. 52 for the bar plus the inset plus a gap. */
  main { padding-bottom: calc(52px + var(--space-6) + env(safe-area-inset-bottom)); }

  /* The command palette has to sit above the bar, and the Pulse panel must not
     be covered by it either. */
  aside#chat { padding-bottom: calc(52px + env(safe-area-inset-bottom)); }
}

/* ----------------------------------------------- the onboarding question
 *
 * A card on the page, not a modal over it. Nothing gates this terminal, and a
 * wall in front of the market on a first load would be the one place that rule
 * broke, for a preference. So it sits in the flow, above the brand and below
 * the market strip, and scrolls away like anything else. */
.kob {
  margin: var(--space-5) auto var(--space-6);
  max-width: 860px;
  padding: var(--space-5);
  border: 1px solid var(--border);
  border-radius: var(--r-md);
  background: var(--surface);
}
.kob-head { margin-bottom: var(--space-4); }
.kob-h {
  margin: 0 0 var(--space-1);
  font: 600 var(--t-heading)/1.25 var(--display);
  color: var(--ink);
}
.kob-sub {
  margin: 0;
  max-width: 62ch;
  font-size: var(--t-small);
  line-height: 1.5;
  color: var(--ink-muted);
}

/* auto-fit rather than a fixed count: five options at 150px do not fit a phone
   and three rows of oddly-sized buttons is what a fixed grid gives you there. */
.kob-opts {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(150px, 1fr));
  gap: var(--space-2);
}
.kob-opt {
  display: grid;
  gap: var(--space-0);
  padding: var(--space-3);
  border: 1px solid var(--border);
  border-radius: var(--r-sm);
  background: var(--surface-2);
  color: var(--ink-2);
  cursor: pointer;
  text-align: left;
  transition: border-color var(--dur-ui) var(--ease-ui),
              background var(--dur-ui) var(--ease-ui),
              color var(--dur-ui) var(--ease-ui);
}
.kob-opt:hover { border-color: var(--brand); background: var(--raised); color: var(--ink); }
.kob-opt:focus-visible { outline: 2px solid var(--focus); outline-offset: 1px; }
.kob-glyph { font-size: var(--t-heading); line-height: 1; }
.kob-label { font: 600 var(--t-body)/1.25 var(--sans); color: var(--ink); }
.kob-tag { font-size: var(--t-small); line-height: 1.4; }

/* Skip is an answer and is styled as one: quiet, but present and reachable.
   A card with no way out is a modal wearing a card's clothes. */
.kob-skip { margin-top: var(--space-3); align-self: start; }

/* ------------------------------------------- inline term explanations (Simple)
 *
 * The clickable variant of a glossary term. It has to look like it does
 * something, where the hover variant only has to look like it means something:
 * a reader on Simple is the least likely to try hovering a dotted underline on
 * the chance that it responds. */
.gloss-term.is-inline {
  padding: 0;
  border: 0;
  border-bottom: 1px dashed var(--brand);
  background: none;
  color: inherit;
  font: inherit;
  cursor: help;
  transition: border-color var(--dur-ui) var(--ease-ui),
              color var(--dur-ui) var(--ease-ui);
}
.gloss-term.is-inline:hover { color: var(--ink); border-bottom-color: var(--ink); }
.gloss-term.is-inline:focus-visible { outline: 2px solid var(--focus); outline-offset: 2px; }
.gloss-term.is-inline[aria-expanded="true"] { border-bottom-style: solid; }

/* A block rather than a floating panel. The point of this mode is that the
   explanation stays put and can be read next to the thing it explains, which a
   tooltip cannot do: it covers its own subject and vanishes on the way to
   reading it. */
.gloss-inline {
  display: block;
  margin: var(--space-1) 0 var(--space-2);
  padding: var(--space-2) var(--space-3);
  border-left: 2px solid var(--brand);
  border-radius: 0 var(--r-sm) var(--r-sm) 0;
  background: var(--raised);
  color: var(--ink-2);
  font-size: var(--t-small);
  line-height: 1.5;
}

/* ---------------------------------------------------- the knowledge selector
 *
 * One chip, never a row of five. The brief this was built from is explicit on
 * both counts: reachable from Ask Optic rather than buried in Settings, and it
 * must never dominate the interface. A segmented control with five segments
 * would have been the second thing and not the first. */
.km { position: relative; display: inline-flex; }

/* One field box, worn by both controls beside the composer.
 *
 * Optic Mode and Optic Persona sit side by side and have to read as one pair.
 * They did not: measured at 47px tall on --surface-2 with --border against 44px
 * on --surface with --border-strong, and the label was INSIDE the first box and
 * outside the second, so one looked like a designed control and the other like
 * a bare form field someone had dropped next to it.
 *
 * Shared rather than copied, because two lookalike rules drift the first time
 * one is touched. `.km-btn` is the knowledge component's own class and keeps
 * only what is specific to being a button; the persona field wears `.oc-field`
 * too and inherits the box.
 */
.oc-field {
  display: flex;
  flex-direction: column;
  align-items: flex-start;
  gap: 1px;
  padding: var(--space-1) var(--space-3) var(--space-1) var(--space-2);
  border: 1px solid var(--border);
  border-radius: var(--r-md);
  background: var(--surface-2);
  color: var(--ink);
  text-align: left;
  transition: border-color var(--dur-ui) var(--ease-ui),
              background var(--dur-ui) var(--ease-ui);
}
.oc-field:hover { border-color: var(--ink-muted); background: var(--raised); }
.oc-field:focus-within { border-color: var(--ink-muted); }

/* The persona field is a <label> wrapping a <select>, so the box belongs to the
   label and the select has to stop drawing one of its own. The caret is on the
   field rather than the select because an appearance:none select has none, and
   the mode chip beside it draws its own the same way. */
/* No extra right padding: the mode chip beside it reserves none either and
   sits its caret at --space-1, overlapping the padding rather than adding to
   it. Reserving space here made the two boxes 13.8px and 23px on the right,
   which is visible when they are adjacent. */
.pp-field { position: relative; cursor: pointer; }
/* The chosen persona, in the box. Same type as the mode chip's .km-now, so the
   two controls read as one pair when closed as well as when open. */
.pp-now { font: 600 var(--t-body)/1.2 var(--sans); max-width: 22ch; }
.pp { position: relative; display: inline-flex; }
.pp-field:focus-visible { outline: 2px solid var(--focus); outline-offset: 1px; }
/* The same caret as the mode chip, under an aliased selector rather than a
   second copy of the triangle. */

/* The label, inside the box, in both controls. */
.oc-eyebrow {
  font-size: var(--t-micro);
  letter-spacing: 0.01em;
  color: var(--ink-muted);
}

.km-btn {
  cursor: pointer;
}
.km-btn:focus-visible { outline: 2px solid var(--focus); outline-offset: 1px; }

/* Kept as an alias so the component's own markup needs no second class on the
   span. Same declarations as .oc-eyebrow; see the note on .oc-field. */
.km-eyebrow,
.pp-eyebrow {
  font-size: var(--t-micro);
  letter-spacing: 0.01em;
  color: var(--ink-muted);
}
.km-now { font: 600 var(--t-body)/1.2 var(--sans); }
.km-caret,
.pp-caret {
  position: absolute;
  right: var(--space-1);
  top: 50%;
  width: 0; height: 0;
  margin-top: -2px;
  border-left: 3.5px solid transparent;
  border-right: 3.5px solid transparent;
  border-top: 4px solid currentColor;
  opacity: 0.45;
}

/* Shared with the Optic Persona menu. Aliased selectors rather than a second
   copy: the two menus sit under two controls that now wear the same box, and a
   duplicated rule set would drift the first time one was touched. */
.km-menu,
.oc-menu {
  position: absolute;
  top: calc(100% + 4px);
  left: 0;
  z-index: 70;
  width: 320px;
  max-width: calc(100vw - 2 * var(--space-4));
  padding: var(--space-1);
  border: 1px solid var(--border-strong);
  border-radius: var(--r-md);
  background: var(--surface);
  box-shadow: var(--elev-2);
  /* The menu is taller than the space under a chip near the bottom of a panel,
     and a fixed box cannot be scrolled to by the page. Same lesson as the
     section dropdowns. */
  max-height: calc(100vh - var(--topbar-h, 106px) - var(--space-6));
  overflow-y: auto;
}

.km-opt,
.oc-opt {
  display: flex;
  flex-direction: column;
  gap: 1px;
  width: 100%;
  padding: var(--space-2) var(--space-3);
  border: 0;
  border-radius: var(--r-sm);
  background: none;
  color: var(--ink-2);
  cursor: pointer;
  text-align: left;
  transition: background var(--dur-ui) var(--ease-ui),
              color var(--dur-ui) var(--ease-ui);
}
.km-opt:hover,
.oc-opt:hover { background: var(--raised); color: var(--ink); }
.km-opt.on,
.oc-opt.on { background: color-mix(in srgb, var(--brand) 14%, transparent); color: var(--ink); }
.km-opt-head,
.oc-opt-head { display: flex; align-items: baseline; gap: var(--space-2); }
.km-opt-label,
.oc-opt-label { font: 600 var(--t-body)/1.3 var(--sans); }
.km-opt-blurb,
.oc-opt-blurb { font-size: var(--t-small); color: var(--ink-muted); line-height: 1.4; }

/* What it changes, and the one thing it cannot. Both come from the server, so
   the menu's promise and the behaviour have a single source. */
.km-foot,
.oc-foot {
  margin-top: var(--space-1);
  padding: var(--space-2) var(--space-3) var(--space-1);
  border-top: 1px solid var(--grid);
}
.km-foot-h,
.oc-foot-h {
  margin: 0 0 var(--space-0);
  font-size: var(--t-micro);
  letter-spacing: 0.01em;
  color: var(--ink-muted);
}
.km-foot ul,
.oc-foot ul {
  margin: 0 0 var(--space-2);
  padding-left: var(--space-4);
  font-size: var(--t-small);
  color: var(--ink-2);
  line-height: 1.45;
}

/* ------------------------------------------------- the contract cost limit
 *
 * Sits under the panel's own subtitle rather than in a settings page: it is
 * read at the moment somebody is looking at a $4,000 contract, which is the
 * moment it means anything. */
.eb-bar {
  display: flex;
  align-items: center;
  gap: var(--space-3);
  flex-wrap: wrap;
  margin: var(--space-3) 0 var(--space-4);
  padding: var(--space-2) 0;
  border-top: 1px solid var(--grid);
  border-bottom: 1px solid var(--grid);
}
.eb-label { color: var(--ink-2); font-size: var(--t-small); }
.eb-pills { display: flex; gap: var(--space-1); flex-wrap: wrap; }
.eb-note {
  flex: 1 1 100%;
  margin: 0;
  color: var(--ink-muted);
  font-size: var(--t-small);
}
/* The empty case is the one worth colouring. Every other note is a count; this
   one means the panel below it has nothing in it, and a reader who misses that
   reads an empty table as a broken chain. */
.eb-note.is-empty { color: var(--ink-2); }

/* ------------------------------------------- watches that fired, per account
 *
 * Two lines per row rather than a table. The title is the symbol and the
 * condition, the body is the number it tripped on and whatever note the reader
 * left themselves, and those are two weights of one sentence rather than
 * columns that would need headers to be readable. */
.wh-list { list-style: none; margin: 0; padding: 0; display: grid; gap: var(--space-0); }

.wh-row {
  padding: var(--space-3);
  border-radius: var(--r-sm);
  border-left: 2px solid transparent;
  background: var(--raised);
}
/* Unread is marked twice on purpose: a rule down the left edge and the word
   "new". Colour alone would be the only signal for a reader who cannot see it,
   and this list is the one place in the app where missing a row is the whole
   failure. */
.wh-row.is-new { border-left-color: var(--btn-primary); }

.wh-line {
  display: flex;
  align-items: baseline;
  gap: var(--space-2);
  flex-wrap: wrap;
}
.wh-title { font-weight: 600; color: var(--ink); }
.wh-when { margin-left: auto; color: var(--ink-muted); font-size: var(--t-small); }
.wh-new {
  padding: 1px var(--space-2);
  border-radius: var(--r-pill);
  background: color-mix(in srgb, var(--btn-primary) 22%, transparent);
  color: var(--ink);
  font: 600 var(--t-micro)/1.6 var(--sans);
  text-transform: uppercase;
  letter-spacing: 0.04em;
}
.wh-body {
  margin: var(--space-1) 0 0;
  color: var(--ink-2);
  font-size: var(--t-body);
}
/* The count beside the heading, not a floating dot: it has to say how many, and
   next to the words it is counting is where that reads without explanation. */
.wh-count {
  display: inline-block;
  min-width: 20px;
  padding: 0 var(--space-1);
  border-radius: var(--r-pill);
  background: var(--btn-primary);
  color: var(--btn-primary-ink);
  font: 600 var(--t-small)/20px var(--sans);
  text-align: center;
  vertical-align: middle;
}

/* ========================================================= Optic's take ====
 *
 * Above the comparison table. A section rather than a panel for the same reason
 * the Optic loop is: it is the conclusion and the table below is the evidence,
 * and putting both in identical cards makes them look like peers.
 */
.ct-block { padding: var(--space-5) 0; border-bottom: 1px solid var(--border); margin-bottom: var(--space-5); }
.ct-agree { font-size: var(--t-small); color: var(--pos); }
.ct-split { font-size: var(--t-small); color: var(--warn); }
.ct-headline {
  margin: 0 0 var(--space-5);
  font-family: var(--display);
  font-size: var(--t-lead);
  color: var(--ink);
  max-width: 70ch;
}
.ct-row {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(190px, 1fr));
  gap: var(--space-4);
  margin-bottom: var(--space-4);
}
.ct-hz {
  display: grid;
  gap: 2px;
  padding-left: var(--space-3);
  border-left: 2px solid var(--border-strong);
}
/* A horizon whose top two are within five points gets a warning edge, not a
   green one. Presenting a coin flip as a verdict is the failure this avoids. */
.ct-hz.is-close { border-left-color: var(--warn); }
.ct-hz-name {
  font-size: var(--t-micro);
  font-weight: 600;
  letter-spacing: 0.01em;
  color: var(--ink-muted);
}
.ct-hz-win {
  font-family: var(--display);
  font-size: var(--t-heading);
  font-weight: 600;
  color: var(--ink);
}
.ct-hz-gap { font-size: var(--t-caption); color: var(--ink-muted); }
.ct-standouts { list-style: none; margin: 0 0 var(--space-3); padding: 0; display: grid; gap: var(--space-1); }
.ct-standouts li { font-size: var(--t-small); color: var(--ink-2); }
.ct-standouts strong { color: var(--ink); }

/* ========================================================== research hub ====
 *
 * Four questions and a saved list, above the scanners. Buttons rather than
 * cards: each one is an action, and a card implies content to read.
 */
.rh-block { padding: var(--space-5) 0; border-bottom: 1px solid var(--border); margin-bottom: var(--space-5); }
.rh-sub { font-size: var(--t-small); color: var(--ink-muted); }
.rh-routes {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(240px, 1fr));
  gap: var(--space-3);
  margin-bottom: var(--space-5);
}
.rh-route {
  display: grid;
  gap: var(--space-1);
  text-align: left;
  padding: var(--space-4);
  font: inherit;
  background: var(--surface);
  border: 1px solid var(--border);
  border-left: 3px solid var(--btn-primary);
  border-radius: 0 var(--r-chrome) var(--r-chrome) 0;
  cursor: pointer;
}
.rh-route:hover { background: var(--surface-2); border-color: var(--border-strong); }
.rh-route:focus-visible { outline: 2px solid var(--focus); outline-offset: 2px; }
.rh-q { font-size: var(--t-body); color: var(--ink); font-weight: 500; }
.rh-note { font-size: var(--t-caption); color: var(--ink-muted); }

.rh-h3 {
  font-size: var(--t-micro);
  font-weight: 600;
  letter-spacing: 0.08em;
  text-transform: uppercase;
  color: var(--ink-muted);
  margin: 0 0 var(--space-2);
}
.rh-count { color: var(--ink-2); }
.rh-list { list-style: none; margin: 0; padding: 0; }
.rh-item { display: flex; border-bottom: 1px solid var(--border); }
.rh-item:last-child { border-bottom: 0; }
.rh-open {
  flex: 1 1 auto;
  min-width: 0;
  display: grid;
  grid-template-columns: 6ch 1fr auto;
  gap: var(--space-3);
  align-items: baseline;
  padding: var(--space-3) var(--space-2);
  font: inherit;
  text-align: left;
  background: none;
  border: 0;
  cursor: pointer;
  border-radius: var(--r-sm);
}
.rh-open:hover { background: var(--surface-2); }
.rh-open:focus-visible { outline: 2px solid var(--focus); outline-offset: -2px; }
.rh-item-sym { font-weight: 600; font-size: var(--t-small); color: var(--ink); }
.rh-item-q {
  font-size: var(--t-small);
  color: var(--ink-2);
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}
.rh-item-when { font-size: var(--t-caption); color: var(--ink-muted); }
.rh-empty, .rh-more { margin: 0; font-size: var(--t-small); color: var(--ink-muted); max-width: 68ch; }

/* The Save button on a question in the Pulse log. Quiet until hovered — it is
   available on every question and should not compete with the text. */
.msg-save {
  justify-self: start;
  margin-top: var(--space-1);
  font: inherit;
  font-size: var(--t-micro);
  padding: var(--space-0) var(--space-2);
  background: none;
  border: 1px solid var(--border);
  border-radius: var(--r-sm);
  color: var(--ink-muted);
  cursor: pointer;
}
.msg-save:hover { color: var(--ink); border-color: var(--border-strong); }
.msg-save:disabled { color: var(--pos); border-color: transparent; cursor: default; }

@media (max-width: 559px) {
  .rh-routes { grid-template-columns: 1fr; }
  .rh-open { grid-template-columns: 6ch 1fr; }
  .rh-item-when { display: none; }
}

/* ==========================================================================
   Accounts
   ==========================================================================

   Sign-in, the account menu, and the Security and Profile blocks in Settings.
   One dialog, one dropdown, one toast stack. Tokens throughout — no hex values,
   4px spacing grid, and motion on paint-level properties only, which is the
   same rule the rest of this stylesheet follows.

   The dialog is a centred card on a desktop and a bottom sheet on a phone. Not
   a full-screen page: the whole point of the interruption model is that the
   work behind it stays visible, so somebody who pressed Save on an NVDA read
   can still see the NVDA read while they sign in. */

/* ------------------------------------------------------- the topbar slot */

/* Fixed 38px square whatever state it is in, so the answer arriving from
   /api/auth/me does not move the Pulse button sideways. The loading state is a
   dim circle rather than nothing for the same reason. */
.account-slot {
  position: relative;
  flex: none;
  display: flex;
  align-items: center;
  justify-content: center;
  min-width: 38px;
  height: 38px;
}

.acct-wait {
  width: 30px;
  height: 30px;
  border-radius: var(--r-pill);
  background: var(--raised);
  border: 1px solid var(--border);
}

.acct-signin {
  padding: var(--space-2) var(--space-4);
  font: 600 var(--t-body)/1 var(--sans);
  color: var(--ink);
}

.acct-btn {
  display: flex;
  align-items: center;
  justify-content: center;
  width: 34px;
  height: 34px;
  padding: 0;
  background: none;
  border: 0;
  border-radius: var(--r-pill);
  cursor: pointer;
}

.acct-btn:focus-visible { outline: 2px solid var(--focus); outline-offset: 2px; }

.acct-face {
  display: flex;
  align-items: center;
  justify-content: center;
  width: 32px;
  height: 32px;
  border-radius: var(--r-pill);
  background: var(--surface-2);
  border: 1px solid var(--border-strong);
  object-fit: cover;
  font: 600 var(--t-caption)/1 var(--sans);
  letter-spacing: 0.04em;
  color: var(--ink);
}

.acct-btn:hover .acct-face { border-color: var(--s1); }

/* ------------------------------------------------------ the account menu */

.acct-menu {
  position: absolute;
  top: calc(100% + var(--space-2));
  right: 0;
  z-index: 80;
  width: 248px;
  padding: var(--space-2);
  background: var(--surface);
  border: 1px solid var(--border-strong);
  border-radius: var(--r-md);
  box-shadow: 0 12px 32px var(--scroll-shade);
}

.acct-head {
  display: flex;
  flex-direction: column;
  gap: var(--space-0);
  padding: var(--space-2) var(--space-3) var(--space-3);
  border-bottom: 1px solid var(--border);
}

.acct-name { font: 600 var(--t-base)/1.3 var(--sans); color: var(--ink); }

/* Email addresses are long and this menu is narrow, so the address truncates
   rather than wrapping to three lines and pushing the sign-out button off. */
.acct-mail {
  font: 400 var(--t-caption)/1.4 var(--sans);
  color: var(--ink-muted);
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

.acct-plan {
  margin-top: var(--space-1);
  font: 600 var(--t-micro)/1 var(--sans);
  text-transform: uppercase;
  letter-spacing: 0.08em;
  color: var(--ink-2);
}

/* The owner marker, beside the plan chip.
   --btn-primary on the border only, and --ink for the text. Measured against
   the menu's own surface: the blue comes out at 4.02:1, which clears the 3:1
   that applies to a border but not the 4.5:1 that applies to text, and this
   chip is --t-micro. So the blue stays where it reads as a shape and the label
   takes the ink that is legible there (16.87:1). --s1 would have passed as text
   at 5.21:1 and was rejected on other grounds: it is a chart-series slot, and a
   label is not a series.
   Not a filled chip: --btn-primary is the one blue picked for white text on
   top of it, and filling this would make a label look like the menu's primary
   action. `align-self` because .acct-head is a flex column, where the default
   stretch would run the border the full 230px. */
.acct-admin {
  align-self: flex-start;
  margin-top: var(--space-1);
  padding: 0 var(--space-1);
  border: 1px solid var(--btn-primary);
  border-radius: var(--r-sm);
  font: 600 var(--t-micro)/1.6 var(--sans);
  text-transform: uppercase;
  letter-spacing: 0.08em;
  color: var(--ink);
}

.acct-warn {
  margin: var(--space-2) 0 0;
  padding: var(--space-2) var(--space-3);
  background: var(--raised);
  border-radius: var(--r-sm);
  font: 400 var(--t-caption)/1.45 var(--sans);
  color: var(--ink-2);
}

.acct-warn button {
  background: none;
  border: 0;
  padding: 0;
  font: 600 var(--t-caption)/1.45 var(--sans);
  color: var(--s1);
  cursor: pointer;
  text-decoration: underline;
}

.acct-items { display: flex; flex-direction: column; padding: var(--space-1) 0; }

.acct-items button,
.acct-out {
  display: block;
  width: 100%;
  padding: var(--space-2) var(--space-3);
  background: none;
  border: 0;
  border-radius: var(--r-sm);
  font: 500 var(--t-body)/1 var(--sans);
  color: var(--ink);
  text-align: left;
  cursor: pointer;
}

.acct-items button:hover, .acct-out:hover { background: var(--surface-2); }

.acct-out {
  margin-top: var(--space-1);
  border-top: 1px solid var(--border);
  border-radius: 0 0 var(--r-sm) var(--r-sm);
  color: var(--ink-2);
}

/* ------------------------------------------------------------- the dialog */

.auth-modal {
  position: fixed;
  inset: 0;
  z-index: 200;
  display: flex;
  align-items: center;
  justify-content: center;
  padding: var(--space-5);
}

.auth-backdrop {
  position: absolute;
  inset: 0;
  background: var(--glass-strong);
  backdrop-filter: blur(var(--glass-blur)) saturate(var(--glass-sat));
  -webkit-backdrop-filter: blur(var(--glass-blur)) saturate(var(--glass-sat));
}

/* Scroll is locked while the dialog is up.
 *
 * `overflow: hidden` on body also disables `position: sticky` on the topbar,
 * which is documented in CLAUDE.md as a trap. It is deliberate here and it is
 * safe *because* it is scoped to a class that only exists while the dialog is
 * open: the topbar is behind an opaque backdrop for exactly that long, and the
 * declaration is gone before it is visible again. */
body.auth-open { overflow: hidden; }

.auth-card {
  position: relative;
  width: 100%;
  max-width: 420px;
  max-height: calc(100vh - 2 * var(--space-5));
  overflow-y: auto;
  padding: var(--space-7) var(--space-6) var(--space-5);
  background: var(--surface);
  border: 1px solid var(--border-strong);
  border-radius: var(--r-lg);
  box-shadow: var(--elev-3);
}

.auth-close {
  position: absolute;
  top: var(--space-3);
  right: var(--space-3);
  width: 28px;
  height: 28px;
  background: none;
  border: 0;
  border-radius: var(--r-sm);
  font: 400 20px/1 var(--sans);
  color: var(--ink-muted);
  cursor: pointer;
}

.auth-close:hover { background: var(--surface-2); color: var(--ink); }

.auth-card h2 {
  margin: 0 0 var(--space-2);
  font: 600 var(--t-heading)/1.25 var(--display);
  letter-spacing: -0.01em;
  color: var(--ink);
}

.auth-sub {
  margin: 0 0 var(--space-5);
  font: 400 var(--t-body)/1.5 var(--sans);
  color: var(--ink-muted);
}

/* Why the dialog opened, when it opened because somebody tried to keep
   something. Left border rather than a filled panel: it is context, not an
   alert. */
.auth-reason {
  margin: 0 0 var(--space-5);
  padding-left: var(--space-3);
  border-left: 2px solid var(--s1);
  font: 400 var(--t-body)/1.5 var(--sans);
  color: var(--ink-2);
}

/* ------------------------------------------------------------- providers */

.auth-providers {
  display: flex;
  flex-direction: column;
  gap: var(--space-2);
}

.auth-provider {
  display: flex;
  align-items: center;
  justify-content: center;
  gap: var(--space-2);
  /* 44px: a comfortable touch target, and the row these buttons form is the
     first thing on the screen on a phone. */
  min-height: 44px;
  padding: 0 var(--space-4);
  background: var(--surface-2);
  border: 1px solid var(--border-strong);
  border-radius: var(--r-md);
  font: 600 var(--t-body)/1 var(--sans);
  color: var(--ink);
  cursor: pointer;
}

.auth-provider:hover { border-color: var(--ink-muted); background: var(--raised); }
.auth-provider:focus-visible { outline: 2px solid var(--focus); outline-offset: 2px; }

.auth-glyph { width: 17px; height: 17px; flex: none; }

.auth-or {
  display: flex;
  align-items: center;
  gap: var(--space-3);
  margin: var(--space-5) 0;
  font: 500 var(--t-micro)/1 var(--sans);
  text-transform: uppercase;
  letter-spacing: 0.1em;
  color: var(--ink-muted);
}

.auth-or::before, .auth-or::after {
  content: "";
  flex: 1;
  height: 1px;
  background: var(--border);
}

/* ----------------------------------------------------------------- fields */

.auth-form { display: flex; flex-direction: column; gap: var(--space-4); }

.auth-row2 {
  display: grid;
  grid-template-columns: 1fr 1fr;
  gap: var(--space-3);
}

.auth-field { display: flex; flex-direction: column; gap: var(--space-2); }

/* Remember me, between the password and the button. */
.auth-remember {
  display: flex;
  align-items: center;
  gap: var(--space-2);
  font-size: var(--t-caption);
  color: var(--ink-2);
  cursor: pointer;
  user-select: none;
}
.auth-remember input { width: 16px; height: 16px; margin: 0; accent-color: var(--brand); cursor: pointer; }

.auth-field > span {
  font: 500 var(--t-caption)/1 var(--sans);
  color: var(--ink-2);
}

.auth-field input {
  width: 100%;
  min-height: 42px;
  padding: 0 var(--space-3);
  background: var(--plane);
  border: 1px solid var(--border-strong);
  border-radius: var(--r-sm);
  font: 400 var(--t-base)/1 var(--sans);
  color: var(--ink);
}

.auth-field input:focus {
  outline: none;
  border-color: var(--s1);
  box-shadow: 0 0 0 3px color-mix(in srgb, var(--s1) 22%, transparent);
}

.auth-pw-wrap { position: relative; display: block; }

/* Padded so the value never runs under the Show control. */
.auth-pw-wrap input { padding-right: 60px; }

.auth-peek {
  position: absolute;
  top: 50%;
  right: var(--space-2);
  transform: translateY(-50%);
  min-width: 44px;
  min-height: 30px;
  padding: 0 var(--space-2);
  background: none;
  border: 0;
  border-radius: var(--r-xs);
  font: 600 var(--t-caption)/1 var(--sans);
  color: var(--ink-muted);
  cursor: pointer;
}

.auth-peek:hover { color: var(--ink); background: var(--surface-2); }

.auth-policy {
  margin: calc(-1 * var(--space-2)) 0 0;
  padding: 0 0 0 var(--space-4);
  list-style: none;
  font: 400 var(--t-caption)/1.7 var(--sans);
  color: var(--ink-muted);
}

/* No composition rules to tick off, so these are statements rather than a
   checklist with live validation. NIST advises against the rules; showing
   what is actually checked is the honest version. */
.auth-policy li { position: relative; }

.auth-policy li::before {
  content: "";
  position: absolute;
  left: calc(-1 * var(--space-3));
  top: 0.62em;
  width: 3px;
  height: 3px;
  border-radius: var(--r-pill);
  background: var(--ink-muted);
}

.auth-error,
.auth-note {
  padding: var(--space-3);
  border-radius: var(--r-sm);
  font: 400 var(--t-body)/1.5 var(--sans);
}

.auth-error {
  background: color-mix(in srgb, var(--neg) 12%, transparent);
  border: 1px solid color-mix(in srgb, var(--neg) 34%, transparent);
  color: var(--ink);
}

.auth-note {
  background: var(--raised);
  border: 1px solid var(--border);
  color: var(--ink-2);
}

/* Standing guidance rather than a response to something the reader did, so it
   is quieter than the note that appears after a failed press: no box, smaller
   type, sitting under the buttons it explains. A bordered panel here would read
   as a warning about a button nobody has touched yet. */
/* Hidden while the note below is showing. The standing hint and the note after
   a failed press say the same thing in the same words, and the screenshot that
   reported the overlap had both on screen at once, forty pixels apart. The one
   that answers something the reader just did is the one worth keeping. */
/* Scoped from .auth-body, because the hint is a sibling of the form rather than
   inside it: both hang off .auth-body, so a :has() rooted at .auth-form matches
   nothing. Checked in the DOM rather than assumed.
   Where :has() is unsupported both simply show, which is today's behaviour. */
.auth-body:has(.auth-form [data-error]:not([hidden])) .auth-passkey-hint {
  display: none;
}

.auth-passkey-hint {
  /* A positive margin, not a negative one.
     The negative was meant to cancel the gap inside .auth-providers, and this
     sits *after* that block rather than in it, so there was no gap to cancel
     and it pulled the sentence 9px up into the button: measured with the modal
     open, the button ran 185 to 229 and the hint started at 220. */
  margin: var(--space-2) 0 0;
  padding: 0 var(--space-1);
  background: none;
  border: 0;
  font-size: var(--t-small);
  color: var(--ink-muted);
}

.auth-submit { min-height: 44px; font: 600 var(--t-base)/1 var(--sans); }
.auth-submit:disabled { opacity: 0.65; cursor: progress; }

.auth-link,
.auth-alt button {
  align-self: center;
  background: none;
  border: 0;
  padding: var(--space-1);
  font: 500 var(--t-body)/1 var(--sans);
  color: var(--s1);
  cursor: pointer;
}

.auth-link:hover, .auth-alt button:hover { text-decoration: underline; }

.auth-alt {
  margin: var(--space-5) 0 0;
  text-align: center;
  font: 400 var(--t-body)/1.5 var(--sans);
  color: var(--ink-muted);
}

.auth-alt button { display: inline; padding: 0; }

.auth-legal {
  margin: var(--space-5) 0 0;
  padding-top: var(--space-4);
  border-top: 1px solid var(--border);
  font: 400 var(--t-caption)/1.5 var(--sans);
  color: var(--ink-muted);
  text-align: center;
}

/* ------------------------------------------------------------------ toasts */

.auth-toasts {
  position: fixed;
  bottom: var(--space-5);
  left: 50%;
  transform: translateX(-50%);
  z-index: 300;
  display: flex;
  flex-direction: column;
  gap: var(--space-2);
  width: min(440px, calc(100vw - 2 * var(--space-5)));
  pointer-events: none;
}

.auth-toast {
  padding: var(--space-3) var(--space-4);
  background: var(--surface-2);
  border: 1px solid var(--border-strong);
  border-left: 3px solid var(--s1);
  border-radius: var(--r-sm);
  box-shadow: var(--elev-2);
  font: 400 var(--t-body)/1.5 var(--sans);
  color: var(--ink);
  cursor: pointer;
  pointer-events: auto;
  opacity: 1;
  transition: opacity var(--dur-ui) var(--ease-ui);
}

.auth-toast.bad { border-left-color: var(--neg); }
.auth-toast.warn { border-left-color: var(--warn); }
.auth-toast.gone { opacity: 0; }

/* --------------------------------------------------------- passkey promo */

.auth-promo {
  position: fixed;
  bottom: var(--space-5);
  right: var(--space-5);
  z-index: 150;
  display: flex;
  flex-direction: column;
  gap: var(--space-4);
  width: min(340px, calc(100vw - 2 * var(--space-4)));
  padding: var(--space-4);
  background: var(--surface);
  border: 1px solid var(--border-strong);
  border-radius: var(--r-md);
  box-shadow: 0 16px 40px var(--scroll-shade);
}

.auth-promo-copy { display: flex; flex-direction: column; gap: var(--space-2); }
.auth-promo-copy strong { font: 600 var(--t-base)/1.3 var(--sans); color: var(--ink); }

.auth-promo-copy span {
  font: 400 var(--t-body)/1.5 var(--sans);
  color: var(--ink-muted);
}

.auth-promo-act { display: flex; gap: var(--space-2); }
.auth-promo-act .btn { flex: 1; min-height: 38px; }

/* --------------------------------------------------- Settings: the account */

.set-acct { display: flex; flex-direction: column; gap: var(--space-3); }

.set-acct-id {
  display: flex;
  align-items: center;
  gap: var(--space-4);
  padding-bottom: var(--space-4);
  border-bottom: 1px solid var(--border);
}

.set-acct-face {
  display: flex;
  align-items: center;
  justify-content: center;
  width: 48px;
  height: 48px;
  flex: none;
  border-radius: var(--r-pill);
  background: var(--surface-2);
  border: 1px solid var(--border-strong);
  object-fit: cover;
  font: 600 var(--t-base)/1 var(--sans);
  color: var(--ink);
}

.set-acct-who { display: flex; flex-direction: column; gap: var(--space-0); min-width: 0; }
.set-acct-who strong { font: 600 var(--t-base)/1.3 var(--sans); color: var(--ink); }

.set-acct-who span {
  font: 400 var(--t-caption)/1.4 var(--sans);
  color: var(--ink-muted);
}

/* One row per thing that can be connected or revoked. The grid keeps the
   right-hand control aligned down the column however long the label is. */
.set-row {
  display: grid;
  grid-template-columns: 1fr auto;
  align-items: center;
  gap: var(--space-4);
  padding: var(--space-3) 0;
  border-bottom: 1px solid var(--border);
}

.set-row:last-child { border-bottom: 0; }

.set-row-main { display: flex; flex-direction: column; gap: var(--space-1); min-width: 0; }

.set-row-title {
  display: flex;
  align-items: center;
  gap: var(--space-2);
  font: 500 var(--t-body)/1.3 var(--sans);
  color: var(--ink);
}

.set-row-note {
  font: 400 var(--t-caption)/1.45 var(--sans);
  color: var(--ink-muted);
}

/* Wraps rather than running out of its panel: at 320 with Large text, "Create
   an account" and "Sign in" side by side ran 35px past the Account panel's
   edge and the whole panel scrolled sideways. */
.set-row-act { display: flex; flex-wrap: wrap; gap: var(--space-2); }
.set-row-act .btn { padding: var(--space-1) var(--space-3); font-size: var(--t-caption); }

.set-tag {
  padding: 2px var(--space-2);
  border-radius: var(--r-pill);
  font: 600 var(--t-micro)/1.6 var(--sans);
  text-transform: uppercase;
  letter-spacing: 0.06em;
}

.set-tag.on {
  background: color-mix(in srgb, var(--good) 18%, transparent);
  color: var(--ink);
}

.set-tag.off { background: var(--raised); color: var(--ink-muted); }

.set-tag.here {
  background: color-mix(in srgb, var(--s1) 20%, transparent);
  color: var(--ink);
}

.set-danger .btn { border-color: color-mix(in srgb, var(--neg) 45%, transparent); }
.set-danger .btn:hover { background: color-mix(in srgb, var(--neg) 14%, transparent); }

.set-guest {
  display: flex;
  flex-direction: column;
  align-items: flex-start;
  gap: var(--space-4);
  padding: var(--space-5);
  background: var(--raised);
  border: 1px solid var(--border);
  border-radius: var(--r-md);
}

.set-guest p { margin: 0; font: 400 var(--t-body)/1.55 var(--sans); color: var(--ink-2); }

/* ------------------------------------------------------------------ phone */

@media (max-width: 559px) {
  /* A bottom sheet. A centred card on a 375px screen leaves a strip of
     backdrop above and below that does nothing except make the card smaller. */
  .auth-modal { align-items: flex-end; padding: 0; }

  .auth-card {
    max-width: none;
    max-height: 92vh;
    padding: var(--space-6) var(--space-4) var(--space-5);
    border-radius: var(--r-lg) var(--r-lg) 0 0;
    border-bottom: 0;
    /* Clears the home indicator on a notched phone. */
    padding-bottom: calc(var(--space-5) + env(safe-area-inset-bottom, 0px));
  }

  .auth-row2 { grid-template-columns: 1fr; }

  .acct-menu {
    position: fixed;
    top: auto;
    bottom: 0;
    right: 0;
    left: 0;
    width: auto;
    border-radius: var(--r-lg) var(--r-lg) 0 0;
    padding-bottom: calc(var(--space-3) + env(safe-area-inset-bottom, 0px));
  }

  .auth-promo {
    right: var(--space-3);
    left: var(--space-3);
    bottom: var(--space-3);
    width: auto;
  }

  .set-row { grid-template-columns: 1fr; }
  .set-row-act { justify-content: flex-start; }
}

@media (prefers-reduced-motion: reduce) {
  .auth-toast { transition: none; }
}

/* ------------------------------------------- Settings: security sub-blocks */

.set-h3 {
  margin: var(--space-6) 0 var(--space-2);
  font: 600 var(--t-caption)/1 var(--sans);
  text-transform: uppercase;
  letter-spacing: 0.08em;
  color: var(--ink-muted);
}

.set-h3:first-child { margin-top: 0; }

.set-count {
  margin-left: var(--space-1);
  padding: 1px var(--space-2);
  background: var(--raised);
  border-radius: var(--r-pill);
  font-weight: 600;
  letter-spacing: 0;
  color: var(--ink-2);
}

.set-profile .set-row-act { flex-wrap: wrap; }
.set-profile input { max-width: 132px; }

.set-pw {
  display: flex;
  flex-direction: column;
  gap: var(--space-3);
  max-width: 340px;
  margin: var(--space-3) 0 var(--space-4);
  padding: var(--space-4);
  background: var(--raised);
  border: 1px solid var(--border);
  border-radius: var(--r-md);
}

/* An author `display` beats the UA stylesheet's `[hidden] { display: none }`
   whatever the specificity, so the form rendered open on every visit to
   Settings. Any rule that sets `display` on an element that also carries the
   `hidden` attribute needs this pair. */
.set-pw[hidden] { display: none; }

/* --------------------------------------------------- the Pulse allowance */

/* Above the input, not below the answer. The point is to say what is left
   before somebody types, because a 429 arriving mid-answer reads as a fault. */
.chat-allow {
  margin: 0 0 var(--space-2);
  font: 400 var(--t-caption)/1.5 var(--sans);
  color: var(--ink-muted);
}

/* Pulse with no credential. Warmer than an error and plainer than a nudge:
   nothing is broken, the feature is simply not configured here, and the rest
   of the terminal is unaffected. Shares .chat-allow's placement so the reader
   looks in the one place above the composer for anything about Pulse's
   availability. */
.chat-off {
  color: var(--ink-2);
  border-left: 2px solid var(--border-strong);
  padding-left: var(--space-3);
}
/* The composer, switched off. Not hidden: a reader has to see what is
   unavailable to understand the sentence above it. */
#chat-input:disabled,
#chat-send:disabled,
#chat-research:disabled { opacity: 0.45; cursor: not-allowed; }

.chat-allow.spent { color: var(--ink-2); }

.chat-allow button {
  padding: 0;
  background: none;
  border: 0;
  font: 600 var(--t-caption)/1.5 var(--sans);
  color: var(--s1);
  cursor: pointer;
  text-decoration: underline;
}

.auth-toast-act {
  display: block;
  margin-top: var(--space-2);
  padding: var(--space-1) var(--space-3);
  background: var(--btn-primary);
  border: 0;
  border-radius: var(--r-sm);
  font: 600 var(--t-caption)/1.6 var(--sans);
  color: var(--btn-primary-ink);
  cursor: pointer;
}

/* ==========================================================================
   Watches
   ==========================================================================

   The builder and the list on the Alerts tab. Two layout decisions:

   The builder is one row on a wide screen and stacked on a phone, because the
   four fields read as a sentence — "when NVDA rises above 200" — and breaking
   that sentence across four lines loses the reading of it.

   Each watch row is a grid with the condition on the left, the state chip next,
   and the evidence taking whatever is left. The evidence is the part that
   varies from nothing to a full sentence, so it gets the flexible track: giving
   it a fixed width would either truncate the useful ones or leave a hole. */

/* No margin of its own. It was the only separation these blocks had when they
   were bare `.wv` sections; now they are panels and the panel's own
   `margin-bottom` spaces them, so this override made the gap under "Your
   watches" 27.6px against 23px under the block above it. */

.wd-new {
  display: flex;
  flex-wrap: wrap;
  align-items: flex-end;
  gap: var(--space-3);
  padding: var(--space-4);
  margin-bottom: var(--space-2);
  background: var(--raised);
  border: 1px solid var(--border);
  border-radius: var(--r-md);
}

.wd-param { display: flex; flex-direction: column; gap: var(--space-1); }

.wd-param > span {
  font: 500 var(--t-caption)/1 var(--sans);
  color: var(--ink-2);
}

/* The builder's own control styling rather than `.settings-select`.
 *
 * That class carries `flex: 0 1 340px`, written for the Settings page's *row*
 * layout where 340px is a width. `.wd-param` is a column, so flex-basis became
 * the main-axis size and every input rendered 340px TALL. Same class, same
 * declaration, different axis — which is why it is not shared. */
.wd-param input, .wd-param select {
  min-width: 104px;
  min-height: 38px;
  padding: var(--space-2) var(--space-3);
  background: var(--surface);
  border: 1px solid var(--border-strong);
  border-radius: var(--r-md);
  color: var(--ink);
  font: 400 var(--t-base)/1.2 var(--sans);
}

.wd-param input:focus, .wd-param select:focus {
  outline: 2px solid var(--focus);
  outline-offset: 1px;
}

.wd-param-wide select { min-width: 208px; }

.wd-noparam {
  flex: 1 1 200px;
  align-self: center;
  font: 400 var(--t-caption)/1.45 var(--sans);
  color: var(--ink-muted);
}

.wd-why {
  margin: 0 0 var(--space-5);
  font: 400 var(--t-caption)/1.5 var(--sans);
  color: var(--ink-muted);
}

.wd-sym { margin-bottom: var(--space-5); }

.wd-sym-head {
  display: flex;
  align-items: center;
  gap: var(--space-3);
  padding-bottom: var(--space-2);
  border-bottom: 1px solid var(--border);
}

/* No mono token exists in this stylesheet, and inventing `--mono` with a
   fallback would have referenced something that is not there. The chosen faces
   carry proper tabular figures (see the font comment in index.html), which is
   what a price actually needs. */
.wd-price {
  font: 600 var(--t-body)/1 var(--sans);
  font-variant-numeric: tabular-nums;
  color: var(--ink);
}

.wd-when {
  flex: 1;
  font: 400 var(--t-caption)/1 var(--sans);
  color: var(--ink-muted);
}

.wd-check { padding: var(--space-1) var(--space-3); font-size: var(--t-caption); }

.wd-err {
  margin: var(--space-2) 0 0;
  font: 400 var(--t-caption)/1.45 var(--sans);
  color: var(--neg);
}

.wd-list { margin: 0; padding: 0; list-style: none; }

.wd-row {
  display: grid;
  grid-template-columns: minmax(140px, auto) auto 1fr auto auto;
  align-items: center;
  gap: var(--space-3);
  padding: var(--space-2) 0;
  border-bottom: 1px solid var(--border);
}

.wd-row:last-child { border-bottom: 0; }

/* A paused watch stays legible. Dropping it to 0.4 made it look broken rather
   than switched off, and the row still has to be readable to be un-paused. */
.wd-row.is-off .wd-cond, .wd-row.is-off .wd-ev { opacity: 0.55; }

.wd-cond { font: 500 var(--t-body)/1.35 var(--sans); color: var(--ink); }

.wd-ev {
  font: 400 var(--t-caption)/1.45 var(--sans);
  color: var(--ink-muted);
}

.wd-chip {
  padding: 2px var(--space-2);
  border-radius: var(--r-pill);
  font: 600 var(--t-micro)/1.6 var(--sans);
  text-transform: uppercase;
  letter-spacing: 0.06em;
  white-space: nowrap;
}

/* Four states, not two. "Met" and "still met" are different facts and
   collapsing them is what makes a notification feature report the same news on
   every check until somebody switches it off. */
.wd-chip.hit { background: color-mix(in srgb, var(--warn) 24%, transparent); color: var(--ink); }
.wd-chip.still { background: var(--raised); color: var(--ink-2); }
.wd-chip.no { background: var(--raised); color: var(--ink-muted); }
.wd-chip.was { background: var(--raised); color: var(--ink-muted); }
.wd-chip.idle { background: transparent; color: var(--ink-muted); }

.wd-btn {
  padding: var(--space-1) var(--space-2);
  background: none;
  border: 1px solid var(--border-strong);
  border-radius: var(--r-xs);
  font: 500 var(--t-micro)/1.4 var(--sans);
  color: var(--ink-2);
  cursor: pointer;
}

.wd-btn:hover { color: var(--ink); border-color: var(--ink-muted); }

.wd-foot {
  margin: var(--space-4) 0 0;
  padding-top: var(--space-3);
  border-top: 1px solid var(--border);
  font: 400 var(--t-caption)/1.5 var(--sans);
  color: var(--ink-muted);
}

@media (max-width: 719px) {
  .wd-new { flex-direction: column; align-items: stretch; }
  .wd-param input, .wd-param select, .wd-param-wide select { min-width: 0; width: 100%; }

  /* The chip and the buttons keep their own row so the condition is never
     squeezed to two characters. */
  .wd-row {
    grid-template-columns: 1fr auto auto;
    grid-template-areas: "cond chip x" "ev ev ev" "btn btn btn";
    row-gap: var(--space-1);
  }

  .wd-cond { grid-area: cond; }
  .wd-chip { grid-area: chip; }
  .wd-ev { grid-area: ev; }
  .wd-btn { grid-area: btn; justify-self: start; }
  .wd-row .wl-x { grid-area: x; }
}

/* A hidden overlay keeps its legend row.
 *
 * Struck through and dimmed rather than removed: the row is how you get it
 * back, and the value is still worth reading with the line off the chart. Not
 * dropped below 0.55 opacity — at 0.4 it read as broken rather than switched
 * off, and it still has to be legible to be clicked. */
.ws-leg-row.is-hidden .ws-leg-name { text-decoration: line-through; opacity: 0.6; }
.ws-leg-row.is-hidden .ws-leg-val { opacity: 0.55; }
.ws-leg-btn.is-off { opacity: 0.6; }

/* Why there is no plot, in the space the plot would have taken.
 *
 * An empty chart with axes drawn on it reads as "this symbol does not trade",
 * which is a different claim from "the bars have not arrived yet". */
.ws-chart-note {
  display: flex;
  align-items: center;
  justify-content: center;
  min-height: 360px;
  padding: var(--space-6);
  font: 400 var(--t-body)/1.5 var(--sans);
  color: var(--ink-muted);
  text-align: center;
}

/* A pill that reports rather than offers.
 *
 * The interval pills are replaced by one of these on an intraday range, where
 * Daily and Weekly mean nothing: the bars come from the intraday endpoint at
 * its own resolution. Disabled rather than removed so the toolbar does not
 * change width when a range pill is pressed. */
.pill:disabled { cursor: default; opacity: 0.85; }

/* ==========================================================================
   Price head
   ==========================================================================

   The one number the page is about, sized like it. `--t-d1` is the token for
   exactly that job ("the one number a panel is about") and this is the first
   place in the app where it applies to a whole page rather than a panel.

   Two columns on a desktop: the price on the left, the day's facts on the
   right. One column on a phone, price first — the brief's mobile order starts
   with the price and this is what makes that true. */

/* Robinhood-style hero: the identity sits small on top, the price is the one
   big thing, the change carries the day's single accent directly beneath it,
   and the dense reference facts drop below a divider so they support the number
   rather than crowding it. A column, not a two-up row: the facts read as a
   footer to the price rather than as a second panel competing with it. Still
   `display: flex` -- .px-ext-note spans the header with flex-basis:100%. */
.px-head {
  display: flex;
  flex-direction: column;
  gap: var(--space-5);
  margin-bottom: var(--space-5);
  padding: var(--space-6);
  background: var(--surface);
  border: 1px solid var(--border);
  border-radius: var(--r-lg);
}

.px-main { display: flex; flex-direction: column; gap: var(--space-3); min-width: 0; }

.px-id { display: flex; align-items: baseline; gap: var(--space-3); flex-wrap: wrap; }

.px-sym {
  margin: 0;
  font: 600 var(--t-heading)/1 var(--display);
  letter-spacing: 0.01em;
  color: var(--ink);
}

.px-name {
  font: 400 var(--t-body)/1.3 var(--sans);
  color: var(--ink-2);
  /* Long company names truncate rather than pushing the exchange onto its own
     line and dragging the price down with it. */
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
  max-width: 30ch;
}

.px-exch {
  font: 600 var(--t-micro)/1 var(--sans);
  text-transform: uppercase;
  letter-spacing: 0.08em;
  color: var(--ink-muted);
}

/* Price stacked over change, tight, so the two read as one lockup. */
.px-now { display: flex; flex-direction: column; align-items: flex-start; gap: var(--space-1); }

.px-last {
  /* The one big thing on the page. A step above --t-d1, the top of the type
     scale, because this is the number the whole view exists to state; the
     multiple keeps it on --ui-scale so it still tracks the reader's size
     preference. */
  font: 650 calc(var(--t-d1) * 1.28)/0.95 var(--sans);
  font-variant-numeric: tabular-nums;
  letter-spacing: -0.03em;
  /* The price itself is not coloured by direction. The *change* is. A price
     painted red reads as "this number is wrong" rather than "it is down
     today", and it is the same number whichever way the day went. */
  color: var(--ink);
}

/* The day's single accent. Colour plus a directional triangle, weight 700, so
   one glance reads the direction before the digits do -- the Robinhood cue,
   drawn from the same up/down pair every percentage in the app uses. The
   triangle is a CSS marker, not markup, so it stays out of the read-aloud text
   and the figure the tests assert on. */
.px-chg {
  display: flex;
  align-items: center;
  gap: var(--space-2);
  font: 700 var(--t-d3)/1 var(--sans);
  font-variant-numeric: tabular-nums;
}
.px-chg.up::before,
.px-chg.down::before { font-size: 0.62em; line-height: 1; }
.px-chg.up::before { content: "\25B2"; }
.px-chg.down::before { content: "\25BC"; }

.px-chg.up, .px-chg-pct.up { color: var(--pos); }
.px-chg.down, .px-chg-pct.down { color: var(--neg); }
.px-chg.flat, .px-last.flat { color: var(--ink-2); }
.px-chg-pct { font-size: var(--t-lead); font-weight: 600; opacity: 0.95; }

/* The extended-hours print. Its own row with a rule above it, because it is a
   different session and a different claim from the number above. */
.px-ext {
  display: flex;
  align-items: baseline;
  gap: var(--space-2);
  padding-top: var(--space-2);
  border-top: 1px solid var(--border);
  font: 400 var(--t-body)/1.3 var(--sans);
  font-variant-numeric: tabular-nums;
  color: var(--ink-2);
}

.px-ext-kind {
  font: 600 var(--t-micro)/1.6 var(--sans);
  text-transform: uppercase;
  letter-spacing: 0.07em;
  color: var(--ink-muted);
}

.px-ext-px { font-weight: 600; color: var(--ink); }
.px-ext.up .px-ext-pct { color: var(--pos); }
.px-ext.down .px-ext-pct { color: var(--neg); }

.px-facts {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(132px, max-content));
  gap: var(--space-3) var(--space-6);
  /* A hairline sets the reference facts apart from the hero above them, so the
     price stands alone and these read as its supporting detail. */
  padding-top: var(--space-4);
  border-top: 1px solid var(--border);
}

.px-fact { display: flex; flex-direction: column; gap: var(--space-1); }

.px-fact-label {
  font: 500 var(--t-micro)/1 var(--sans);
  letter-spacing: 0.01em;
  color: var(--ink-muted);
}

.px-fact-value {
  font: 500 var(--t-body)/1.3 var(--sans);
  font-variant-numeric: tabular-nums;
  color: var(--ink);
}

.px-fact-note { color: var(--ink-muted); font-weight: 400; }

/* The extended-hours caveat, inherited from the Quote panel that used to
   carry it. Spans the whole header rather than sitting beside the facts,
   because it qualifies every figure in them and not one of them.

   `flex-basis: 100%`, not `grid-column`: .px-head is a wrapping flex row, and
   a grid property there is ignored in silence -- the note would have sat in
   the row squeezing the price instead of below it. */
.px-ext-note {
  flex: 1 0 100%;
  margin: var(--space-3) 0 0;
  font-size: var(--t-caption);
  line-height: 1.55;
  color: var(--ink-muted);
  max-width: 72ch;
}

.px-range { grid-column: span 2; min-width: 200px; }

.px-range-bar {
  display: grid;
  grid-template-columns: auto 1fr auto;
  align-items: center;
  gap: var(--space-2);
  font: 500 var(--t-caption)/1 var(--sans);
  font-variant-numeric: tabular-nums;
  color: var(--ink-2);
}

.px-range-track {
  position: relative;
  height: 4px;
  border-radius: var(--r-pill);
  background: var(--surface-2);
  border: 1px solid var(--border);
}

/* Translated by half its own width so the marker is centred on the value
   rather than starting at it: at 100% it would otherwise sit entirely past the
   right end of the track. */
.px-range-dot {
  position: absolute;
  top: 50%;
  width: 8px;
  height: 8px;
  border-radius: var(--r-pill);
  background: var(--ink);
  transform: translate(-50%, -50%);
}

@media (max-width: 719px) {
  .px-head {
    flex-direction: column;
    gap: var(--space-4);
    padding: var(--space-4);
  }

  /* Still the biggest thing on the screen, but 44px of tabular figures plus a
     change is wider than 375px. */
  .px-last { font-size: var(--t-d2); }
  .px-chg { font-size: var(--t-lead); }
  .px-facts { grid-template-columns: 1fr 1fr; gap: var(--space-3) var(--space-4); }
  .px-range { grid-column: span 2; }
  .px-name { max-width: 100%; }
}

/* ==========================================================================
   Compare: horizon bars
   ==========================================================================

   Three groups, one per horizon, each a small ranked bar chart. The grid keeps
   the tracks aligned down the column whatever the ticker lengths are, so the
   bars can be compared by eye without a shared axis line to follow. */

.cb-groups {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(240px, 1fr));
  gap: var(--space-5);
  margin-top: var(--space-4);
}

.cb-group {
  display: flex;
  flex-direction: column;
  gap: var(--space-2);
  padding: var(--space-4);
  background: var(--raised);
  border: 1px solid var(--border);
  border-radius: var(--r-md);
}

.cb-head {
  display: flex;
  align-items: baseline;
  flex-wrap: wrap;
  gap: var(--space-2);
  margin-bottom: var(--space-1);
}

.cb-name { font: 600 var(--t-body)/1.2 var(--sans); color: var(--ink); }

.cb-hz {
  flex: 1;
  font: 400 var(--t-caption)/1.2 var(--sans);
  color: var(--ink-muted);
}

/* The gap, which is the whole point of drawing bars rather than printing
   ranks. Highlighted only when it clears the same threshold compare.take uses
   for `decisive`, so the two cannot disagree. */
.cb-gap {
  font: 500 var(--t-micro)/1.4 var(--sans);
  letter-spacing: 0.01em;
  color: var(--ink-muted);
}

.cb-gap.is-clear { color: var(--ink-2); font-weight: 600; }

.cb-row {
  display: grid;
  grid-template-columns: 4.5rem 1fr 2.2rem;
  align-items: center;
  gap: var(--space-2);
}

.cb-tkr {
  padding: 0;
  background: none;
  border: 0;
  font: 600 var(--t-caption)/1.2 var(--sans);
  color: var(--ink-2);
  text-align: left;
  cursor: pointer;
  overflow: hidden;
  text-overflow: ellipsis;
}

.cb-row.is-lead .cb-tkr { color: var(--ink); }
.cb-tkr:hover { color: var(--s1); text-decoration: underline; }

.cb-track {
  height: 10px;
  border-radius: var(--r-xs);
  background: var(--surface-2);
  border: 1px solid var(--border);
  overflow: hidden;
}

.cb-fill {
  display: block;
  height: 100%;
  border-radius: var(--r-xs);
  /* Not the directional pair. A composite score is not a gain or a loss, and
     painting the leader green would claim a direction the number does not
     carry — every one of these is positive by construction. */
  background: var(--baseline);
}

.cb-row.is-lead .cb-fill { background: var(--brand); }

.cb-score {
  font: 600 var(--t-caption)/1.2 var(--sans);
  font-variant-numeric: tabular-nums;
  color: var(--ink-2);
  text-align: right;
}

.cb-row.is-lead .cb-score { color: var(--ink); }

/* Watchlist filters, on their own row below the sorts.
 *
 * A sort answers "in what order" and a filter answers "which of these at all".
 * They were one row of pills where pressing one silently replaced the other. */
.wv-filters {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: var(--space-2);
  margin-bottom: var(--space-4);
}

.wv-filter-label {
  font: 500 var(--t-micro)/1 var(--sans);
  letter-spacing: 0.01em;
  color: var(--ink-muted);
}

.wv-shown { color: var(--ink-2); }

/* The indicator panes' own thin header.
 *
 * They live inside the chart panel on purpose, so they have no <h2> to hang an
 * Ask Pulse on. This is that hook and nothing else: no border, no background,
 * because the panes below it are already visually grouped. */
.ind-ask {
  display: flex;
  align-items: center;
  gap: var(--space-2);
  margin: var(--space-4) 0 var(--space-1);
}

/* ---------------------------------------------------- security workspace ---
   The header that makes seven pages read as one company. Sticky, because the
   whole point is knowing what you are looking at while you scroll a long
   facet — losing the symbol on the Analysis tab is how the pages felt
   unrelated in the first place. */
.sec-head {
  position: sticky;
  /* Below the top bar, whose height is published as --topbar-h by
     trackTopbarHeight. At top: 0 this rendered correctly and was painted
     underneath a z-index: 50 bar, which looks exactly like it never stuck.

     z-index above .sec-index's 20, and the header is inserted before it, so the
     two sticky bars stack in reading order: which ticker you are on, then which
     section. Left at 5 the index painted over the header. */
  top: var(--topbar-h, 106px);
  z-index: 21;
  display: flex;
  flex-direction: column;
  gap: var(--space-2);
  padding: var(--space-3) 0 0;
  margin-bottom: var(--space-3);
  /* --plane, which is what header.topbar uses. This said var(--bg), which is
     not a token this stylesheet defines and has no fallback, so it resolved to
     nothing: the sticky header was transparent and the panels scrolled through
     it. Visible for the whole time the workspace existed. */
  background: var(--plane);
  border-bottom: 1px solid var(--border);
}
.sec-id {
  display: flex;
  align-items: baseline;
  flex-wrap: wrap;
  gap: var(--space-2) var(--space-3);
}
/* The number first, then the name.
 *
 * Measured on AAPL at 1440x950 before this: the symbol was 27.6px/700, the
 * price 14.95px/600 and the change 14.95px/400 -- so the price was the same
 * size as the exchange and the sector beside it, the change was the lightest
 * thing in the row, and the loudest was the four letters the reader had just
 * typed into the box above.
 *
 * The price takes the display size and the symbol steps down to a heading.
 * The symbol is identity and the reader already knows it; the price is the
 * thing they opened the page for. The change goes up with it and keeps its
 * weight, because a direction read at a glance is half of what that line is
 * for, and it sat below the company name in the old order. */
.sec-sym { font-size: var(--t-heading); font-weight: 700; letter-spacing: 0.01em; }
.sec-name { color: var(--ink-2); font-size: var(--t-body); }
.sec-px {
  font-variant-numeric: tabular-nums;
  font-size: var(--t-d3);
  font-weight: 600;
  letter-spacing: -0.01em;
}
.sec-chg { font-variant-numeric: tabular-nums; font-size: var(--t-base); font-weight: 600; }
.sec-chg.up { color: var(--pos); }
.sec-chg.down { color: var(--neg); }
.sec-chg.flat { color: var(--ink-muted); }
.sec-meta { color: var(--ink-muted); font-size: var(--t-small); margin-left: auto; }
/* `.sec-id` aligns on baseline, which suits text beside text and gives a
   bordered chip nowhere sensible to sit beside a display-size price. Centred
   on its own line instead. */
.sec-id .stage-chip { align-self: center; }
/* On Charting the strip is the whole header, so it does not need the padding
   that separates it from a price line that is not there. */
.sec-head.compact { padding-top: var(--space-1); margin-bottom: var(--space-2); }

/* Scrolls sideways rather than wrapping to a second line: seven tabs plus a
   long company name wrapped on a laptop and pushed the page down by a row. */
.sec-tabs {
  display: flex;
  gap: var(--space-1);
  overflow-x: auto;
  scrollbar-width: none;
}
.sec-tabs::-webkit-scrollbar { display: none; }
.sec-tab {
  flex: none;
  padding: var(--space-2) var(--space-3);
  border: 0;
  border-bottom: 2px solid transparent;
  background: none;
  color: var(--ink-muted);
  font-size: var(--t-small);
  font-weight: 600;
  cursor: pointer;
  transition: color 0.12s ease, border-color 0.12s ease;
}
.sec-tab:hover { color: var(--ink); }
.sec-tab.on { color: var(--ink); border-bottom-color: var(--btn-primary); }

/* The overview's facet row. A grid rather than flex so the cards line up in
   columns whatever the length of the figure inside them. */
.ov-cards {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(150px, 1fr));
  gap: var(--space-2);
  margin-top: var(--space-3);
}
.ov-card {
  display: flex;
  flex-direction: column;
  gap: var(--space-0);
  padding: var(--space-3);
  border: 1px solid var(--border);
  border-radius: var(--r-md);
  background: var(--surface-2);
  text-align: left;
  cursor: pointer;
  transition: border-color 0.12s ease, background 0.12s ease;
}
.ov-card:hover { border-color: var(--border-strong); background: var(--surface); }
.ov-card-lab {
  font-size: var(--t-micro);
  letter-spacing: 0.01em;
  color: var(--ink-muted);
}
.ov-card-val { font-size: var(--t-d3); font-weight: 700; font-variant-numeric: tabular-nums; }
/* A missing reading is said in words at body weight, not shown as a blank or a
   dash: "not available" is information, an empty box is a rendering fault. */
.ov-card-val.none { font-size: var(--t-small); font-weight: 500; color: var(--ink-muted); }
.ov-card-note { font-size: var(--t-small); color: var(--ink-2); }
.ov-more { margin-top: var(--space-3); }

/* News rows. Lifted out of renderSwing, where they carried inline styles. */
.nw-row { padding: var(--space-3) 0; border-bottom: 1px solid var(--grid); }
.nw-row:last-of-type { border-bottom: 0; }
.nw-line {
  display: flex;
  gap: var(--space-3);
  align-items: baseline;
  flex-wrap: wrap;
}
.nw-title {
  color: var(--ink);
  text-decoration: none;
  flex: 1;
  min-width: 240px;
  transition: color 0.12s ease;
}
/* Focus gets the same treatment as hover: this is a list of links and a
   keyboard reader needs to see which one they are on. */
.nw-title:hover,
.nw-title:focus-visible { color: var(--ink-2); text-decoration: underline; }
.nw-sum { color: var(--ink-2); font-size: var(--t-small); margin-top: var(--space-1); }
.nw-tags { display: flex; flex-wrap: wrap; gap: var(--space-1); margin-top: var(--space-2); }

/* ---- news tiers -----------------------------------------------------------
   A tier says how much a headline matters, which is not a direction, so these
   deliberately do not touch --pos/--neg. That pair means up and down in every
   percentage, candle and volume bar in the app, and painting "Breaking" green
   would say a fresh headline is good news before anybody has read it. The VIX
   tile already shipped that mistake in the other direction.

   --warn is the one accent that means attention without meaning direction.
   Below it the tiers separate by ink weight rather than hue, so the ranking
   reads as one scale instead of four unrelated labels. */
.nw-tier {
  flex: 0 0 auto;
  font-size: var(--t-micro);
  font-weight: 700;
  letter-spacing: 0.06em;
  text-transform: uppercase;
  padding: var(--space-0) var(--space-2);
  border-radius: var(--r-sm);
  border: 1px solid var(--grid);
  color: var(--ink-2);
  white-space: nowrap;
}
.nw-tier.t-breaking {
  color: var(--warn);
  border-color: var(--warn);
}
.nw-tier.t-major { color: var(--ink); border-color: var(--ink-2); }
.nw-tier.t-notable { color: var(--ink-2); }
.nw-tier.t-background { color: var(--ink-muted); }

/* ---- home story slot ----
   Three links above the cross-asset list. Compact by construction: this page's
   one recurring defect is height, so a story flows as one wrapped line with the
   tier as a kicker, rather than as a card.

   Deliberately NOT a flex row. That was the first version and it cost 121px per
   story in this column: the block lives in a 401px grid cell, the title became
   a full-width flex item, and the badge was pushed onto its own line. */
.cc-stories { list-style: none; margin: var(--space-3) 0 0; padding: 0; }
.cc-stories li {
  padding: var(--space-2) 0;
  border-top: 1px solid var(--grid);
  font-size: var(--t-small);
  line-height: 1.45;
}
.cc-stories .nw-tier { margin-right: var(--space-1); }
.cc-stories .subnote { margin-left: var(--space-1); }
a.cc-story-t {
  color: var(--ink);
  text-decoration: none;
  /* Colour transitions, the underline does not: the same pairing .nw-title
     uses. A hover that only underlines snaps, and animating the decoration
     would be animating a layout property once the line wraps. */
  transition: color var(--dur-ui) var(--ease-ui);
}
a.cc-story-t:hover,
a.cc-story-t:focus-visible { color: var(--ink-2); text-decoration: underline; }

.nw-count {
  font-size: var(--t-micro);
  font-weight: 600;
  color: var(--ink-muted);
  border: 1px solid var(--grid);
  border-radius: var(--r-sm);
  padding: 0 var(--space-1);
  vertical-align: middle;
}
.nw-legend ul { margin: var(--space-2) 0 0; padding-left: 0; list-style: none; }
.nw-legend li {
  display: flex;
  gap: var(--space-2);
  align-items: baseline;
  margin-bottom: var(--space-1);
  font-size: var(--t-small);
  color: var(--ink-2);
}
.nw-legend p { font-size: var(--t-small); color: var(--ink-2); margin: var(--space-2) 0 0; }
.nw-limit { color: var(--ink-muted); }

/* ------------------------------------------------------ named watchlists ---
   Chips rather than a select: the count is small and bounded, and seeing that
   you have several lists is most of the point of having them. */
.wl-bar {
  display: flex;
  align-items: center;
  gap: var(--space-3);
  flex-wrap: wrap;
  margin-bottom: var(--space-3);
  padding-bottom: var(--space-2);
  border-bottom: 1px solid var(--border);
}
.wl-chips { display: flex; gap: var(--space-1); flex-wrap: wrap; }
.wl-chip {
  display: inline-flex;
  align-items: center;
  gap: var(--space-1);
  padding: var(--space-1) var(--space-3);
  border: 1px solid var(--border);
  border-radius: var(--r-sm);
  background: var(--surface-2);
  color: var(--ink-2);
  font-size: var(--t-small);
  font-weight: 600;
  cursor: pointer;
  transition: color 0.12s ease, border-color 0.12s ease, background 0.12s ease;
}
.wl-chip:hover { color: var(--ink); border-color: var(--border-strong); }
.wl-chip.on { color: var(--ink); border-color: var(--btn-primary); background: var(--surface); }
.wl-chip.is-new { color: var(--ink-muted); border-style: dashed; }
.wl-chip-n {
  font-variant-numeric: tabular-nums;
  font-size: var(--t-micro);
  color: var(--ink-muted);
}
.wl-bar-tools { display: flex; gap: var(--space-1); margin-left: auto; }
.wl-tool {
  padding: var(--space-1) var(--space-2);
  border: 1px solid var(--border);
  border-radius: var(--r-sm);
  background: none;
  color: var(--ink-muted);
  font-size: var(--t-small);
  cursor: pointer;
  transition: color 0.12s ease, border-color 0.12s ease;
}
.wl-tool:hover:not(:disabled) { color: var(--ink); border-color: var(--border-strong); }
.wl-tool:disabled { opacity: 0.4; cursor: default; }

.wv-search { margin: var(--space-3) 0; display: flex; align-items: center; gap: var(--space-2); }
.wv-search input {
  flex: 1 1 260px;
  max-width: 340px;
  padding: var(--space-2) var(--space-3);
  border: 1px solid var(--border);
  border-radius: var(--r-sm);
  background: var(--surface-2);
  color: var(--ink);
  font-size: var(--t-small);
}

/* Reorder handles. Shown only under "My order" with nothing filtering, so a
   move always lands where the reader can see it. */
.wl-move { display: inline-flex; flex-direction: column; gap: 1px; margin-right: var(--space-2); }
.wl-move-btn {
  width: 18px;
  height: 14px;
  padding: 0;
  border: 1px solid var(--border);
  border-radius: 3px;
  background: none;
  color: var(--ink-muted);
  font-size: 9px;
  line-height: 1;
  cursor: pointer;
  transition: color 0.12s ease, border-color 0.12s ease;
}
.wl-move-btn:hover:not(:disabled) { color: var(--ink); border-color: var(--border-strong); }
.wl-move-btn:disabled { opacity: 0.3; cursor: default; }

/* ------------------------------------------------------------- screener ---
   The builder. Rows of field + bounds, which is a form, so it gets form
   spacing rather than the panel grid. */
.scan-modes { display: flex; gap: var(--space-2); margin-bottom: var(--space-3); }

/* Find stocks: a request in words, its reading and its matches. The box is a
   plain field in the page's own surface, the two answers to it side by side. */
.sc-ask-form {
  display: flex;
  align-items: flex-start;
  gap: var(--space-2);
  flex-wrap: wrap;
  margin: var(--space-2) 0;
}
.sc-ask-text {
  flex: 1 1 360px;
  min-width: 0;
  min-height: 52px;
  resize: vertical;
  font: inherit;
  padding: var(--space-2) var(--space-3);
  border: 1px solid var(--border);
  border-radius: var(--r-md);
  background: var(--surface-2);
  color: var(--ink);
}
.sc-ask-text:focus-visible { outline: 2px solid var(--focus); outline-offset: 1px; }
.sc-ask-actions { display: flex; flex-direction: column; gap: var(--space-1); }
.sc-ask-examples, .sc-ask-read {
  display: flex;
  align-items: center;
  flex-wrap: wrap;
  gap: var(--space-1);
  margin: var(--space-2) 0;
}
.sc-ask-chip {
  padding: var(--space-0) var(--space-2);
  border: 1px solid var(--border);
  border-radius: var(--r-sm);
  font-size: var(--t-caption);
  color: var(--ink-2);
}
.sc-ask-missing { margin: var(--space-1) 0; font-size: var(--t-caption); color: var(--ink-muted); }
.sc-filters { display: flex; flex-direction: column; gap: var(--space-2); margin: var(--space-3) 0; }
.sc-filter {
  display: flex;
  align-items: center;
  gap: var(--space-2);
  flex-wrap: wrap;
  padding: var(--space-2);
  border: 1px solid var(--border);
  border-radius: var(--r-sm);
  background: var(--surface-2);
}
/* flex-basis is width in a row: this class is only ever used inside .sc-filter
   and .sc-sort, both of them rows. See CLAUDE.md on .settings-select, which was
   340px tall inside a column. */
.sc-field { flex: 1 1 220px; max-width: 320px; }
.sc-bound {
  display: inline-flex;
  align-items: center;
  gap: var(--space-1);
  font-size: var(--t-small);
  color: var(--ink-muted);
}
.sc-bound input { width: 92px; }
.sc-filter select, .sc-filter input, .sc-sort select {
  padding: var(--space-1) var(--space-2);
  border: 1px solid var(--border);
  border-radius: var(--r-sm);
  background: var(--surface);
  color: var(--ink);
  font-size: var(--t-small);
}
.sc-unit { font-size: var(--t-small); color: var(--ink-muted); min-width: 12px; }
.sc-drop {
  margin-left: auto;
  width: 24px;
  height: 24px;
  border: 1px solid var(--border);
  border-radius: var(--r-sm);
  background: none;
  color: var(--ink-muted);
  cursor: pointer;
  transition: color 0.12s ease, border-color 0.12s ease;
}
.sc-drop:hover { color: var(--ink); border-color: var(--border-strong); }
/* Full width so it sits under the row rather than squeezing the controls. */
.sc-help { flex: 1 0 100%; margin: 0; font-size: var(--t-caption); color: var(--ink-muted); }
.sc-actions { display: flex; gap: var(--space-2); margin-bottom: var(--space-4); }
.sc-states { display: flex; flex-wrap: wrap; gap: var(--space-3); margin: var(--space-2) 0 var(--space-4); }
.sc-state { display: inline-flex; align-items: center; gap: var(--space-2); font-size: var(--t-small); }
.sc-sort { display: flex; align-items: center; gap: var(--space-3); flex-wrap: wrap; }

/* The funnel. Reads as a sentence per line, so a bound that emptied the screen
   is visible without counting. */
.sc-funnel {
  list-style: none;
  margin: 0 0 var(--space-3);
  padding: 0;
  display: flex;
  flex-direction: column;
  gap: var(--space-0);
}
.sc-funnel li {
  display: flex;
  justify-content: space-between;
  gap: var(--space-3);
  padding: var(--space-1) 0;
  border-bottom: 1px solid var(--grid);
  font-size: var(--t-small);
  color: var(--ink-2);
}
.sc-fn-n { font-variant-numeric: tabular-nums; color: var(--ink-muted); }

/* -------------------------------------------------------- alerts centre ---
   Grouped by urgency, which was on every alert and shown on none of them. */
.al-group { margin-top: var(--space-4); }
.al-group-h {
  display: flex;
  align-items: baseline;
  gap: var(--space-2);
  margin: 0 0 var(--space-1);
  font-size: var(--t-micro);
  letter-spacing: 0.01em;
  color: var(--ink-muted);
}
.al-group-n { font-variant-numeric: tabular-nums; }
/* The alert's own body, under its title. The record has carried this since the
   table was created and the row rendered neither. */
.al-sub { display: block; color: var(--ink-2); font-size: var(--t-small); }
.al-tags { display: flex; align-items: center; gap: var(--space-2); flex-wrap: wrap; margin-top: var(--space-1); }
.al-why {
  padding: 0;
  border: 0;
  background: none;
  color: var(--ink-muted);
  font-size: var(--t-caption);
  text-decoration: underline;
  cursor: pointer;
  transition: color 0.12s ease;
}
.al-why:hover { color: var(--ink); }
.al-explain {
  margin-top: var(--space-2);
  padding: var(--space-2) var(--space-3);
  border-left: 2px solid var(--border-strong);
  background: var(--surface-2);
  border-radius: 0 var(--r-sm) var(--r-sm) 0;
}
.al-explain p { margin: 0 0 var(--space-2); font-size: var(--t-small); color: var(--ink-2); }
.al-explain p:last-child { margin-bottom: 0; }
/* The readings the rule fired on. A grid so the labels line up down the left
   whatever the length of the value beside them. */
.al-readings {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(140px, 1fr));
  gap: var(--space-1) var(--space-3);
  margin: 0;
}
.al-readings div { display: flex; justify-content: space-between; gap: var(--space-2); }
.al-readings dt { font-size: var(--t-caption); color: var(--ink-muted); text-transform: capitalize; }
.al-readings dd { margin: 0; font-size: var(--t-caption); font-variant-numeric: tabular-nums; }
.al-blockers { margin: var(--space-2) 0 0; padding-left: var(--space-4); }
.al-blockers li { font-size: var(--t-small); color: var(--ink-2); margin-bottom: var(--space-1); }

/* ------------------------------------------------------- simple / pro mode ---
   The class carries the display and the [hidden] attribute carries the
   semantics. Both, because an author `display` beats the UA stylesheet's
   [hidden] { display: none } whatever the specificity — see CLAUDE.md on
   .set-pw, which rendered a password form open on every visit to Settings. */
.panel.is-advanced { display: none; }

/* A wrapper holding nothing but hidden panels must not keep its own margin.
 *
 * `applyUiMode` hides panels and never their wrappers, because it iterates
 * `.panel`. A hidden panel costs nothing: `display: none` generates no box, so
 * its margin does not apply. The wrapper is the problem. It stays
 * `display: grid` with every child hidden, which makes it a 0px-tall box still
 * carrying `.gap`'s 23px, and 23px of nothing reads as a gap somebody forgot.
 *
 * Measured on the Options tab at Financially Literate: four `grid c2 gap`
 * wrappers in that state, 92px of empty page. The three `panel gap is-advanced`
 * siblings in the same measurement contribute nothing, which is the difference
 * this rule is about.
 *
 * Requires at least one hidden panel rather than matching any childless grid.
 * A grid with no children at all is usually an async host waiting on a fetch,
 * and hiding one of those would fight the reveal animation that fills it. This
 * fires only when there is content and all of it is hidden.
 *
 * `:has()` rather than a second pass in `applyUiMode`, so it tracks the class
 * that function already maintains and there is no ordering to get wrong: a
 * panel toggled from the chooser updates the wrapper in the same frame, with
 * nothing to re-run. Unsupported `:has()` drops the rule and restores today's
 * behaviour rather than breaking the page. */
.grid:has(> .panel.is-advanced):not(:has(> *:not(.is-advanced))) { display: none; }
/* The note that stands where the hidden panels were. Quieter than a panel,
   because it is a statement about the page rather than a reading. */
.mode-note {
  border-style: dashed;
  background: none;
}
.mode-note p { margin: 0; }

/* ------------------------------------------------------------ panel chooser ---
   One dialog per view rather than a control on each of twenty-two headings: a
   heading already carries a collapse toggle and an Ask-Pulse button, and a third
   would make the thing you click to read a panel the smallest target on it. */
.pch-back {
  position: fixed;
  inset: 0;
  z-index: 60;
  display: flex;
  align-items: flex-start;
  justify-content: center;
  padding: var(--space-5) var(--space-4);
  background: var(--scrim);
  overflow-y: auto;
}
.pch {
  width: 100%;
  max-width: 460px;
  padding: var(--space-4);
  border: 1px solid var(--border-strong);
  border-radius: var(--r-lg);
  background: var(--surface);
  box-shadow: var(--elev-3);
}
.pch-head { display: flex; align-items: baseline; justify-content: space-between; gap: var(--space-3); }
.pch-head h3 { margin: 0; }
.pch-x {
  width: 28px;
  height: 28px;
  border: 1px solid var(--border);
  border-radius: var(--r-sm);
  background: none;
  color: var(--ink-muted);
  font-size: var(--t-lead);
  line-height: 1;
  cursor: pointer;
  transition: color 0.12s ease, border-color 0.12s ease;
}
.pch-x:hover { color: var(--ink); border-color: var(--border-strong); }
/* Capped and scrolled: Macro has fourteen panels and Analysis twenty-two, and a
   dialog taller than the window puts its reset button off screen. */
.pch-list {
  list-style: none;
  margin: var(--space-3) 0;
  padding: 0;
  max-height: 46vh;
  overflow-y: auto;
}
.pch-list li {
  display: flex;
  align-items: center;
  gap: var(--space-2);
  padding: var(--space-1) 0;
  border-bottom: 1px solid var(--grid);
}
.pch-list li:last-child { border-bottom: 0; }
.pch-list label {
  display: flex;
  align-items: center;
  gap: var(--space-2);
  flex: 1;
  font-size: var(--t-small);
  cursor: pointer;
}
.pch-tag {
  flex: none;
  padding: 0 var(--space-1);
  border: 1px solid var(--border);
  border-radius: var(--r-sm);
  font-size: var(--t-micro);
  text-transform: uppercase;
  letter-spacing: 0.04em;
  color: var(--ink-muted);
}
.pch-foot {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--space-3);
  padding-top: var(--space-3);
  border-top: 1px solid var(--border);
}

/* ---------------------------------------------------------- insider filings ---
   The market-wide Form 4 feed. Date and time stacked in one cell: the reader
   asked for both, and a filing at 09:31 and one at 15:58 are differently
   interesting on the same day. */
.ins-t { display: block; font-variant-numeric: tabular-nums; font-size: var(--t-small); }
.ins-clock { display: block; font-size: var(--t-caption); color: var(--ink-muted); }
.ins-role { display: block; font-size: var(--t-caption); color: var(--ink-muted); }
/* The transaction code, in words. Coloured only where the direction is
   unambiguous: a grant is neither a buy nor a sell, so it takes no colour, for
   the same reason VIX takes none on the pulse strip. */
.ins-code { font-size: var(--t-small); }
.ins-code.up { color: var(--pos); }
.ins-code.down { color: var(--neg); }
.ins-find { display: flex; align-items: center; gap: var(--space-2); margin: var(--space-3) 0; }
.ins-find input {
  flex: 0 1 260px;
  padding: var(--space-2) var(--space-3);
  border: 1px solid var(--border);
  border-radius: var(--r-sm);
  background: var(--surface-2);
  color: var(--ink);
  font-size: var(--t-small);
  text-transform: uppercase;
}
/* The placeholder is a sentence, not a symbol, so it must not be shouted. */
.ins-find input::placeholder { text-transform: none; }

/* Segment tables. Many narrow numeric columns, one wide label column, so the
   label is left alone and everything else is tabular and tight. */
.seg-table { font-variant-numeric: tabular-nums; }
.seg-table th.num, .seg-table td.num { white-space: nowrap; }
.seg-table td.name { white-space: nowrap; }
/* A negative segment is normal, not an error: Reality Labs has lost money every
   quarter it has been reported. It gets the directional red and nothing louder. */
.seg-table td.num.neg { color: var(--neg); }

/* Placed here, not up with the other phone overrides.
   `.hm-greet` is defined further down this file than that block was, and at
   equal specificity the later rule wins: the early copy of this media query
   set `display: none` and the greeting stayed on screen anyway. Measured, not
   reasoned about. Any phone override for a command-centre class belongs after
   the class it overrides. */
/* Phone: three things go, and all three are duplicates of something already
   on screen rather than information being taken away.
   Measured at 375x812 before this: the topbar is 217px of stacked rows, the
   session block 115, the strip 67, and then a brand row, a search form, eight
   ticker pills and a greeting between the strip and the first market section,
   which put "what matters now" at y=839 on an 812px screen. So the day's
   cross-asset moves were off the first screen on the page that exists to show
   them.

   * the brand row: the page header says OPTIC TERMINAL two inches above it
   * the greeting: the session block directly above says REGULAR SESSION and
     the local time, which is the same fact with more in it
   * the ticker pills: eight arbitrary symbols directly under a search box, on
     the one screen size where vertical space is the scarce resource

   The search form stays. It is the page's primary action and the brief this
   came from is explicit that search is the central primitive. */
@media (max-width: 719px) {
  /* The greeting stays hidden. The session block directly above it says
     AFTER HOURS and the local time, which is the same fact with more in it,
     and that has not changed. */
  .hm-greet { display: none; }
  /* And the fourth: the page's own search form.
     The topbar carries `#ticker-input` about 250px above it, with the same
     live results (measured: ten matches for "NV" on this viewport) and the
     command bar behind it. Two search boxes on one 375px screen is the
     repetition the redesign is meant to remove, and on a phone the form plus
     its full-width Analyze button is ~180px of it. Search is still the first
     thing on the screen, which is what the brief asks for; it is just the one
     that is always there rather than a second copy on one view. */
  /* The brand row, the pills and the search form are back, and the reason the
     first of them was hidden no longer holds: it was a duplicate of a topbar
     that said OPTIC TERMINAL, and the topbar is now the mark alone. A phone
     had no brand on it at all.
     The search form is a genuine duplicate of `#ticker-input` and is restored
     anyway, on the call that a page whose primary action is search should
     show it where the eye lands rather than only in the chrome. It costs
     about 180px with its Analyze button, which on a phone is the real price
     of this and is worth re-checking if the page starts feeling long. */
}

/* ===================================================== report a problem ====
 *
 * Fixed bottom right on every view. It replaced a link that opened a prefilled
 * GitHub issue, which meant a reader had to hold a GitHub account and sign in
 * to it before they could say a chart was wrong. Almost nobody will, and the
 * readers whose problems most need hearing are exactly the ones who will not.
 *
 * Small on purpose. This sits over the page permanently, so it is a pill and a
 * four-row textarea rather than a modal: a dialog that dims the terminal to ask
 * one question is louder than the question.
 */
.rp {
  position: fixed;
  right: var(--space-4);
  bottom: var(--space-4);
  z-index: 45;                    /* under the topbar (50) and Pulse (60) */
  display: flex;
  flex-direction: column;
  align-items: flex-end;
  gap: var(--space-2);
}

/* Out of Pulse's way when it is docked, rather than underneath it. `--chat-w`
   is published by installChatResize, so this tracks a drag in real time. */
body.chat-open .rp { right: calc(var(--chat-w, 382px) + var(--space-4)); }
/* Pulse covering the screen leaves nowhere to sit, so it stands down. */
body.chat-full .rp { display: none; }

.rp-btn {
  display: inline-flex;
  align-items: center;
  gap: var(--space-2);
  min-height: 44px;
  padding: var(--space-2) var(--space-4);
  border: 1px solid var(--border-strong);
  border-radius: var(--r-pill);
  background: var(--surface);
  color: var(--ink-2);
  font: 500 var(--t-body)/1 var(--sans);
  cursor: pointer;
  box-shadow: var(--elev-2);
  transition: color var(--dur-ui) var(--ease-ui),
              border-color var(--dur-ui) var(--ease-ui),
              background-color var(--dur-ui) var(--ease-ui);
}
.rp-btn:hover {
  color: var(--ink);
  border-color: var(--ink-muted);
  background: var(--bg-hover);
}
.rp-icon { width: 17px; height: 17px; flex: none; }

.rp-panel {
  width: 310px;
  padding: var(--space-3);
  border: 1px solid var(--border-strong);
  border-radius: var(--r-md);
  background: var(--surface);
  box-shadow: 0 16px 40px var(--shadow-ink);
}
/* An author `display` beats the UA stylesheet's [hidden], whatever the
   specificity, so the pair is mandatory. `.set-pw` shipped without it once and
   rendered its password form open on every visit to Settings. */
.rp-panel[hidden] { display: none; }

/* A Settings change crossfades from the old page to the new one (see
   applySettingInPlace), a little slower than the browser's default so a theme
   change reads as a fade rather than a flicker. */
::view-transition-old(root),
::view-transition-new(root) {
  animation-duration: 260ms;
  animation-timing-function: ease;
}

/* The title and its close button, one row: the x sits in the corner, as the
   sign-in dialog's does, and the title keeps its own line length. */
.rp-head {
  display: flex;
  align-items: flex-start;
  justify-content: space-between;
  gap: var(--space-2);
  margin-bottom: var(--space-2);
}
.rp-title {
  margin: 0;
  font: 600 var(--t-lead)/1.3 var(--sans);
  color: var(--ink);
}
/* The sign-in dialog's close button (.auth-close), in the row rather than
   positioned absolutely: the panel is not a positioned box. */
.rp-close,
.explain-close {
  flex: none;
  width: 28px;
  height: 28px;
  margin: calc(-1 * var(--space-1)) calc(-1 * var(--space-1)) 0 0;
  background: none;
  border: 0;
  border-radius: var(--r-sm);
  font: 400 20px/1 var(--sans);
  color: var(--ink-muted);
  cursor: pointer;
  transition: background var(--dur-ui) var(--ease-ui), color var(--dur-ui) var(--ease-ui);
}
.rp-close:hover,
.explain-close:hover { background: var(--surface-2); color: var(--ink); }
.rp-close:focus-visible,
.explain-close:focus-visible { outline: 2px solid var(--focus); outline-offset: 1px; }

/* A chip's free answer, beside the chip (see ASK_EXPLAINERS). In the page, so
   it scrolls with what it explains, and never wider than the window. */
.explain-pop {
  position: absolute;
  z-index: 80;
  width: min(460px, calc(100vw - 24px));
  /* A five-paragraph answer measured 664px tall at 400px wide, past the foot
     of a 785px window. Wider, and it scrolls inside past 70% of the window. */
  max-height: 70vh;
  overflow-y: auto;
  padding: var(--space-3) var(--space-4) var(--space-3);
  border: 1px solid var(--border-strong);
  border-radius: var(--r-md);
  background: var(--surface);
  box-shadow: var(--elev-2);
  color: var(--ink-2);
  font-size: var(--t-small);
  line-height: 1.55;
}
.explain-pop:focus { outline: none; }
.explain-head {
  display: flex;
  align-items: flex-start;
  justify-content: space-between;
  gap: var(--space-2);
  margin-bottom: var(--space-2);
}
.explain-title { font: 600 var(--t-body)/1.35 var(--sans); color: var(--ink); }
.explain-body p { margin: 0 0 var(--space-2); }
.explain-more {
  padding: 0;
  border: 0;
  background: none;
  color: var(--brand);
  font: 600 var(--t-small)/1.4 var(--sans);
  cursor: pointer;
  transition: color var(--dur-ui) var(--ease-ui);
}
.explain-more:hover { color: var(--ink); }
.explain-cost { margin-left: var(--space-2); font-size: var(--t-caption); color: var(--ink-muted); }
.rp-label {
  display: block;
  margin-bottom: var(--space-1);
  font-size: var(--t-caption);
  color: var(--ink-muted);
}
.rp-text {
  width: 100%;
  padding: var(--space-2);
  border: 1px solid var(--border-strong);
  border-radius: var(--r-sm);
  background: var(--plane);
  color: var(--ink);
  font: 400 var(--t-body)/1.5 var(--sans);
  resize: vertical;
}
.rp-text:focus { outline: 2px solid var(--focus); outline-offset: 1px; }

.rp-row {
  display: flex;
  align-items: center;
  gap: var(--space-2);
  margin-top: var(--space-2);
}
/* The count takes the slack so the two buttons stay together on the right. */
.rp-count { flex: 1 1 auto; font-size: var(--t-caption); color: var(--ink-muted); }

.rp-note {
  margin: var(--space-2) 0 0;
  font-size: var(--t-caption);
  line-height: 1.5;
}
.rp-note.is-ok { color: var(--pos); }
.rp-note.is-bad { color: var(--neg); }
.rp-note.is-warn { color: var(--warn); }

/* Weinstein stage chip. Its dot is the colour the chart draws that stage's
   bars in, --stage-1..4, so the chip doubles as the key. The text follows the
   dot for Stages 2 to 4. Stage 1 keeps neutral text, as the reference chart
   labels it: its lighter green is a colour for a mark, and in the light theme
   it is 3.17:1, which is enough for a candle and not enough for words.
   `cursor: help` because the reasoning and the limits are in the title. */
.stage-chip {
  display: inline-flex; align-items: center; gap: var(--space-1);
  padding: var(--space-0) var(--space-2);
  border: 1px solid var(--border-strong); border-radius: var(--r-pill);
  font-size: var(--t-caption); font-weight: 600; color: var(--ink-2);
  white-space: nowrap; cursor: help;
}
.stage-chip[hidden] { display: none; }
.stage-dot { width: 7px; height: 7px; border-radius: 50%; background: currentColor; }

.stage-chip.stage-1 { color: var(--ink-2); }
.stage-chip.stage-1 .stage-dot { background: var(--stage-1); }
.stage-chip.stage-2 { color: var(--stage-2); }
.stage-chip.stage-3 { color: var(--stage-3); }
.stage-chip.stage-4 { color: var(--stage-4); }
.stage-tag { font-weight: 500; color: var(--ink-muted); }
.stage-row { margin: var(--space-2) 0 0; }

/* The owner's list of reader-reported problems.
 *
 * Rows rather than a table: a report is a paragraph somebody typed, and the
 * two useful columns beside it (when, and whether it went out) are short
 * enough to sit above it without a header row to explain them. */
.rp-list { list-style: none; margin: var(--space-4) 0 0; padding: 0;
  display: flex; flex-direction: column; gap: var(--space-3); }
.rp-item { padding: var(--space-3); border: 1px solid var(--border);
  border-radius: var(--r-md); background: var(--surface-2); }
.rp-item-head { display: flex; align-items: baseline; gap: var(--space-2);
  margin-bottom: var(--space-2); }
.rp-item-when { font-size: var(--t-caption); color: var(--ink-muted);
  font-variant-numeric: tabular-nums; }
.rp-item-sent { margin-left: auto; font-size: var(--t-caption);
  color: var(--ink-muted); }
/* pre-wrap, because a report is typed prose and the line breaks the reader
   put in it are part of what they were trying to show you. */
.rp-item-msg { margin: 0; white-space: pre-wrap; overflow-wrap: anywhere;
  color: var(--ink); }
.rp-item-meta { margin: var(--space-2) 0 0; font-size: var(--t-caption);
  color: var(--ink-muted); overflow-wrap: anywhere; }
.rp-item-meta:empty { display: none; }
/* The page's two actions, and each report's own. The button sits on the head
   row, after the when and the sent tag, so it lines up down the list. */
.rp-tools { display: flex; flex-wrap: wrap; gap: var(--space-2); margin: var(--space-3) 0 0; }
.rp-resolve { flex: none; }
/* Open and Resolved: the Dossier's section tabs, with a rule under the pair so
   the list below reads as the selected tab's, and each tab's count beside its
   name. The rule is an inset shadow so the selected tab's underline covers it.
   A real space before the count, not only a margin, so a screen reader says
   "Open 3" rather than "Open3". */
.rp-tabs { margin-top: var(--space-3); box-shadow: inset 0 -1px 0 var(--border); }
.rp-tab-n { margin-left: var(--space-1); font-variant-numeric: tabular-nums;
  font-weight: 500; color: var(--ink-muted); }
.sec-tab.on .rp-tab-n { color: var(--ink-2); }
.rp-tabpanel > .sub:first-child { margin-top: var(--space-3); }
/* Wraps, so on a phone the button drops under the time rather than squeezing
   the time into two lines beside it: at 375px the head row was four. */
.rp-item-head { flex-wrap: wrap; row-gap: var(--space-2); }
.rp-item-when { white-space: nowrap; }
.rp-item-resolved { margin: var(--space-2) 0 0; font-size: var(--t-caption); color: var(--pos); }
.rp-item.is-resolved { background: transparent; }
.rp-item.is-resolved .rp-item-msg { color: var(--ink-2); }
.rp-note:empty { display: none; }

.rp-caveat {
  margin: var(--space-2) 0 0;
  font-size: var(--t-caption);
  line-height: 1.5;
  color: var(--ink-muted);
}

/* A phone has no room for a 310px card beside the page, so it spans the width
   it has. Still not a modal: the page behind it is untouched. */
@media (max-width: 559px) {
  .rp { left: var(--space-3); right: var(--space-3); bottom: var(--space-3); }
  body.chat-open .rp { right: var(--space-3); }
  .rp-panel { width: auto; }
}

/* ========================================================= morning desk ====
 *
 * Prose in a terminal, which needs a different measure from its neighbours. The
 * blocks beside this are tables and figures at full column width, and a
 * paragraph across 945px is about 160 characters a line, which nobody reads.
 * The text is capped and the headings are quiet, so it reads as a note rather
 * than as another dashboard tile.
 */
/* 54ch, and the unit is why it is not 70.
   `ch` is the width of the "0" glyph, and Inter sets digits much wider than its
   average lowercase: measured here, 1ch is 12.63px against a mean character
   nearer 9.9px. So 74ch computed to 934px, which is wider than the 908px block
   this sits in, and the cap never bound at all. 54ch lands at 682px, which
   counts 80 characters on a rendered line at this size: inside the 45 to 90 a
   reader tracks comfortably, and a third of the way back from where it was.
   Re-measure by counting characters on a line with a Range, not by converting
   ch to characters in your head, which is the error above. */
.hm-block.md { --md-measure: 54ch; }
.md-p {
  max-width: var(--md-measure);
  margin: 0 0 var(--space-3);
  font-size: var(--t-base);
  line-height: 1.65;
  color: var(--ink-2);
}
.md-p:last-of-type { margin-bottom: 0; }
/* The synthesis, the one paragraph carrying a conclusion. */
.md-overall { color: var(--ink); }

/* The branches. A label and its consequence rather than a bulleted list: the
   label is a condition and has to read as one, so it takes the emphasis and the
   consequence stays quiet beside it. */
.md-branches {
  display: flex;
  flex-direction: column;
  gap: var(--space-2);
  max-width: var(--md-measure);
  margin: 0 0 var(--space-3);
  padding-left: var(--space-3);
  border-left: 2px solid var(--border-strong);
}
.md-branch { font-size: var(--t-body); line-height: 1.6; }
.md-branch-label { color: var(--ink); font-weight: 600; }
.md-branch-label::after { content: ": "; }
.md-branch-text { color: var(--ink-2); }

.md-note {
  max-width: var(--md-measure);
  margin: 0 0 var(--space-3);
  font-size: var(--t-body);
  line-height: 1.6;
  color: var(--ink-2);
}
.md-note strong { color: var(--ink); }

.md-h3 {
  margin: var(--space-4) 0 var(--space-2);
  font: 600 var(--t-lead)/1.3 var(--sans);
  color: var(--ink);
}
.md-rel { margin-bottom: var(--space-3); max-width: var(--md-measure); }
.md-rel-head {
  display: flex;
  align-items: baseline;
  flex-wrap: wrap;
  gap: var(--space-2);
}
.md-rel-title { font-weight: 600; color: var(--ink); font-size: var(--t-body); }
.md-rel-time { font-size: var(--t-caption); color: var(--ink-muted); }
.md-rel-detail {
  margin: var(--space-1) 0 0;
  font-size: var(--t-body);
  line-height: 1.6;
  color: var(--ink-2);
}

/* What it cannot tell you. Every panel here carries one; on this one it matters
   more than most, because "no consensus estimates" is the thing a reader is
   likeliest to read as an omission rather than as a refusal to invent a
   number. */
.md-limits {
  max-width: var(--md-measure);
  margin-top: var(--space-4);
  padding-top: var(--space-3);
  border-top: 1px solid var(--border);
}
.md-limits-h {
  display: block;
  margin-bottom: var(--space-1);
  font-size: var(--t-micro);
  letter-spacing: 0.01em;
  color: var(--ink-muted);
}
.md-limits ul { margin: 0; padding-left: var(--space-4); }
.md-limits li {
  font-size: var(--t-caption);
  line-height: 1.6;
  color: var(--ink-muted);
  margin-bottom: var(--space-1);
}

/* Which voice wrote the desk. Sits with the limits because that is the same
   kind of fact: not a caveat about the numbers, which are identical either way,
   but about how the sentences around them were produced. */
.md-voice {
  margin: var(--space-2) 0 0;
  font-size: var(--t-caption);
  line-height: 1.6;
  color: var(--ink-muted);
}

/* When a book's record begins. Printed because the three do not begin together:
   a scan used to fill the balanced book's risk budget and stop before the
   others were offered the same candidate, so their returns are only comparable
   from the latest start onward. The record is kept rather than reset, because
   it is the only backtest this app has. */
.bk-since {
  display: block;
  margin-bottom: var(--space-1);
  font-size: var(--t-micro);
  letter-spacing: 0.01em;
  color: var(--ink-muted);
}

/* The resolved-catalyst line in the Pulse hero. Sits between the stance and the
   factor bars because it changes how those bars read: momentum hard down under
   a neutral stance looks like a contradiction until you know the chart is
   measuring the week before the catalyst. */
.pl-catalyst {
  margin: var(--space-2) 0 0;
  padding: var(--space-2) var(--space-3);
  border-left: 2px solid var(--border-strong);
  font-size: var(--t-body);
  line-height: 1.6;
  color: var(--ink-2);
}
.pl-catalyst strong { color: var(--ink); font-weight: 600; }

/* The pending variant of the catalyst line. A dashed edge rather than solid:
   the resolved line reports something that happened, this one reports
   something outstanding, and the two sit next to each other on a name awaiting
   a second decision after a first one landed. */
.pl-catalyst.is-pending { border-left-style: dashed; }



/* ======================================================= Asset restyle ====
 *
 * The reference template, applied across the terminal rather than sampled from.
 * Measured at 1440 on https://asset.framer.ai: plane rgb(8,10,9), surface
 * rgb(19,21,23), raised rgb(35,35,38), accent rgb(212,177,68), Host Grotesk,
 * h1 48px/400, h2 32px/400, prose measured at 480-640px, and 9,971px of page
 * for seven sections.
 *
 * This block is the entire change. Deleting it and the four `--asset-*` tokens
 * returns the terminal exactly as it was, which is why it is one contiguous
 * block at the foot of the file rather than edits threaded through the rules
 * it overrides.
 *
 * What it costs, stated plainly because it is the point of contention: the
 * reference is a brochure and this is an instrument. Its spacing exists to
 * give seven ideas a screen each, and a research terminal that breathes that
 * way puts fewer readings in front of you. The density work done on request
 * over this project pulls the other way.
 */
:root {
  --plane: var(--asset-plane);
  --surface: var(--asset-surface);
  --surface-2: var(--asset-raised);
  --btn-primary: var(--asset-brand);
  --btn-primary-ink: var(--asset-brand-ink);
  /* One family for everything, as the reference does. */
  --display: "Host Grotesk Variable", "Host Grotesk", var(--sans);
}

body, .msg { font-family: "Host Grotesk Variable", "Host Grotesk", system-ui, sans-serif; }

/* Light and large. Weight 400 at 32px is the single loudest signal in the
   reference: every section heading on it is set that way. */
h1, .hero, .home-title {
  font-family: var(--display);
  /* All three terms scale. Scaling only the bounds would leave the middle
     term deciding at every width where the clamp is not already pinned, and
     on a 1440px desktop it is pinned to the maximum -- so the setting would
     have moved this by a pixel. Measured across compact/default/large at
     1440px: 42 / 48.3 / 54.6, against 48 / 48 / 48 before. */
  font-size: clamp(calc(30px * var(--ui-scale)),
                   calc(3.4vw * var(--ui-scale)),
                   var(--t-hero));
  font-weight: 400;
  letter-spacing: -0.02em;
  line-height: 1.1;
}
/* `.panel > h2` used to be in this list and is deliberately not any more.
 *
 * It was the third of three declarations of the same selector -- 17.25px/600
 * at line 1644, 20.7px/600 at line 6035, and this one -- all at specificity
 * (0,1,2), so source order decided and this one won. Every panel heading in
 * the product rendered at 32px in the display face at weight 400, against
 * 13px table text beneath it. On the Financials facet that is four cards in a
 * two-column grid, each shouting its own name louder than the numbers it
 * introduces, which is the "generic card, weak hierarchy" reading.
 *
 * A display size is for the one thing a page is about. These repeat. */
h2, .hm-h, .md-h3 {
  font-family: var(--display);
  font-size: var(--t-title);
  font-weight: 400;
  letter-spacing: -0.02em;
  line-height: 1.2;
}
h3 { font-weight: 400; letter-spacing: -0.01em; }
.pl-lede { font-weight: 400; letter-spacing: -0.02em; }

/* Prose to a reading measure. The reference runs 480-640px; panels here are
   often twice that, which is what makes a paragraph in one look like a wall. */
.panel > p.sub, .hm-block > p, .md-p, .brief-summary p {
  max-width: 640px;
}

/* Air. Roughly double, which is still nowhere near the reference's own, and
   the compromise is deliberate: its spacing on a data table would be absurd. */
.grid { gap: var(--space-6); }
.hm-board { gap: var(--space-7); }
.hm-block { padding: var(--space-6) var(--space-7); }
main { padding-top: var(--space-7); }
.panel > h2 { margin-bottom: var(--space-4); }

/* The reference's cards: flatter, rounder, and a border that is barely there.
   `.panel` itself is changed in place further up rather than re-declared here:
   it was five top-level rules once, silently overriding each other, and the
   guard against that cannot tell a documented override from an accident. The
   undo for this whole restyle is reverting its commit. */
.hm-block, .tile {
  border-radius: var(--r-lg);
  border-color: rgba(255, 255, 255, 0.08);
}

/* ---- Asset restyle: the phone -------------------------------------------
 *
 * This block sits at the foot of the file, so every rule in it beats the
 * responsive rules written earlier at equal specificity. That is exactly the
 * trap it walked into: `.hm-block` had a 559px rule at line 9437 and the
 * restyle's unscoped `padding: var(--space-6) var(--space-7)` silently won,
 * which measured 74px of horizontal padding on a 390px screen. Nineteen per
 * cent of the viewport, leaving 275px of content.
 *
 * So the restyle has to carry its own phone behaviour rather than assume the
 * earlier rules still apply. The reference's spacing is for a brochure read on
 * a desktop; none of it survives contact with a 390px screen.
 */
@media (max-width: 559px) {
  /* Padding back to something a phone can afford. 18.4px a side is 9% of a
     390px screen against the restyle's 19%. */
  .panel { padding: var(--space-3) var(--space-4); }
  .hm-block { padding: var(--space-3) var(--space-4); }
  .grid { gap: var(--space-3); }
  .hm-board { gap: var(--space-4); }
  main { padding-top: var(--space-4); }

  /* 32px headings are a desktop measurement. At 390px they take three words a
     line and push the reading they head off the screen. */
  h1, .hero, .home-title { font-size: calc(23px * var(--ui-scale)); line-height: 1.15; }
  /* `.panel > .wv-head h2` is (0,2,1) and outranks `.panel > h2` here, so
     without it in this list the Watchlist and Alerts titles stayed at
     20.7px while every other panel heading on the page went to 21px. */
  /* A page title drops to the panel-heading size here rather than to another
     literal. 21px was 0.3px off `--t-heading` at the default scale and frozen
     at every other. */
  h2, .panel > h2, .panel > .wv-head h2, .hm-h, .md-h3 {
    font-size: var(--t-heading);
  }
  .pl-lede { font-size: var(--t-heading); line-height: 1.3; }

  /* The measure cap is wider than the screen here, so it does nothing but
     stop the text using the width it has. */
  .panel > p.sub, .hm-block > p, .md-p, .brief-summary p {
    max-width: none;
  }

  /* The session legend wrapped to one key per line, four lines for what is a
     colour key. Tighter gaps let two sit together. */
  .ses-legend { gap: var(--space-2) var(--space-3); }
  .ses-key { font-size: var(--t-caption); }

  /* The header was six rows and 234px on an 844px screen: 28% of the phone
     spent on chrome before a single reading. Pulse had been pushed onto a row
     of its own, which is the worst of it, because the row above it was one
     word too wide.
     The wordmark going entirely is what buys that word back; see `.brand-mark`
     for why the name is not lost with it. */
  .brand { gap: var(--space-1); padding: var(--space-1) var(--space-2); }
  #chat-toggle { padding: var(--space-2) var(--space-3); margin-left: var(--space-1); }
  .search input { width: 100%; min-width: 0; }
  .search { flex: 1 1 auto; min-width: 0; }
}

/* ======================================================= home: Today ========
 *
 * A digest band, not a dashboard card. The distinction is load-bearing: this
 * sits above the search on the first screen a reader sees, so it has to be
 * readable at a glance and must not push the search below the fold. Three
 * columns, two rows each, one line per row.
 *
 * Measured at 880x965 before it existed: 364px of chrome, then the brand
 * lockup, search and quick picks taking 320px of the 601 left above the fold.
 * The terminal's own name was the first thing on the page and the next five
 * days of scheduled releases were on another tab.
 */
.hm-today {
  margin: var(--space-3) 0 var(--space-4);
  padding: var(--space-4) var(--space-5);
  background: var(--surface);
  border: 1px solid var(--border);
  border-radius: var(--r-lg);
}
.ht-head {
  display: flex;
  align-items: baseline;
  gap: var(--space-3);
  margin-bottom: var(--space-3);
}
.ht-title { font-size: var(--t-lead); margin: 0; }
.ht-sub { font-size: var(--t-small); color: var(--ink-muted); }
/* Pushed to the far end rather than sitting beside the title: it leaves the
   page, and a link that leaves should not read as part of the heading. */
.ht-more {
  margin-left: auto;
  font: inherit;
  font-size: var(--t-small);
  background: none;
  border: 0;
  padding: 0;
  color: var(--s1);
  cursor: pointer;
  /* Required, and the app-wide rule is right: fifty-eight of eighty-three
     hover rules once snapped while a handful eased, which reads as unfinished.
     See test_every_hover_state_transitions. */
  transition: color 0.12s ease;
}
.ht-more:hover { color: var(--ink); text-decoration: underline; }
.ht-more:focus-visible { outline: 2px solid var(--focus); outline-offset: 3px; border-radius: var(--r-xs); }

/* auto-fit rather than three fixed columns: a degraded feed can return one or
   two, and two columns holding a third of the width each with a gap where the
   third should be reads as a missing panel rather than as fewer panels. */
.ht-cols {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(210px, 1fr));
  gap: var(--space-4) var(--space-5);
}
.ht-col { min-width: 0; }
.ht-col-name {
  font-size: var(--t-caption);
  text-transform: uppercase;
  letter-spacing: 0.05em;
  color: var(--ink-muted);
  /* 600, with the other fifteen uppercase labels in this file. It was 500,
     which is the weight the two non-headings use -- `.cal-impact` is a chip
     and `.bk-active` is a span subordinate to the h2 it sits inside. This is
     an h3. */
  font-weight: 600;
  margin: 0 0 var(--space-2);
}
.ht-list { list-style: none; margin: 0; padding: 0; display: grid; gap: var(--space-1); }
/* Three parts on one line: impact, what, when. `what` is the only one allowed
   to shrink, and it truncates rather than wrapping -- a two-line row in a
   digest breaks the scan down the column. The full text is in the title. */
.ht-row {
  display: flex;
  align-items: baseline;
  gap: var(--space-2);
  font-size: var(--t-small);
}
.ht-what {
  min-width: 0;
  flex: 1 1 auto;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
  color: var(--ink);
}
.ht-when { flex: none; color: var(--ink-muted); font-variant-numeric: tabular-nums; }
/* One step smaller than the rest: this one sits inside a digest row rather
   than under a panel. */
.ht-rest { margin: var(--space-1) 0 0; font-size: var(--t-caption); }

/* On a phone the three columns stack, and the band is the first thing under
   the market strip -- so it is capped to what fits without pushing the search
   off the screen entirely. */
@media (max-width: 559px) {
  .hm-today { padding: var(--space-3) var(--space-4); }
  .ht-cols { gap: var(--space-3); }
}

/* ---------------------------------------------------- compare: somewhere to start
 *
 * Replaces a panel that said "Enter two tickers and press Compare" in a box of
 * its own, directly beneath a box holding two ticker fields and a Compare
 * button. Two panels, one instruction, and the reader still typing symbols
 * from memory on the page whose job is putting two of them side by side.
 */
.cmp-picks {
  display: flex;
  align-items: baseline;
  flex-wrap: wrap;
  gap: var(--space-2);
  margin-top: var(--space-3);
}
.cmp-picks-lab {
  font-size: var(--t-caption);
  text-transform: uppercase;
  letter-spacing: 0.05em;
  color: var(--ink-muted);
}
/* The hint only appears when there is no history to offer, so it is the empty
   state of the picks row rather than a second instruction beside it. */
.cmp-hint { margin: var(--space-3) 0 0; }

/* ------------------------------------------- session: the description folds
 *
 * Only in the phases where it is background. See SESSION_CAVEAT_PHASES: the
 * overnight, holiday and weekend copy says the price on screen is not live,
 * and that never folds. The rest explains what a session is, which is worth
 * reading once and not on every page load for the life of the account.
 *
 * Measured at 800x835 with NVDA loaded: the bar was 253px and the ticker
 * header did not appear until y=468, so the security a reader had just
 * searched for sat in the bottom 44% of the first screen. The description was
 * 76px of that.
 *
 * Matches details.ind-explain rather than inventing a second fold: same
 * marker suppression, same muted summary, same focus ring.
 */
.ses-fold > summary {
  display: inline-flex;
  align-items: center;
  gap: var(--space-2);
  cursor: pointer;
  list-style: none;
  font-size: var(--t-micro);
  text-transform: uppercase;
  letter-spacing: 0.04em;
  color: var(--ink-muted);
  padding: var(--space-1) 0;
  transition: color 0.12s ease;
}
.ses-fold > summary::-webkit-details-marker { display: none; }
.ses-fold > summary:hover { color: var(--ink); }
.ses-fold > summary:focus-visible { outline: 2px solid var(--focus); outline-offset: 2px; border-radius: var(--r-xs); }
.ses-fold-body { margin-top: var(--space-2); }

/* ------------------------------------------- charting: give the chart the screen
 *
 * Measured at 1280x900 with NVDA charted: 347px of chrome above the workspace
 * (top bar, session bar, facet strip), a 97px four-row toolbar inside it, and
 * a 309px chart. A third of the screen, on the one tab whose entire job is the
 * chart.
 *
 * The session bar's `#ses-detail` is the part this view can give up. The phase
 * legend decodes a 24-hour strip that the chart's own time axis already
 * covers, and the phase is named in words in `.ses-main` directly above it.
 * Every other view keeps both, because on those the strip is the only session
 * context on the page.
 *
 * `.ses-fold` and not `.ses-desc`: the fold only exists in the phases where
 * the description is background (see SESSION_CAVEAT_PHASES). Overnight,
 * holiday and weekend render the description as a plain div with no `.ses-fold`
 * class, so the sentence saying the price on screen is not live survives here
 * exactly as it does everywhere else. Hiding a staleness warning to buy chart
 * height would be the worst version of this trade.
 *
 * Keyed on body[data-view] because #sessionbar is a sibling of the views
 * rather than a child of one, so the workspace cannot reach it by descent.
 */
body[data-view="chart"] .ses-legend,
body[data-view="chart"] .ses-fold { display: none; }

/* ===================================================== the workspace frame ==
 *
 * Rail down the side, content beside it. This replaced a page-shaped shell:
 * one full-width bar carrying the brand, the search box, eight nav groups,
 * settings, the account slot and Pulse, over a centred column. Two things were
 * wrong with it and both were structural rather than cosmetic.
 *
 * The navigation competed with the search box for horizontal room -- measured
 * at 1411px, the narrowest width where the strip may not wrap, eight groups
 * ended at 1125 against a settings gear at 1160, and a ninth group left 18px.
 * Navigation that cannot grow is navigation that cannot be labelled.
 *
 * And it cost vertical space on every page. Measured at 1280x900 on the
 * charting tab: 347px of chrome above the workspace, on the one view whose
 * entire job is a chart. A rail costs none: it uses width the content column
 * was not using anyway, and on a 1280px screen there is more of that than
 * there is height.
 *
 * Grid rather than flex so the rail can be a fixed track that the content
 * column simply does not overlap -- the previous shell reserved the Pulse
 * panel with a margin on `main` and a padding on the header, two rules that
 * had to agree with each other and once did not.
 */
.app {
  display: grid;
  grid-template-columns: auto minmax(0, 1fr);
  min-height: 100dvh;
}
.app-main { min-width: 0; display: flex; flex-direction: column; }

/* The sidebar's text, a step past --ink-2 toward --ink: asked for as "make the
   side bar text a bit more visible". Measured in the dark theme, --ink-2 on the
   rail was 10.7:1 and this is about 14.8:1; in the light theme the same mix
   darkens instead. The current section keeps the full --ink, its raised
   background and its blue edge, so it still reads as the one you are in. */
:root { --rail-ink: color-mix(in srgb, var(--ink) 70%, var(--ink-2)); }
.rail {
  position: sticky;
  top: 0;
  align-self: start;
  height: 100dvh;
  display: flex;
  flex-direction: column;
  gap: var(--space-1);
  width: 212px;
  padding: var(--space-3) var(--space-2);
  background: var(--surface);
  border-right: 1px solid var(--border);
  /* Not `overflow-y: auto`.
   *
   * Per spec, an `overflow` of `visible` on one axis computes to `auto` when
   * the other is not visible -- so asking for a vertical scrollbar silently
   * clipped the horizontal axis too, and the section fly-outs open sideways.
   * Measured: the Dossier menu ran 206 to 396 against a rail ending at 212 and
   * was cut off at its own edge, which read as the menus not opening at all.
   *
   * Visible is safe here because the rail's content does not grow with data:
   * a brand row, six sections and two footer buttons, about 380px, against a
   * 100dvh column. It would need a viewport under ~400px tall to overflow, and
   * below 560px wide the rail is a horizontal row with its own rule. */
  overflow: visible;
  z-index: 40;
  transition: width 160ms cubic-bezier(0.22, 0.61, 0.36, 1);
}
/* The brand sits at the top of the rail rather than in the bar. */
.rail > .brand {
  margin: 0 0 var(--space-3);
  padding: var(--space-2);
  width: 100%;
  justify-content: flex-start;
}
.rail .brand-text {
  display: inline;
  font-size: var(--t-caption);
  letter-spacing: 0.08em;
  color: var(--rail-ink);
  white-space: nowrap;
}
/* Follows the nav rather than being pinned to the floor.
 *
 * `margin-top: auto` resolved to 339px on an 835px rail -- measured -- so the
 * sidebar read as two clusters with something missing between them rather than
 * as one list that had ended. The empty space still exists, because the rail is
 * as tall as the window either way; it now sits below the last item, where the
 * end of a list is what it looks like.
 *
 * Safe to unpin because this rail's content does not grow with data: a brand
 * row, six sections and two footer buttons, about 470px all told. That is the
 * same assumption the `overflow: visible` note above already rests on. */
.rail-foot { margin-top: var(--space-5); display: flex; flex-direction: column; gap: var(--space-1); }
/* Settings is a destination and Collapse is a control of the rail itself, so
   a hairline separates them rather than a gap: at `--space-1` they read as one
   pair of related buttons, and clicking the wrong one of a pair costs a page.
   Drawn as a pseudo-element, not `border-top` on the button -- a border there
   is inside the hover background and squares off its top corner. */
.rail-toggle { position: relative; margin-top: var(--space-2); }
.rail-toggle::before {
  content: "";
  position: absolute;
  left: var(--space-2);
  right: var(--space-2);
  top: calc(var(--space-2) / -2);
  border-top: 1px solid var(--border);
}
.rail-foot .icon-btn {
  display: flex;
  align-items: center;
  gap: var(--space-2);
  width: 100%;
  justify-content: flex-start;
  padding: var(--space-2);
  color: var(--rail-ink);
}
.rail-label { font-size: var(--t-small); color: var(--rail-ink); white-space: nowrap; }

/* Collapsed: icons only.
 *
 * Two faults in the first version, and the second is why the first was
 * invisible. It hid text with `font-size: 0`, which leaves the text in flow --
 * and with `overflow: visible` on the rail (which the fly-out menus need) it
 * escaped the 56px column and painted over the page. And the rule that was
 * meant to do the hiding never applied at all: `body.rail-tight .rail
 * .nav-top` and `.rail nav.tabs-group .nav-top` are both specificity (0,3,1),
 * so source order decided, and the section styling came later in the file.
 *
 * `display: none` on the label element rather than a size on its parent, and
 * every collapsed rule carries the same `.rail nav.tabs-group` prefix as the
 * rule it overrides, so the two cannot end up tied again. The accessible name
 * is untouched -- the label is hidden, not removed, and the button carries a
 * title. */
body.rail-tight .rail { width: 56px; }
body.rail-tight .rail .brand-text,
body.rail-tight .rail .rail-label,
body.rail-tight .rail nav.tabs-group .nav-label,
body.rail-tight .rail nav.tabs-group .nav-caret { display: none; }
body.rail-tight .rail nav.tabs-group .nav-top,
body.rail-tight .rail-foot .icon-btn,
body.rail-tight .rail > .brand {
  justify-content: center;
  padding-left: 0;
  padding-right: 0;
}
/* Nothing may paint outside the column. The rail keeps `overflow: visible` for
   the fly-outs, so the clipping has to be the width itself holding. */
body.rail-tight .rail nav.tabs-group .nav-top { overflow: hidden; }

/* The top bar is a context strip now, not the navigation.
 *
 * The nav strip used to sit between the search box and the account controls
 * and grow to fill the row, which is what held those controls against the
 * right edge. Moving it to the rail took the spacer with it and left Sign in
 * and Pulse bunched against the Load button with most of the bar empty to
 * their right. The push is explicit now rather than a side effect of what
 * happened to be next to them, and it is a class rather than an id so it can
 * follow whichever control ends up first -- it sat on the gear until the gear
 * moved to the rail. */
.app-main > header.topbar { gap: var(--space-3); }
.topbar-right { margin-left: auto; }

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

/* ---------------------------------------------- the nav, now down the side
 *
 * `paintNav` is unchanged: it writes the same markup into `nav.tabs-group`
 * wherever that element lives. What changes is the axis, and with it three
 * rules that only made sense in a row -- the wrap band between 560 and 1410,
 * the measured left/right menu flip, and the phone's horizontal scroller.
 * A column needs none of them: it has as much height as it needs and its
 * menus open sideways.
 */
.rail nav.tabs-group {
  display: flex;
  flex-direction: column;
  flex-wrap: nowrap;
  align-items: stretch;
  gap: var(--space-0);
  overflow: visible;
  min-height: 0;
}
.rail nav.tabs-group > .nav-item { position: relative; }
.rail nav.tabs-group .nav-top {
  display: flex;
  align-items: center;
  gap: var(--space-2);
  width: 100%;
  justify-content: flex-start;
  /* Square padding, so the glyph column is straight.
   *
   * With --space-3 on the sides these icons sat at x=24 while the brand mark
   * above and the gear below sat at 19 -- measured against the rail's own left
   * edge. Five pixels on the one vertical line the eye follows all the way
   * down the sidebar, and it put the nav labels at 51 against the footer's 47
   * at the same time. Both columns land together now: icons at 19, nav and
   * footer labels at 46. The brand mark keeps its own 22px box and sits a
   * little wider, which is what a logo is allowed to do. */
  padding: var(--space-2);
  border-radius: var(--r-sm);
  font-size: var(--t-small);
  color: var(--rail-ink);
  text-align: left;
  white-space: nowrap;
  /* The app-wide rule, and it is right: fifty-eight of eighty-three hover
     states once snapped while a handful eased, which reads as unfinished.
     Paint-level properties only -- animating a layout property forces a
     document reflow per frame. */
  transition: background-color var(--dur-ui, 120ms) ease, color var(--dur-ui, 120ms) ease;
}
.rail nav.tabs-group .nav-top:hover { background: var(--bg-hover); color: var(--ink); }
/* Fixed, so a section's glyph sits on the same axis whether the rail is open
   or collapsed -- the label appears and disappears beside it rather than
   moving it. */
.nav-icon { width: 18px; height: 18px; flex: none; opacity: 1; }
.rail nav.tabs-group .nav-top[aria-selected="true"] .nav-icon,
.rail nav.tabs-group .nav-item.on > .nav-top .nav-icon { opacity: 1; }
.nav-label { min-width: 0; overflow: hidden; text-overflow: ellipsis; }
/* The current section, marked on the edge the eye already follows down. */
.rail nav.tabs-group .nav-top[aria-selected="true"],
.rail nav.tabs-group .nav-item.on > .nav-top {
  background: var(--surface-2);
  color: var(--ink);
  box-shadow: inset 2px 0 0 var(--s1);
}
/* The caret points along the axis the menu opens on. */
.rail nav.tabs-group .nav-caret { margin-left: auto; transform: rotate(-90deg); }

/* Menus fly out to the right of the rail rather than dropping below a tab. */
.rail nav.tabs-group .nav-menu {
  top: 0;
  left: 100%;
  right: auto;
  margin-left: var(--space-1);
  box-shadow: var(--elev-2);
}
body.rail-tight .rail nav.tabs-group .nav-menu { left: 56px; }

/* A phone has no room for a rail. It becomes a row along the bottom edge of
   the frame instead, above the existing mobile tab bar's territory, and the
   fly-out menus drop upward from it. */
@media (max-width: 559px) {
  .app { grid-template-columns: minmax(0, 1fr); }

  /* The rail is FIXED under the top bar here. It was `order: 2` in a one-column
     grid, which does not mean "after the search box" -- `.app-main` is the
     whole page, header and content and footer, so the rail landed after all of
     it. Measured on the home page at 375x812: the rail sat at y=6479 of a
     6542px document. It was reported as the sidebar not existing on a phone,
     and that is the honest description: no reader scrolls six thousand pixels
     to find the navigation.
     
     Fixed rather than sticky because the rail and the top bar are grid
     siblings and the bar lives inside `.app-main`, so no ordering can put the
     rail between the bar and the content. Taking it out of flow can.
     
     `--topbar-h` is measured and republished as the bar wraps to two or three
     rows, so the rail follows it instead of guessing. And this is the position
     the rest of the phone CSS already assumed: the section fly-outs anchor to
     `calc(var(--topbar-h) + var(--rail-h))`, which was correct for a rail
     under the bar and opened the menu six thousand pixels from its button. */
  .rail {
    position: fixed;
    top: var(--topbar-h, 66px);
    left: 0;
    right: 0;
    /* Under the top bar's 50, over the page. */
    z-index: 49;
    height: auto;
    width: auto;
    flex-direction: row;
    align-items: center;
    gap: var(--space-2);
    /* The NAV scrolls here, not the rail -- see the tabs-group rule below.
       The phone menus are position: fixed and escape either container rather
       than being clipped by it, which is what makes the choice free. */
    overflow-x: hidden;
    border-right: 0;
    border-bottom: 1px solid var(--border);
    padding: var(--space-2) var(--space-3);
  }
  .rail > .brand { width: auto; margin: 0 var(--space-2) 0 0; flex: none; }
  /* Mark only. The wordmark took 161px of a 375px row and left the nav 121px
     of scroll box, which is about one group button visible at a time. The
     mark is still the home button, and it is the one place in the app where
     the name is not already on screen twice. */
  .rail > .brand .brand-text { display: none; }
  .rail nav.tabs-group {
    flex-direction: row;
    gap: var(--space-1);
    /* `.rail nav.tabs-group` carries `overflow: visible` so the fly-outs can
       open sideways out of the column, which is right at every other width.
       In a row it meant the seven group buttons ran from x=193 to x=969 out
       of a nav box measured at 112px, and the RAIL took the scroll instead:
       the footer, a flex sibling placed after that shrunken box, landed at
       x=314 on top of nav buttons three through seven and scrolled away with
       them. Measured after: rail scrollWidth 375 against clientWidth 375, nav
       scrollWidth 794 against 121, footer fixed at 323-361. */
    /* `flex` is not set here. An unmediated `.rail nav.tabs-group { flex: 0 1
       auto }` sits below this block and would discard it; the grow is
       declared again after that rule instead. */
    min-width: 0;
    overflow-x: auto;
    scrollbar-width: none;
  }
  .rail nav.tabs-group::-webkit-scrollbar { display: none; }
  /* `width: 100%` is a column rule. In a row it made every group that renders
     as a bare button -- Home and Compare, the ones with a single page -- take
     the whole scroll box, so the strip showed one entry at a time while the
     grouped ones beside them sized to their label. Measured at 375x812:
     Home 239px and Compare 239px against Dossier 98 and Markets 103, with one
     visible at once. */
  .rail nav.tabs-group .nav-top {
    padding: var(--space-1) var(--space-2);
    width: auto;
  }
  /* Inline, not absolute.
   *
   * The base rule pins the caret to `top: --space-1; right: --space-1` of the
   * nav item, which was right when the nav was a full-width top bar and the
   * corner was the right-hand end of a wide button. In this row the buttons
   * size to their label, so a 7x4px triangle at the top-right corner of a
   * 98px pill reads as a stray speck floating above the strip -- which is
   * exactly how it was reported. Measured: caret at y=141 against a button
   * spanning 135 to 179, so it sat on the top edge rather than beside the
   * word.
   *
   * `margin-left: auto` did nothing here either: an absolutely positioned
   * box is placed by its offsets, not by its margins. */
  .rail nav.tabs-group .nav-caret {
    position: static;
    margin-left: var(--space-1);
    transform: none;
    flex: none;
  }
  /* And it has to flip when the menu is open, which is the one thing the
     glyph is for. `.nav-item.open .nav-caret` is (0,2,0) and the rule above
     is (0,3,0), so the reset won and the caret never moved. */
  .rail nav.tabs-group .nav-item.open .nav-caret { transform: rotate(180deg); }
  /* Last in the row and not shrinkable, so the gear is always at the right
     end and always on screen. Measured at 375x812: 323-361 whatever the nav's
     scroll position. */
  .rail-foot { margin-top: 0; flex-direction: row; flex: none; }
  /* The rail is out of flow, so the content has to be pushed past it. On the
     header, not on `.statusline` or `main`: the header is sticky and therefore
     still in flow, so its bottom margin reserves the space whatever follows it
     -- and the statusline does not always render. */
  .app-main > header.topbar { margin-bottom: var(--rail-h, 0px); }
  /* The section fly-outs. Fixed, not absolute.
   *
   * Absolute positioning left the menu anchored to a nav item that can sit
   * anywhere along a scrolling strip, so on a 375px screen the one under
   * "Analysis" started at x=190 and its 190px min-width ran to 380 -- five
   * pixels past the viewport, which is the whole horizontal scroll. Pinning it
   * to both edges of the screen removes the arithmetic entirely.
   *
   * Below the bar AND the rail, not over them. `top: auto` on a fixed box
   * falls back to its static position, which for a menu inside the strip is
   * the strip's own top, so the panel opened across the tabs and hid which
   * section you were in while you chose one. A percentage is no use either: on
   * a fixed box it resolves against the viewport. Both heights are measured
   * and republished -- the bar wraps to two or three rows, and `--rail-h` is
   * 0 on a desktop, where these fly out sideways instead.
   *
   * THE SELECTOR AND ITS PLACE IN THE FILE ARE BOTH LOAD-BEARING. This lived
   * a thousand lines earlier as a bare `.nav-menu`, which is (0,1,0), against
   * `.rail nav.tabs-group .nav-menu` at (0,3,0) further down -- and a media
   * query adds no specificity, so every declaration in it was discarded.
   * Widening the selector alone was not enough either: at equal specificity
   * the later rule wins, and the desktop ones are later. It matches both of
   * them now and sits after both. Measured at 375x812 before: the Dossier
   * menu opened at top 0, left 257, over the top bar and over its own
   * button. */
  .rail nav.tabs-group .nav-menu,
  body.rail-tight .rail nav.tabs-group .nav-menu {
    position: fixed;
    top: calc(var(--topbar-h, 165px) + var(--rail-h, 0px) + var(--space-1));
    left: var(--space-3);
    right: var(--space-3);
    min-width: 0;
    max-width: none;
    /* The rail's desktop rule offsets the fly-out from the column by a step;
       pinned to both screen edges that offset is just a shift. */
    margin-left: 0;
  }
  /* And the measured side-flip has to be undone by name.
   *
   * `navMenuSides()` marks a menu that would run off the right edge, and
   * `nav.tabs-group > .nav-item.menu-right .nav-menu` is (0,3,2) -- higher
   * than the rule above, so it kept `left: auto; right: 0` and the panel sized
   * itself to its content against the right edge instead of spanning the
   * screen. Measured at 375x812: the Dossier menu resolved to left 257, width
   * 118. There is no side to flip to when both edges are pinned. */
  nav.tabs-group > .nav-item.menu-right .nav-menu {
    left: var(--space-3);
    right: var(--space-3);
  }
  /* The open menu once liftNavMenu has moved it to <body>, out of the strip
     that clipped it in Safari. Same place and size as the rule above; open,
     because the `.nav-item.open` rule that shows it no longer reaches it. */
  nav.tabs.nav-lift { display: contents; }
  .nav-lift .nav-menu {
    position: fixed;
    top: calc(var(--topbar-h, 165px) + var(--rail-h, 0px) + var(--space-1));
    left: var(--space-3);
    right: var(--space-3);
    min-width: 0;
    max-width: none;
    margin-left: 0;
    opacity: 1;
    visibility: visible;
    transform: none;
  }
  .rail-label { display: none; }
  /* Square icon buttons, not the full-width rows they are in the column. In a
     row `width: 100%` resolved against the footer itself and gave the gear an
     81px hit area with 18px of glyph in the middle of it. */
  .rail-foot .icon-btn { width: 38px; justify-content: center; padding: 0; }
  /* Nothing to collapse: the rail is already a row here.
     `.rail-foot .icon-btn` is (0,2,0) and sets `display: flex`, so a bare
     `.rail-toggle` (0,1,0) lost the cascade and this rule did nothing -- the
     toggle was live on phones, and tapping it set `body.rail-tight`, whose
     `body.rail-tight .rail { width: 56px }` also outranks the `width: auto`
     above and squeezed the whole nav strip to 56px. Measured before the fix:
     display computed `flex`. */
  .rail-foot .rail-toggle { display: none; }
}

/* --------------------------------------------- the rail, corrected in place
 *
 * Three faults, all from the nav having been a horizontal strip inside the
 * top bar until now, measured at 1280x900:
 *
 * `nav.tabs-group` carried `order: 3` and `flex: 1 0 100%` -- both written to
 * place it on the bar's third row and make it claim the full width. In a
 * column those put the settings footer ABOVE the sections (foot at y=87,
 * nav at y=172) and forced the nav to a 100% basis.
 *
 * `main` is a flex item now, and a flex item's default `min-width: auto` lets
 * it refuse to shrink below its content. It measured 1129px inside a 1068px
 * column and ran 61px past the viewport -- the whole of the horizontal page
 * scroll. The same `min-width: 0` the panel grids already use.
 *
 * And the rail's icons had no size, so the collapse glyph rendered at 105px
 * square and pushed everything under it down a screen.
 */
.rail nav.tabs-group { order: 0; flex: 0 1 auto; }

@media (max-width: 559px) {
  /* Grow to fill the row. The phone block sets this too and cannot keep it:
     the rule directly above is unmediated and later, so `flex: 0 1 auto` won
     and the strip only ever took its content width. */
  .rail nav.tabs-group { flex: 1 1 auto; }
}

/* In the column, the sections never give up height to what is under them.
 *
 * With `min-height: 0` and a shrink of 1, a rail with less height than its
 * content took the shortfall out of the sections, and they draw overflow
 * rather than clip it (the fly-outs need that), so the last ones slid over
 * what was below. Reported with a screenshot of Reports printed across the
 * RECENT heading of the recent-symbols list the rail carried then (removed
 * since, at the owner's request). Measured at 1280x845: the sections' box
 * 81-437, their content 440 tall, Reports at 436-484. The space between the
 * sections and the foot is what gives way. */
@media (min-width: 560px) {
  .rail nav.tabs-group { flex-shrink: 0; }
}

/* A short window, where even the sections, the brand and the foot fill it.
 *
 * Under 700px the rows tighten, so Settings stays on screen
 * rather than below the rail's edge, where nothing scrolls to it. Under 520px,
 * which is a phone on its side, the brand row and the collapse toggle go too:
 * Home is the first section, and a phone has no use for a narrower rail.
 *
 * And the touch floor gives a little there. `pointer: coarse` holds every nav
 * button and icon button at 44px, and eight of those plus Settings are 420px:
 * at 740x360 Settings sat at 386-430, below the screen. At 34px, still well
 * over WCAG's 24px, the whole rail fits with room to spare. */
@media (min-width: 560px) and (max-height: 699px) {
  .rail nav.tabs-group .nav-top { padding-top: var(--space-1); padding-bottom: var(--space-1); }
}
@media (min-width: 560px) and (max-height: 519px) {
  .rail > .brand, .rail-foot .rail-toggle { display: none; }
  /* Without the brand the second section sits level with the top bar, and a
     fly-out opens from its own top: at 740x360 the Dossier menu ran from 41,
     under a bar ending at 61 that paints over the rail (50 against 40). Above
     the bar here, still under Pulse (60). */
  .rail { padding-top: var(--space-1); padding-bottom: var(--space-1); z-index: 55; }
  .rail nav.tabs-group .nav-top { padding-top: var(--space-0); padding-bottom: var(--space-0); }
  .rail nav.tabs-group .nav-top, .rail .rail-foot .icon-btn { min-height: 34px; }
}
.app-main > * { min-width: 0; }
.rail .icon-btn svg,
.rail .brand-mark { width: 18px; height: 18px; flex: none; }
.rail > .brand .brand-mark { width: 22px; height: 22px; }

/* The book a set of figures belongs to, named in its own heading. Three
   portfolios share this panel and the selector above switches them; without
   the name, switching looked like the page failing to update. */
.bk-active {
  font-size: var(--t-small);
  font-weight: 500;
  letter-spacing: 0.04em;
  text-transform: uppercase;
  color: var(--ink-2);
  border: 1px solid var(--border-strong);
  border-radius: var(--r-pill);
  padding: var(--space-0) var(--space-2);
  vertical-align: middle;
  margin-left: var(--space-2);
}

/* ------------------------------------------------- keyboard keeps its ring
 *
 * Four controls answered :focus with `outline: none` and a border tint. On a
 * text input that is survivable -- there is a caret saying where you are --
 * but `:focus` also matches keyboard focus, and on a <select> there is no
 * caret, so tabbing into the range picker or the Roth controls moved focus
 * with nothing but one pixel of border colour to show for it.
 *
 * The tint stays: it is the right weight for a pointer, which already knows
 * where it is. These restate the app's own ring for the keyboard case only,
 * and sit after those rules because :focus-visible and :focus are the same
 * specificity -- source order is what decides between them. */
.home-search input:focus-visible,
.range-pick select:focus-visible,
.roth-controls textarea:focus-visible,
.roth-controls input:focus-visible,
.roth-controls select:focus-visible {
  outline: 2px solid var(--focus);
  outline-offset: 1px;
}

/* #cp-input is an id, so its base `outline: none` outranks the global
 * `:focus-visible` rule (0,1,0) and no amount of state could get a ring back.
 * Matching the id here is the fix, not a preference. */
#cp-input:focus-visible {
  outline: 2px solid var(--focus);
  outline-offset: 2px;
}

/* ---- readings panel: one lead shape, four pages -------------------------------------------- */
/* A reading per row rather than a grid of cards: these are sentences, and a
   sentence set in a 232px column wraps to five lines. auto-fit with a wide
   minimum gives two columns on a desktop and one on anything narrower without
   a second breakpoint to keep in step. */
.lead-reads {
  display: grid;
  gap: var(--space-4);
  grid-template-columns: repeat(auto-fit, minmax(320px, 1fr));
  margin-top: var(--space-4);
}
.lead-read-head {
  display: flex;
  align-items: center;
  gap: var(--space-2);
  flex-wrap: wrap;
  margin-bottom: var(--space-1);
}
/* The label is the quiet half. The chip beside it carries the reading, so the
   two must not compete -- this is the security header's lesson in a smaller
   frame. */
.lead-read-lab {
  font-size: var(--t-micro);
  text-transform: uppercase;
  letter-spacing: 0.06em;
  color: var(--ink-muted);
  font-weight: 600;
}
.lead-read-detail {
  margin: 0;
  font-size: var(--t-small);
  color: var(--ink-2);
  line-height: 1.5;
}

/* ---- catalyst detail ----------------------------------------------------- */
/* A summary chip that opens. The caret is the only thing marking it apart from
   the row chips, which do not open -- they already sit on the story they came
   from. */
.chip.cat-open {
  cursor: pointer;
  font: inherit;
  font-size: var(--t-caption);
}
.cat-caret {
  width: 0;
  height: 0;
  margin-left: var(--space-1);
  border-left: 3.5px solid transparent;
  border-right: 3.5px solid transparent;
  border-top: 4px solid currentColor;
  opacity: 0.55;
  transition: transform var(--dur-ui, 120ms) ease;
}
.chip.cat-open.on .cat-caret { transform: rotate(180deg); opacity: 0.9; }
.chip.cat-open.on { border-color: var(--border-strong); }
.chip.cat-open:focus-visible { outline: 2px solid var(--focus); outline-offset: 2px; }

.cat-detail {
  margin-top: var(--space-3);
  padding: var(--space-3) var(--space-4);
  background: var(--surface-2);
  border: 1px solid var(--border);
  border-radius: var(--r-lg);
}
.cat-detail-head {
  margin: 0 0 var(--space-2);
  font-size: var(--t-micro);
  text-transform: uppercase;
  letter-spacing: 0.06em;
  color: var(--ink-muted);
  font-weight: 600;
}
/* A hairline between entries rather than a gap: these are short and a gap at
   this size reads as three unrelated blocks. */
.cat-detail-row + .cat-detail-row {
  margin-top: var(--space-3);
  padding-top: var(--space-3);
  border-top: 1px solid var(--border);
}
.cat-detail-row a { font-weight: 600; }
.cat-detail-sum {
  margin: var(--space-1) 0 0;
  font-size: var(--t-small);
  color: var(--ink-2);
  line-height: 1.5;
}

/* ------------------------------------------------------- chart full screen
 *
 * Scoped to the chart view as well as the class. The class is cleared when the
 * reader navigates away, but a scope costs nothing and means a class left
 * behind by a reload, a crash or a half-finished transition cannot take the
 * rail off some other page.
 *
 * At the end of the file on purpose: `main`'s padding and `.view-workspace`'s
 * height are both declared earlier at equal specificity, and equal specificity
 * is decided by source order. Placing this block up with the layout rules left
 * the padding in and the workspace 24px short -- the same trap the phone
 * command-centre block at the foot of this file records.
 */
body.ws-max[data-view="chart"] .rail,
body.ws-max[data-view="chart"] .topbar,
body.ws-max[data-view="chart"] .legal,
/* The Dossier section strip goes too, and it is the one that needs saying.
 *
 * Those seven names navigate to OTHER pages, so they are app chrome rather
 * than chart chrome -- 48px of "leave this chart" inside a mode whose whole
 * purpose is the chart. The way back is the Exit full screen button, which the
 * rule below pins to the end of the toolbar.
 *
 * It also removes a fault rather than only some pixels. `.sec-head` is sticky
 * at `top: var(--topbar-h)`, and with the top bar hidden that property keeps
 * whatever it last measured -- so the strip was displaced 24px DOWN from its
 * flow position while the toolbar stayed put, and the two overlapped. Measured
 * in full screen: strip 66-114, toolbar starting at 90. */
body.ws-max[data-view="chart"] .sec-head { display: none; }

/* And the Report a Problem pill, which is app chrome like the rest and was the
   one piece left: fixed at the foot of the screen, it sat on the widget rail's
   last button in a mode that does not scroll. Measured at 1280x900 with Pulse
   docked: the rail's Pulse button at 912-955 x 859-890 under the pill at
   728-939 x 835-879, so the button could not be clicked at all. The way out of
   full screen is one press away, and the pill is back with the rest. */
body.ws-max[data-view="chart"] .rp { display: none; }

/* No height is set here, deliberately. `.view-workspace.active` already sizes
   itself as the viewport minus `--chrome-h`, and wsSyncChromeHeight measures
   that chrome from where the workspace actually starts rather than assuming a
   number -- so with the chrome gone the existing calc resolves to the whole
   viewport on its own. A `height: 100dvh` here would be a second answer to a
   question that already has one, and the two would drift. */
body.ws-max[data-view="chart"] main { padding: 0; }

/* The way out needs no rule of its own here. It used to be the last button
   in the toolbar, pinned sticky to the end so a wrapped or scrolled bar could
   not hide it; it is on the chart's header now, which full screen keeps. */

/* ---------------------------------------------------------- Insiders page
 *
 * Optic's own tokens throughout. The directional pair is --pos / --neg, the
 * same one the candles, the volume strip and every percentage in this app
 * already use, so a green bar here means what a green bar means everywhere
 * else. No new colours were introduced for this page.
 */
.ins-head h1 { margin: 0 0 var(--space-1); font-size: var(--t-title); }
.ins-head .sub { max-width: 68ch; }

.ins-facets {
  display: flex;
  gap: var(--space-1);
  margin-top: var(--space-3);
  flex-wrap: wrap;
}
.ins-facet {
  padding: var(--space-2) var(--space-4);
  border: 1px solid var(--border);
  border-radius: var(--r-pill);
  background: var(--surface);
  color: var(--ink-2);
  font: inherit;
  font-size: var(--t-small);
  font-weight: 600;
  cursor: pointer;
  transition: color var(--dur-ui) var(--ease-ui),
              border-color var(--dur-ui) var(--ease-ui),
              background-color var(--dur-ui) var(--ease-ui);
}
.ins-facet:hover { color: var(--ink); border-color: var(--border-strong); }
.ins-facet.on { background: var(--plane); color: var(--ink); border-color: var(--accent); }

/* The filter row. A grid rather than a flex row: five controls of different
   natural widths wrapped into a ragged two-and-a-half lines, and the labels
   stopped lining up with each other. */
.ins-filters {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(150px, 1fr));
  gap: var(--space-3);
  align-items: end;
}
.ins-field { display: flex; flex-direction: column; gap: var(--space-1); min-width: 0; }
.ins-field label {
  font-size: var(--t-caption);
  color: var(--ink-muted);
  /* Written case, not uppercase.
   *
   * These are field labels on a form, and one of them is a whole clause --
   * "WHY, AND WHAT WOULD PROVE YOU WRONG" is thirty-four characters of capitals
   * asking a question, which is shouting rather than labelling. Uppercase is
   * still right for the micro-headings elsewhere in this file (RECENT, SECTOR
   * READ-THROUGH): those are one or two words naming a block, and they are
   * read once. A form label is read every time the field is filled in.
   *
   * The markup already says `Symbol`, `Entry price`, `Why, and what would
   * prove you wrong` -- only the transform was changing them, so nothing in
   * the JavaScript moves for this. */
  letter-spacing: 0.01em;
}
/* flex-basis is a WIDTH in a row and a HEIGHT in a column, and these labels are
   columns: the shared control classes in this file carry `flex: 0 1 340px` for
   the Settings page's rows, which reused here made every input 340px tall. */
.ins-field input,
.ins-field select {
  width: 100%;
  min-width: 0;
  flex: none;
  padding: var(--space-2);
  border: 1px solid var(--border);
  border-radius: var(--r-sm);
  background: var(--surface);
  color: var(--ink);
  font: inherit;
  font-size: var(--t-small);
}
.ins-acts {
  display: flex;
  gap: var(--space-2);
  align-items: center;
  flex-wrap: wrap;
  /* Spans the whole grid, so the buttons sit under the fields rather than
     becoming a sixth column of a different height. */
  grid-column: 1 / -1;
}

.ins-windows { display: flex; gap: var(--space-1); margin-bottom: var(--space-3); }

/* The activity chart. preserveAspectRatio: none on the SVG, so the bars
   stretch to the panel's width and the height is set here -- the viewBox is a
   percentage grid, not pixels, and letting it letterbox would centre a strip
   of bars in the middle of an empty panel. */
.ca-wrap { margin-bottom: var(--space-2); }
.ca-chart { display: block; width: 100%; height: 120px; }
.ca-buy { fill: var(--pos); }
.ca-sell { fill: var(--neg); }
.ca-other { fill: var(--ink-muted); }
.ca-axis {
  display: flex;
  justify-content: space-between;
  font-size: var(--t-caption);
  color: var(--ink-muted);
  margin-top: var(--space-1);
}
.ca-key { font-size: var(--t-caption); color: var(--ink-muted); margin: var(--space-2) 0; }
.ca-dot {
  display: inline-block;
  width: 9px;
  height: 9px;
  border-radius: 2px;
  margin: 0 var(--space-0) 0 var(--space-2);
}
/* The key's swatches are spans, not SVG shapes, so they need background rather
   than fill -- the fill rules above do nothing to an HTML element. */
.ca-dot.ca-buy { background: var(--pos); }
.ca-dot.ca-sell { background: var(--neg); }

/* The ranking. */
.ct-list { list-style: none; margin: 0; padding: 0; display: grid; gap: var(--space-3); }
.ct-row { display: grid; grid-template-columns: auto 1fr; gap: var(--space-2) var(--space-3); align-items: baseline; }
.ct-sym {
  background: none;
  border: none;
  padding: 0;
  font: inherit;
  font-weight: 700;
  color: var(--ink);
  cursor: pointer;
  font-variant-numeric: tabular-nums;
}
.ct-sym { transition: color var(--dur-ui) var(--ease-ui); }
.ct-sym:hover { color: var(--accent); text-decoration: underline; }
.ct-counts { font-size: var(--t-caption); color: var(--ink-muted); }
.ct-bar {
  grid-column: 1 / -1;
  display: flex;
  height: 6px;
  border-radius: 3px;
  overflow: hidden;
  background: var(--border);
  min-width: 2px;
}
.ct-buy { background: var(--pos); }
.ct-sell { background: var(--neg); }

.ins-side { font-weight: 600; }

/* Analyst actions: the action and the firm's rating carry the row, so they
   take the weight; the colour is the global up/down, by which way each went. */
.an-action, .an-rating { font-weight: 600; }
/* The search over the feed: a ticker or a company, with suggestions under it. */
.an-search {
  display: flex;
  align-items: center;
  flex-wrap: wrap;
  gap: var(--space-2);
  margin: var(--space-2) 0 var(--space-3);
}
.an-combo { flex: 0 1 320px; min-width: 0; }
.an-ticker {
  width: 100%;
  font: inherit;
  padding: var(--space-2) var(--space-3);
  border: 1px solid var(--border);
  border-radius: var(--r-md);
  background: var(--surface-2);
  color: var(--ink);
}
.an-ticker:focus-visible { outline: 2px solid var(--focus); outline-offset: 1px; }
.an-table td { white-space: nowrap; }
.ws-sub-h {
  margin: var(--space-3) 0 var(--space-1);
  font-size: var(--t-caption);
  font-weight: 600;
  color: var(--ink-2);
}
.ins-row .ins-more { white-space: nowrap; font-size: var(--t-caption); }
/* The filer's own words about the trade, which run to a sentence about an
   option's strike and expiry: wrapped in a column of its own width rather
   than stretching the table a sentence wide. */
.ins-row .ins-desc {
  white-space: normal;
  min-width: 14rem;
  max-width: 24rem;
  font-size: var(--t-caption);
  color: var(--ink-2);
}

/* --------------------------------------------------------- the rail's space
 *
 * Measured at 965px tall: the brand took 42px, the sections 313px and the foot
 * 90px, and the remaining 460px was nothing -- with Settings and Collapse
 * floating in the middle of it rather than sitting at the bottom, which is
 * what made it read as unfinished rather than as roomy. After this, 14px.
 *
 * The whole block is inside `min-width: 560px`, which is where the rail is a
 * COLUMN. Below it the rail is a row along the bottom of the screen, and both
 * of these rules are actively wrong there: `margin-top: auto` in a row pushes
 * nothing anywhere useful, and the phone block already sets `margin-top: 0` --
 * but it sits earlier in this file, so an unscoped rule here would win on
 * source order and undo it. The first draft of this did exactly that, and also
 * hid the recents at every width under 1080px, where the rail is still a
 * perfectly good column with 460px going spare.
 */
@media (min-width: 560px) {
  .rail-foot {
    /* auto, not a fixed gap. The foot is the bottom of the rail; at
       `margin-top: var(--space-5)` it sat wherever the sections happened to
       end and left the void underneath it. */
    margin-top: auto;
  }

  .rail nav.tabs {
    /* The gap down the left of the sections, reported three times and never
       the vertical one I kept measuring.

       `nav.tabs` carries `margin-left: auto` because in the TOPBAR it is a
       horizontal strip that claims the free space to its left -- the comment
       on `nav.tabs ~ .icon-btn` says so. The rail reuses the class in a
       COLUMN, where that same auto margin does something else entirely: it
       pushes the whole group to the right edge, and because an auto margin
       also cancels the stretch an item would otherwise get from
       `align-items: normal`, the group stops filling the rail. Measured at
       212px wide: every other child sat 9px from the left edge and 193px
       wide, and the sections sat at 91px and 111px wide -- an 82px channel of
       nothing down the left of the nav, with the labels hanging off the right.

       The same shape as the `flex-basis` note in CLAUDE.md: a control class
       written for one axis is not reusable on the other. */
    margin-left: 0;
  }

}

/* ------------------------------------------------ the digest opens out
 *
 * The rows are one line each and truncate, because a digest is scanned down a
 * column and a wrapping row breaks that. That is still true -- so the way to
 * read the rest of a row is to open it, not to make every row taller.
 */
.ht-row-item { min-width: 0; }
.ht-det { min-width: 0; }

/* The summary IS the row, so it keeps the flex layout the row had. Both marker
   rules: Safari needs the webkit pseudo-element and everything else takes
   `list-style`. Without them a disclosure triangle sits in front of the impact
   chip and pushes the whole column in by 14px. */
.ht-det > summary { cursor: pointer; list-style: none; }
.ht-det > summary::-webkit-details-marker { display: none; }
.ht-det > summary::marker { content: ""; }

/* Opened, the title is allowed to wrap -- which is the entire point, since the
   thing being revealed is usually the end of it. */
.ht-det[open] > summary .ht-what {
  white-space: normal;
  overflow: visible;
  text-overflow: clip;
}
/* And the impact chip stops sitting on the baseline of a two-line title. */
.ht-det[open] > summary { align-items: flex-start; }

.ht-why {
  margin: var(--space-1) 0 var(--space-2);
  /* Indented past the impact chip so the sentence reads as belonging to the
     row above it rather than as a new row. The chip is a fixed width; this is
     measured against it rather than guessed. */
  padding-left: calc(var(--space-6) + var(--space-2));
  font-size: var(--t-caption);
  color: var(--ink-2);
  line-height: 1.5;
}

/* The count is a control now, so it looks and behaves like one. */
.ht-rest {
  margin: var(--space-2) 0 0;
  padding: 0;
  background: none;
  border: none;
  font: inherit;
  font-size: var(--t-caption);
  color: var(--ink-muted);
  cursor: pointer;
  transition: color var(--dur-ui) var(--ease-ui);
}
.ht-rest:hover { color: var(--ink); }
.ht-rest:focus-visible { outline: 2px solid var(--focus); outline-offset: 3px; border-radius: var(--r-xs); }

/* ------------------------------------------------------------ paper trades
 *
 * Optic's own tokens. The directional pair is --pos / --neg, the same one the
 * candles, the volume strip and every percentage already use, so a green P&L
 * here means what green means everywhere else in the terminal.
 */
.pt-head h1 { margin: 0 0 var(--space-1); font-size: var(--t-title); }
.pt-head .sub { max-width: 70ch; }

.pt-ticket { display: grid; gap: var(--space-3); }
.pt-grid {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(150px, 1fr));
  gap: var(--space-3);
  align-items: end;
}

/* The contract the ticket is pointed at, and what it would cost to trade it. */
.pt-quote {
  margin: 0;
  font-size: var(--t-caption);
  color: var(--ink-2);
  font-variant-numeric: tabular-nums;
}

/* Cost, risk and reward:risk, before the trade exists. */
.pt-preview {
  display: flex;
  flex-wrap: wrap;
  gap: var(--space-2) var(--space-5);
  padding: var(--space-3);
  border: 1px solid var(--border);
  border-radius: var(--r-sm);
  background: var(--plane);
  font-size: var(--t-small);
  font-variant-numeric: tabular-nums;
}
.pt-k { color: var(--ink-muted); margin-right: var(--space-1); }

/* Optic's read on the ticket's symbol. */
.pt-read {
  margin: 0;
  padding: var(--space-2) var(--space-3);
  border-left: 2px solid var(--accent);
  font-size: var(--t-small);
  color: var(--ink-2);
}
.pt-read-none { border-left-color: var(--border); }
.pt-read-note { display: block; color: var(--ink-muted); font-size: var(--t-caption); }
.pt-note { margin: 0; font-size: var(--t-small); color: var(--ink-2); }

/* A direction on a position line, not a chip: it is part of the name of the
   thing, and a pill here would compete with the impact chips elsewhere. */
.pt-dir {
  margin-left: var(--space-2);
  font-size: var(--t-caption);
  text-transform: uppercase;
  letter-spacing: 0.04em;
  font-weight: 600;
}
/* Where the mark came from. Quiet: it is provenance, not a reading. */
.pt-src { font-size: var(--t-caption); color: var(--ink-muted); }
.pt-read-cell { font-size: var(--t-caption); }
.pt-when { font-size: var(--t-caption); color: var(--ink-muted); white-space: nowrap; }
.pt-close { padding: var(--space-0) var(--space-2); font-size: var(--t-caption); }

.pt-h3 {
  margin: var(--space-5) 0 var(--space-2);
  font-size: var(--t-caption);
  text-transform: uppercase;
  letter-spacing: 0.05em;
  font-weight: 600;
  color: var(--ink-muted);
}
.pt-stats {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(180px, 1fr));
  gap: var(--space-4);
}
.pt-stat { display: flex; flex-direction: column; gap: var(--space-0); min-width: 0; }
.pt-stat strong { font-size: var(--t-lead); font-variant-numeric: tabular-nums; }
.pt-sub { font-size: var(--t-caption); color: var(--ink-muted); }

/* A segmented control in a field column fills the column.
 *
 * `.seg` is `inline-flex`, so it hugs its buttons -- but inside `.ins-field`,
 * which is a flex COLUMN, it is stretched to the column's width by the default
 * cross-axis stretch. The box grew and the buttons did not, so Long/Short sat
 * in the left half of a full-width bordered box with the rest of it empty.
 * Measured on the ticket at a 241px column: Direction filled 53%, Instrument
 * 61% and Call/Put 43% -- 114px, 93px and 138px of dead space inside a border
 * that looked like a control.
 *
 * Scoped to `.ins-field`, and that scope is the whole care here: the same
 * class carries Line/Candles on two chart toolbars and three rows in Settings,
 * where the control is meant to hug its labels and stretching it would leave
 * a Line button half a toolbar wide.
 */
.ins-field > .seg { display: flex; }
.ins-field > .seg button {
  /* Equal shares of the box rather than each button's own text width: a
     "Shares"/"Option" pair with one longer word would otherwise split the
     border unevenly and read as one of them being selected. */
  flex: 1 1 0;
  min-width: 0;
  text-align: center;
  justify-content: center;
}

/* The armed half of the clear-the-book control. Danger is carried by the text
   colour and the border, not a fill: a solid red button beside a plain one
   reads as the recommended action, and the recommended action here is Keep. */
.pt-danger {
  color: var(--neg);
  border-color: var(--neg);
  /* Paint-level only, like every other hover in this file: animating a layout
     property forces a full-document reflow per frame. */
  transition: color var(--dur-ui) var(--ease-ui),
              border-color var(--dur-ui) var(--ease-ui),
              background-color var(--dur-ui) var(--ease-ui);
}
.pt-danger:hover { color: var(--neg); border-color: var(--neg); }

/* The way out of an instrument chart.
 *
 * Above the kicker rather than beside the title: it is not part of the
 * instrument's identity, it is how you leave, and a control sharing a line
 * with an h2 competes with it. Quiet by default and only as loud as a link,
 * because the chart is what the reader came for.
 */
.inst-back {
  display: inline-flex;
  align-items: center;
  gap: var(--space-1);
  margin: 0 0 var(--space-2);
  padding: var(--space-0) var(--space-2) var(--space-0) var(--space-1);
  border: 1px solid transparent;
  border-radius: var(--r-sm);
  background: none;
  color: var(--ink-muted);
  font: inherit;
  font-size: var(--t-caption);
  cursor: pointer;
  transition: color var(--dur-ui) var(--ease-ui),
              border-color var(--dur-ui) var(--ease-ui),
              background-color var(--dur-ui) var(--ease-ui);
}
.inst-back:hover { color: var(--ink); border-color: var(--border); background: var(--surface); }
.inst-back:focus-visible {
  outline: 2px solid var(--focus);
  outline-offset: 2px;
}
/* The arrow shifts, not the button: moving the control itself on hover makes
   the label jump under a cursor that is already on it. */
.inst-back-arrow { transition: transform var(--dur-ui) var(--ease-ui); }
.inst-back:hover .inst-back-arrow { transform: translateX(-2px); }

/* The Insiders page's one input.
 *
 * It replaced a six-field filter panel -- symbol, member, direction, two dates,
 * Apply / Reset / Save this search. The question this page answers is "who
 * bought this", and a symbol is the whole of it.
 */
.ins-find-form {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: var(--space-2);
}
.ins-find-lab { font-size: var(--t-caption); color: var(--ink-muted); }
.ins-find-form input {
  /* Wide enough for the placeholder, which is a sentence rather than a hint:
     a blank box on a page about filings does not say that blank means all of
     them. */
  flex: 1 1 260px;
  min-width: 0;
  padding: var(--space-2);
  border: 1px solid var(--border);
  border-radius: var(--r-sm);
  background: var(--surface);
  color: var(--ink);
  font: inherit;
  font-size: var(--t-small);
}

/* The seconds a symbol's first load has been waiting, beside "Loading HOOD...".
   Tabular figures so the count does not shuffle the line as it grows. */
.load-elapsed { margin-left: 6px; color: var(--ink-muted); font-variant-numeric: tabular-nums; }


/* "Optic has been updated", for a page opened before the latest deploy. Fixed
   at the foot of the window, clear of the Report a Problem button's corner. */
.build-notice {
  position: fixed;
  left: 50%;
  bottom: var(--space-4);
  transform: translateX(-50%);
  z-index: 60;
  display: flex;
  align-items: center;
  gap: var(--space-3);
  max-width: calc(100vw - 2 * var(--space-4));
  padding: var(--space-2) var(--space-3);
  border: 1px solid var(--border);
  border-radius: var(--r-md);
  background: var(--raised);
  color: var(--ink);
  font-size: var(--t-small);
  box-shadow: 0 6px 24px rgb(0 0 0 / 0.35);
}
.build-x {
  background: none;
  border: 0;
  color: var(--ink-muted);
  cursor: pointer;
  font-size: var(--t-base);
  line-height: 1;
  transition: color var(--dur-ui) var(--ease-ui);
}
.build-x:hover { color: var(--ink); }


/* ---------------------------------------- fair value, analysts, dividend score
 * The range bar under the fair value tiles: the 25th to 75th percentile as a
 * band, the middle as a tick and today's price as a dot. Paint-level only. */
.fv-bar { position: relative; height: 8px; margin: var(--space-4) var(--space-2) var(--space-1);
  border-radius: var(--r-pill); background: var(--surface-2); border: 1px solid var(--border); }
.fv-band { position: absolute; top: 0; bottom: 0; border-radius: var(--r-pill);
  background: color-mix(in srgb, var(--s1) 38%, transparent); }
.fv-mid { position: absolute; top: -3px; bottom: -3px; width: 2px; background: var(--ink-muted);
  transform: translateX(-1px); }
.fv-now { position: absolute; top: 50%; width: 12px; height: 12px; border-radius: var(--r-pill);
  background: var(--ink); border: 2px solid var(--surface); transform: translate(-50%, -50%); }
.fv-now.up { background: var(--pos); }
.fv-now.down { background: var(--neg); }
.fv-key { display: flex; justify-content: space-between; color: var(--ink-muted);
  font-size: var(--t-caption); margin: 0 var(--space-2) var(--space-3);
  font-variant-numeric: tabular-nums; }
/* Buys, holds and sells as one bar of the analysts who rate it. */
.fv-split { display: flex; height: 8px; margin-top: var(--space-4); border-radius: var(--r-pill);
  overflow: hidden; background: var(--surface-2); }
.fv-split .up { background: var(--pos); }
.fv-split .flat { background: var(--ink-muted); opacity: 0.55; }
.fv-split .down { background: var(--neg); }
.fv-score { display: grid; grid-template-columns: minmax(120px, 160px) 1fr; gap: var(--space-5);
  align-items: center; margin-top: var(--space-3); }
.fv-score-n { font-size: var(--t-d1, 44px); font-weight: 700; font-variant-numeric: tabular-nums;
  letter-spacing: -0.02em; }
.fv-score-n.up { color: var(--pos); }
.fv-score-n.down { color: var(--neg); }
.fv-score-of { color: var(--ink-muted); margin-left: var(--space-1); }
.fv-score-band { color: var(--ink-2); font-size: var(--t-small); }
@media (max-width: 719px) { .fv-score { grid-template-columns: 1fr; } }
/* The stars in the terminal's gold, so the rating reads at a glance. */
.ms-stars { color: var(--warn); letter-spacing: 0.08em; }
/* Optic's star rating: the stars large, the reading beside them. */
.os-row { display: flex; align-items: center; gap: var(--space-5); flex-wrap: wrap; }
.os-stars { font-size: var(--t-d3); line-height: 1.1; }
.os-read { flex: 1 1 260px; min-width: 0; }

/* ================================================================ the phone ===
 *
 * Asked for as "It is tough to use on the phone as a first time user, make the
 * structure simple and understandable". Measured at 375x812 on AAPL: the top
 * bar, the sections strip, the status line and the market hours took 470px
 * before the company's name, and four destinations were in two navigations at
 * once. On a phone there is now one of each thing:
 *
 *   - one navigation, the bottom bar (Home, Stocks, Markets, Discover, More);
 *     the sections strip and the status line are not drawn
 *   - one search box, in the top bar beside Pulse, and on Home not even that,
 *     because Home's own search is the page's first thing
 *   - the market hours as one line: the session and when the next one starts
 *   - a section's pages as chips under the top bar, and a stock's sections as
 *     tabs that wrap onto a second line rather than scroll out of sight
 *
 * Last in the file, so it wins every tie with the rules above it. */
@media (max-width: 559px) {
  #rail.rail { display: none; }
  #statusline.statusline { display: none; }

  header.topbar { flex-wrap: wrap; gap: var(--space-2); padding: var(--space-2) var(--space-3); }
  header.topbar #ticker-form { order: -1; flex: 1 1 0; min-width: 0; }
  header.topbar #ticker-form button[type="submit"] { display: none; }
  header.topbar .account-slot { margin-left: auto; }
  header.topbar .account-slot:has(.acct-signin) { display: none; }
  body[data-view="home"] header.topbar #ticker-form { display: none; }

  /* A section's pages: Read, Macro and Indices under Markets. */
  header.topbar #subnav:not([hidden]) {
    display: flex !important;
    flex: 1 0 100%;
    gap: var(--space-1);
    overflow-x: auto;
    scrollbar-width: none;
    margin: 0;
  }
  header.topbar #subnav::-webkit-scrollbar { display: none; }
  header.topbar #subnav button {
    flex: none;
    padding: var(--space-1) var(--space-3);
    border-radius: var(--r-pill);
    border: 1px solid var(--border);
    background: none;
    color: var(--ink-2);
    font: 500 var(--t-small)/1.4 var(--sans);
  }
  header.topbar #subnav button[aria-selected="true"] {
    color: var(--ink);
    border-color: var(--btn-primary);
    background: color-mix(in srgb, var(--btn-primary) 14%, transparent);
  }
  /* The gold outline says which page this is. The blue underline every
     selected tab in nav.tabs carries drew a second mark under the chip. */
  header.topbar #subnav button[aria-selected="true"]::after { content: none; }

  /* The market hours, one line. The day's timeline, the legend and the
     company block are a desktop's; the session and the countdown stay. */
  .sessionbar { padding: var(--space-2) var(--space-3); }
  .sessionbar .ses-main { gap: var(--space-2) var(--space-3); }
  .sessionbar .ses-strip,
  .sessionbar .ses-detail-btn,
  .sessionbar .ses-detail { display: none; }
  /* The dot beside the words, not on a line of its own above them. */
  .sessionbar .ses-head { flex: 1 1 0; min-width: 0; }

  /* The new-build notice, across the screen above the bottom bar. Centred by
     `left: 50%` it was sized against half the screen and wrapped one word to a
     line. */
  .build-notice {
    left: var(--space-3); right: var(--space-3); transform: none;
    bottom: calc(60px + env(safe-area-inset-bottom));
    max-width: none;
  }

  /* A stock's sections, every one in sight. */
  .sec-head .sec-tabs { flex-wrap: wrap; overflow: visible; row-gap: 0; }
  .sec-head .sec-tab { padding: var(--space-2) var(--space-2); }

  /* More. */
  .more-back {
    position: fixed; inset: 0; z-index: 90;
    background: var(--scrim);
    display: flex; align-items: flex-end;
  }
  .more-sheet {
    width: 100%; max-height: 80vh; overflow-y: auto;
    background: var(--surface);
    border-top: 1px solid var(--border);
    border-radius: var(--r-lg) var(--r-lg) 0 0;
    padding: var(--space-3) var(--space-3) calc(var(--space-5) + env(safe-area-inset-bottom));
  }
  .more-head { display: flex; align-items: center; justify-content: space-between;
    padding: var(--space-1) var(--space-1) var(--space-2); }
  .more-group { border-top: 1px solid var(--border); padding: var(--space-2) 0; }
  .more-glab { color: var(--ink-muted); font-size: var(--t-caption); padding: var(--space-1) var(--space-2); }
  .more-row {
    display: grid; gap: 2px; width: 100%; text-align: left;
    padding: var(--space-2); border: 0; border-radius: var(--r-sm);
    background: none; color: var(--ink); font: inherit; cursor: pointer;
    transition: background-color var(--dur-ui) var(--ease-ui);
  }
  .more-row:active, .more-row:hover { background: var(--surface-2); }
  .more-lab { font-weight: 600; font-size: var(--t-body); }
  .more-what { color: var(--ink-muted); font-size: var(--t-small); }

  /* Pulse covers the screen on a phone, and the bottom bar sat on top of its
     box to type into. Closing Pulse brings it back. */
  body.chat-open .mtabs { display: none; }
}

/* ================================================================ Pulse ===
 *
 * Asked for with a picture of another broker's assistant: "make pulse look
 * like this". Its ideas in Optic's colours: the reader's question a filled
 * bubble at the right and the answer plain text across the panel; what Pulse
 * is doing as a short list ticked step by step; a chart inside an answer; Copy
 * and Retry under it; and a rounded box to type into. */
#chat .msg.user, #chat .msg.assistant { display: flex; flex-direction: column; }
#chat .msg.user .who, #chat .msg.assistant .who { display: none; }
#chat .msg.user .bubble {
  align-self: flex-end;
  max-width: 88%;
  background: var(--surface-2);
  border: 1px solid var(--border-strong);
  border-radius: var(--r-lg) var(--r-lg) var(--r-xs) var(--r-lg);
  padding: var(--space-2) var(--space-3);
}
#chat .msg.user .msg-save { align-self: flex-end; }
#chat .msg.assistant .bubble { background: none; border: 0; padding: 0; }

/* What Pulse is doing. */
.pulse-steps {
  border: 1px solid var(--border);
  border-radius: var(--r-md);
  padding: var(--space-2) var(--space-3);
  margin-bottom: var(--space-2);
  font-style: normal;
}
.pulse-steps summary {
  display: flex; align-items: center; gap: var(--space-2);
  cursor: pointer; list-style: none;
  font-size: var(--t-caption); font-weight: 600; letter-spacing: 0.04em;
  text-transform: uppercase; color: var(--ink-2);
}
.pulse-steps summary::-webkit-details-marker { display: none; }
.ps-list { list-style: none; margin: var(--space-2) 0 0; padding: 0; display: grid; gap: var(--space-1); }
.ps-list li {
  display: flex; align-items: center; gap: var(--space-2);
  font-size: var(--t-small); color: var(--ink-muted);
}
.ps-list li::before {
  content: ""; flex: none; width: 14px; height: 14px; border-radius: 50%;
  border: 1.5px solid var(--border-strong);
}
.ps-list li.on { color: var(--ink); }
.ps-list li.on::before { border-color: var(--btn-primary); border-right-color: transparent;
  animation: spin 0.8s linear infinite; }
.ps-list li.done { color: var(--ink-2); }
.ps-list li.done::before {
  border-color: var(--pos);
  background: var(--pos)
    url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16'%3E%3Cpath d='M4 8.5l2.5 2.5L12 5.5' fill='none' stroke='white' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'/%3E%3C/svg%3E")
    center / 12px no-repeat;
}
.ps-list li.fail::before { border-color: var(--neg); background: var(--neg); }

/* A chart inside an answer. */
/* A source chip after a figure in a Pulse answer: names the panel it came
   from and opens it. Small and quiet so a paragraph with three of them still
   reads as prose. */
.pulse-src {
  display: inline-flex;
  align-items: center;
  margin: 0 0 0 2px;
  padding: 0 var(--space-1);
  border: 1px solid var(--border);
  border-radius: var(--r-sm);
  background: var(--surface-2);
  color: var(--ink-muted);
  font: inherit;
  font-size: var(--t-caption);
  line-height: 1.5;
  vertical-align: baseline;
  cursor: pointer;
  transition: color 120ms ease, border-color 120ms ease;
}
.pulse-src:hover,
.pulse-src:focus-visible { color: var(--ink); border-color: var(--ink-muted); }

.pulse-chart {
  margin: var(--space-3) 0;
  padding: var(--space-3);
  border: 1px solid var(--border);
  border-radius: var(--r-md);
  background: var(--raised);
  white-space: normal;
}
.pulse-chart .pc-plot { min-height: 190px; }
.pulse-chart .pc-cap { color: var(--ink-muted); font-size: var(--t-caption); margin-top: var(--space-1); }

/* Under an answer. */
.pulse-acts { display: flex; gap: var(--space-1); margin-top: var(--space-2); }
.pa-btn {
  padding: var(--space-0) var(--space-2);
  border: 1px solid transparent; border-radius: var(--r-sm);
  background: none; color: var(--ink-muted);
  font: 500 var(--t-caption)/1.6 var(--sans); cursor: pointer;
  transition: color var(--dur-ui) var(--ease-ui), border-color var(--dur-ui) var(--ease-ui);
}
.pa-btn:hover { color: var(--ink); border-color: var(--border); }

/* The box to type into. */
#chat .chat-foot { border-top: 0; }
#chat .chat-foot textarea {
  border-radius: var(--r-lg);
  border: 1px solid var(--border-strong);
  background: var(--surface-2);
  padding: var(--space-3);
}
#chat .chat-actions #chat-send { border-radius: var(--r-pill); }

/* Who filed a report, above the page and browser it came from. */
.rp-item-from { margin: var(--space-2) 0 0; font-size: var(--t-small); color: var(--ink-2); }
.rp-item-from a { color: var(--ink); }

/* Key stats on the Overview: label and figure in rows, four columns on a
   desktop and two on a phone. */
.ks-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(200px, 1fr));
  gap: 0 var(--space-5); margin: var(--space-2) 0 0; }
.ks-row { display: flex; justify-content: space-between; gap: var(--space-3);
  padding: var(--space-2) 0; border-bottom: 1px solid var(--border); }
.ks-row dt { color: var(--ink-muted); font-size: var(--t-small); }
.ks-row dd { margin: 0; font-variant-numeric: tabular-nums; font-weight: 600;
  font-size: var(--t-small); text-align: right; }
@media (max-width: 559px) {
  .ks-grid { grid-template-columns: 1fr 1fr; gap: 0 var(--space-3); }
  .ks-row { flex-direction: column; gap: 2px; }
  .ks-row dd { text-align: left; }
}

/* ================================================================ accounts ===
   The owner's Accounts table. It scrolls sideways inside its panel on a narrow
   screen, and the first column (name over email) stays put while it does, so
   the dates and status scrolled into view still sit beside whose they are. */
.ac-scroll { overflow-x: auto; overscroll-behavior-x: contain; scrollbar-width: thin; }
.ac-scroll:focus-visible { outline: 2px solid var(--focus, var(--brand)); outline-offset: 2px; }
.accounts-table th:first-child,
.accounts-table td.ac-who {
  position: sticky;
  left: 0;
  z-index: 1;
  background: var(--surface);
  box-shadow: 1px 0 0 var(--grid);
}
.accounts-table tr { transition: background 90ms ease; }
.accounts-table tr:hover td.ac-who { background: var(--surface-2); }
table.data.accounts-table td.ac-who { white-space: normal; min-width: 120px; width: min(240px, 40vw); max-width: min(240px, 40vw); }
.ac-name { display: block; color: var(--ink); }
.ac-email {
  display: block;
  color: var(--ink-muted);
  font-size: var(--t-caption);
  overflow-wrap: anywhere;
}
