/* ============================================================================
   DeviceSignal design tokens - the ONLY file in src/ allowed to contain a
   colour literal. `npm run check:tokens` enforces that.
   ========================================================================= */

:root {
  /* ---- Colour ------------------------------------------------------------
     Three jobs, and a hue may only ever do one of them.

       INTERACTIVE (blue)  Anything you can click: links, buttons, focus rings,
                           the current nav item. If it is blue, it does something.

       BRAND (teal)        Personality on marketing surfaces: eyebrows, check
                           marks, hero accents, and chart marks on series that
                           carry no severity. Never on an FDA record.

       SEVERITY (red,      How serious an FDA record is, and nothing else. A
       amber)              recall class or a complaint spike above a code's own
                           baseline. This is the rule that actually matters: a
                           reader who has learned that red means "recall" must
                           never be shown red for a full inbox or a failed form,
                           because the next red thing they skip might be a Class I.

     An earlier pass deleted the first two categories entirely, on the theory
     that colour should mean severity and nothing else. That is a real design
     position and it produced a monochrome product - the discipline that makes a
     page feel considered is confining each hue to one job, not removing hues.
     Apple's own language is neutral surfaces plus ONE vivid accent on anything
     tappable plus semantic status colours; it is restrained, not colourless.

     `scripts/check-tokens.mjs` enforces the one rule a stylesheet can be checked
     against: severity tokens may not appear in page chrome or marketing
     components. Nav, Pricing, Footer and friends carry no FDA data, so a red
     there is always a mistake.

     Bright variants are for fills, borders, icons and chart bars ONLY. Coloured
     TEXT must use the -text variant; the bright ones fail WCAG contrast on
     white. And colour is never the sole carrier of severity - see
     SEVERITY_GLYPHS in packages/core/src/severity.ts; every severity mark ships
     a glyph and a word so it survives a greyscale print and a colour-blind
     reader. */
  --ink: #0b1b2b;          /* headings, primary text */
  --ink-soft: #4a5764;     /* body text, secondary - 7.4:1 on white */
  --ink-faint: #7a8794;    /* NON-TEXT only: strike lines, hairlines, icons */
  /* Grey used as TEXT. The specified --ink-faint measures 3.67:1 on white,
     which fails WCAG 2.2 AA (4.5:1) for anything under 24px - and this page
     puts 13–15px captions in it. Same bright/-text split the design system
     already uses for amber and red. 5.08:1 on white, 4.54:1 on --mist. */
  --ink-faint-text: #62707d;
  --paper: #ffffff;        /* page background */
  --mist: #edf3f7;         /* section backgrounds, cards */
  --rule: #d9e2e9;         /* hairline borders */

  /* INTERACTIVE. Specified as #2F6BFF, which puts white 16px/600 button text at
     4.4979:1 - 0.0021 short of AA. One step on the blue channel clears it at
     4.51:1 and is not perceptibly different. */
  --blue: #2f6bfe;         /* primary buttons, links, focus rings (fills) */
  --blue-text: #2558d6;    /* blue used as TEXT - contrast safe */

  /* BRAND. Named "signal" historically; it is the brand accent, never a grade. */
  --signal: #0fb5a0;       /* fills, chart marks, accents ONLY */
  --signal-text: #0b7a6c;  /* teal used as TEXT */

  /* SEVERITY. FDA records only. */
  --alert: #f2a63b;        /* fills only - complaint spikes */
  --alert-text: #8a5a05;
  --risk: #e2574c;         /* fills only - recalls */
  --risk-text: #b3261e;

  /* ---- Severity, graded ---------------------------------------------------
     The two hues above say WHICH signal (a recall, a spike). These say HOW
     SERIOUS, which is what a reader actually scans for, and they are what
     components reference. A Class II recall and a complaint spike get the same
     treatment because they warrant the same amount of attention - under the old
     names that had to be spelled --alert for one and --risk for the other,
     which read as a contradiction.

     Class III is deliberately greyscale. "Not likely to cause adverse health
     consequences" is FDA's own wording; giving it a hue would spend the reader's
     alarm on the least serious thing on the page. Same for an unclassified
     recall, which is a NULL from the enforcement endpoint rather than a
     judgement - see the LEFT JOIN in apps/app/src/routes/insights.ts. */
  --sev-1: var(--risk);              /* Class I · elevated predicate risk */
  --sev-1-text: var(--risk-text);
  --sev-2: var(--alert);             /* Class II · complaint spike above band */
  --sev-2-text: var(--alert-text);
  --sev-3: var(--ink-faint);         /* Class III - non-text (glyph strokes) */
  --sev-3-text: var(--ink-soft);
  --sev-none-text: var(--ink-faint-text);  /* class not published */

  /* ---- Derived: interaction states -------------------------------------- */
  --blue-hover: #2b62eb;         /* --blue darkened 8% */
  --blue-tint: #f0f4ff;          /* outline-button hover wash, status pills */
  /* Faint washes behind status pills. Fills only - the pill's text uses the
     matching -text variant, checked at 5.3:1 or better over each wash. */
  --risk-wash: rgba(226, 87, 76, 0.1);
  --alert-wash: rgba(242, 166, 59, 0.14);
  /* The brand wash, for a settled state that is emphatically NOT a severity. The support
     page needs "Answered" to read as done, and every green-ish thing in this palette is
     the brand teal - a severity token there would put an FDA grade on a support ticket.
     --signal-text measures 5.6:1 over this on white. */
  --signal-wash: rgba(15, 181, 160, 0.12);
  --sev-1-wash: var(--risk-wash);
  --sev-2-wash: var(--alert-wash);
  --sev-3-wash: var(--mist);
  /* The focus ring is blue, and it is backed by paper.
     Blue alone measures 4.5:1 on white but only ~2.6:1 on the dark --ink
     sections, which fails WCAG 2.2 SC 1.4.11 exactly where a keyboard user most
     needs to find themselves. A 3px paper ring inside the blue one fixes that
     without a per-section override: on white the blue carries it, on ink the
     paper does, and the two never both disappear. */
  --focus-ring: var(--blue);
  --focus-backing: var(--paper);
  --nav-paper: rgba(255, 255, 255, 0.85);

  /* ---- Derived: text and surfaces on the dark (--ink) sections ----------- */
  --on-ink: #ffffff;
  --on-ink-body: rgba(255, 255, 255, 0.75);
  --on-ink-lead: rgba(255, 255, 255, 0.7);
  --on-ink-faint: rgba(255, 255, 255, 0.6);
  --on-ink-rule: rgba(255, 255, 255, 0.25);
  --on-ink-surface: rgba(255, 255, 255, 0.06);
  --on-ink-field-rule: rgba(255, 255, 255, 0.3);
  --risk-on-ink-text: #ff8078;   /* contrast-safe red ON DARK only */
  --risk-on-ink-fill: rgba(226, 87, 76, 0.12);
  --sev-1-on-ink-text: var(--risk-on-ink-text);
  --sev-1-on-ink-fill: var(--risk-on-ink-fill);

  /* The other three grades on dark, which only sev-1 had.
     A predicate chain renders on the landing page's --ink section as well as on the
     white code pages, and any ancestor in it can carry any class - so a Class II pill
     was resolving to --sev-2-text (#8a5a05) over a 14% amber wash on #0b1b2b, which
     measures 2.3:1. Amber needs no lightening to work on dark: the bright --alert is
     6.7:1 over its own wash there, where on white it would have failed. Class III and
     unclassified stay greyscale, as they are on white, but at the on-ink weights. */
  --alert-on-ink-text: var(--alert);
  --alert-on-ink-fill: rgba(242, 166, 59, 0.14);
  --sev-2-on-ink-text: var(--alert-on-ink-text);
  --sev-2-on-ink-fill: var(--alert-on-ink-fill);
  --sev-3-on-ink-text: var(--on-ink-body);
  --sev-3-on-ink-fill: var(--on-ink-surface);
  --sev-none-on-ink-text: var(--on-ink-faint);
  --ink-fade-0: rgba(11, 27, 43, 0);
  --ink-fade-1: rgba(11, 27, 43, 1);
  --paper-fade-0: rgba(255, 255, 255, 0);

  /* ---- Marketing surfaces: the product previews and the hero ------------
     The landing page now shows the product rather than describing it, and a
     screen drawn on a white page needs something to sit on. These are washes
     and frames for those previews and nothing else - brand and interactive
     hues at low alpha, never a severity hue, so a preview frame can never be
     mistaken for an FDA finding inside it. */
  --hero-wash-blue: rgba(47, 107, 254, 0.09);
  --hero-wash-teal: rgba(15, 181, 160, 0.10);
  --window-bar: #f5f8fa;       /* the title bar of a product preview */
  --window-rail: #f8fafc;      /* the preview's sidebar */
  --ink-raised: #12263a;       /* a card on the dark --ink section */

  /* ---- Shadows - three levels. Never coloured. Never glow. -------------- */
  --shadow-sm: 0 1px 2px rgba(11, 27, 43, 0.06);
  --shadow-md: 0 4px 16px rgba(11, 27, 43, 0.08);
  /* Product previews only: a screen floating above the page has to read as a
     screen, and the two flatter levels make it read as a card. */
  --shadow-lg: 0 24px 60px -12px rgba(11, 27, 43, 0.22), 0 2px 6px rgba(11, 27, 43, 0.06);

  /* ---- Type families ---------------------------------------------------- */
  --font-sans: "Inter", "Inter Fallback", -apple-system, BlinkMacSystemFont,
    "Segoe UI", Arial, sans-serif;
  /* Identifiers only: product codes, K-numbers, CFR citations. */
  --font-mono: "JetBrains Mono", "JetBrains Mono Fallback", ui-monospace,
    SFMono-Regular, Menlo, Consolas, monospace;

  /* ---- Type scale (desktop; mobile step is at the 768px breakpoint) ------
     Headlines always tight. Body never tracked. That contrast is what makes
     type feel expensive. */
  --fs-display: 56px;
  --lh-display: 1.08;
  --ls-display: -0.025em;

  --fs-h2: 40px;
  --lh-h2: 1.15;
  --ls-h2: -0.02em;

  --fs-h3: 24px;
  --lh-h3: 1.3;

  --fs-card-title: 18px;
  --lh-card-title: 1.35;

  --fs-lead: 21px;
  --lh-lead: 1.5;

  --fs-body: 17px;
  --lh-body: 1.6;

  /* Slightly larger body, used only for the flagship section's paragraph. */
  --fs-body-lg: 19px;
  /* One-line card body in "Who it's for". */
  --fs-body-compact: 16px;

  --fs-small: 15px;
  --lh-small: 1.5;

  --fs-eyebrow: 13px;
  --ls-eyebrow: 0.12em;

  --fs-legal: 14px;

  --fs-price: 44px;
  --fs-price-old: 24px;
  --fs-price-talk: 28px;
  --fs-cadence: 17px;

  --fs-brand: 20px;
  --fs-nav: 16px;
  --fs-mono: 15px;
  --fs-mono-chip: 14px;

  /* ---- Spacing - 8px base scale only. No arbitrary values. -------------- */
  --s-4: 4px;
  --s-8: 8px;
  --s-12: 12px;
  --s-16: 16px;
  --s-24: 24px;
  --s-32: 32px;
  --s-48: 48px;
  --s-64: 64px;
  --s-96: 96px;
  --s-128: 128px;
  --s-160: 160px;

  --section-y: var(--s-128);   /* 72px on mobile - see breakpoint below */

  /* ---- Radius ----------------------------------------------------------- */
  --radius: 8px;
  --radius-pill: 999px;

  /* ---- Layout ----------------------------------------------------------- */
  --max-w: 1200px;
  --max-text: 720px;
  --max-hero: 860px;
  --measure: 62ch;             /* ≈68 characters */
  --gutter: var(--s-24);
  --nav-h: 64px;
  --tap-min: 44px;             /* minimum interactive target */

  /* ---- Motion ----------------------------------------------------------- */
  --ease: cubic-bezier(0.2, 0, 0, 1);
  --dur-hover: 180ms;
  --dur-reveal: 400ms;
  --stagger: 60ms;

  /* ---- App surfaces (signed-in product only) -----------------------------
     The marketing site is a document read once; the app is a workspace read every
     morning, with tables in it. It gets its own canvas, depth and type steps so the
     tools can be dense without the marketing pages getting smaller. Nothing here
     changes a value above, and none of it is used by apps/web.

     Canvas behind, paper in front: every card, table and form sits on a cool, barely
     tinted ground, which is what lets a white surface read as a surface without a
     heavy border or shadow doing the work. Same blue-grey family as --mist and --rule,
     one step lighter, so the app and the marketing site are one palette. */
  --canvas: #f5f7fa;           /* signed-in page background, behind every card */
  --rule-soft: #e7edf2;        /* row dividers inside a card: quieter than --rule */
  --rule-strong: #bfcad4;      /* the border of a control (input, secondary button) */
  --rule-strong-hover: #94a2af;
  --row-hover: #f6f9fc;        /* a table row under the pointer */
  --rail-hover: #f0f4f8;       /* a nav item under the pointer - not the current one */
  --track: #e4eaf0;            /* the empty part of a progress bar or meter */
  --topbar-paper: rgba(255, 255, 255, 0.88);

  /* Two-layer shadows tinted with --ink rather than black, so depth reads as the
     same cool family as the canvas instead of a grey smudge on it. */
  --shadow-card: 0 1px 2px rgba(11, 27, 43, 0.04), 0 1px 3px rgba(11, 27, 43, 0.05);
  --shadow-raise: 0 2px 4px rgba(11, 27, 43, 0.04), 0 8px 20px -6px rgba(11, 27, 43, 0.10);
  --shadow-pop: 0 2px 6px rgba(11, 27, 43, 0.06), 0 16px 36px -10px rgba(11, 27, 43, 0.22);
  --blue-ring: rgba(47, 107, 254, 0.18);   /* the soft halo around a focused field */

  /* App type steps. Headlines stay tight; the body is a reading size for a tool,
     not for a landing page. Product codes stay monospaced at a size that sits on the
     same x-height as the Inter around them. */
  --fs-app-title: 26px;
  --fs-app-section: 18px;
  --fs-app-card: 15px;
  --fs-app-body: 15px;
  --fs-app-table: 14px;
  --fs-app-small: 13px;
  --fs-app-label: 12px;
  --fs-app-kpi: 28px;
  --fs-app-mono: 13px;

  --radius-sm: 6px;
  --radius-lg: 12px;
  --rail-w: 248px;
  --topbar-h: 56px;
  --pane-max: 1200px;
}

/* Mobile type step. Body, small, eyebrow and legal sizes never shrink -
   nothing on this page goes below 16px except the 14px legal footer and the
   13px uppercase eyebrow label, which is a label, not reading text. */
@media (max-width: 767px) {
  :root {
    --fs-display: 36px;
    --fs-h2: 30px;
    --fs-h3: 22px;
    --fs-lead: 19px;
    --fs-price: 40px;
    --section-y: 72px;
  }
}

/* ============================================================================
   Base layer: self-hosted fonts, reset, focus, shared type + layout classes.
   Every colour here comes from tokens.css.
   ========================================================================= */

/* ---- Self-hosted fonts (copied out of @fontsource by scripts/copy-fonts) --
   Never a Google CDN request: the visitor is behind a corporate VPN and a
   third-party font host is both a latency tax and a procurement question. */
@font-face {
  font-family: "Inter";
  font-style: normal;
  font-weight: 400;
  font-display: swap;
  src: url("/fonts/inter-latin-400-normal.woff2") format("woff2");
  unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA,
    U+02DC, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215,
    U+FEFF, U+FFFD;
}
@font-face {
  font-family: "Inter";
  font-style: normal;
  font-weight: 500;
  font-display: swap;
  src: url("/fonts/inter-latin-500-normal.woff2") format("woff2");
  unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA,
    U+02DC, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215,
    U+FEFF, U+FFFD;
}
@font-face {
  font-family: "Inter";
  font-style: normal;
  font-weight: 600;
  font-display: swap;
  src: url("/fonts/inter-latin-600-normal.woff2") format("woff2");
  unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA,
    U+02DC, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215,
    U+FEFF, U+FFFD;
}
@font-face {
  font-family: "Inter";
  font-style: normal;
  font-weight: 700;
  font-display: swap;
  src: url("/fonts/inter-latin-700-normal.woff2") format("woff2");
  unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA,
    U+02DC, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215,
    U+FEFF, U+FFFD;
}
@font-face {
  font-family: "JetBrains Mono";
  font-style: normal;
  font-weight: 400;
  font-display: swap;
  src: url("/fonts/jetbrains-mono-latin-400-normal.woff2") format("woff2");
  unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA,
    U+02DC, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215,
    U+FEFF, U+FFFD;
}
@font-face {
  font-family: "JetBrains Mono";
  font-style: normal;
  font-weight: 500;
  font-display: swap;
  src: url("/fonts/jetbrains-mono-latin-500-normal.woff2") format("woff2");
  unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA,
    U+02DC, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215,
    U+FEFF, U+FFFD;
}

/* Metric-matched fallbacks. font-display: swap only costs layout stability if
   the fallback has different metrics - these overrides make the swap invisible,
   which is how CLS stays at exactly 0 rather than merely "low". */
@font-face {
  font-family: "Inter Fallback";
  src: local("Arial");
  size-adjust: 107.12%;
  ascent-override: 90.49%;
  descent-override: 22.56%;
  line-gap-override: 0%;
}
@font-face {
  font-family: "JetBrains Mono Fallback";
  src: local("Courier New");
  size-adjust: 100%;
  ascent-override: 102%;
  descent-override: 30%;
  line-gap-override: 0%;
}

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

html {
  -webkit-text-size-adjust: 100%;
  scroll-behavior: smooth;
  /* Tabular numerals everywhere: prices, counts and dates must align. */
  font-feature-settings: "tnum" 1, "cv05" 1;
}

body {
  margin: 0;
  background: var(--paper);
  color: var(--ink-soft);
  font-family: var(--font-sans);
  font-size: var(--fs-body);
  line-height: var(--lh-body);
  font-weight: 400;
  -webkit-font-smoothing: antialiased;
  text-rendering: optimizeLegibility;
}

h1,
h2,
h3,
h4,
p,
figure,
blockquote,
dl,
dd {
  margin: 0;
}

ul,
ol {
  margin: 0;
  padding: 0;
  list-style: none;
}

img,
svg {
  display: block;
  max-width: 100%;
}

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

button {
  cursor: pointer;
}

/* ---- Links -------------------------------------------------------------
   Blue and underlined, both. Blue makes a link findable at a glance; the
   underline is what makes it findable to a reader who cannot see the blue, which
   is why WCAG 1.4.1 forbids the common inverse - a coloured link with no
   underline. Components that draw their own obvious affordance (a filled button,
   a bordered filter chip, a table row) opt out locally. */
a {
  color: var(--blue-text);
  text-decoration: underline;
  text-decoration-thickness: 1px;
  text-underline-offset: 2px;
}

a:hover {
  text-decoration-thickness: 2px;
}

/* ---- Focus: visible on everything, always. `outline: none` appears nowhere
   in this codebase. -------------------------------------------------------
   A blue ring at 3px, backed by a 3px paper ring inside it.

   The blue is the product's interactive colour, so the focus state says the same
   thing as every other blue on the page. The paper backing is not decoration: on
   a dark --ink section the blue drops to about 2.6:1, which fails WCAG 2.2 SC
   1.4.11 exactly where a keyboard user most needs to find themselves. Those
   sections use hashed CSS-module class names, so no global selector can reach in
   and swap the colour - the ring has to work without knowing its background. It
   does: on white the blue carries it, on ink the paper does, and neither
   disappears at the same time as the other.

   The box-shadow does replace a component's own resting shadow while focused.
   That is deliberate - --shadow-sm is 6% ink and would read as a smudge behind
   the ring rather than as elevation. */
:focus-visible {
  outline: 3px solid var(--focus-ring);
  outline-offset: 3px;
  box-shadow: 0 0 0 3px var(--focus-backing);
}

/* ---- Utilities ---------------------------------------------------------- */
.sr-only {
  position: absolute;
  width: 1px;
  height: 1px;
  padding: 0;
  margin: -1px;
  overflow: hidden;
  clip: rect(0, 0, 0, 0);
  white-space: nowrap;
  border: 0;
}

.container {
  width: 100%;
  max-width: var(--max-w);
  margin-inline: auto;
  padding-inline: var(--gutter);
}

.section {
  padding-block: var(--section-y);
}

.measure {
  max-width: var(--measure);
}

.eyebrow {
  font-size: var(--fs-eyebrow);
  font-weight: 600;
  letter-spacing: var(--ls-eyebrow);
  text-transform: uppercase;
  margin: 0 0 var(--s-16);
  color: var(--ink-faint-text);
}

.h2 {
  font-size: var(--fs-h2);
  line-height: var(--lh-h2);
  letter-spacing: var(--ls-h2);
  font-weight: 700;
  color: var(--ink);
}

.h3 {
  font-size: var(--fs-h3);
  line-height: var(--lh-h3);
  font-weight: 600;
  color: var(--ink);
}

.lead {
  font-size: var(--fs-lead);
  line-height: var(--lh-lead);
  font-weight: 400;
  color: var(--ink-soft);
}

.mono {
  font-family: var(--font-mono);
  font-feature-settings: "tnum" 1;
}

/* Scroll targets sit below the 64px sticky nav. */
[id] {
  scroll-margin-top: calc(var(--nav-h) + var(--s-24));
}

/* ---- Scroll reveal ------------------------------------------------------
   Gated on the `js` class (set synchronously by the entry module before first
   paint), so with JavaScript disabled every section is simply visible. */
/* The 16px rise uses `translate`, not `transform`, so a card's hover lift and
   its reveal never fight over the same property. */
html.js [data-reveal] {
  opacity: 0;
  translate: 0 16px;
  transition: opacity var(--dur-reveal) var(--ease),
    translate var(--dur-reveal) var(--ease);
  transition-delay: var(--reveal-delay, 0ms);
  will-change: opacity, translate;
}

html.js [data-reveal="in"] {
  opacity: 1;
  translate: none;
  will-change: auto;
}

/* Printing never scrolls, so the observer never fires for most of the page.
   Without this, a printed copy comes out with only the hero on it. */
@media print {
  html.js [data-reveal] {
    opacity: 1 !important;
    translate: none !important;
  }
}

/* ---- Reduced motion: mandatory, and it must never hide content ---------- */
@media (prefers-reduced-motion: reduce) {
  html {
    scroll-behavior: auto;
  }

  *,
  *::before,
  *::after {
    transition: none !important;
    animation: none !important;
    transform: none !important;
  }

  /* Reduced motion must never mean hidden content. */
  html.js [data-reveal] {
    opacity: 1 !important;
    translate: none !important;
  }
}

/* ---- Forced colours (Windows High Contrast and friends) -----------------
   The reader has told the operating system which colours they can see. That
   instruction outranks every decision in tokens.css, so this block hands the
   palette over rather than defending it.

   It exists because an axe run under forced-colors emulation put --ink
   (#0b1b2b) on a forced black canvas at 1.2:1 - near-black text on black. Any
   place this stylesheet names an ink is a place that can happen, and there are
   too many to enumerate, so the reset is deliberately broad. In this mode blunt
   is correct: the OS palette is meant to win.

   Real Windows High Contrast has NOT been verified - this was written and
   reviewed against Playwright's emulation, on macOS, which is not the same
   thing. Treat it as a considered starting point, not a tested result. */
@media (forced-colors: active) {
  html {
    color: CanvasText;
    background: Canvas;
  }

  /* Anything that named an ink gets the system's instead. Links are excluded so
     they keep LinkText below and stay distinguishable from body copy.
     `!important` for the same reason the reduced-motion block above uses it: a
     bare `*:not(a)` carries the specificity of a type selector and loses to every
     `.brand` and `.rail-link` in the stylesheets, which is precisely the rule set
     that has to give way here. */
  *:not(a) {
    color: CanvasText !important;
  }

  a {
    color: LinkText !important;
  }

  /* Highlight is the one colour guaranteed to contrast with Canvas in every
     high-contrast theme. The paper backing ring is dropped: box-shadow is not
     painted in forced-colors mode, so leaving it in would imply a second ring
     that is not there. */
  :focus-visible {
    outline: 3px solid Highlight;
    outline-offset: 3px;
    box-shadow: none;
  }

  /* Severity loses its hue here, which is exactly the case the glyph and the
     label were designed for. The border thickens so the pill still reads as a
     distinct object once its wash is gone. */
  .sev {
    border: 2px solid CanvasText;
  }

  /* Buttons lose their ink fill, so they need a drawn edge to still look
     pressable. ButtonText/ButtonFace are the system's own pair for this. */
  button,
  .button,
  .button-quiet {
    border: 1px solid ButtonText;
    color: ButtonText !important;
    background: ButtonFace;
  }
}

/* ---- The footer sits on the bottom edge -----------------------------------
   A short page - the 404, a thin prose page, an error - used to end wherever its
   content ended, leaving the footer floating in the middle of a tall window with
   blank paper below it. That reads as a page that failed to load the rest of
   itself, which on the one page whose job is to say "this URL is wrong, here is
   where to go instead" is the worst place to look broken.

   So the document fills the viewport and `main` takes the slack: the footer is
   pushed to the bottom edge when there is not enough content to reach it, and is
   pushed off the bottom by content when there is. It is NOT `position: fixed` -
   a 120px band of legal text pinned over every scroll of a 200-row table would
   cost more than it buys, and would need padding compensation on every page.

   `#root` rather than `body`: the app's signed-in shell is a CSS grid on the body
   and already does this with `grid-template-rows: 1fr auto` (see app.css). This
   selector matches only the marketing site's React container, so the two cannot
   fight. `main` is an only-child of the flex container on every page here.
   ------------------------------------------------------------------------- */
#root {
  display: flex;
  flex-direction: column;
  min-height: 100vh;
}

#root > main {
  /* Grow to fill, never shrink below its content, ignore the flex-basis shorthand
     trap where `flex: 1` would set basis to 0 and collapse a tall page. */
  flex: 1 0 auto;
}

/* ---- Print ---------------------------------------------------------------
   The marketing site had almost no print handling: two narrow rules and
   nothing else, so a printed code page came out with the sticky nav floating
   over the first section, the footer nav in the middle of the paper, and every
   link's destination invisible. A regulatory reader prints these and files
   them, so this is a real output medium rather than a courtesy.

   apps/app/public/report.css is the purpose-built A4 stylesheet for the
   Client-Ready PDF; this is the general case for every other page. -------- */
@media print {
  @page {
    /* Generous, because a printed page gets annotated in the margin. */
    margin: 16mm 14mm 18mm;
  }

  html {
    /* Points, not pixels: a printer has no CSS pixel and browsers guess. */
    font-size: 10.5pt;
  }

  body {
    background: var(--paper);
    color: var(--ink);
  }

  /* Chrome is navigation, and paper cannot navigate. */
  nav[aria-label="Primary"],
  header[role="banner"],
  footer nav,
  .skip {
    display: none;
  }

  /* Where a link went is the whole value of a link on paper. Only absolute and
     root-relative hrefs - expanding an in-page #anchor would print "(#faq)",
     which is noise, and a javascript: or mailto: reads fine as its own text. */
  a[href^="http"]::after,
  a[href^="/"]::after {
    content: " (" attr(href) ")";
    font-size: 0.85em;
    font-weight: 400;
    word-break: break-all;
    color: var(--ink-soft);
  }

  /* A heading alone at the foot of a page is the clearest tell of a document
     nobody laid out. */
  h1,
  h2,
  h3 {
    break-after: avoid;
    page-break-after: avoid;
  }

  /* Two lines of a paragraph stranded either side of a page break. */
  p,
  li,
  figure,
  table {
    orphans: 3;
    widows: 3;
  }

  table {
    break-inside: auto;
  }

  tr {
    break-inside: avoid;
    page-break-inside: avoid;
  }

  /* A long table's header repeated on page four, which is where it is needed. */
  thead {
    display: table-header-group;
  }

  /* Anything that scrolls sideways on screen is clipped at the paper edge.
     Unwrapping it lets the content wrap instead of vanishing. */
  [class*="scroller"],
  [class*="tableWrap"],
  [class*="chart-wrap"] {
    overflow: visible !important;
  }

  /* Severity is the one thing on the page that still gets colour, so it is the
     one thing that must not be dropped to save toner. */
  [class*="sev"],
  [class*="tone1"],
  [class*="tone2"],
  [class*="tone3"] {
    -webkit-print-color-adjust: exact;
    print-color-adjust: exact;
  }

  /* The viewport-height floor is a screen idea. On paper it means "at least one
     screen tall", which on a short document is a second, blank sheet. */
  #root {
    display: block;
    min-height: 0;
  }
}

/*
 * App styles.
 *
 * Colour literals live only in packages/tokens/src/tokens.css, which is concatenated
 * ahead of this file at request time - the repo-wide token guard checks that rule, so
 * a hex code here would fail the build. Everything below is layout and type.
 */

.skip {
  position: absolute;
  top: var(--s-8);
  left: var(--s-8);
  z-index: 30;
  display: inline-flex;
  align-items: center;
  min-height: var(--tap-min);
  padding: var(--s-8) var(--s-16);
  border: 1px solid var(--rule);
  border-radius: var(--radius);
  background: var(--paper);
  box-shadow: var(--shadow-md);
  color: var(--ink);
  font-size: var(--fs-nav);
  font-weight: 600;
  text-decoration: none;
  clip-path: inset(50%);
}

.skip:focus-visible {
  clip-path: none;
}

.container {
  width: 100%;
  max-width: var(--max-w);
  margin-inline: auto;
  padding-inline: var(--gutter);
}

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

.chrome {
  border-bottom: 1px solid var(--rule);
  background: var(--nav-paper);
  position: sticky;
  top: 0;
  z-index: 20;
}

.chrome-inner {
  display: flex;
  align-items: center;
  gap: var(--s-24);
  min-height: var(--nav-h);
}

.brand {
  display: inline-flex;
  align-items: center;
  gap: var(--s-8);
  font-size: var(--fs-brand);
  font-weight: 700;
  letter-spacing: -0.01em;
  /* Kept for the text fallback only - the mark is an <img> now. */
  color: var(--ink);
  text-decoration: none;
  margin-right: auto;
}

/* The supplied wordmark, which already contains the name - so there is no sibling
   text span to keep in step with it, and the accessible name is the alt attribute.
   Height-driven with an intrinsic width, like the marketing nav.

   Every height here dropped by an eighth when the lockup was replaced. The old file
   carried 19px of transparent padding above the artwork and 8px below on a 128px
   canvas, so a CSS height of 26px only ever drew about 21px of logo; the new one is
   trimmed tight, and 21px of it draws the same 21px. Match the numbers below against
   the artwork, not against each other. */
.brand-logo {
  display: block;
  height: 21px;
  width: auto;
}

.nav {
  display: flex;
  align-items: center;
  gap: var(--s-8);
  margin: 0;
  padding: 0;
  list-style: none;
}

.nav-link {
  display: inline-flex;
  align-items: center;
  min-height: var(--tap-min);
  padding: 0 var(--s-12);
  border-radius: var(--radius);
  font-size: var(--fs-nav);
  font-weight: 600;
  color: var(--ink-soft);
  text-decoration: none;
}

.nav-link:hover {
  background: var(--mist);
  color: var(--ink);
}

/* The current page is marked by weight, colour and a rule - and by
   `aria-current="page"` in the markup for anyone who cannot see any of the three. */
.nav-link-current {
  color: var(--ink);
  font-weight: 600;
  box-shadow: inset 0 -2px 0 var(--blue);
}

.chrome-link {
  font-size: var(--fs-nav);
  font-weight: 600;
  color: var(--blue-text);
}

.signout {
  margin: 0;
}

/* ------------------------------------------------------------------ text */

main {
  display: block;
  padding-block: var(--s-48) var(--s-96);
  min-height: 60vh;
}

.page-head {
  display: flex;
  align-items: flex-start;
  gap: var(--s-24);
  flex-wrap: wrap;
  margin-bottom: var(--s-32);
}

.page-head-text {
  max-width: var(--max-text);
}

/* Actions sit beside the heading rather than under it, so a code page's exports are
   reachable without scrolling past the paragraph that explains the code. */
.page-actions {
  display: flex;
  gap: var(--s-8);
  flex-wrap: wrap;
  margin-left: auto;
  padding-top: var(--s-4);
}

.h1 {
  margin: 0 0 var(--s-12);
  font-size: var(--fs-h2);
  line-height: var(--lh-h2);
  font-weight: 700;
  letter-spacing: var(--ls-h2);
  color: var(--ink);
}

.h2 {
  margin: 0 0 var(--s-16);
  font-size: var(--fs-h3);
  line-height: var(--lh-h3);
  font-weight: 700;
  color: var(--ink);
}

.h3 {
  margin: var(--s-24) 0 var(--s-8);
  font-size: var(--fs-card-title);
  line-height: var(--lh-card-title);
  font-weight: 700;
  color: var(--ink);
}

.lead {
  margin: 0;
  max-width: var(--measure);
  font-size: var(--fs-lead);
  line-height: var(--lh-lead);
  color: var(--ink-soft);
}

.body {
  margin: 0 0 var(--s-16);
  max-width: var(--measure);
  font-size: var(--fs-body);
  line-height: var(--lh-body);
  color: var(--ink-soft);
}

.small {
  font-size: var(--fs-small);
  line-height: var(--lh-small);
  color: var(--ink-faint-text);
}

.mono {
  font-family: var(--font-mono);
  font-size: var(--fs-mono);
}

.legal {
  margin: 0 0 var(--s-8);
  font-size: var(--fs-legal);
  line-height: var(--lh-small);
  color: var(--ink-faint-text);
}

.legal a {
  color: var(--ink-soft);
}

.foot {
  border-top: 1px solid var(--rule);
  padding-block: var(--s-32);
  background: var(--mist);
}

.foot-brand {
  display: inline-flex;
  align-items: center;
  min-height: var(--tap-min);
  margin-bottom: var(--s-16);
}

.foot-brand .brand-logo {
  height: 26px;
}

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

.panel {
  padding: var(--s-24);
  border: 1px solid var(--rule);
  border-radius: var(--radius-lg);
  background: var(--paper);
  box-shadow: var(--shadow-card);
}

.panel + .panel {
  margin-top: var(--s-16);
}

/* A panel reporting that nothing happened.
   Quieter than a default panel on purpose: "nothing changed" is the commonest state and
   the product's own argument for existing, so it should read as calm rather than as an
   empty result. */
.panel-quiet {
  border-color: var(--rule);
  background: var(--mist);
  box-shadow: none;
}

/* ------------------------------------------------------ since your last visit

   A changelog, not a table. Each row is one published record, and the four parts read
   left to right in the order a reader scans them: what kind of thing, which code, what
   it says, and the FDA id plus date that let them check it. */

.change-list {
  display: flex;
  flex-direction: column;
  gap: 0;
  margin: var(--s-16) 0 0;
  padding: 0;
  list-style: none;
}

.change {
  display: grid;
  grid-template-columns: auto auto 1fr;
  align-items: baseline;
  gap: var(--s-8) var(--s-12);
  padding-block: var(--s-12);
  font-size: var(--fs-small);
  line-height: var(--lh-small);
}

.change + .change {
  border-top: 1px solid var(--rule);
}

/* The kind, as a word rather than only a colour. A recall row carries FDA's published
   class where there is one, so the label is "Class II" rather than a hue the reader has
   to have learned. */
.change-kind {
  padding: 2px var(--s-8);
  border: 1px solid var(--rule);
  border-radius: var(--radius-pill);
  font-size: var(--fs-eyebrow);
  font-weight: 600;
  letter-spacing: var(--ls-eyebrow);
  text-transform: uppercase;
  white-space: nowrap;
  color: var(--ink-faint-text);
  background: var(--paper);
}

/* Severity hues, and only here: a recall IS an FDA record, which is the one thing this
   palette allows red to mean. A clearance is not a severity, so it stays neutral. */
.change-kind-recall {
  color: var(--sev-1-text);
  border-color: var(--sev-1);
  background: var(--sev-1-wash);
}

.change-code {
  font-weight: 600;
  color: var(--blue-text);
  white-space: nowrap;
}

.change-detail {
  color: var(--ink-soft);
  /* Long recall reasons are a paragraph; two lines is enough to identify one and the
     code page carries the whole text. */
  display: -webkit-box;
  -webkit-line-clamp: 2;
  -webkit-box-orient: vertical;
  overflow: hidden;
}

.change-meta {
  grid-column: 1 / -1;
  display: flex;
  flex-wrap: wrap;
  gap: var(--s-8);
  font-size: var(--fs-legal);
  color: var(--ink-faint-text);
}

@media (min-width: 768px) {
  .change {
    grid-template-columns: auto auto 1fr auto;
  }

  .change-meta {
    grid-column: auto;
    justify-content: flex-end;
    white-space: nowrap;
  }
}

/* A panel that is asking for one specific action, not reporting a state.
   Used by the post-sign-in "confirm the watch you asked for" step, where the whole
   reason the reader is on this page is that button. Blue rather than a severity hue:
   it is an invitation, and severity colours in this product mean FDA records. */
.panel-invite {
  border-color: var(--blue);
  background: var(--blue-tint);
}

/* A finding. One accent edge, the same device the marketing site uses.
 *
 * The edge used to default to brand blue and turn red when severe, which meant
 * the severity contrast on this page was blue-versus-red. That was the weakest
 * part of the old palette: blue was also every button and every link, so a red
 * edge only read as "not the usual colour" rather than as "recall". The default
 * is now --rule - the same hairline as the rest of the card, i.e. no edge at
 * all - so a coloured edge is genuinely exceptional.
 *
 * The edge is never the only carrier. Every finding also renders a .sev pill
 * with a glyph and a word; see packages/core/src/severity.ts. */
/* The severity edge is an inset shadow rather than a border, so it follows the card's
   rounded corners instead of squaring them off, and the card keeps one hairline all the
   way round. Same hue, same width, same meaning as before. */
.finding {
  --edge: var(--rule);
  padding: var(--s-16) var(--s-24) var(--s-16) calc(var(--s-24) + 4px);
  border: 1px solid var(--rule);
  border-radius: var(--radius-lg);
  background: var(--paper);
  box-shadow: inset 4px 0 0 var(--edge), var(--shadow-card);
}

.finding + .finding {
  margin-top: var(--s-12);
}

.finding-sev-1 {
  --edge: var(--sev-1);
}

.finding-sev-2 {
  --edge: var(--sev-2);
}

/* Class III and unclassified keep the hairline. Both are real findings and both
   are listed; neither earns a hue it would then have to share with a Class I. */
.finding-sev-3 {
  --edge: var(--sev-3);
}

.finding-head {
  display: flex;
  flex-wrap: wrap;
  align-items: baseline;
  gap: var(--s-8) var(--s-12);
  margin: 0 0 var(--s-8);
}

.finding-code {
  font-family: var(--font-mono);
  font-size: var(--fs-card-title);
  font-weight: 700;
  color: var(--ink);
  text-decoration: none;
}

.finding-device {
  font-size: var(--fs-body-compact);
  color: var(--ink-soft);
}

/* A neutral pill. Carries NO severity meaning - it is also "Your plan" on the
   account page and "Muted" on the watch list, so it cannot be trusted to mean
   anything on its own. Severity uses .sev below. */
.band {
  display: inline-block;
  padding: 2px var(--s-8);
  border: 1px solid var(--rule);
  border-radius: var(--radius-pill);
  font-size: var(--fs-eyebrow);
  font-weight: 600;
  letter-spacing: var(--ls-eyebrow);
  text-transform: uppercase;
  color: var(--ink-faint-text);
  background: var(--mist);
  white-space: nowrap;
}

/* ------------------------------------------------------------- severity pill
 * Glyph + words + tone, and the words are not optional. See
 * packages/core/src/severity.ts for why: a mark that renders the shape and the
 * hue without the label would be colour alone, which fails WCAG 2.2 SC 1.4.1 and
 * fails a reader on a projector, on a greyscale printout, or with the ~8%
 * prevalence of red-green colour blindness in this audience.
 *
 * The glyph is aria-hidden. It is a duplicate of the label, not a supplement to
 * it, so announcing both would just say everything twice.
 */
.sev {
  display: inline-flex;
  align-items: center;
  gap: 6px;
  padding: 2px var(--s-8);
  border: 1px solid var(--rule);
  border-radius: var(--radius-pill);
  font-size: var(--fs-eyebrow);
  font-weight: 600;
  letter-spacing: var(--ls-eyebrow);
  text-transform: uppercase;
  white-space: nowrap;
}

.sev-glyph {
  width: 14px;
  height: 14px;
  flex: none;
  fill: currentColor;
}

/* Class III and unclassified. Outlined rather than filled, so the grade is still
   legible once both hues have flattened to the same grey on paper. */
.sev-glyph-outline {
  fill: none;
  stroke: currentColor;
  stroke-width: 2.25;
  stroke-linecap: round;
}

/* Text on wash, measured: #b3261e on the 10% red wash is 5.6:1, #8a5a05 on the
   14% amber wash is 5.4:1. Both clear AA for 13px. */
.sev-1 {
  color: var(--sev-1-text);
  background: var(--sev-1-wash);
  border-color: var(--sev-1);
}

.sev-2 {
  color: var(--sev-2-text);
  background: var(--sev-2-wash);
  border-color: var(--sev-2);
}

.sev-3 {
  color: var(--sev-3-text);
  background: var(--sev-3-wash);
  border-color: var(--rule);
}

.finding-summary {
  margin: 0;
  max-width: var(--measure);
  font-size: var(--fs-body);
  line-height: var(--lh-body);
  color: var(--ink);
}

/* The card's opening sentence: which code, and how many findings.

   Quieter than the findings themselves, because it is orientation rather than news -
   the reader already knows which code they watch, and every card on the screen opens
   the same way. */
.finding-lead {
  margin: 0;
  max-width: var(--measure);
  font-size: var(--fs-body-compact);
  line-height: var(--lh-body);
  color: var(--ink-faint-text);
}

/* One row per finding.

   This card was one paragraph containing every finding, followed by a grey line of
   every record id with nothing saying which id belonged to which. Eight watched codes
   made eight identical-looking blocks; the news was in there and could not be found.
   The words are the same - see composeParts in packages/core - and only the shape
   changed. */
.finding-list {
  list-style: none;
  margin: var(--s-12) 0 0;
  padding: 0;
  display: flex;
  flex-direction: column;
  gap: var(--s-8);
}

.finding-item {
  display: grid;
  /* A fixed label column so the sentences line up down the card and the eye can travel
     the kinds without reading the prose. It collapses below, where there is no room to
     spend on it. */
  grid-template-columns: 8.5rem minmax(0, 1fr);
  gap: var(--s-4) var(--s-16);
  padding-top: var(--s-8);
  border-top: 1px solid var(--rule);
}

.finding-item:first-child {
  padding-top: 0;
  border-top: 0;
}

.finding-kind {
  font-size: var(--fs-eyebrow);
  font-weight: 600;
  letter-spacing: var(--ls-eyebrow);
  text-transform: uppercase;
  color: var(--ink-faint-text);
  /* Aligned with the first line of the sentence beside it rather than its box. */
  padding-top: 3px;
}

.finding-detail {
  max-width: var(--measure);
  font-size: var(--fs-body-compact);
  line-height: var(--lh-body);
  color: var(--ink);
}

@media (max-width: 40rem) {
  .finding-item {
    grid-template-columns: minmax(0, 1fr);
  }
  .finding-kind { padding-top: 0; }
}

/* The FDA record ids under one finding on a card.
 *
 * A run of monospaced ids rather than a list: they are citations for the sentence
 * above them, and giving each one its own row would make a three-record finding
 * four lines tall on the screen somebody scans fastest. Dimmer and a step smaller
 * than the finding, so the sentence stays the thing being read. */
.finding-records {
  display: block;
  margin-top: var(--s-4);
  font-size: var(--fs-mono-chip);
  line-height: 1.8;
  word-break: break-word;
}

.finding-records .fda-id {
  margin-right: var(--s-8);
  color: var(--ink-faint-text);
}

.finding-records .fda-id:hover,
.finding-records .fda-id:focus-visible {
  color: var(--blue-text);
}

.records {
  margin: var(--s-12) 0 0;
  font-family: var(--font-mono);
  font-size: var(--fs-mono-chip);
  line-height: 1.7;
  color: var(--ink-faint-text);
  word-break: break-word;
}

/* ------------------------------------------------------------------ grid */

.grid {
  display: grid;
  gap: var(--s-16);
  grid-template-columns: repeat(auto-fit, minmax(13rem, 1fr));
}

/* A KPI tile: a small tracked label, then the number as the largest thing in it.
   Tabular figures so a row of four tiles lines its digits up, and a column layout so
   a note under the number sits in the same place on every tile. */
.stat {
  display: flex;
  flex-direction: column;
  gap: var(--s-4);
  padding: var(--s-16) var(--s-24) var(--s-16) var(--s-16);
  border: 1px solid var(--rule);
  border-radius: var(--radius-lg);
  background: var(--paper);
  box-shadow: var(--shadow-card);
}

/* --------------------------------------------------------------- plan cards

   The three paid plans on /account, and the one screen in this app where the
   reader is deciding whether to spend money.

   They used to be three identical `.panel`s: same border, same weight, a plain
   bulleted list, and a price set in the body face. Three identical cards make the
   reader compare five bullet lists to find the difference, which is work, and work
   at the moment of deciding costs the sale. So the shape does the comparing:

     - One card is featured. Accent border, solid button, and it is the $49 pass,
       because that is the likeliest first purchase and the least commitment.
     - The price is the largest thing on the card, at the same --fs-price the
       marketing site uses, so the two surfaces agree about what a plan costs.
     - Every card lists the SAME rows in the SAME order, included or not, so the
       eye can scan horizontally instead of reading three lists.
     - The card you are on is marked and has no button.

   `align-items: stretch` on the grid plus a column layout with the foot pushed to
   the bottom keeps all three buttons on one line however long the copy runs. */

/* A heading with a line of orientation under it, for the sections that are a group of
   cards rather than a panel. The h2 alone left the reader to work out from three price
   tags what the three cards had in common. */
.section-head {
  margin-bottom: var(--s-16);
  max-width: var(--measure);
}

.section-head > .small {
  margin: var(--s-4) 0 0;
  color: var(--ink-soft);
}

/* ------------------------------------------------------------------ switch

   Two links dressed as one control: monthly, or pay up front.

   A segmented control rather than a checkbox or a pair of buttons, because that is what
   the thing is - two mutually exclusive views of the same page, each with its own URL.
   The inset track and the raised active segment are the platform's own idiom for it. */
.switch {
  display: inline-flex;
  padding: 3px;
  margin-bottom: var(--s-12);
  border: 1px solid var(--rule);
  border-radius: 999px;
  background: var(--mist);
}

.switch-option {
  display: block;
  padding: var(--s-8) var(--s-24);
  border-radius: 999px;
  font-size: var(--fs-nav);
  font-weight: 600;
  line-height: 1.2;
  color: var(--ink-soft);
  text-decoration: none;
  white-space: nowrap;
  /* Colour only. The active segment already moves against the track by gaining a
     background, and animating position on a link that navigates would animate after the
     page has changed - which reads as lag rather than as feedback. */
  transition: background var(--dur-hover, 180ms) ease, color var(--dur-hover, 180ms) ease;
}

.switch-option:hover { color: var(--ink); }

/* Selected. Three carriers, not one: the raised paper, the ink weight, and aria-pressed
   for anybody who sees neither. */
.switch-option-on {
  background: var(--paper);
  color: var(--ink);
  box-shadow: var(--shadow-sm);
}

/* The line under the switch that says what choosing the other one is worth. A control
   whose options are unlabelled asks the reader to click each to find out. */
.switch-note {
  max-width: var(--measure);
  margin-top: 0;
  margin-bottom: var(--s-24);
}

.plan-grid {
  display: grid;
  gap: var(--s-16);
  grid-template-columns: repeat(auto-fit, minmax(17rem, 1fr));
  align-items: stretch;
}

/* One card should not stretch to the full width of a dashboard pane: a $149 price on a
   1,180px card reads as a banner rather than a plan. The committed side keeps auto-fit,
   which lands the three terms on one row and wraps them together on a narrow screen. */
.plan-grid-monthly {
  grid-template-columns: minmax(0, 22rem);
}

.plan-card {
  display: flex;
  flex-direction: column;
  padding: var(--s-24);
  border: 1px solid var(--rule);
  border-radius: var(--radius);
  background: var(--paper);
  box-shadow: var(--shadow-sm);
  transition: box-shadow var(--dur-hover, 180ms) ease, transform var(--dur-hover, 180ms) ease;
}

/* A lift on hover, matching the marketing site's cards. Suppressed under reduced
   motion by the global rule in base.css, which sets transform: none. */
.plan-card:hover {
  box-shadow: var(--shadow-md);
  transform: translateY(-2px);
}

/* The recommended card. Two carriers, not one: a heavier accent border AND the solid
   button, so it still reads as the recommendation in a greyscale print. */
.plan-card-featured {
  border-color: var(--blue);
  box-shadow: var(--shadow-md);
}

/* The plan you are on. Deliberately the quietest of the three - it is a statement of
   fact, not an offer, and it should not compete with the two cards that are. */
.plan-card-current {
  border-color: var(--rule);
  background: var(--mist);
  box-shadow: none;
}

.plan-card-current:hover {
  transform: none;
  box-shadow: none;
}

.plan-head {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--s-8);
  flex-wrap: wrap;
  margin-bottom: var(--s-16);
}

.plan-name {
  margin: 0;
  font-size: var(--fs-card-title);
  line-height: var(--lh-card-title);
  font-weight: 700;
  color: var(--ink);
}

.band-current {
  color: var(--blue-text);
  background: var(--blue-tint);
  border-color: var(--blue);
}

.plan-price {
  display: flex;
  align-items: baseline;
  gap: var(--s-8);
  margin: 0 0 var(--s-8);
}

.plan-amount {
  font-size: var(--fs-price, 40px);
  line-height: 1;
  font-weight: 700;
  letter-spacing: -0.02em;
  color: var(--ink);
}

.plan-cadence {
  font-size: var(--fs-small);
  color: var(--ink-faint-text);
}

.plan-line {
  margin: 0 0 var(--s-16);
  font-size: var(--fs-small);
  line-height: var(--lh-small);
  color: var(--ink-soft);
}

.plan-features {
  margin: 0;
  padding: 0;
  list-style: none;
  display: flex;
  flex-direction: column;
  gap: var(--s-8);
}

/* The shared list, out of the cards and into its own panel so it can use the width.

   `display: block` is doing real work here rather than restating a default: `columns` has
   no effect on a flex container, and `.plan-features` is one - so the first version of
   this rule set `columns: 2` on a flex box and silently rendered a single column. The row
   gap has to come back as a margin for the same reason, since `gap` goes with the flex
   context that is being given up. */
.plan-features-shared {
  display: block;
  columns: 2;
  column-gap: var(--s-32);
}

.plan-features-shared .plan-row {
  /* `columns` will happily break a row across the column boundary, putting a tick at the
     bottom of one column and its label at the top of the next. */
  break-inside: avoid;
  margin-bottom: var(--s-8);
}

/* One column where two would leave four words per line. */
@media (max-width: 44rem) {
  .plan-features-shared { columns: 1; }
}

.plan-row {
  display: grid;
  grid-template-columns: 18px 1fr;
  gap: var(--s-8);
  align-items: start;
  font-size: var(--fs-small);
  line-height: var(--lh-small);
  color: var(--ink-soft);
}

/* An excluded row: struck through in colour weight rather than with a line, which
   would make the words harder to read for the sake of a decoration. The glyph is a
   dash rather than a tick, and the screen-reader text says "Not included". */
.plan-row-out {
  color: var(--ink-faint-text);
}

.plan-glyph {
  width: 18px;
  height: 18px;
  /* Optical alignment: the cap height of 15px type sits a shade below the box top. */
  margin-top: 2px;
  color: var(--signal-text);
}

.plan-row-out .plan-glyph {
  color: var(--ink-faint);
}

/* A qualifier under a feature row's label.

   One row needs it and the design has to allow for it rather than leave the label to
   imply something untrue: the free plan does send a daily email about its one watched
   code, so "Daily Intelligence Digest" sitting greyed in the locked list would otherwise
   read as "nothing arrives", which is the opposite of what every public code page
   promises. Legal size and faint, so it reads as the footnote it is and does not compete
   with the label above it. */
.plan-row-note {
  display: block;
  margin-top: var(--s-4);
  font-size: var(--fs-legal);
  line-height: var(--lh-small);
  color: var(--ink-faint-text);
}

/* -------------------------------------------------------------- watching line

   "Watching 1 of 1 free codes", on every dashboard view including the one where the
   count is zero. A free plan's whole ceiling is one code, so the difference between
   "set up" and "full" is a single click and cannot be read off a list with one row in
   it - the ratio has to be in words, before it is reached.

   Two states. Under the limit it is a quiet fact in the muted colour; at the limit it
   takes the interactive border and tint, which is the same treatment .locked uses for
   the panel that follows it further down the page. Never a severity hue: red and amber
   in this product mean an FDA record, and a full watch list is not one. */
.watching-line {
  display: inline-block;
  margin: 0 0 var(--s-16);
  padding: var(--s-4) var(--s-12);
  border: 1px solid var(--rule);
  border-radius: var(--radius);
  font-size: var(--fs-small);
  line-height: var(--lh-small);
  font-weight: 600;
  color: var(--ink-soft);
}

.watching-line-full {
  border-color: var(--blue);
  background: var(--blue-tint);
  color: var(--blue-text);
}

/* ------------------------------------------------------------ locked on free

   What a free account that has spent its one watch is not getting, and the button that
   ends it. Greyed rather than hidden: somebody who has watched one code has seen the
   product work once, and "what else is there" is a question a menu they cannot read does
   not answer.

   Blue-tinted like .panel-invite, and for the same stated reason - this panel is asking
   for one specific action rather than reporting a state, and severity hues in this
   product mean FDA records. */
.locked {
  border-color: var(--blue);
  background: var(--blue-tint);
}

/* The list and the button side by side, because the button is the answer to the list.
   Wraps to one column below roughly 40rem, where the button lands under the list at full
   width - the same order read top to bottom. */
.locked-inner {
  display: flex;
  flex-wrap: wrap;
  align-items: flex-start;
  gap: var(--s-24);
  margin-top: var(--s-16);
}

.locked-list {
  flex: 1 1 20rem;
}

.locked-action {
  flex: 0 1 17rem;
  margin: 0;
}

/* Full width of its own column so the call to action is the widest thing in it, and
   never narrower than the greyed list it is answering. */
.locked-cta {
  width: 100%;
  /* `.button` sets `padding: 0 var(--s-24)` and leans on `min-height` for its height,
     which is right for a two-word label and wrong for this one: "Unlock everything –
     Founding Member $99/mo" wraps to two lines in the 17rem column and to three at
     390px, and with no block padding the text would sit against the fill's edges. */
  padding-block: var(--s-12);
  text-align: center;
}

/* What the price becomes, in the same panel as the price. An introductory rate that hides
   what it renews at is a misleading omission under the UK CPRs and the EU UCPD, both of
   which reach this product because Paddle sells into them as Merchant of Record - the
   same argument .upsell-terms carries, and the reason it is not optional here either. */
.locked-terms {
  display: block;
  margin-top: var(--s-8);
  font-size: var(--fs-legal);
  line-height: var(--lh-small);
  color: var(--ink-faint-text);
}

.plan-note {
  margin: var(--s-12) 0 0;
  font-size: var(--fs-legal);
  line-height: var(--lh-small);
  color: var(--ink-faint-text);
}

.plan-note-current {
  margin-top: 0;
}

/* Pushed to the bottom so the buttons line up across cards of different heights. */
.plan-foot {
  margin-top: auto;
  padding-top: var(--s-24);
}

.plan-cta {
  width: 100%;
}

.stat dt {
  margin: 0;
  font-size: var(--fs-eyebrow);
  font-weight: 600;
  letter-spacing: var(--ls-eyebrow);
  text-transform: uppercase;
  color: var(--ink-faint-text);
}

.stat dd {
  margin: 0;
  font-size: var(--fs-app-kpi);
  line-height: 1.15;
  font-weight: 700;
  letter-spacing: -0.02em;
  font-variant-numeric: tabular-nums;
  color: var(--ink);
}

/* A tile whose value is a word or a date rather than a count: still the loudest line in
   the tile, but at a size where "28 August 2026" fits on one line of a quarter-width
   tile. The qualifier inside a value ("of 10", "monthly") steps down to body weight. */
.stat dd.text-body {
  font-size: var(--fs-app-section);
  line-height: 1.35;
  letter-spacing: -0.01em;
  padding-top: var(--s-4);
}

.stat dd .small {
  font-size: var(--fs-small);
  font-weight: 500;
  letter-spacing: 0;
  color: var(--ink-faint-text);
}

/* ----------------------------------------------------------------- forms */

.field {
  display: block;
  margin-bottom: var(--s-16);
  max-width: 26rem;
}

.field label {
  display: block;
  margin-bottom: var(--s-8);
  font-size: var(--fs-small);
  font-weight: 600;
  color: var(--ink);
}

.field input {
  display: block;
  width: 100%;
  min-height: var(--tap-min);
  padding: var(--s-8) var(--s-12);
  border: 1px solid var(--rule-strong);
  border-radius: var(--radius);
  background: var(--paper);
  box-shadow: var(--shadow-sm);
  transition: border-color var(--dur-hover) var(--ease), box-shadow var(--dur-hover) var(--ease);
  /* 16px minimum, so iOS does not zoom the viewport on focus. */
  font-size: var(--fs-body);
  font-family: inherit;
  color: var(--ink);
}

/* Only the border takes the accent. The ring itself is the global blue-on-paper
   indicator in base.css - overriding `outline` here would replace it with a
   weaker one, which is what this rule used to do. */
.field input:hover,
.field select:hover,
.field textarea:hover { border-color: var(--rule-strong-hover); }

.field input:focus-visible,
.field select:focus-visible {
  border-color: var(--blue);
}

/* A select is a field like any other: same height, border, radius and 16px type, so a row
   of filters beside a search box reads as one form rather than two kinds of control. */
.field select {
  display: block;
  width: 100%;
  min-height: var(--tap-min);
  padding: var(--s-8) var(--s-12);
  border: 1px solid var(--rule-strong);
  border-radius: var(--radius);
  background: var(--paper);
  box-shadow: var(--shadow-sm);
  font-size: var(--fs-body);
  font-family: inherit;
  color: var(--ink);
}

.field-hint {
  margin: var(--s-8) 0 0;
  font-size: var(--fs-small);
  color: var(--ink-faint-text);
}

.code-input {
  font-family: var(--font-mono);
  font-size: var(--fs-h3);
  letter-spacing: 0.32em;
  text-align: center;
}

/* A multi-line field, inheriting everything the single-line one already establishes.

   Sharing `.field input`'s rules rather than restating them: the 16px minimum that stops
   iOS zooming on focus, the border, the radius and the focus treatment are all decisions
   already made once, and a textarea styled separately is a textarea that drifts. */
.field textarea {
  display: block;
  width: 100%;
  padding: var(--s-8) var(--s-12);
  border: 1px solid var(--rule-strong);
  border-radius: var(--radius);
  background: var(--paper);
  font-size: var(--fs-body);
  font-family: inherit;
  line-height: var(--lh-body);
  color: var(--ink);
  /* Vertical only: a textarea a reader can widen escapes the measure and, inside a
     panel, can push the panel itself sideways. */
  resize: vertical;
}

.field textarea:focus-visible { border-color: var(--blue); }

/* The support form's fields are prose rather than a code or an address, so they get the
   reading measure instead of the 26rem the short fields use. */
.field-wide { max-width: var(--measure); }

/* ------------------------------------------------------------------ support

   A radio group where each option carries an explanation, because the five categories
   are not self-evident and picking the wrong one costs a round trip.

   `fieldset` and `legend` rather than a heading and a div: a group of radios is exactly
   what they are for, and it is what makes a screen reader announce "What kind of thing
   is this?" before the first option instead of leaving five unlabelled choices. */
.field-set {
  margin: 0 0 var(--s-24);
  padding: 0;
  border: 0;
  max-width: var(--measure);
}

.field-legend {
  padding: 0;
  margin-bottom: var(--s-12);
  font-size: var(--fs-small);
  font-weight: 600;
  color: var(--ink);
}

/* Aligned to the top rather than centred, because these options are two lines each and
   a radio centred against a wrapped explanation reads as belonging to neither line. */
.field-set .choice {
  align-items: flex-start;
  gap: var(--s-12);
  padding: var(--s-8) 0;
}

.field-set .choice input {
  /* Nudged down onto the first line's baseline rather than its box top. */
  margin-top: 3px;
  flex: none;
}

.choice-text { display: block; }

.choice-label {
  display: block;
  font-size: var(--fs-body-compact);
  font-weight: 600;
  color: var(--ink);
}

.choice-hint {
  display: block;
  margin-top: 2px;
  font-size: var(--fs-small);
  line-height: 1.45;
  color: var(--ink-faint-text);
}

.issue-list {
  list-style: none;
  margin: 0;
  padding: 0;
  display: flex;
  flex-direction: column;
  gap: var(--s-12);
}

.issue {
  padding: var(--s-16) var(--s-24);
  border: 1px solid var(--rule);
  border-radius: var(--radius);
  background: var(--paper);
  box-shadow: var(--shadow-sm);
}

.issue-head {
  display: flex;
  flex-wrap: wrap;
  align-items: baseline;
  gap: var(--s-8) var(--s-12);
  margin: 0;
}

.issue-subject {
  font-size: var(--fs-body);
  font-weight: 600;
  color: var(--ink);
}

/* Status, which is never a severity.

   Deliberately not the --sev tokens: those mean "how serious is this FDA record" and
   nothing else, and an amber "Being looked at" beside an amber "Class II recall" would
   spend the reader's alarm on a support ticket. Ink, blue and teal are the three jobs
   the palette actually has for chrome - see the note at the top of tokens.css. */
.issue-status {
  flex: none;
  padding: 2px var(--s-8);
  border-radius: 999px;
  font-size: var(--fs-eyebrow);
  font-weight: 600;
  letter-spacing: var(--ls-eyebrow);
  text-transform: uppercase;
  background: var(--mist);
  color: var(--ink-faint-text);
}

.issue-status-acknowledged {
  background: var(--blue-tint);
  color: var(--blue-text);
}

.issue-status-closed {
  background: var(--signal-wash);
  color: var(--signal-text);
}

.issue-meta {
  margin: var(--s-4) 0 0;
  font-size: var(--fs-small);
  color: var(--ink-faint-text);
}

.issue-body {
  margin: var(--s-12) 0 0;
  max-width: var(--measure);
  font-size: var(--fs-body-compact);
  line-height: var(--lh-body);
  color: var(--ink-soft);
  /* The reader's own line breaks, kept. They wrote steps in a list and a support view
     that reflows them into a paragraph is one nobody can read back. */
  white-space: pre-wrap;
  overflow-wrap: break-word;
}

/* The answer, set apart from the report it answers. */
.issue-resolution {
  margin: var(--s-12) 0 0;
  padding-left: var(--s-12);
  border-left: 3px solid var(--blue);
  max-width: var(--measure);
  font-size: var(--fs-body-compact);
  line-height: var(--lh-body);
  color: var(--ink);
  white-space: pre-wrap;
}

.issue-resolution-label {
  display: block;
  font-size: var(--fs-eyebrow);
  font-weight: 600;
  letter-spacing: var(--ls-eyebrow);
  text-transform: uppercase;
  color: var(--blue-text);
}

/*
 * `button.button` and `button.button-quiet` used to be in this selector list, and their
 * presence was a bug rather than belt-and-braces.
 *
 * They were presumably here to beat the user-agent button styles, which they did not need
 * to do: a class selector already outranks `button {}`. What they did instead was outrank
 * `.button-quiet` below - `button.button-quiet` is one element more specific - so every
 * <button class="button-quiet"> in this app took the blue fill from this rule and the
 * override never applied. Five of them, across the watch list, the code page and this
 * account page, all rendering as primary buttons; the outline variant only ever worked on
 * <a> elements. The whole point of two variants is that a screen has one obvious next
 * action, and there were pages here offering three.
 */
.button,
.button-quiet {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: var(--s-8);
  min-height: var(--tap-min);
  padding: 0 var(--s-16);
  border: 1px solid transparent;
  border-radius: var(--radius);
  box-shadow: var(--shadow-sm);
  transition: background-color var(--dur-hover) var(--ease),
    border-color var(--dur-hover) var(--ease), color var(--dur-hover) var(--ease);
  white-space: nowrap;
  /* Primary is a solid blue fill; .button-quiet below is a hairline outline. Two
     axes carry the hierarchy, fill-versus-outline and colour, so it survives a
     greyscale print as well as a screen. `text-decoration: none` is deliberate -
     the underline is the link affordance and a button already looks like one. */
  background: var(--blue);
  color: var(--paper);
  font-size: var(--fs-nav);
  font-family: inherit;
  font-weight: 600;
  text-decoration: none;
  cursor: pointer;
}

.button:hover {
  background: var(--blue-hover);
}

.button-quiet {
  background: var(--paper);
  border-color: var(--rule-strong);
  color: var(--ink);
}

.button-quiet:hover {
  background: var(--canvas);
  border-color: var(--rule-strong-hover);
  color: var(--ink);
}

/* Disabled has to differ from enabled, and it no longer can by losing a hue -
   an ink fill at reduced weight would just read as the enabled button. So it
   inverts: mist fill, hairline border, faint text. It also fixes a contrast bug
   this rule had before, which put --ink-faint-text (#62707d) on a solid ink
   fill at 3.1:1; on --mist the same grey measures 4.54:1. */
.button[aria-disabled="true"] {
  background: var(--mist);
  border-color: var(--rule);
  color: var(--ink-faint-text);
  cursor: not-allowed;
}

.actions {
  display: flex;
  flex-wrap: wrap;
  gap: var(--s-8);
  align-items: center;
}

/* --------------------------------------------------------------- notices */

/* Notices and errors are greyscale, which is the part of the severity-only rule
 * that costs something and is still right.
 *
 * A red validation box teaches the reader that red means "you did something
 * wrong". A red recall badge means "this device hurt someone". Those cannot both
 * be true on the same page, and only one of them is worth a hue. Amber on a
 * "watch list updated" confirmation had the same problem: it spends the alarm
 * the amber on a complaint spike needs.
 *
 * What carries urgency instead: position (immediately above or beside the thing
 * it concerns), an ink edge, a mist fill against the white page, and weight. An
 * inline error is unambiguous from where it sits and what it says - it never
 * needed the colour to be found. */
.notice {
  margin: 0 0 var(--s-24);
  padding: var(--s-12) var(--s-16) var(--s-12) calc(var(--s-16) + 4px);
  border: 1px solid var(--rule);
  border-radius: var(--radius);
  background: var(--paper);
  box-shadow: inset 4px 0 0 var(--signal), var(--shadow-card);
  font-size: var(--fs-body-compact);
  color: var(--ink);
  max-width: var(--max-text);
}

.notice-warn {
  background: var(--mist);
  box-shadow: inset 4px 0 0 var(--ink);
}

.error {
  margin: 0 0 var(--s-16);
  padding: var(--s-12) var(--s-16);
  border: 1px solid var(--ink);
  border-left-width: 4px;
  border-radius: var(--radius);
  background: var(--mist);
  font-size: var(--fs-body-compact);
  font-weight: 600;
  color: var(--ink);
  max-width: 26rem;
}

/* ------------------------------------------------------------ FDA provenance
 *
 * The visual language of "this came from FDA and you can check it". Three
 * elements, and the restraint is the design: this appears on every dense table in
 * the product, so anything with a fill, an icon or a hue would compete with the
 * severity pills - which are the one thing on these screens that is allowed to
 * shout, because they carry a recall class.
 *
 * So provenance is quiet by default and loud only where it is being read: an id
 * looks like the monospaced id it always was, and reveals itself as a link on
 * hover and focus. The reader who is scanning a table is not interrupted; the
 * reader who is checking one finds the link exactly where they expect it.
 */

/* An FDA record id, linked to FDA's own copy.
 *
 * The dotted underline rather than a solid one, and ink rather than blue: fifteen
 * blue underlined identifiers down the first column of a recall table turn the
 * column into a wall of links and stop it being a column of record numbers, which
 * is what it is for. The underline is `text-decoration` rather than a border so it
 * skips descenders, and `text-underline-offset` keeps it clear of a monospaced
 * digit's baseline. Colour is never the only signal here: the underline is present
 * at rest, so the affordance survives greyscale and a colour-blind reader. */
.fda-id {
  color: var(--ink);
  text-decoration: underline dotted var(--ink-faint);
  text-underline-offset: 3px;
  text-decoration-thickness: 1px;
  /* One unbreakable token. A hyphen is a break opportunity, so without this a recall
     number wraps as "Z-" / "2645-2026" - and a record number split across two lines
     stops being quotable, which is the only reason it is printed. The run of ids around
     it still wraps, between links rather than inside one. */
  white-space: nowrap;
}

.fda-id:hover,
.fda-id:focus-visible {
  color: var(--blue-text);
  text-decoration: underline solid var(--blue-text);
}

/* The line under a table naming the openFDA endpoint its rows came from. */
.source-note {
  margin: var(--s-12) 0 0;
  font-size: var(--fs-small);
  line-height: var(--lh-small);
  color: var(--ink-faint-text);
  max-width: var(--max-text);
}

.source-note .mono {
  font-size: var(--fs-mono-chip);
  color: var(--ink-soft);
}

/* "Official FDA source". A label, not a badge - uppercase and letter-spaced like
   the eyebrow type on the marketing site, so it reads as a category rather than as
   a status someone needs to act on. Ink on mist: no hue, for the reason above. */
.source-tag {
  display: inline-block;
  margin-right: var(--s-8);
  padding: 2px var(--s-8);
  border: 1px solid var(--rule);
  border-radius: var(--radius-pill);
  background: var(--mist);
  color: var(--ink);
  font-size: var(--fs-eyebrow);
  font-weight: 600;
  letter-spacing: var(--ls-eyebrow);
  text-transform: uppercase;
  white-space: nowrap;
  vertical-align: 2px;
}

/* The standing statement that FDA's data is not altered here.
 *
 * Bordered on the left like `.notice`, and for the same reason - it is an aside
 * about the page rather than part of it - but with the hairline rather than the
 * teal accent, because a notice is something that just happened and this is
 * something that is always true. Two of them on one screen would be the product
 * protesting; there is one per page. */
.integrity {
  margin: var(--s-24) 0 0;
  padding: var(--s-12) var(--s-16);
  border: 1px solid var(--rule);
  border-left: 4px solid var(--ink-faint);
  border-radius: 0 var(--radius) var(--radius) 0;
  background: var(--paper);
  max-width: var(--max-text);
}

.integrity-line {
  margin: 0;
  font-size: var(--fs-body-compact);
  line-height: var(--lh-small);
  color: var(--ink);
}

.integrity-sources {
  margin: var(--s-8) 0 0;
  font-size: var(--fs-small);
  line-height: 1.8;
  color: var(--ink-faint-text);
}

/* The endpoint beside each link, so the sentence names what it queries rather
   than only what it is about. Parenthesised by CSS so the markup stays a list of
   links, which is what a screen reader should be given. */
.integrity-endpoint {
  margin-left: var(--s-4);
  font-size: var(--fs-mono-chip);
  color: var(--ink-faint-text);
}

.integrity-endpoint::before { content: "("; }
.integrity-endpoint::after { content: ")"; }

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

.scroller {
  overflow-x: auto;
  box-shadow: var(--shadow-card);
  /* A containing block, so an absolutely positioned descendant is clipped with the rest
     of the table rather than escaping it.

     The watch list has `<th scope="col"><span class="sr-only">Add to your list</span></th>`
     in its last column. `.sr-only` is `position: absolute`, and with no positioned
     ancestor its containing block was the viewport - so at 390px, where the table is
     wider than the screen and scrolls, that span was laid out at x=435 *outside* this
     box's clip and gave the whole document a horizontal scrollbar. Nothing looked wrong:
     the span is 1px and invisible, and the page simply slid sideways under a thumb.

     One line here rather than a fix at the call site, because the next visually-hidden
     label inside a wide table would have done it again. */
  position: relative;
  border: 1px solid var(--rule);
  border-radius: var(--radius-lg);
  background: var(--paper);
}

.table {
  width: 100%;
  min-width: 34rem;
  border-collapse: collapse;
  text-align: left;
}

.table caption {
  padding: var(--s-12) var(--s-16);
  text-align: left;
  font-size: var(--fs-small);
  color: var(--ink-faint-text);
  border-bottom: 1px solid var(--rule-soft);
}

.table th,
.table td {
  padding: 10px var(--s-16);
  font-size: var(--fs-body-compact);
  line-height: 1.45;
  vertical-align: top;
  border-bottom: 1px solid var(--rule-soft);
}

.table thead th {
  padding-block: var(--s-8);
  font-size: var(--fs-eyebrow);
  font-weight: 600;
  letter-spacing: var(--ls-eyebrow);
  text-transform: uppercase;
  color: var(--ink-faint-text);
  background: var(--canvas);
  border-bottom-color: var(--rule);
  white-space: nowrap;
}

/* The row under the pointer, so a wide row can be followed across to its last column. */
.table tbody tr {
  transition: background-color var(--dur-hover) var(--ease);
}

.table tbody tr:hover {
  background: var(--row-hover);
}

/* Links inside a table are the row's name or identifier, not a word in a sentence, so
   the cell itself is the affordance (WCAG 1.4.1 concerns links within running text).
   They keep the interactive blue and weight, and the underline returns on hover and
   focus so the target is still unmistakable. FDA record ids are the exception: they
   are ink rather than blue, so their dotted underline is their only cue and stays. */
.table tbody a:not(.button):not(.button-quiet):not(.button-small):not(.fda-id) {
  font-weight: 500;
  text-decoration: none;
}

.table tbody a:not(.button):not(.button-quiet):not(.button-small):not(.fda-id):hover,
.table tbody a:not(.button):not(.button-quiet):not(.button-small):not(.fda-id):focus-visible {
  text-decoration: underline;
}

.table tbody th {
  font-weight: 600;
  color: var(--ink);
  white-space: nowrap;
}

/* A row header that is a name rather than an identifier.

   `nowrap` above is right for the things a row is usually keyed on - a three-letter
   code, a K-number, an FDA recall id - which read worse broken across lines than
   scrolled to. It is wrong for a company: "GE Medical Systems Ultrasound and Primary
   Care Diagnostics, LLC" on one unbreakable line pushed the last two columns of the
   competitor table off a 1440px screen, so the whole table scrolled sideways to show
   one name in full. A measure and a wrap, and the table fits.

   The device-name subtitle gets the same treatment wherever it appears, for the same
   reason: FDA writes "Integrated Continuous Glucose Monitoring System, Factory
   Calibrated, Not For Use With Automated Insulin Delivery Systems". */
.table tbody th.cell-name,
.table tbody th .cell-sub {
  white-space: normal;
}

.table tbody th.cell-name {
  max-width: 20rem;
  overflow-wrap: break-word;
}

.table td {
  color: var(--ink-soft);
}

.table tbody tr:last-child th,
.table tbody tr:last-child td {
  border-bottom: 0;
}

/* --------------------------------------------------------------- empties */

.empty {
  padding: var(--s-48) var(--s-24);
  border: 1px dashed var(--rule-strong);
  border-radius: var(--radius-lg);
  background: var(--paper);
  text-align: center;
}

.empty-icon {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 44px;
  height: 44px;
  margin-bottom: var(--s-12);
  border-radius: var(--radius-lg);
  background: var(--blue-tint);
  color: var(--blue-text);
}

.empty-icon svg {
  width: 22px;
  height: 22px;
  fill: none;
  stroke: currentColor;
  stroke-width: 1.7;
  stroke-linecap: round;
  stroke-linejoin: round;
}

.empty-title {
  margin: 0 0 var(--s-8);
  font-size: var(--fs-card-title);
  font-weight: 700;
  color: var(--ink);
}

.empty-body {
  margin: 0 auto var(--s-24);
  max-width: 34rem;
  font-size: var(--fs-body);
  line-height: var(--lh-body);
  color: var(--ink-soft);
}

/* ----------------------------------------------------------------- chain */

.chain {
  margin: var(--s-16) 0 0;
  padding: 0;
  list-style: none;
}

.chain li {
  padding: var(--s-12) var(--s-16);
  border: 1px solid var(--rule);
  border-radius: var(--radius);
  background: var(--paper);
  font-size: var(--fs-body-compact);
  color: var(--ink-soft);
}

.chain li + li {
  margin-top: var(--s-8);
}

.chain-recalled {
  border-left: 4px solid var(--sev-1);
}

@media (max-width: 40rem) {
  .chrome-inner {
    flex-wrap: wrap;
    gap: var(--s-12);
    padding-block: var(--s-12);
  }

  .panel,
  .finding {
    padding: var(--s-16);
  }
}

@media print {
  /* The rail is navigation, and paper cannot navigate - it was printing the whole
     sidebar down the left of every page. Also the skip link and the export buttons,
     which are the same category of thing. */
  .chrome,
  .topbar,
  .rail,
  .skip,
  .foot,
  .actions,
  .page-actions {
    display: none;
  }

  /* The shell is a CSS grid with a rail column. With the rail gone the content must
     take the full width rather than leaving a 240px hole where it used to be. */
  body.shell {
    display: block;
    background: var(--paper);
  }

  /* Same for the signed-out flex column, and the viewport floor goes with it: on
     paper "at least one screen tall" is a second, blank sheet. */
  body.plain {
    display: block;
    min-height: 0;
  }

  .pane {
    max-width: none;
    padding: 0;
  }

  /* A scroll container is clipped at the paper edge; unwrapped it wraps instead. */
  .scroller,
  .chart-wrap {
    overflow: visible;
    border: 0;
  }

  .table {
    min-width: 0;
    font-size: 9pt;
  }

  .table thead {
    display: table-header-group;
  }

  .table tr {
    break-inside: avoid;
  }

  /* Severity is the one thing that must survive a monochrome office laser, so it
     asks for its colour explicitly rather than being dropped to save toner. */
  .sev,
  .finding {
    -webkit-print-color-adjust: exact;
    print-color-adjust: exact;
  }

  .finding,
  .panel {
    break-inside: avoid;
  }
}

/* ---------------------------------------------------------------- utilities
 *
 * A small set of single-purpose classes, added because the app's own
 * Content-Security-Policy forbids inline styles - `style-src 'self'` with no
 * 'unsafe-inline'. A browser silently dropped every style="" attribute and the
 * layout broke, which is exactly the right outcome: the alternative was widening
 * the policy, and a strict CSP is worth more than the convenience of an inline
 * declaration. The email templates are unaffected - they are not served under a
 * policy and cannot use classes anyway.
 */

.mt-0 { margin-top: 0; }
.mt-12 { margin-top: var(--s-12); }
.mt-16 { margin-top: var(--s-16); }
.mt-24 { margin-top: var(--s-24); }
.mt-32 { margin-top: var(--s-32); }
.mt-48 { margin-top: var(--s-48); }
.mb-24 { margin-bottom: var(--s-24); }
.mb-32 { margin-bottom: var(--s-32); }

.narrow { max-width: 30rem; }
.narrower { max-width: 32rem; }

.text-body { font-size: var(--fs-body); }
.text-title { font-size: var(--fs-card-title); }

/* Product codes are uppercase and monospaced wherever they are typed or shown. */
.input-code {
  text-transform: uppercase;
  font-family: var(--font-mono);
}

/* A number or short text field that should not stretch to the measure. A threshold is
   three characters wide; a full-width box invites a sentence. */
.input-narrow {
  max-width: 9rem;
  font-family: var(--font-mono);
}

/* A radio or checkbox beside its label, without the full-width field styling. */
.choice {
  display: flex;
  gap: var(--s-8);
  align-items: center;
}

.choice input {
  width: auto;
  min-height: auto;
}

.feature-list {
  padding-left: 1.1rem;
  line-height: 1.9;
}

/* ================================================================== shell
   The signed-in layout: a fixed left rail and a scrolling content pane.

   CSS grid on the body, which is what lets the rail be full-height and sticky with no
   JavaScript and no absolute positioning. `.plain` (sign-in, errors) keeps the old
   single-column chrome, so a signed-out page is unaffected.
   ------------------------------------------------------------------------ */
/* The signed-out pages: sign-in, the code screen, errors, and the anonymous /check
   teaser. No rail, one column - and until now no floor either, so the footer ended
   where the content ended and floated in the middle of a tall window. The sign-in
   screen and the teaser are the two pages a stranger sees first, which makes this
   the cheapest fix on the surface with the least room for a bad first impression.

   Flex rather than the grid `body.shell` uses, because there is no rail column to
   describe: `main` takes the slack and the footer is pushed to the bottom edge.
   Deliberately not `position: fixed` - see the note in tokens/base.css. */
body.plain {
  display: flex;
  flex-direction: column;
  min-height: 100vh;
}

body.plain > main {
  /* `1 0 auto`, not `1`: the shorthand's zero basis would collapse a page that is
     taller than the viewport, which is most of them once a result renders. */
  flex: 1 0 auto;
}

body.shell {
  display: grid;
  grid-template-columns: var(--rail-w) minmax(0, 1fr);
  grid-template-areas: "rail top" "rail main" "rail foot";
  grid-template-rows: auto 1fr auto;
  min-height: 100vh;
  background: var(--canvas);
}

body.shell > .topbar { grid-area: top; }
body.shell > .rail { grid-area: rail; }
body.shell > main { grid-area: main; min-width: 0; }
body.shell > .foot { grid-area: foot; }

/* The rail. Paper on the canvas, a hairline on its right, and a brand band exactly as
   tall as the top bar so the two bottom rules meet in one unbroken line across the
   whole window - the rail and the bar read as one frame around the work. */
.rail {
  display: flex;
  flex-direction: column;
  border-right: 1px solid var(--rule);
  background: var(--paper);
  position: sticky;
  top: var(--promo-h, 0);
  align-self: start;
  height: calc(100vh - var(--promo-h, 0px));
  overflow-y: auto;
}

.rail-brand {
  display: flex;
  align-items: center;
  flex: none;
  height: var(--topbar-h);
  padding-inline: var(--s-24);
  border-bottom: 1px solid var(--rule);
}

/* The rail is 248px wide, so the wordmark gets a little more room here than in the
   horizontal chrome, where it competes with a sign-in link. */
.rail .brand-logo {
  height: 22px;
}

.rail-nav {
  display: flex;
  flex-direction: column;
  gap: var(--s-16);
  padding: var(--s-16) var(--s-12);
}

.rail-group {
  margin: 0 0 var(--s-4);
  padding-inline: var(--s-12);
  font-size: 11px;
  font-weight: 600;
  letter-spacing: 0.08em;
  text-transform: uppercase;
  color: var(--ink-faint-text);
}

.rail-list {
  list-style: none;
  margin: 0;
  padding: 0;
  display: flex;
  flex-direction: column;
  gap: 2px;
}

.rail-link {
  position: relative;
  display: flex;
  align-items: center;
  gap: var(--s-12);
  min-height: 36px;
  padding: 0 var(--s-12);
  border-radius: var(--radius);
  font-size: 14px;
  font-weight: 500;
  color: var(--ink-soft);
  text-decoration: none;
  transition: background-color var(--dur-hover) var(--ease), color var(--dur-hover) var(--ease);
}

.rail-link:hover { background: var(--rail-hover); color: var(--ink); }

/* Selected. The blue tint distinguishes it from .rail-link:hover, which is the
   neutral canvas grey - without that, hovering any item looked identical to the page
   you are already on. Weight and a short rounded marker carry it as well as the colour,
   and `aria-current="page"` carries it for anyone seeing none of the three. */
.rail-link-current,
.rail-link-current:hover {
  background: var(--blue-tint);
  color: var(--blue-text);
  font-weight: 600;
}

.rail-link-current::before {
  content: "";
  position: absolute;
  left: -12px;
  top: 8px;
  bottom: 8px;
  width: 3px;
  border-radius: 0 3px 3px 0;
  background: var(--blue);
}

.rail-icon {
  width: 18px;
  height: 18px;
  flex: none;
  fill: none;
  stroke: currentColor;
  stroke-width: 1.7;
  stroke-linecap: round;
  stroke-linejoin: round;
}

.rail-foot {
  margin-top: auto;
  padding: var(--s-12);
  border-top: 1px solid var(--rule-soft);
}

/* Quieter than a nav item and louder than nothing: it is the thing you reach for when
   something is wrong, which is rarely, and it must not compete with Today. */
.rail-report {
  display: flex;
  align-items: center;
  gap: var(--s-12);
  min-height: 36px;
  padding: 0 var(--s-12);
  border-radius: var(--radius);
  font-size: var(--fs-small);
  color: var(--ink-faint-text);
  text-decoration: none;
}

.rail-report:hover { background: var(--rail-hover); color: var(--ink); }
.rail-report .rail-icon { width: 16px; height: 16px; }

/* ------------------------------------------------------------------ top bar

   Sticky, translucent paper with a blur behind it, so a long table scrolls visibly
   underneath rather than vanishing at a hard edge. Where backdrop-filter is not
   supported the paper is 88% opaque, which is still legible. */
.topbar {
  position: sticky;
  top: var(--promo-h, 0);
  z-index: 40;
  display: flex;
  align-items: center;
  gap: var(--s-16);
  height: var(--topbar-h);
  padding-inline: var(--s-32);
  border-bottom: 1px solid var(--rule);
  background: var(--topbar-paper);
  -webkit-backdrop-filter: saturate(180%) blur(12px);
  backdrop-filter: saturate(180%) blur(12px);
}

/* The wordmark lives in the rail on a wide screen; the bar only carries it when the
   rail has turned into a tab strip. */
.topbar-brand { display: none; margin-right: 0; }
.topbar-brand .brand-logo { height: 20px; }

.crumbs { min-width: 0; flex: 0 1 auto; }

.crumbs-list {
  display: flex;
  align-items: center;
  gap: var(--s-8);
  margin: 0;
  padding: 0;
  list-style: none;
  font-size: var(--fs-small);
  white-space: nowrap;
}

.crumb {
  color: var(--ink-faint-text);
  overflow: hidden;
  text-overflow: ellipsis;
}

.crumb-sep {
  margin-right: var(--s-8);
  color: var(--ink-faint);
}

.crumb a {
  color: var(--ink-faint-text);
  text-decoration: none;
}

.crumb a:hover { color: var(--ink); text-decoration: underline; }

.crumb-current {
  color: var(--ink);
  font-weight: 600;
}

.topbar-end {
  display: flex;
  align-items: center;
  gap: var(--s-12);
  margin-left: auto;
  flex: none;
}

/* The plan, stated. Free is a link to the plans, and says so; a paid plan is a quiet
   badge that opens the account page. Blue, never a severity hue - this is chrome. */
.plan-chip {
  display: inline-flex;
  align-items: center;
  min-height: 28px;
  padding: 0 var(--s-12);
  border: 1px solid var(--rule);
  border-radius: var(--radius-pill);
  background: var(--paper);
  font-size: var(--fs-app-label);
  font-weight: 600;
  color: var(--ink-soft);
  text-decoration: none;
  white-space: nowrap;
}

.plan-chip:hover { border-color: var(--rule-strong-hover); color: var(--ink); }

.plan-chip-free {
  border-color: var(--blue-tint);
  background: var(--blue-tint);
  color: var(--blue-text);
}

.plan-chip-free:hover { border-color: var(--blue); color: var(--blue-text); }

.plan-chip-icon {
  width: 14px;
  height: 14px;
  margin-right: var(--s-4);
  fill: none;
  stroke: currentColor;
  stroke-width: 2.2;
  stroke-linecap: round;
  stroke-linejoin: round;
}

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

.user-menu { position: relative; }

.user-chip {
  display: flex;
  align-items: center;
  gap: var(--s-8);
  min-height: 36px;
  padding: 2px var(--s-8) 2px 2px;
  border: 1px solid transparent;
  border-radius: var(--radius-pill);
  list-style: none;
  cursor: pointer;
  color: var(--ink);
}

.user-chip::-webkit-details-marker { display: none; }
.user-chip::marker { content: ""; }
.user-chip:hover,
.user-menu[open] > .user-chip { border-color: var(--rule); background: var(--paper); }

.avatar {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 32px;
  height: 32px;
  border-radius: var(--radius-pill);
  background: var(--blue-tint);
  color: var(--blue-text);
  font-size: var(--fs-app-label);
  font-weight: 700;
  letter-spacing: 0.02em;
  flex: none;
}

.user-chip-email {
  max-width: 14rem;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
  font-size: var(--fs-small);
  font-weight: 500;
}

.user-chip-caret {
  width: 16px;
  height: 16px;
  fill: none;
  stroke: var(--ink-faint);
  stroke-width: 2;
  stroke-linecap: round;
  stroke-linejoin: round;
}

.user-menu-panel {
  position: absolute;
  right: 0;
  top: calc(100% + var(--s-8));
  z-index: 50;
  width: 17rem;
  max-width: calc(100vw - var(--s-32));
  padding: var(--s-8);
  border: 1px solid var(--rule);
  border-radius: var(--radius-lg);
  background: var(--paper);
  box-shadow: var(--shadow-pop);
}

.user-menu-head {
  display: flex;
  flex-direction: column;
  gap: 2px;
  margin: 0 0 var(--s-8);
  padding: var(--s-8) var(--s-12) var(--s-12);
  border-bottom: 1px solid var(--rule-soft);
}

.user-menu-email {
  font-size: var(--fs-small);
  font-weight: 600;
  color: var(--ink);
  overflow-wrap: anywhere;
}

.user-menu-plan { font-size: var(--fs-app-label); color: var(--ink-faint-text); }

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

.user-menu-list a,
.user-menu-signout {
  display: flex;
  align-items: center;
  width: 100%;
  min-height: 36px;
  padding: 0 var(--s-12);
  border: 0;
  border-radius: var(--radius-sm);
  background: none;
  font: inherit;
  font-size: var(--fs-small);
  color: var(--ink-soft);
  text-align: left;
  text-decoration: none;
  cursor: pointer;
}

.user-menu-list a:hover,
.user-menu-signout:hover { background: var(--rail-hover); color: var(--ink); }

.user-menu-foot {
  margin: var(--s-8) 0 0;
  padding-top: var(--s-8);
  border-top: 1px solid var(--rule-soft);
}

/* ------------------------------------------------------------------- search

   The box in the rail, and its bigger twin on the results page. One control, so one
   set of rules: the difference between the two is width and whether the submit button
   is visible, and everything else - the field, its focus, its clear affordance - is
   identical because it is the same element in two places.

   `appearance: none` on the search input, because Safari's native search field draws its
   own inset shadow and rounded corners over anything set here, and the result was a
   control that matched the rest of the app on Chrome and did not on the browser half
   this product's readers actually use. The type stays `search` for the keyboard label
   and the clear button; only the chrome is taken back. */

.search-input {
  appearance: none;
  -webkit-appearance: none;
  width: 100%;
  min-width: 0;
  min-height: 40px;
  padding: var(--s-8) var(--s-12);
  border: 1px solid var(--rule-strong);
  border-radius: var(--radius);
  background: var(--paper);
  box-shadow: var(--shadow-sm);
  font: inherit;
  font-size: var(--fs-nav);
  color: var(--ink);
  transition: border-color var(--dur-hover) var(--ease), box-shadow var(--dur-hover) var(--ease);
}

.search-input:hover { border-color: var(--rule-strong-hover); }

.search-input { text-overflow: ellipsis; }
.search-input::placeholder { color: var(--ink-faint-text); }

.search-input:focus-visible {
  border-color: var(--blue);
  /* The same two-ring treatment as every other focusable thing here: blue carries it on
     paper, the paper backing carries it anywhere blue would not. */
  outline: 2px solid var(--focus-ring);
  outline-offset: 1px;
}

/* Safari puts a magnifier and a cancel button inside the field. The cancel button is
   genuinely useful and is kept; the decoration is not. */
.search-input::-webkit-search-decoration { -webkit-appearance: none; }

/* The global box in the top bar: a leading magnifier inside the field, and a width
   that holds the whole placeholder without taking the bar from the breadcrumb. */
.top-search {
  position: relative;
  flex: 0 1 26rem;
  min-width: 0;
  margin-left: auto;
}

.top-search .search-input {
  min-height: 36px;
  padding-left: 36px;
  background: var(--canvas);
  border-color: var(--rule);
  box-shadow: none;
  font-size: var(--fs-small);
}

.top-search .search-input:hover { border-color: var(--rule-strong); }
.top-search .search-input:focus-visible { background: var(--paper); border-color: var(--blue); }

.top-search-icon {
  position: absolute;
  left: 11px;
  top: 50%;
  width: 16px;
  height: 16px;
  transform: translateY(-50%);
  fill: none;
  stroke: var(--ink-faint);
  stroke-width: 2;
  stroke-linecap: round;
  pointer-events: none;
}

.page-search {
  display: flex;
  gap: var(--s-8);
  align-items: center;
  max-width: 34rem;
}

.page-search .search-input { flex: 1 1 auto; }

/* ------------------------------------------------------- long text in a cell

   A recall reason is two sentences of FDA prose in a table of numbers. Without a
   measure it takes the whole row and pushes every other column off the screen; with
   one it wraps and the table still reads left to right. `break-word` because FDA
   publishes catalogue numbers with no spaces in them. */
.cell-prose {
  max-width: 26rem;
  white-space: normal;
  overflow-wrap: break-word;
  color: var(--ink-soft);
}

/* ------------------------------------------------------- the merged spellings

   Two columns, name and count, on one line each. A definition list would be the
   semantic choice if the count were a definition of the name; it is not, it is how
   many records carry it, so this stays a list of rows. */
.variant-list {
  list-style: none;
  margin: var(--s-16) 0 0;
  padding: 0;
  display: flex;
  flex-direction: column;
  gap: var(--s-4);
}

.variant-list li {
  display: flex;
  justify-content: space-between;
  align-items: baseline;
  gap: var(--s-16);
  padding: var(--s-8) 0;
  border-top: 1px solid var(--rule);
}

.variant-name {
  font-family: var(--mono);
  font-size: var(--fs-small);
  color: var(--ink-soft);
  overflow-wrap: anywhere;
}

.variant-count {
  flex: none;
  font-size: var(--fs-small);
  color: var(--ink-faint-text);
  font-variant-numeric: tabular-nums;
}

/* The content pane. Wider than .container because the rail already spends 248px, and a
   dashboard with charts needs the room a prose page does not.

   --pane-max (1200px) rather than 1080: the six-column competitor table needs about 1150px before it
   starts scrolling sideways, and on a 1440px screen the room was there and unused. Prose
   is unaffected - every paragraph in here is capped at --measure independently, so this
   widens tables and charts and nothing else. */
.pane {
  width: 100%;
  max-width: var(--pane-max);
  margin-inline: auto;
  padding: var(--s-32) var(--s-32) var(--s-64);
}

/* Narrow: the rail becomes a horizontal strip above the content. No drawer, no toggle,
   no JavaScript - see the comment in layout.ts. */
@media (max-width: 860px) {
  body.shell {
    grid-template-columns: minmax(0, 1fr);
    grid-template-areas: "top" "rail" "main" "foot";
    grid-template-rows: auto auto 1fr auto;
  }

  .topbar {
    gap: var(--s-12);
    padding-inline: var(--s-16);
  }

  .topbar-brand { display: inline-flex; }
  .crumbs { display: none; }
  .top-search { flex: 1 1 auto; }
  .user-chip-email { display: none; }
  .user-chip { padding-right: 2px; }
  .user-chip-caret { display: none; }
  .plan-chip { display: none; }

  /* The rail becomes one row of tabs that scrolls sideways inside itself. The page
     never scrolls sideways - the strip is the only thing that does, and it is a
     labelled region a keyboard can reach. Group headings are orientation for a vertical
     list and noise in a strip, so they go. */
  .rail {
    position: sticky;
    top: calc(var(--promo-h, 0px) + var(--topbar-h));
    z-index: 30;
    height: auto;
    overflow: visible;
    border-right: 0;
    border-bottom: 1px solid var(--rule);
  }

  .rail-brand,
  .rail-group,
  .rail-foot { display: none; }

  .rail-nav {
    flex-direction: row;
    gap: var(--s-4);
    padding: 0 var(--s-8);
    overflow-x: auto;
    scrollbar-width: none;
    -webkit-overflow-scrolling: touch;
  }

  .rail-nav::-webkit-scrollbar { display: none; }

  .rail-list {
    flex-direction: row;
    gap: var(--s-4);
    flex: none;
  }

  .rail-link {
    min-height: 44px;
    padding: 0 var(--s-12);
    border-radius: 0;
    white-space: nowrap;
    background: none;
  }

  .rail-link:hover { background: none; }

  .rail-link-current,
  .rail-link-current:hover { background: none; }

  .rail-link-current::before {
    left: var(--s-8);
    right: var(--s-8);
    top: auto;
    bottom: 0;
    width: auto;
    height: 2px;
    border-radius: 2px 2px 0 0;
  }

  .pane { padding: var(--s-24) var(--s-16) var(--s-48); }
}

/* ================================================================= insights
   Charts and the comparison table. Every colour comes from packages/tokens, and
   every meaning that a colour carries is also carried by a word - the tables beside
   these charts say "above expected", the bars only show it.
   ------------------------------------------------------------------------ */

/* A stat card that is carrying bad news. The left border is the signal; the number
   itself stays ink, because a red number reads as an error rather than a count. */
.stat-risk { box-shadow: inset 3px 0 0 var(--sev-1), var(--shadow-card); padding-left: calc(var(--s-16) + 3px); }
.stat-warn { box-shadow: inset 3px 0 0 var(--sev-2), var(--shadow-card); padding-left: calc(var(--s-16) + 3px); }

.stat-note {
  /* Inside the <dd>, not a sibling of it: a <dl> may only contain dt/dd (or a div
     wrapping those), and a <p> alongside them is a WCAG 1.3.1 failure that axe catches
     as `only-dlitems`. Nesting it means inheriting the dd's 700-weight card type, so
     everything it needs is reset here rather than assumed. */
  margin: var(--s-4) 0 0;
  font-size: var(--fs-small);
  font-weight: 400;
  line-height: var(--lh-small);
  letter-spacing: 0;
  color: var(--ink-faint-text);
}

.chart-wrap {
  margin: var(--s-16) 0;
  overflow-x: auto;
  /* Same reason as .scroller: this box scrolls sideways, so it has to be the containing
     block for anything positioned inside it - otherwise an absolutely positioned label
     escapes the clip and widens the whole document. */
  position: relative;
}

.chart {
  display: block;
  width: 100%;
  /* Fixed height so nothing reflows when the page paints. */
  height: 200px;
  min-width: 520px;
}

.chart-grid { stroke: var(--rule); stroke-width: 1; }
.chart-axis { stroke: var(--ink-faint); stroke-width: 1; }
.chart-tick {
  fill: var(--ink-faint-text);
  font-family: var(--font-mono);
  font-size: 10px;
  text-anchor: end;
}
/* The band is the plot's own background, not a finding, so it takes the neutral
   interactive wash rather than a severity hue. */
.chart-band { fill: var(--blue-tint); }
/* A month inside its expected range is the ordinary case, so it stays ink - the
   bars are data, and data is not decorated. */
.chart-bar { fill: var(--ink); }
/* A month above the band. --sev-2 (#f2a63b) against a --mist band measures about
   1.8:1, which is under the 3:1 WCAG 2.2 SC 1.4.11 wants for a graphic that
   carries meaning - so the fill alone would not be a legitimate signal. The
   stroke in --sev-2-text (#8a5a05) is what makes it one. The bar is also never
   the only carrier: the table beside the chart says "Above expected (n–m)" in
   words for the same month. */
.chart-bar-high {
  fill: var(--sev-2);
  stroke: var(--sev-2-text);
  stroke-width: 1;
}
/* A month with no baseline behind it: hatched rather than filled, so it cannot be
   mistaken for a measured comparison - and so the distinction survives a
   greyscale print, where a pale fill and a pale band look identical. */
.chart-bar-unknown { stroke: var(--rule); stroke-width: 1; }
.chart-hatch-line { stroke: var(--ink-faint); stroke-width: 1; }

.spark {
  display: block;
  width: 84px;
  height: 22px;
  overflow: visible;
}
.spark-line {
  fill: none;
  stroke: var(--ink-soft);
  stroke-width: 1.5;
  stroke-linecap: round;
  stroke-linejoin: round;
}
/* The last point, marked so the eye knows which end is now. The brand accent,
   not a severity hue - a trend's latest value is not a grade. */
.spark-dot { fill: var(--signal); }

.meter {
  display: block;
  width: 72px;
  height: 8px;
}
.meter-track { fill: var(--mist); }
.meter-fill { fill: var(--signal); }

/* A second line inside a table cell: the device name under a code, the sample size
   under a trend. Keeps the cell scannable without a second column. */
.cell-sub {
  display: block;
  margin-top: 2px;
  font-size: var(--fs-small);
  line-height: 1.35;
  color: var(--ink-faint-text);
}

/* A link on the sub-line is a short label - "Predicate check", "FDA record" - and it
   sits under a record id that is itself unbreakable, so the column is sized by the id
   and the label wraps to two lines inside it. Keeping it whole widens the column by a
   few characters and is what stops the first column of every record table reading as
   "Predicate / check". The table scrolls inside its own box if that costs width. */
.cell-sub a {
  white-space: nowrap;
}

/* Direction always carries a word as well as an arrow.
   "Faster" takes the brand accent because it is good news about a queue. "Slower"
   deliberately does NOT take red: FDA taking longer to clear a submission is a
   fact about a queue, not a severity, and a red arrow here would compete for
   attention with a Class I recall two rows down. Ink plus the word plus the arrow
   is enough. */
.trend-slower { color: var(--ink); font-weight: 600; }
.trend-faster { color: var(--signal-text); font-weight: 600; }
.trend-flat { color: var(--ink-faint-text); }

/* ================================================================= the chain
   The predicate chain diagram. Its twin is
   apps/web/src/components/PredicateChain.module.css, and the two must match:
   the same ancestry appears on the public code page and on this one.
   ------------------------------------------------------------------------ */

.chain-figure { margin: 0 0 var(--s-16); }

.chain-caption {
  margin: 0 0 var(--s-12);
  font-size: var(--fs-eyebrow);
  font-weight: 600;
  letter-spacing: var(--ls-eyebrow);
  text-transform: uppercase;
  color: var(--ink-faint-text);
}

/* Scrolls sideways on a narrow viewport. tabindex and role="group" are in the
   markup, because a region that scrolls with a mouse and not a keyboard is the
   axe failure scrollable-region-focusable. */
.chain-scroll {
  overflow-x: auto;
  padding-bottom: var(--s-8);
}

/*
 * A long chain scrolls inside its own box instead of pushing the page down.
 *
 * Eighteen generations stacked is about 3,000px of diagram - JOW, and real chains reach
 * back to 1976 - which puts every section under it three screens away. The picture is
 * still drawn in full and nothing is truncated; it is the viewport onto it that is
 * bounded, and the indented outline underneath is the copy a reader actually reads.
 *
 * The wrapper already carries tabindex="0" and role="group" with a name, so a region that
 * now scrolls on two axes is still reachable from a keyboard - which is the axe failure
 * scrollable-region-focusable, and the reason those attributes were put there for the
 * horizontal case.
 *
 * :has() rather than a class on the wrapper: the flag belongs to the svg, which is where
 * layoutChain puts it. An engine without :has() simply gets the tall version, which is
 * what it renders today - a degradation, not a break.
 */
.chain-scroll:has(.chain-diagram-stacked) {
  max-height: 30rem;
  overflow-y: auto;
  /* And no wider than the column it holds - a 260px diagram inside a 1,000px box reads as
     a panel that failed to load its right-hand side. */
  width: fit-content;
  max-width: 100%;
}

.chain-diagram { display: block; height: auto; min-width: 100%; }

/*
 * A chain drawn as a column keeps its natural width.
 *
 * The rule above fills the container, which is what a row of four boxes wants. A column is
 * 132 units wide and as tall as the chain, so filling a 1,000px container scales it by
 * seven and a half in both directions - JOW's eighteen generations came out around twelve
 * thousand pixels tall. 132 units at the diagram's own scale is about 260px, which is the
 * width the boxes were drawn to be read at.
 *
 * `min-width: 0` is not redundant: it is what overrides the 100% above, which would
 * otherwise win over max-width and re-stretch it.
 */
.chain-diagram-stacked {
  min-width: 0;
  width: 260px;
  max-width: 100%;
}

.chain-box { fill: var(--paper); stroke: var(--rule); stroke-width: 1; }

/* A recalled ancestor: hue, heavier stroke, the class in words inside the box,
   and the same class again in the list below. Four carriers, because this is the
   one mark on the page a reader must not miss. */
.chain-box-recalled {
  fill: var(--sev-1-wash);
  stroke: var(--sev-1);
  stroke-width: 2;
}

/* 1976 is a boundary, not a device - dashed, because there is no further ancestor
   to find rather than one we failed to read. */
.chain-box-terminal { fill: var(--mist); stroke-dasharray: 4 3; }

.chain-k {
  font-family: var(--font-mono);
  font-size: 15px;
  font-weight: 500;
  fill: var(--ink);
}
.chain-node-sev { font-size: 12px; font-weight: 600; fill: var(--sev-1-text); }
.chain-node-detail { font-size: 12px; fill: var(--ink-faint-text); }
.chain-connector { stroke: var(--ink-faint); stroke-width: 1.5; }
.chain-arrowhead {
  fill: none;
  stroke: var(--ink-faint);
  stroke-width: 1.5;
  stroke-linecap: round;
  stroke-linejoin: round;
}

/* Visually hidden, but never display:none - it is the accessible copy of the
   diagram and the printed one. clip-path rather than a transform, because the
   reduced-motion block in base.css sets `transform: none !important`. */
.chain-list {
  position: absolute;
  width: 1px;
  height: 1px;
  overflow: hidden;
  clip-path: inset(50%);
  white-space: nowrap;
  margin: 0;
  padding: 0;
}

@media print {
  .chain-scroll { display: none; }
  .chain-list {
    position: static;
    width: auto;
    height: auto;
    clip-path: none;
    white-space: normal;
  }
  .chain-list li { display: block; margin-bottom: 2mm; }
}

/* ==================================================================== auth
   The sign-in, sign-up and code screens.

   These three pages are the only ones in the app whose content is shorter than the
   viewport, and the ordinary layout served them badly for exactly that reason: the
   shared `.container` is 1200px and the form panel was `.narrow` at 480px, so a 480px
   form sat hard against the left edge of a mostly empty page, with the heading pinned
   to the top by `main`'s fixed top padding. Nothing was broken and it read as though
   something was - which is a poor first impression on the two pages where a stranger
   forms one.

   So the auth pages get their own shell: one column, centred horizontally, and
   vertically balanced in whatever height is left between the header and the footer.
   The markup is unchanged - same fields, same handler, same no-JavaScript form.
   ------------------------------------------------------------------------ */

/* `justify-content` rather than `margin: auto`, so the column stays put when the
   content grows past the viewport - an error message and a hint can push the code
   screen taller than a short laptop window, and a centred block that overflows loses
   its top edge behind the header where it cannot be scrolled back to. */
.auth-page > main {
  display: flex;
  flex-direction: column;
  justify-content: center;
  padding-block: var(--s-48);
}

.auth-shell {
  width: 100%;
  /* The measure for the whole screen: heading, lead and panel all set to this, so the
     three read as one column rather than three widths stacked. Wide enough that the
     field hint sets in four lines rather than six, narrow enough to stay a form. */
  max-width: 28rem;
  margin-inline: auto;
  padding-inline: var(--gutter);
}

/* Centred, and the only place in the app where a heading is. A single form with no
   siblings has no left edge to align to, and centring it puts the heading, the field
   and the button on one axis the eye can follow straight down. */
.auth-head {
  display: block;
  text-align: center;
  margin-bottom: var(--s-24);
}

.auth-head .page-head-text {
  max-width: none;
}

.auth-head .lead {
  /* `.lead` caps at 62ch, which is far wider than this column. Released to the shell's
     own width instead, so the sentence breaks on the same margin as the panel under it
     rather than on a narrower one of its own. */
  max-width: none;
  font-size: var(--fs-body-lg);
}

.auth-panel {
  padding: var(--s-32);
}

/* The field is capped at 26rem for forms that sit in a wide page. Here the panel is
   already the measure, so the cap only introduces a gap on its right. */
.auth-panel .field {
  max-width: none;
  margin-bottom: var(--s-24);
}

/* Full width, because it is the only action and a button narrower than the field it
   submits reads as secondary. */
.auth-panel > .button {
  width: 100%;
  justify-content: center;
}

/* The code screen has two: submit, and "use a different email" under it. */
.auth-panel .actions {
  display: flex;
  flex-direction: column;
  gap: var(--s-12);
  align-items: stretch;
  text-align: center;
}

/* The "already have an account?" line under the panel. */
.auth-page .small {
  margin-top: var(--s-16);
  text-align: center;
}

/* Below this the vertical centring is worth less than the space it spends: on a short
   phone in landscape a centred column can leave the button under the fold. */
@media (max-width: 30rem) {
  .auth-page > main {
    justify-content: flex-start;
    padding-block: var(--s-32);
  }

  .auth-panel {
    padding: var(--s-24);
  }
}

/* ---------------------------------------------------------------------------
   The launch offer.

   Deliberately blue rather than amber or red. Those two are severity in this
   product - an amber wash means a device risk level, and it is used that way on
   every code page - so dressing a promotion in one would make a discount look
   like a recall. Blue is already the informational accent (.band-current, the
   focus ring), which is what this is.
   --------------------------------------------------------------------------- */
/*
  A solid blue bar with white text, fixed to the top of the viewport.

  Blue rather than amber or red: those are severity in this product - an amber wash is an
  FDA risk level on every code page - so a promotion in one would read as a recall, which
  is the single misreading this product cannot afford.

  White on --blue (#2f6bfe) measures 4.51:1, clearing AA for body text. That is why
  nothing below is a faded white: dropping the terms line to 70% would take it to about
  3.2:1 and fail. Hierarchy comes from size and weight instead.
*/
.promo-banner {
  position: fixed;
  inset-inline: 0;
  top: 0;
  z-index: 100;
  padding: var(--s-8) var(--s-16);
  background: var(--blue);
  color: var(--on-ink);
  text-align: center;
}

.promo-line {
  margin: 0;
  display: flex;
  flex-wrap: wrap;
  align-items: baseline;
  justify-content: center;
  gap: var(--s-8);
}

.promo-flag { font-size: var(--fs-lead); line-height: 1; }

.promo-flag-text {
  font-size: var(--fs-small);
  font-weight: 700;
  text-transform: uppercase;
  letter-spacing: 0.08em;
}

/* The number is the message, so it is the largest thing in the bar. */
.promo-head {
  font-size: var(--fs-h3);
  font-weight: 700;
  letter-spacing: -0.01em;
}

.promo-scope { font-size: var(--fs-small); }

.promo-ends {
  font-size: var(--fs-small);
  text-transform: uppercase;
  letter-spacing: 0.08em;
}

/* Tabular figures, so a ticking clock cannot reflow a wrapping flex row every second. */
.promo-clock {
  font-size: var(--fs-lead);
  font-weight: 700;
  font-variant-numeric: tabular-nums;
}

/* Reversed out of the bar - the only element that looks pressable. */
.promo-cta {
  display: inline-block;
  padding: var(--s-4) var(--s-16);
  border-radius: var(--radius-pill);
  background: var(--on-ink);
  color: var(--blue-text);
  font-size: var(--fs-small);
  font-weight: 700;
  text-decoration: none;
  white-space: nowrap;
}

.promo-cta:hover { background: var(--blue-tint); }

/* Present, and quiet. Full white rather than faded, for the contrast reason above. */
.promo-terms {
  margin: var(--s-4) 0 0;
  font-size: var(--fs-small);
}

/* The bar is out of flow, so the page has to be told it exists. --promo-h is published by
   app.js from the rendered height; with no offer running it is absent and this is zero. */
body {
  padding-top: var(--promo-h, 0);
}

/* The app's own sticky header, pushed below the bar for the same reason. */
.chrome {
  top: var(--promo-h, 0);
}

.plan-promo-line {
  color: var(--blue-text);
}

/* ===========================================================================
   The upsell bar.

   Pinned to the bottom edge, not thrown over the page as a dialog. Every screen
   it appears on is a screen somebody came here to read - a search result, a code
   page - and a modal over the work is a bounce. A strip at the bottom leaves the
   table visible and can be scrolled past.

   Bottom rather than top because the promo banner already owns the top edge and
   a page with a fixed strip on both ends is a letterbox.
   ========================================================================= */
.upsell {
  position: fixed;
  inset-inline: 0;
  bottom: 0;
  z-index: 90;
  padding: var(--s-12) var(--s-16);
  padding-bottom: calc(var(--s-12) + env(safe-area-inset-bottom, 0px));
  border-top: 1px solid var(--rule);
  background: var(--paper);
  box-shadow: var(--shadow-md);
}

.upsell-inner {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: var(--s-12);
  max-width: var(--max-w);
  margin-inline: auto;
}

.upsell-text {
  flex: 1 1 260px;
  margin: 0;
  font-size: var(--fs-small);
  line-height: var(--lh-small);
}

.upsell-found {
  color: var(--ink);
  font-weight: 700;
}

.upsell-sub {
  display: block;
  color: var(--ink-faint-text);
}

.upsell-actions {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: var(--s-12);
}

/* The free action is filled and the paid one is a link, in that order. Watching a
   code is the Value Moment and costs the reader nothing; somebody who has never
   seen a digest arrive is not the person to sell a subscription to. */
.upsell-primary {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  min-height: var(--tap-min);
  padding-inline: var(--s-24);
  border: 0;
  border-radius: var(--radius);
  background: var(--blue);
  color: var(--paper);
  font-family: inherit;
  font-size: var(--fs-nav);
  font-weight: 600;
  text-decoration: none;
  white-space: nowrap;
  cursor: pointer;
}

.upsell-primary:hover {
  background: var(--blue-hover);
}

.upsell-secondary {
  display: inline-flex;
  align-items: center;
  min-height: var(--tap-min);
  font-size: var(--fs-small);
  font-weight: 600;
  color: var(--blue-text);
  white-space: nowrap;
}

/* The material term of the offer, in the same bar as the offer. An introductory
   price that hides what it renews at is a misleading omission under the UK CPRs
   and the EU UCPD, both of which reach this product because Paddle sells into
   them as Merchant of Record. */
.upsell-terms {
  flex: 1 1 100%;
  margin: 0;
  font-size: var(--fs-legal);
  line-height: var(--lh-small);
  color: var(--ink-faint-text);
}

/* Room for the bar, reserved in flow and only when the bar is there. Generous
   rather than exact: the bar wraps to three rows at 360px, where the terms line
   and the actions each take one of their own. */
.upsell-spacer {
  height: 150px;
}

/* A row-level button: the Watch control in the search results table. Short enough
   not to widen a column that already scrolls sideways on a phone. */
.button-small {
  min-height: 36px;
  padding: var(--s-4) var(--s-12);
  font-size: var(--fs-small);
  white-space: nowrap;
}

/* Already on the list. Stated in words, never as a tick alone - the column is
   otherwise all buttons, and a reader scanning it has to be able to tell an action
   from a state. */
.cell-action {
  white-space: nowrap;
}

.cell-watching {
  font-size: var(--fs-small);
  font-weight: 600;
  color: var(--signal-text);
}

/* The payback sum, under the price on the plans screen. Ink and 500, so it reads
   as an argument rather than as the caption the muted `.small` around it is. */
.plan-payback {
  display: inline-block;
  margin-top: var(--s-8);
  color: var(--ink);
  font-weight: 600;
}

/* ------------------------------------------------------------------ watch lead
 *
 * The watch invitation, which is now the most prominent thing on a search result,
 * a company page and a code page. See `watchLead` in layout.ts for why it is one
 * component rather than three.
 *
 * It reads as a panel rather than as a banner: the accent edge is the same device
 * the findings use, so a reader already knows the shape means "this is the thing
 * on this screen". Blue rather than a severity hue, for the same reason
 * .panel-invite is - severity colours in this product mean FDA records, and an
 * offer is not a finding.
 *
 * Flex with wrap rather than a grid, so the button drops under the text at narrow
 * widths instead of squeezing the sentence into a column two words wide. The
 * button keeps `white-space: nowrap` because "Watch GEI" broken across two lines
 * is the least confident possible rendering of the one action this page wants.
 */
.watch-lead {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  justify-content: space-between;
  gap: var(--s-16) var(--s-24);
  margin-bottom: var(--s-24);
  padding: var(--s-24);
  border: 1px solid var(--blue);
  border-left: 4px solid var(--blue);
  border-radius: 0 var(--radius) var(--radius) 0;
  background: var(--blue-tint);
  box-shadow: var(--shadow-sm);
}

/* Enough to keep the sentence on a reading measure, and enough to make it give way
   before the button does. */
.watch-lead-text {
  flex: 1 1 22rem;
  min-width: 0;
}

.watch-lead-title {
  margin: 0 0 var(--s-4);
  font-size: var(--fs-h3);
  font-weight: 700;
  line-height: var(--lh-h3);
  color: var(--ink);
}

.watch-lead-sub {
  margin: 0;
  font-size: var(--fs-small);
  line-height: var(--lh-small);
  color: var(--ink-soft);
}

.watch-lead-device {
  font-weight: 600;
  color: var(--ink);
}

.watch-lead-action {
  display: flex;
  flex-direction: column;
  align-items: flex-start;
  gap: var(--s-8);
  /*
   * Shrinkable, and min-width:0 so it actually can.
   *
   * This was flex: 0 0 auto, which means the column is laid out at the max-content
   * width of its widest child - so the sentence under the button never wrapped, at any
   * viewport, and simply widened the document instead. It fit at 390px only because the
   * line happened to be short; lengthening it by a dozen characters pushed /code/:code
   * and the search results to 406px in a 390px viewport, which check-layout.mjs caught.
   * A flex item's default min-width is auto, so the min-width:0 is the half that does
   * the work - without it the item refuses to shrink below max-content whatever the
   * flex-basis says. .watch-lead-text beside it has had both from the start.
   */
  flex: 0 1 auto;
  min-width: 0;
}

/* Taller and wider than the standard button, which is the whole point: this is the
   one control on the page that has to win against a table of links. */
.button-lead {
  min-height: 48px;
  padding-inline: var(--s-32);
  font-size: var(--fs-body);
  white-space: nowrap;
}

/* The three facts that answer the reasons somebody does not click: what it costs,
   what it needs, and how to undo it. */
.watch-lead-free {
  font-size: var(--fs-legal);
  line-height: var(--lh-small);
  color: var(--ink-faint-text);
}

/* Already watching. Not an offer any more, so it loses the fill and the accent edge
   and becomes a statement - a panel still shouting an action the reader has taken is
   a panel that has stopped reading the account. */
.watch-lead-on {
  border-color: var(--rule);
  border-left-color: var(--signal);
  background: var(--paper);
}

.watch-lead-state {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: var(--s-8);
  margin: 0 0 var(--s-4);
  font-size: var(--fs-body);
  color: var(--ink);
}

/* Optional on the form, because "we already know your account" is the argument the
   support form makes and this one borrows it. */
.field-optional {
  font-weight: 400;
  color: var(--ink-faint-text);
}

/* ---------------------------------------------------------------------------
 * Getting started
 *
 * The first-run checklist, the answer to a first watch, and the empty dashboard
 * that precedes both. Three components, one feature - see
 * apps/app/src/onboarding.ts for what each of them is for.
 *
 * All three are blue or neutral and never a severity hue. The rule the whole
 * product runs on is that red and amber mean how serious an FDA record is, and a
 * teal tick on a checklist would be the second meaning that unteaches the first.
 * ------------------------------------------------------------------------- */

/* The two glyph paints, as classes rather than attributes.
 *
 * A knockout tick needs a white stroke and no colour may be written in a source
 * file - check-tokens.mjs fails the build on a hex outside the token file - and
 * a presentation attribute cannot read a custom property dependably. So the
 * shapes are markup and the paint is here, which is also what lets the disc take
 * its colour from whatever is around it. */
.glyph-disc { fill: currentColor; }
.glyph-knockout {
  fill: none;
  stroke: var(--paper);
  stroke-width: 2.2;
  stroke-linecap: round;
  stroke-linejoin: round;
}
.glyph-open {
  fill: none;
  stroke: currentColor;
  stroke-width: 1.5;
}

.onboard {
  border-color: var(--rule);
  background: var(--mist);
}

/* The heading and the dismiss button on one line, and the button gives way first
   at narrow widths - a "Hide this" wrapping under a two-word heading is the least
   important control on the panel taking a row of its own. */
.onboard-head {
  display: flex;
  flex-wrap: wrap;
  align-items: flex-start;
  justify-content: space-between;
  gap: var(--s-8) var(--s-16);
  margin-bottom: var(--s-16);
}

.onboard-progress {
  margin: var(--s-4) 0 0;
  font-size: var(--fs-small);
  line-height: var(--lh-small);
  color: var(--ink-faint-text);
}

.onboard-dismiss { margin: 0; }

/* An ordered list with its numbers suppressed: the order is real - each step is
   easier once the one above it is done - and the marker would be a second counter
   next to the ticks. */
.onboard-list {
  margin: 0;
  padding: 0;
  list-style: none;
}

.onboard-step {
  display: flex;
  align-items: flex-start;
  gap: var(--s-12);
  padding: var(--s-12) 0;
  border-top: 1px solid var(--rule);
  color: var(--ink-faint);
}

.onboard-step:first-child { border-top: 0; padding-top: 0; }

/* Done, and the glyph is the only thing that changes colour. The text stays at
   full contrast rather than fading: a completed step is still something the
   reader may want to read, and greying it out is how a checklist becomes four
   lines nobody can make out. */
.onboard-step-done { color: var(--blue); }

.onboard-glyph {
  flex: 0 0 auto;
  width: 20px;
  height: 20px;
  margin-top: 2px;
}

.onboard-step-text { min-width: 0; }

.onboard-step-title {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: var(--s-8);
  margin: 0 0 var(--s-4);
  font-size: var(--fs-body-compact);
  font-weight: 600;
  color: var(--ink);
}

.onboard-step-body {
  margin: 0;
  font-size: var(--fs-small);
  line-height: var(--lh-small);
  color: var(--ink-soft);
}

/* ------------------------------------------------------------ the first watch
 *
 * What a reader sees the instant they have done the one thing this funnel is
 * measured against. Louder than a notice and quieter than a finding: a filled
 * disc, a heading, and the sentence that says what will now arrive. */
.win {
  display: flex;
  align-items: flex-start;
  gap: var(--s-16);
  padding: var(--s-24);
  border: 1px solid var(--blue);
  border-left: 4px solid var(--blue);
  border-radius: 0 var(--radius) var(--radius) 0;
  background: var(--blue-tint);
  box-shadow: var(--shadow-sm);
}

.win-mark {
  flex: 0 0 auto;
  margin: 0;
  color: var(--blue);
}

.win-glyph {
  display: block;
  width: 28px;
  height: 28px;
}

.win-text { min-width: 0; }

.win-title {
  margin: 0 0 var(--s-8);
  font-size: var(--fs-h3);
  font-weight: 700;
  line-height: var(--lh-h3);
  color: var(--ink);
}

.win-body {
  margin: 0;
  font-size: var(--fs-body);
  line-height: var(--lh-body);
  color: var(--ink-soft);
}

/* The upgrade, on the same line as the question it answers and wrapping under it
   on a phone. A question with its answer three lines below is two separate asks. */
.win-more {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: var(--s-12);
  margin: var(--s-16) 0 0;
}

.win-ask {
  font-size: var(--fs-body);
  font-weight: 600;
  color: var(--ink);
}

/* ------------------------------------------------------- the empty dashboard
 *
 * Not a dashed box. On the one screen where the product has nothing to show, the
 * single click that starts it working is the page. */
.first-run {
  padding: var(--s-48) var(--s-24);
  border: 1px solid var(--rule);
  border-radius: var(--radius);
  background: var(--paper);
  box-shadow: var(--shadow-sm);
  text-align: center;
}

.first-run-eyebrow {
  margin: 0 0 var(--s-8);
  font-size: var(--fs-eyebrow);
  font-weight: 600;
  letter-spacing: var(--ls-eyebrow);
  text-transform: uppercase;
  color: var(--ink-faint-text);
}

.first-run-title {
  margin: 0 0 var(--s-16);
  font-size: var(--fs-h3);
  font-weight: 700;
  line-height: var(--lh-h3);
  color: var(--ink);
}

.first-run-body {
  margin: 0 auto var(--s-24);
  max-width: 38rem;
  font-size: var(--fs-body);
  line-height: var(--lh-body);
  color: var(--ink-soft);
}

.first-run-action { margin: 0 0 var(--s-16); }

.first-run-alt {
  margin: 0 auto;
  max-width: 38rem;
  font-size: var(--fs-small);
  line-height: var(--lh-small);
  color: var(--ink-faint-text);
}

/* ====================================================== the risk score gauge
   The Predicate Risk Score (packages/core/src/predicaterisk.ts), drawn by
   riskGauge in charts.ts.

   The band word carries the meaning and the fill reinforces it, never the other
   way round - the same rule .sev holds to, and for the same three readers: a
   greyscale print, a forced-colours client, and a red-green colour deficiency.
   Every band therefore has a label with its own weight, and the meter is
   aria-labelled with the number AND the word.
   ------------------------------------------------------------------------ */

.risk {
  margin: 0 0 var(--s-16);
  padding: var(--s-16);
  border: 1px solid var(--rule);
  border-radius: var(--radius);
  background: var(--mist);
}

/* The eyebrow's default bottom margin is 16px, which is a gap between a label and the
   number it labels rather than a join. */
.risk-label { margin-bottom: var(--s-8); }

.risk-head {
  display: flex;
  flex-wrap: wrap;
  gap: var(--s-16);
  align-items: center;
  margin-bottom: var(--s-12);
}

.risk-figure {
  display: flex;
  align-items: baseline;
  gap: var(--s-8);
  margin: 0;
}

.risk-number {
  font-family: var(--font-mono);
  font-size: var(--fs-h2);
  line-height: 1;
  color: var(--ink);
}

.risk-of {
  font-size: var(--fs-small);
  color: var(--ink-faint-text);
}

/* Grows to fill the row so the meter is a usable length at every width, and
   wraps under the number rather than shrinking to a stub on a phone. */
.risk-band {
  flex: 1 1 12rem;
  min-width: 10rem;
}

.risk-band-label {
  margin: 0 0 var(--s-8);
  font-size: var(--fs-eyebrow);
  font-weight: 700;
  letter-spacing: var(--ls-eyebrow);
  text-transform: uppercase;
}

/* Four bands, four weights of the same message. "None found" is deliberately
   the brand accent rather than a severity hue: the absence of a recall is good
   news about a record, not a grade on a device. */
.risk-band-high { color: var(--sev-1-text); }
.risk-band-elevated { color: var(--sev-2-text); }
.risk-band-low { color: var(--ink); }
.risk-band-none-found { color: var(--signal-text); }
.risk-band-not-known { color: var(--ink-faint-text); }

.risk-meter {
  display: block;
  width: 100%;
  height: 8px;
}

.risk-track { fill: var(--paper); stroke: var(--rule); }
.risk-fill-high { fill: var(--sev-1); }
.risk-fill-elevated { fill: var(--sev-2); }
.risk-fill-low { fill: var(--ink-faint); }
.risk-fill-none-found { fill: var(--signal); }
.risk-fill-not-known { fill: var(--rule); }

/* The working, collapsed by default.

   Open by default was tried and is wrong here: the arithmetic is six lines of
   record ids, and a reader who has not yet decided whether they care about the
   number should not have to scroll past its derivation to reach the recalls it
   was derived from. A <details> is the honest control - it needs no script, it
   prints open in browsers that expand them, and it is keyboard-operable for
   free. */
.risk-working {
  margin-top: var(--s-12);
  font-size: var(--fs-small);
}

.risk-working summary {
  cursor: pointer;
  font-weight: 600;
  color: var(--blue-text);
}

.risk-parts {
  margin: var(--s-12) 0 0;
  padding: 0;
  list-style: none;
}

.risk-parts li {
  display: flex;
  gap: var(--s-12);
  padding: var(--s-8) 0;
  border-top: 1px solid var(--rule);
  line-height: var(--lh-small);
  color: var(--ink-soft);
}

.risk-part-points {
  flex: 0 0 3rem;
  font-family: var(--font-mono);
  font-weight: 600;
  text-align: right;
  color: var(--ink);
}

.risk-part-total {
  font-weight: 600;
  color: var(--ink);
}

/* ================================================= the chain, as a read list
   chainOutline in charts.ts. The diagram above it is the picture; this is the
   thing a reader actually reads, and the indentation is the information.
   ------------------------------------------------------------------------ */

.chain-outline {
  margin: 0 0 var(--s-16);
  padding: 0;
  list-style: none;
}

.chain-step {
  padding: var(--s-12) var(--s-12) var(--s-12) var(--s-16);
  border-left: 3px solid var(--rule);
  border-bottom: 1px solid var(--rule);
}

/* Indentation by generation, capped at six steps in the markup. A real chain
   reaches fifteen to twenty generations back to 1976, and an unbounded indent
   pushes the oldest device - often the recalled one - off the right edge of a
   phone. */
.chain-step-1 { margin-left: var(--s-16); }
.chain-step-2 { margin-left: calc(var(--s-16) * 2); }
.chain-step-3 { margin-left: calc(var(--s-16) * 3); }
.chain-step-4 { margin-left: calc(var(--s-16) * 4); }
.chain-step-5 { margin-left: calc(var(--s-16) * 5); }
.chain-step-6 { margin-left: calc(var(--s-16) * 6); }

/* At phone width the indent is the first thing to go. Six levels of 16px is
   96px of a 375px screen spent on hierarchy the border already carries. */
@media (max-width: 40rem) {
  .chain-step-1,
  .chain-step-2,
  .chain-step-3,
  .chain-step-4,
  .chain-step-5,
  .chain-step-6 {
    margin-left: var(--s-8);
  }
}

.chain-step-recalled {
  border-left-color: var(--sev-1);
  background: var(--sev-1-wash);
}

.chain-step-head {
  display: flex;
  flex-wrap: wrap;
  gap: var(--s-8);
  align-items: center;
  margin: 0 0 4px;
}

.chain-step-k { font-weight: 600; color: var(--ink); }

.chain-step-depth {
  font-size: var(--fs-small);
  color: var(--ink-faint-text);
}

.chain-step-detail {
  margin: 0;
  font-size: var(--fs-small);
  line-height: var(--lh-small);
  color: var(--ink-soft);
}

.chain-step-recalls {
  margin: var(--s-8) 0 0;
  padding: 0 0 0 var(--s-16);
  list-style: none;
  font-size: var(--fs-small);
  color: var(--sev-1-text);
}

.chain-step-recalls li { padding: 2px 0; }

/* An amber pill for "this code is running above its own baseline". It IS a
   severity statement about an FDA record, so it takes a severity token rather
   than the neutral .band greys it inherits from. */
.band-alert {
  color: var(--sev-2-text);
  background: var(--sev-2-wash);
  border-color: var(--sev-2);
}

/* ------------------------------------------------- the chain device switcher
   Plain links, not a select and not tabs. Switching which device's chain is
   drawn is navigation: it reloads the page, so it must be back-buttonable and
   linkable, and it has to work with JavaScript off on a managed browser.

   aria-current marks the one being shown. Not aria-pressed - that is only valid
   on something with a button role, and axe reports it on a link as
   aria-allowed-attr, critical.
   ------------------------------------------------------------------------ */

.chain-switch {
  display: flex;
  flex-wrap: wrap;
  gap: var(--s-8);
  margin: 0 0 var(--s-16);
}

.chain-switch-link {
  padding: 4px var(--s-12);
  border: 1px solid var(--rule);
  border-radius: var(--radius-pill);
  font-family: var(--font-mono);
  font-size: var(--fs-small);
  text-decoration: none;
  color: var(--ink-soft);
  background: var(--paper);
}

.chain-switch-link:hover { border-color: var(--blue); color: var(--blue-text); }

.chain-switch-link[aria-current] {
  border-color: var(--ink);
  font-weight: 600;
  color: var(--ink);
  background: var(--mist);
}

/* ================================================= the watch list, organised
   The watch list stopped being a table when it grew a note field and a group.

   A table was right when every row was three dates and a Remove button. It is
   wrong now: each row carries two inputs and a note that runs to a sentence, and
   a table cell containing a form either stretches the column to the width of the
   longest note or clips it. A list of cards holds a variable-height row without
   the other rows having to agree to it.
   ------------------------------------------------------------------------ */

.watch-group { margin: var(--s-24) 0 0; }

.watch-group-head {
  display: flex;
  flex-wrap: wrap;
  gap: var(--s-12);
  align-items: baseline;
  margin: 0 0 var(--s-12);
  padding-bottom: var(--s-8);
  border-bottom: 1px solid var(--rule);
}

.watch-group-count {
  font-size: var(--fs-small);
  font-weight: 400;
  color: var(--ink-faint-text);
}

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

.watch-row {
  padding: var(--s-12) 0;
  border-bottom: 1px solid var(--rule);
}

.watch-row-head {
  display: flex;
  flex-wrap: wrap;
  gap: var(--s-12);
  align-items: baseline;
}

.watch-row-code { font-size: var(--fs-card-title); font-weight: 600; }

/* Pushes Remove to the right edge on a wide row and lets it wrap under on a
   phone, without a second flex container or a media query. */
.watch-row-dates {
  flex: 1 1 auto;
  font-size: var(--fs-small);
  color: var(--ink-faint-text);
}

/* The link out to FDA's classification record, inside the meta line.
   Inherits the faint size of the line it sits in, so it reads as part of the
   sentence rather than as a button competing with Remove at the other end. */
.watch-row-source {
  color: var(--ink-soft);
  text-decoration: underline dotted var(--ink-faint);
  text-underline-offset: 3px;
}

.watch-row-source:hover,
.watch-row-source:focus-visible {
  color: var(--blue-text);
  text-decoration: underline solid var(--blue-text);
}

.watch-row-remove { flex: 0 0 auto; }

.watch-annotate {
  display: flex;
  flex-wrap: wrap;
  gap: var(--s-12);
  align-items: flex-end;
  margin-top: var(--s-8);
}

/* The note is the one that should take the leftover width: a group is a client
   name and a note is a sentence. */
.watch-annotate-group { flex: 1 1 12rem; margin: 0; }
.watch-annotate-note { flex: 3 1 18rem; margin: 0; }

.watch-annotate .field label {
  font-size: var(--fs-small);
  color: var(--ink-faint-text);
}

/* ---------------------------------------------------- comparing two codes
   Two fields side by side above the table, stacking on a phone. Everything
   below them is an ordinary .table inside a .scroller - a comparison is a
   table, and dressing it as anything else would cost the row headings a screen
   reader announces.
   ------------------------------------------------------------------------ */

.compare-fields {
  display: flex;
  flex-wrap: wrap;
  gap: var(--s-16);
}

.compare-fields .field { flex: 1 1 10rem; }

/* The measure column is the one that should hold its width: its cells carry a
   label and a line of record links, and letting it collapse puts "Recalls not
   yet terminated" on four lines beside two short numbers. */
.compare-table th[scope="row"] { min-width: 14rem; }

/* ===========================================================================
   The signed-in workspace: density, and the finish on shared pieces.

   The app used to inherit the marketing site's type scale outright - a 40px page title,
   21px lead and 17px body - which is right for a page read once and wrong for a tool read
   every morning with a table in it: a four-column KPI row and a twelve-row recall table
   could not share a screen. Inside the signed-in shell the same variables are re-pointed
   at the app steps in tokens.css, so every component that already reads --fs-body or
   --fs-h3 tightens with no per-component rule, and the signed-out pages, the marketing
   site and the client-ready report are untouched.

   Inputs stay at 16px below 768px, where iOS would otherwise zoom on focus.
   =========================================================================== */
body.shell {
  --fs-h2: var(--fs-app-title);
  --lh-h2: 1.2;
  --ls-h2: -0.022em;
  --fs-h3: var(--fs-app-section);
  --lh-h3: 1.35;
  --fs-card-title: var(--fs-app-card);
  --lh-card-title: 1.4;
  --fs-lead: var(--fs-app-body);
  --lh-lead: 1.55;
  --fs-body: var(--fs-app-body);
  --lh-body: 1.55;
  --fs-body-lg: 16px;
  --fs-body-compact: var(--fs-app-table);
  --fs-small: var(--fs-app-small);
  --lh-small: 1.5;
  --fs-eyebrow: 11.5px;
  --ls-eyebrow: 0.06em;
  --fs-legal: 12px;
  --fs-nav: 14px;
  --fs-mono: var(--fs-app-mono);
  --fs-mono-chip: 12px;
  --fs-price: 36px;
  --fs-price-old: 20px;
  --fs-price-talk: 24px;
  --fs-cadence: 15px;
  font-size: var(--fs-body);
  line-height: var(--lh-body);
}

/* A mouse can hit a 40px target; a finger keeps the 44px floor. */
@media (pointer: fine) {
  body.shell { --tap-min: 40px; }
}

@media (max-width: 767px) {
  body.shell { --fs-app-title: 22px; --fs-app-kpi: 22px; }

  /* Two KPI tiles to a row on a phone rather than four full-width cards: the numbers
     are short, and a row of four stacked cards pushed the findings below two screens. */
  body.shell .grid {
    grid-template-columns: repeat(2, minmax(0, 1fr));
    gap: var(--s-12);
  }

  /* An odd tile out takes the whole row rather than sitting half-width on its own. */
  body.shell .grid > .stat:last-child:nth-child(odd) { grid-column: 1 / -1; }

  body.shell .stat { padding: var(--s-12); }
  body.shell .stat-risk,
  body.shell .stat-warn { padding-left: calc(var(--s-12) + 3px); }
  body.shell .stat dt { font-size: 11px; letter-spacing: 0.04em; }
  body.shell .stat dd.text-body { font-size: 15px; }
  body.shell .field input,
  body.shell .field textarea,
  body.shell .search-input { font-size: 16px; }
}

body.shell > main {
  padding-block: 0;
}

/* The page header: title and one line under it, actions on the right edge, and a
   hairline-free gap before the content. */
body.shell .page-head {
  align-items: flex-end;
  gap: var(--s-16) var(--s-24);
  margin-bottom: var(--s-24);
}

body.shell .page-head-text { max-width: 72ch; }
body.shell .h1 { margin-bottom: var(--s-4); }
body.shell .lead { max-width: 72ch; color: var(--ink-soft); }
body.shell .page-actions { padding-top: 0; }

body.shell .h2 { margin-bottom: var(--s-12); letter-spacing: -0.01em; }
body.shell .h3 { letter-spacing: -0.005em; }

/* Section rhythm: a heading that opens a new block gets room above it. Scoped to
   headings in the page flow - a heading inside a card already has the card's padding. */
body.shell .pane .grid + .h2,
body.shell .pane .panel + .h2,
body.shell .pane .scroller + .h2 { margin-top: var(--s-32); }

/* Pills read in sentence case. Uppercase-plus-tracking was the marketing eyebrow, and
   on a dense screen a row of shouting pills is the loudest thing on it whatever they
   say. The words, glyphs and hues are unchanged - only the case and the tracking. */
body.shell .band,
body.shell .sev,
body.shell .change-kind,
body.shell .source-tag {
  padding: 1px var(--s-8);
  font-size: var(--fs-app-label);
  font-weight: 600;
  letter-spacing: 0;
  text-transform: none;
  line-height: 1.6;
}

body.shell .sev { gap: 5px; }

/* The neutral pill ("Free", "Optional", "Watching") at the app's 12px measured 4.53:1 in
   the faint grey on mist - a pass by 0.03. The body grey takes it to 6.5:1, so a tag stays
   legible on a dim laptop screen rather than merely compliant on a good one. */
body.shell .band:not(.band-current):not(.band-alert) { color: var(--ink-soft); }
body.shell .sev-glyph { width: 12px; height: 12px; }

/* The plan count: a quiet meta line rather than a bordered box that looked like a
   button. It still turns blue when the plan is full. */
body.shell .watching-line {
  display: inline-flex;
  align-items: center;
  gap: var(--s-8);
  margin-bottom: var(--s-16);
  padding: 2px var(--s-12);
  border-color: var(--rule);
  border-radius: var(--radius-pill);
  background: var(--paper);
  font-size: var(--fs-app-label);
}

body.shell .watching-line::before {
  content: "";
  width: 6px;
  height: 6px;
  border-radius: var(--radius-pill);
  background: var(--signal);
}

body.shell .watching-line-full { background: var(--blue-tint); border-color: var(--blue-tint); }
body.shell .watching-line-full::before { background: var(--blue); }

/* FDA provenance: still on every screen that reads FDA records, now sized as the
   standing footnote it is rather than as the first panel on the page. */
body.shell .integrity {
  padding: var(--s-12) var(--s-16) var(--s-12) calc(var(--s-16) + 3px);
  border: 1px solid var(--rule);
  border-radius: var(--radius);
  background: var(--paper);
  box-shadow: inset 3px 0 0 var(--ink-faint);
  max-width: none;
}

body.shell .integrity-line { font-size: var(--fs-small); color: var(--ink-soft); }
body.shell .integrity-sources { font-size: var(--fs-app-label); line-height: 1.7; }

/* The watch invitation keeps its job as the loudest thing on the screen, with the
   rounded card and a single accent edge every other card here uses. */
body.shell .watch-lead {
  padding: var(--s-16) var(--s-24) var(--s-16) calc(var(--s-24) + 4px);
  border: 1px solid var(--blue);
  border-radius: var(--radius-lg);
  box-shadow: inset 4px 0 0 var(--blue), var(--shadow-card);
}

/* Already watching: a statement, not an offer, so it keeps the brand-teal edge it has
   always had - distinct from the blue invitation - on a plain paper card. */
body.shell .watch-lead-on {
  border-color: var(--rule);
  background: var(--paper);
  box-shadow: inset 4px 0 0 var(--signal), var(--shadow-card);
}

/* One sentence, so it wraps as a sentence: the pill sits inline at its start rather
   than every link and full stop becoming its own flex item on a narrow screen. */
body.shell .watch-lead-state { display: block; line-height: 1.7; }
body.shell .watch-lead-state .band { margin-right: var(--s-8); vertical-align: 1px; }

/* Panels on the canvas: a touch more air on a wide screen, the same on a phone. */
body.shell .panel { padding: var(--s-24); }
body.shell .panel-quiet { background: var(--paper); box-shadow: none; border-style: dashed; }
body.shell .panel-invite { box-shadow: var(--shadow-card); }

/* A run of finding cards reads as one list. */
body.shell .finding + .finding { margin-top: var(--s-12); }

/* The footer under a signed-in page is the legal line, not a second masthead: the rail
   already carries the mark on this screen, so here it gives way to the work. (The whole
   footer is hidden in print - see the print block.) */
body.shell .foot {
  padding-block: var(--s-24) var(--s-32);
  border-top: 1px solid var(--rule);
  background: transparent;
}

body.shell .foot .container { max-width: var(--pane-max); padding-inline: var(--s-32); }
body.shell .foot .legal { font-size: var(--fs-legal); }

body.shell .foot-brand { display: none; }

@media (max-width: 860px) {
  body.shell .foot .container { padding-inline: var(--s-16); }
}

/* Visible text selection in the brand family. */
body.shell ::selection { background: var(--blue-tint); color: var(--ink); }

/* ----------------------------------------------------------- button family

   Three weights and nothing else: the filled .button is the one next action on a
   screen, .button-quiet is a real but secondary action, and .button-ghost is the
   quietest - a link-like control that still has a button's target size, used for
   destructive or navigational actions that must never be the thing a hurried click
   lands on. */
.button-ghost {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: var(--s-8);
  min-height: var(--tap-min);
  padding: 0 var(--s-12);
  border: 1px solid transparent;
  border-radius: var(--radius);
  background: transparent;
  font: inherit;
  font-size: var(--fs-nav);
  font-weight: 600;
  color: var(--ink-soft);
  text-decoration: none;
  white-space: nowrap;
  cursor: pointer;
  transition: background-color var(--dur-hover) var(--ease), color var(--dur-hover) var(--ease);
}

.button-ghost:hover { background: var(--rail-hover); color: var(--ink); }

.button-icon {
  width: 16px;
  height: 16px;
  flex: none;
  fill: none;
  stroke: currentColor;
  stroke-width: 1.8;
  stroke-linecap: round;
  stroke-linejoin: round;
}

/* Two buttons that are one choice in two formats: joined, so the pair reads as a
   single control with a split rather than as two unrelated actions. */
.button-group {
  display: inline-flex;
  box-shadow: var(--shadow-sm);
  border-radius: var(--radius);
}

.button-group > .button-quiet { box-shadow: none; }
.button-group > .button-quiet:not(:first-child) { margin-left: -1px; border-top-left-radius: 0; border-bottom-left-radius: 0; }
.button-group > .button-quiet:not(:last-child) { border-top-right-radius: 0; border-bottom-right-radius: 0; }
.button-group > .button-quiet:hover { position: relative; z-index: 1; }

/* The last control in a toolbar, pushed to the far edge on a wide row and wrapping
   under the rest on a narrow one. */
.actions-end { margin-left: auto; }

/* --------------------------------------------------------------- onboarding

   On paper like every other card on the canvas, with the count also drawn as a bar. */
body.shell .onboard { background: var(--paper); }

.onboard-meter {
  display: block;
  width: 10rem;
  height: 4px;
  margin-top: var(--s-8);
  border-radius: var(--radius-pill);
  overflow: hidden;
}

.onboard-meter-track { fill: var(--track); }
.onboard-meter-fill { fill: var(--blue); }

/* A KPI tile's value that is a word ("pro") starts with a capital, without changing
   what the page or a test reads. Digits are unaffected. */
.stat dd::first-letter { text-transform: uppercase; }

/* Prose leading into a table, and a search box leading into results: a gap, not a seam. */
body.shell .pane p + .scroller,
body.shell .pane .section-head + .scroller { margin-top: var(--s-12); }
body.shell .page-search { margin-bottom: var(--s-16); }

/* A provenance line rendered inside a table's card, under the last row: a footer strip
   of the card rather than text pressed against its edge. */
.scroller > .source-note {
  margin: 0;
  padding: var(--s-12) var(--s-16);
  max-width: none;
  border-top: 1px solid var(--rule-soft);
  background: var(--canvas);
}

/* The Watch button on every row of a results table. Each one is real - watching is the
   free, one-click Value Moment - but seven filled blue buttons in a column outshout the
   one watch panel above them that is the screen's actual next step. On a row they are
   tinted: unmistakably a button, quieter than the lead. */
body.shell .table .button.button-small {
  background: var(--blue-tint);
  border-color: var(--blue-tint);
  color: var(--blue-text);
  box-shadow: none;
}

body.shell .table .button.button-small:hover {
  background: var(--blue);
  border-color: var(--blue);
  color: var(--paper);
}

/* Small links that sit in runs - FDA record ids under a finding, the "Predicate check"
   and "FDA record" links on a table row's sub-line - keep a 24px-tall target, the WCAG
   2.2 minimum (2.5.8), now that the app's type is tighter. Inline-block so the height is
   the link's own rather than borrowed from the line around it. */
.finding-records .fda-id,
.cell-sub a {
  display: inline-block;
  min-height: 24px;
  line-height: 24px;
}

/* --------------------------------------------------------- focus not obscured

   WCAG 2.2 SC 2.4.11. The top bar is sticky, and on a phone the tab strip sticks under
   it, so without this a keyboard user tabbing back up a table had the focused link
   scrolled to the very top of the viewport - which is behind the bar. Measured: five
   focus stops on a code page and three on Competitors landed entirely under it.

   scroll-padding tells the browser where the visible part of the viewport starts, and
   focus navigation, anchor jumps and scrollIntoView all honour it. --promo-h is the
   launch-offer bar's height (published by app.js; zero when it is absent). */
html {
  scroll-padding-top: calc(var(--promo-h, 0px) + var(--topbar-h) + var(--s-16));
}

@media (max-width: 860px) {
  html {
    /* The bar plus the 44px tab strip under it. */
    scroll-padding-top: calc(var(--promo-h, 0px) + var(--topbar-h) + 44px + var(--s-16));
  }
}

/* The fixed upsell bar covers the bottom edge the same way. :has() so the padding exists
   only while the bar does; a browser without :has() simply keeps today's behaviour. */
html:has(.upsell) {
  scroll-padding-bottom: 160px;
}

/* A short viewport - a phone on its side, or a laptop at 200% zoom - cannot spare the
   top 100px to chrome that never moves. Below 520px of height the bar and the tab strip
   scroll away with the page (WCAG 1.4.10 reflow); everything in them is still at the top
   of the document, one swipe up. */
@media (max-height: 520px) {
  .topbar,
  .rail { position: static; }
  html { scroll-padding-top: var(--s-16); }
}

/* ------------------------------------------------------------ forced colours

   Windows High Contrast replaces every background with the system Canvas colour, so the
   current page in the rail - marked by a blue tint and a 3px bar, both backgrounds - was
   indistinguishable from the links around it. A system-coloured outline survives, and
   the bar is drawn in Highlight with forced-color-adjust switched off for it alone. */
@media (forced-colors: active) {
  .rail-link-current { outline: 2px solid LinkText; outline-offset: -2px; }
  .rail-link-current::before { forced-color-adjust: none; background: Highlight; }
  .onboard-meter-fill { fill: Highlight; }
  .onboard-meter-track { fill: GrayText; }
}

/* ===========================================================================
   Daily-use screens, from the reader's side.

   Today: the way into a code sits in each finding's header; the quiet codes are links.
   Watch list: one white card of rows - code, FDA's device name, dates, a ghost Remove -
   with the note printed on the row and the edit fields behind a native disclosure.
   =========================================================================== */

/* -- Today ---------------------------------------------------------------- */

.finding-head { align-items: center; }

.finding-open {
  margin-left: auto;
  font-size: var(--fs-small);
  font-weight: 600;
  color: var(--blue-text);
  text-decoration: none;
  white-space: nowrap;
}

.finding-open:hover,
.finding-open:focus-visible { text-decoration: underline; }

.quiet-codes {
  display: flex;
  flex-wrap: wrap;
  gap: var(--s-8);
  margin: 0 0 var(--s-24);
}

.quiet-code {
  display: inline-flex;
  align-items: center;
  min-height: 28px;
  padding: 0 var(--s-12);
  border: 1px solid var(--rule);
  border-radius: var(--radius-pill);
  background: var(--paper);
  color: var(--ink);
  font-weight: 600;
  text-decoration: none;
}

.quiet-code:hover { border-color: var(--blue); color: var(--blue-text); }

/* "Nothing new since you were last here" is reassurance, not content: one compact line
   group, so it does not read as a panel of equal weight to the findings beside it. */
body.shell .panel-quiet { padding: var(--s-12) var(--s-16); margin-bottom: var(--s-24); }
body.shell .panel-quiet .h3 { margin: 0 0 2px; font-size: var(--fs-body-compact); }
body.shell .panel-quiet .small { margin: 0; }

/* The checklist, wherever it now follows other content. */
body.shell .pane .onboard { margin-top: var(--s-32); }

/* A tile inside a card is a figure in a group, not a second card: no shadow, a quieter
   ground, so the card around it stays the only raised surface. */
body.shell .panel .stat {
  background: var(--canvas);
  border-color: var(--rule-soft);
  box-shadow: none;
}

/* -- Watch list ----------------------------------------------------------- */

body.shell .watch-add { max-width: 40rem; }
body.shell .watch-add .field { max-width: none; margin-bottom: 0; }

.watch-add-row {
  display: flex;
  flex-wrap: wrap;
  gap: var(--s-8);
  align-items: stretch;
}

.watch-add-row .input-code { width: 8rem; flex: none; }

body.shell .watch-list {
  border: 1px solid var(--rule);
  border-radius: var(--radius-lg);
  background: var(--paper);
  box-shadow: var(--shadow-card);
}

body.shell .watch-row {
  padding: var(--s-12) var(--s-16);
  border-bottom: 1px solid var(--rule-soft);
}

body.shell .watch-row:last-child { border-bottom: 0; }

body.shell .watch-row-head {
  display: grid;
  grid-template-columns: 3.5rem minmax(0, 1fr) auto;
  grid-template-areas: "code device remove" "code dates remove";
  column-gap: var(--s-16);
  row-gap: 2px;
  align-items: center;
}

body.shell .watch-row-code { grid-area: code; align-self: start; padding-top: 1px; }

.watch-row-device {
  grid-area: device;
  min-width: 0;
  font-weight: 500;
  color: var(--ink);
  overflow-wrap: anywhere;
}

body.shell .watch-row-dates { grid-area: dates; }
body.shell .watch-row-remove { grid-area: remove; }

/* The note, and the disclosure that edits it, line up under the device name. */
.watch-row-note,
.watch-edit {
  margin: var(--s-8) 0 0 calc(3.5rem + var(--s-16));
}

.watch-row-note {
  font-size: var(--fs-small);
  color: var(--ink-soft);
  border-left: 2px solid var(--rule);
  padding-left: var(--s-8);
}

.watch-edit-toggle {
  display: inline-flex;
  align-items: center;
  gap: var(--s-4);
  min-height: 24px;
  font-size: var(--fs-small);
  font-weight: 600;
  color: var(--blue-text);
  cursor: pointer;
  list-style: none;
}

.watch-edit-toggle::-webkit-details-marker { display: none; }
.watch-edit-toggle::marker { content: ""; }
.watch-edit-toggle::before { content: "+"; width: 1ch; }
.watch-edit[open] > .watch-edit-toggle::before { content: "−"; }
.watch-edit-toggle:hover { text-decoration: underline; }

@media (max-width: 600px) {
  body.shell .watch-row-head {
    grid-template-columns: minmax(0, 1fr) auto;
    grid-template-areas: "code remove" "device device" "dates dates";
  }
  .watch-row-note,
  .watch-edit { margin-left: 0; }
}

/* The code on a watch row is the row's name, like a table's first column. */
body.shell .watch-row-code { text-decoration: none; }
body.shell .watch-row-code:hover,
body.shell .watch-row-code:focus-visible { text-decoration: underline; }


/* ------------------------------------------------------- predicate research

   /predicates: one form holding the search, the shortlist and the results, so ticks
   survive a new search with no script. */

.research-steps {
  display: flex;
  flex-wrap: wrap;
  gap: var(--s-8) var(--s-24);
  margin: 0 0 var(--s-24);
  padding: 0;
  list-style: none;
  font-size: var(--fs-small);
  color: var(--ink-faint-text);
}

.research-step {
  display: inline-flex;
  align-items: center;
  gap: var(--s-8);
}

.research-step-n {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 1.6rem;
  height: 1.6rem;
  border: 1px solid var(--rule-strong);
  border-radius: var(--radius-pill);
  background: var(--paper);
  font-weight: 700;
}

.research-step-current { color: var(--ink); font-weight: 600; }

.research-step-current .research-step-n {
  border-color: var(--blue);
  background: var(--blue);
  color: var(--paper);
}

.research-step-done .research-step-n {
  border-color: var(--blue);
  color: var(--blue-text);
}

/* The search box takes the width; the three filters sit beside it and wrap under it on a
   phone. Each field keeps its own label, so the wrap costs nothing in meaning. */
.research-grid {
  display: flex;
  flex-wrap: wrap;
  gap: 0 var(--s-16);
  align-items: flex-start;
}

.research-grid .field { flex: 1 1 11rem; max-width: 16rem; margin-bottom: var(--s-12); }
/* The search box gets a row of its own; the filters share the row under it. */
.research-grid .field-wide { flex: 1 1 100%; max-width: none; }

.research-subject {
  margin-top: var(--s-8);
  padding-top: var(--s-12);
  border-top: 1px solid var(--rule);
}

.research-subject summary {
  cursor: pointer;
  font-size: var(--fs-small);
  font-weight: 600;
  color: var(--ink);
}

.research-subject[open] summary { margin-bottom: var(--s-8); }

.research-selection { margin-top: var(--s-24); }

.research-selection-head {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  justify-content: space-between;
  gap: var(--s-12);
}

.research-selection-head .h3 { margin: 0; }

.research-count {
  margin-left: var(--s-8);
  font-size: var(--fs-small);
  font-weight: 600;
  color: var(--ink-faint-text);
}

.research-count-over { color: var(--risk-text); }

.research-chips {
  display: flex;
  flex-wrap: wrap;
  gap: var(--s-8);
  margin: var(--s-12) 0 0;
  padding: 0;
  list-style: none;
}

.research-chip {
  display: inline-flex;
  align-items: center;
  gap: var(--s-8);
  padding: var(--s-4) var(--s-8) var(--s-4) var(--s-12);
  border: 1px solid var(--rule-strong);
  border-radius: var(--radius-pill);
  background: var(--mist);
}

.research-chip-remove {
  font-size: var(--fs-small);
  color: var(--ink-soft);
}

.research-plan-note {
  margin: var(--s-12) 0 0;
  font-size: var(--fs-small);
  color: var(--ink-soft);
}

.research-list { margin: 0; padding-left: var(--s-24); }
.research-list li { margin-bottom: var(--s-8); }

.cell-pick { width: 2.75rem; }

/* A 24px target, which is the WCAG 2.2 minimum, on a control that is otherwise 13px. */
.cell-pick input {
  width: 1.5rem;
  height: 1.5rem;
  margin: 0;
  accent-color: var(--blue);
}

.research-results-foot { margin-top: var(--s-16); }

/* "Class II" and "5 January 2026" are one thing each; split over two lines they read as two. */
.research-results .cell-tight,
.research-results .cell-tight .cell-sub { white-space: nowrap; }
