/* Generated by pipeline/generate_site.py: the app's own styles, then the
   rules that exist only on these static pages. Do not edit by hand. */

/* ###############################################################################
   carstandings.com — stylesheet
   THREE BLOCKS, IN THIS ORDER, AND THE ORDER MATTERS:
     1. design/build/tokens.css      inlined byte-for-byte
     2. design/build/components.css  inlined byte-for-byte
     3. page CSS                     this page's own furniture, tokens only

   INLINED, NOT LINKED. Two reasons.
   (a) These two files are needed for first paint, and a <link> to each is two
       extra render-blocking round trips on a page whose last three commits are
       performance work — 8.1s to 1.8s, and free-tier files moved to R2 for the
       same reason.
   (b) design/build/ is a design deliverable directory: mockups, sample data, a
       144KB landing.html. Pointing production's first paint at a path inside it
       makes any reorganisation of that folder an outage.

   The cost of inlining is that a copy can drift from its source. That is a
   check, not a hope:
     for f in tokens components; do
       diff <(sed -n "/BEGIN \$f.css/,/END \$f.css/p" index.html | sed '1d;$d') \\
            design/build/\$f.css || echo "DRIFTED: \$f.css"
     done
   Both must print nothing. The marker strings are deliberately absent from this
   paragraph so the extraction cannot match its own instructions.
   ############################################################################ */

/* ============================ BEGIN tokens.css ============================ */
/* =============================================================================
   carstandings.com — design tokens
   Version 1.0 · 13 September 2026

   DIRECTION
   This is a statistical annual, not a dashboard: a warm paper ground, one ink in
   four tints, and a single reserved accent that appears only where a person can
   act — links, focus, the one primary button — so that colour in this system
   means "interaction" and nothing else. Data therefore carries no colour at all:
   rank, score and factor bars are ink at varying length, which means the whole
   site survives being rendered in greyscale without losing a single fact, and the
   green/amber/red confidence dot problem disappears by construction. Absence is
   not a faded version of presence — it is its own register, a diagonal hatch with
   full-strength mono text, because a low-contrast grey reads as a broken control
   and this product's credibility rests on gaps looking deliberate.

   HOW TO USE
   1. Load the fonts, then this file, then components.css, then page CSS:
        <link rel="preconnect" href="https://fonts.googleapis.com">
        <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
        <link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=IBM+Plex+Mono:wght@400;500&family=IBM+Plex+Sans:wght@400;500;600;700&family=IBM+Plex+Serif:wght@500;600;700&display=swap">
        <link rel="stylesheet" href="tokens.css">
        <link rel="stylesheet" href="components.css">
   2. Every token is named for its ROLE. There is no --blue and no --grey-400.
      If you need a colour that has no role here, the role is missing, not the colour.
   3. GOTCHA: the CSS `font:` shorthand resets font-variant-numeric to normal.
      The --type-* tokens are shorthands, so always write them in this order:
        font: var(--type-data);
        font-variant-numeric: var(--numeric-tabular);
   4. Theming is three-state. Bare :root is light. The media query is guarded with
      :not([data-theme="light"]) so an explicit light choice wins. An explicit
      [data-theme] on ANY element (not only :root) re-declares the palette for that
      subtree, so a dark panel can sit inside a light page — add class="theme-scope"
      to that element so it paints its own ground.
   ========================================================================== */


/* -----------------------------------------------------------------------------
   1. PALETTE — LIGHT (also any explicit light subtree)
   -------------------------------------------------------------------------- */

:root,
[data-theme="light"] {
  color-scheme: light;

  /* Surfaces — a warm uncoated stock, not UI white.
     The page ground is the paper; sheets sit slightly LIGHTER than it. */
  --surface-page:            #F1EDE4;
  --surface-raised:          #FBF9F4;
  --surface-sunken:          #E5DFD2;
  --surface-hover:           #EAE5D9;
  --surface-overlay:         #FFFDF8;

  /* Ink — one warm near-black with three tints. Chosen, not defaulted:
     every tint carries the same 42-46 degree hue as the paper so nothing
     reads as a dead mid-grey dropped on a warm ground. */
  --ink:                     #1B1A16;  /* 16.4:1 on --surface-raised */
  --ink-secondary:           #46423A;  /*  9.3:1 — labels, bar fills, absence text */
  --ink-tertiary:            #6B6558;  /*  5.4:1 — captions, column heads, metadata */
  --ink-quiet:               #948D7D;  /*  3.1:1 — FURNITURE ONLY. Never body copy,
                                              never a value, never a reason string. */
  --ink-inverse:             #FBF9F4;

  /* Rules — printed table craft: one heavy rule, one normal, one hairline. */
  --rule-heavy:              #2A271F;
  --rule:                    #C4BCAB;
  --rule-soft:               #DED7C7;

  /* Accent — THE ONLY chromatic interaction colour. Links, focus, one primary
     action. Never a score, never a bar, never a badge, never a status. */
  --accent:                  #1B4B85;  /* 8.2:1 on --surface-raised */
  --accent-hover:            #123762;
  --accent-active:           #0D2A4B;
  --accent-surface:          #E4EAF2;
  --accent-ink:              #FBF9F4;

  /* Signal — page-level notices only. Never within one em of a number. */
  --alert:                   #98301F;
  --alert-surface:           #F6E4DF;
  --alert-rule:              #D9A99E;
  --notice:                  #7A5312;
  --notice-surface:          #F3E9D5;
  --notice-rule:             #D6BE8E;

  /* Absence — its own register. Full-strength text on a hatched ground:
     legible, countable, and unmistakably not a disabled control. */
  --absence-ink:             #4A453B;
  --absence-surface:         #EDE7DA;
  --absence-rule:            #BFB6A3;
  --absence-hatch:
    repeating-linear-gradient(45deg,
      rgba(27, 26, 22, 0)    0px, rgba(27, 26, 22, 0)    3px,
      rgba(27, 26, 22, 0.11) 3px, rgba(27, 26, 22, 0.11) 4px);
  --absence-hatch-dense:
    repeating-linear-gradient(45deg,
      rgba(27, 26, 22, 0)    0px, rgba(27, 26, 22, 0)    2px,
      rgba(27, 26, 22, 0.16) 2px, rgba(27, 26, 22, 0.16) 3px);

  /* Held — the fourth register (v2 rework, 2026-09-22). A figure withheld
     until sign-in is NOT absent: it exists, it is counted, and it is one click
     from free. So it is never hatched (hatch means "no value") and never a
     number. A sunken band, a bar the width a value would take, full-strength
     text. The bar is the rule colour on purpose: it is furniture standing
     where a figure will stand, not a faded figure. */
  --held-surface:            #E9E3D6;
  --held-bar:                #C4BCAB;
  --held-ink:                #46423A;  /*  8.6:1 on --held-surface */

  /* v3 rework (data-presentation-v3.md §8.2): one token, not three — the new
     component set (14f) is a bare bar on the ordinary surface, not a sunken
     band, so it needs only the bar's own colour. --held-surface/-bar/-ink
     above stay for the v2 .held-row tint until WS3/WS4 finish migrating. */
  --held:                    #D3CCBC;

  /* Text selection */
  --selection-surface:       #D9E2EF;
  --selection-ink:           #17181A;

  /* Form furniture that cannot be expressed as a colour token */
  --icon-chevron: url("data:image/svg+xml;charset=utf-8,%3Csvg xmlns='http://www.w3.org/2000/svg' width='10' height='7' viewBox='0 0 10 7'%3E%3Cpath d='M1 1.5 5 5.5 9 1.5' fill='none' stroke='%236B6558' stroke-width='1.5' stroke-linecap='round' stroke-linejoin='round'/%3E%3C/svg%3E");
  --icon-check: url("data:image/svg+xml;charset=utf-8,%3Csvg xmlns='http://www.w3.org/2000/svg' width='11' height='9' viewBox='0 0 11 9'%3E%3Cpath d='M1 4.6 4 7.5 10 1.3' fill='none' stroke='%23FBF9F4' stroke-width='1.8' stroke-linecap='round' stroke-linejoin='round'/%3E%3C/svg%3E");

  /* Shadows. Nothing on this page floats except a popover. */
  --shadow-flat:             none;
  --shadow-sheet:            0 1px 0 rgba(27, 26, 22, 0.05);
  --shadow-overlay:          0 10px 28px rgba(27, 26, 22, 0.14),
                             0 2px 6px rgba(27, 26, 22, 0.08);
}


/* -----------------------------------------------------------------------------
   2. PALETTE — DARK BY SYSTEM PREFERENCE
   Designed, not inverted: an ink-washed board with the same warm hue as the
   paper, sheets still lighter than the ground, absence still hatched.
   -------------------------------------------------------------------------- */

@media (prefers-color-scheme: dark) {
  :root:not([data-theme="light"]) {
    color-scheme: dark;

    --surface-page:          #181713;
    --surface-raised:        #21201B;
    --surface-sunken:        #100F0C;
    --surface-hover:         #2A2822;
    --surface-overlay:       #2A2822;

    --ink:                   #EFEBE1;  /* 13.7:1 on --surface-raised */
    --ink-secondary:         #C0B9AB;
    --ink-tertiary:          #A39B8B;  /*  5.9:1 on raised, 5.4:1 on overlay — #948C7C
                                           was 4.42:1 on --surface-overlay (axe, the
                                           specimen's card eyebrow and factor captions) */
    --ink-quiet:             #736C5F;  /*  3.1:1 — furniture only */
    --ink-inverse:           #17160F;

    --rule-heavy:            #736B5C;
    --rule:                  #3D3931;
    --rule-soft:             #2B2822;

    --accent:                #7FAAE0;  /* 6.8:1 */
    --accent-hover:          #A2C3EC;
    --accent-active:         #C0D8F4;
    --accent-surface:        #1C2735;
    --accent-ink:            #10161F;

    --alert:                 #E08A78;
    --alert-surface:         #2E1D18;
    --alert-rule:            #6B3A2C;
    --notice:                #D3AE6A;
    --notice-surface:        #2B2416;
    --notice-rule:           #6A5730;

    --absence-ink:           #B6AF9F;
    --absence-surface:       #221F1A;
    --absence-rule:          #4A4438;
    --absence-hatch:
      repeating-linear-gradient(45deg,
        rgba(239, 235, 225, 0)    0px, rgba(239, 235, 225, 0)    3px,
        rgba(239, 235, 225, 0.13) 3px, rgba(239, 235, 225, 0.13) 4px);
    --absence-hatch-dense:
      repeating-linear-gradient(45deg,
        rgba(239, 235, 225, 0)    0px, rgba(239, 235, 225, 0)    2px,
        rgba(239, 235, 225, 0.18) 2px, rgba(239, 235, 225, 0.18) 3px);

    --held-surface:          #191814;
    --held-bar:              #3D3931;
    --held-ink:              #C0B9AB;  /*  9.4:1 on --held-surface */
    --held:                  #3A362E;

    --selection-surface:     #2B3A4E;
    --selection-ink:         #EFEBE1;

    --icon-chevron: url("data:image/svg+xml;charset=utf-8,%3Csvg xmlns='http://www.w3.org/2000/svg' width='10' height='7' viewBox='0 0 10 7'%3E%3Cpath d='M1 1.5 5 5.5 9 1.5' fill='none' stroke='%23948C7C' stroke-width='1.5' stroke-linecap='round' stroke-linejoin='round'/%3E%3C/svg%3E");
    --icon-check: url("data:image/svg+xml;charset=utf-8,%3Csvg xmlns='http://www.w3.org/2000/svg' width='11' height='9' viewBox='0 0 11 9'%3E%3Cpath d='M1 4.6 4 7.5 10 1.3' fill='none' stroke='%2310161F' stroke-width='1.8' stroke-linecap='round' stroke-linejoin='round'/%3E%3C/svg%3E");

    --shadow-flat:           none;
    --shadow-sheet:          0 1px 0 rgba(0, 0, 0, 0.30);
    --shadow-overlay:        0 10px 28px rgba(0, 0, 0, 0.55),
                             0 2px 6px rgba(0, 0, 0, 0.40);
  }
}


/* -----------------------------------------------------------------------------
   3. PALETTE — DARK BY EXPLICIT CHOICE
   Identical values to section 2. :root[data-theme="dark"] outranks the media
   query; the bare [data-theme="dark"] lets a dark island live inside a light page.
   -------------------------------------------------------------------------- */

:root[data-theme="dark"],
[data-theme="dark"] {
  color-scheme: dark;

  --surface-page:            #181713;
  --surface-raised:          #21201B;
  --surface-sunken:          #100F0C;
  --surface-hover:           #2A2822;
  --surface-overlay:         #2A2822;

  --ink:                     #EFEBE1;
  --ink-secondary:           #C0B9AB;
  --ink-tertiary:            #A39B8B;
  --ink-quiet:               #736C5F;
  --ink-inverse:             #17160F;

  --rule-heavy:              #736B5C;
  --rule:                    #3D3931;
  --rule-soft:               #2B2822;

  --accent:                  #7FAAE0;
  --accent-hover:            #A2C3EC;
  --accent-active:           #C0D8F4;
  --accent-surface:          #1C2735;
  --accent-ink:              #10161F;

  --alert:                   #E08A78;
  --alert-surface:           #2E1D18;
  --alert-rule:              #6B3A2C;
  --notice:                  #D3AE6A;
  --notice-surface:          #2B2416;
  --notice-rule:             #6A5730;

  --absence-ink:             #B6AF9F;
  --absence-surface:         #221F1A;
  --absence-rule:            #4A4438;
  --absence-hatch:
    repeating-linear-gradient(45deg,
      rgba(239, 235, 225, 0)    0px, rgba(239, 235, 225, 0)    3px,
      rgba(239, 235, 225, 0.13) 3px, rgba(239, 235, 225, 0.13) 4px);
  --absence-hatch-dense:
    repeating-linear-gradient(45deg,
      rgba(239, 235, 225, 0)    0px, rgba(239, 235, 225, 0)    2px,
      rgba(239, 235, 225, 0.18) 2px, rgba(239, 235, 225, 0.18) 3px);

  --held-surface:            #191814;
  --held-bar:                #3D3931;
  --held-ink:                #C0B9AB;
  --held:                    #3A362E;

  --selection-surface:       #2B3A4E;
  --selection-ink:           #EFEBE1;

  --icon-chevron: url("data:image/svg+xml;charset=utf-8,%3Csvg xmlns='http://www.w3.org/2000/svg' width='10' height='7' viewBox='0 0 10 7'%3E%3Cpath d='M1 1.5 5 5.5 9 1.5' fill='none' stroke='%23948C7C' stroke-width='1.5' stroke-linecap='round' stroke-linejoin='round'/%3E%3C/svg%3E");
  --icon-check: url("data:image/svg+xml;charset=utf-8,%3Csvg xmlns='http://www.w3.org/2000/svg' width='11' height='9' viewBox='0 0 11 9'%3E%3Cpath d='M1 4.6 4 7.5 10 1.3' fill='none' stroke='%2310161F' stroke-width='1.8' stroke-linecap='round' stroke-linejoin='round'/%3E%3C/svg%3E");

  --shadow-flat:             none;
  --shadow-sheet:            0 1px 0 rgba(0, 0, 0, 0.30);
  --shadow-overlay:          0 10px 28px rgba(0, 0, 0, 0.55),
                             0 2px 6px rgba(0, 0, 0, 0.40);
}


/* -----------------------------------------------------------------------------
   4. NON-THEMED TOKENS
   Type, space, line, motion, geometry. Identical in both themes.
   -------------------------------------------------------------------------- */

:root {

  /* --- 4.1 Typefaces -------------------------------------------------------
     Three roles, one superfamily. IBM Plex was drawn for a technical
     institution's publications, which is exactly the register an almanac wants:
     authority without nostalgia, and complete lining/tabular figure sets in all
     three widths. Serif for titling, Sans for text AND for every number, Mono
     reserved for identifiers, market codes, keys and API examples — not for
     numbers, which is the habit this replaces. */
  --font-display: "IBM Plex Serif", "Iowan Old Style", Charter, Georgia, "Times New Roman", serif;
  --font-text:    "IBM Plex Sans", -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;
  --font-mono:    "IBM Plex Mono", ui-monospace, SFMono-Regular, "SF Mono", Menlo, Consolas, "Liberation Mono", monospace;

  /* --- 4.2 Figure styles ---------------------------------------------------
     The numerals are the product. Columns get tabular; running prose gets
     proportional. Never oldstyle: this is a record, not a novel. */
  --numeric-tabular:      tabular-nums lining-nums;
  --numeric-proportional: proportional-nums lining-nums;

  /* --- 4.3 Size scale ------------------------------------------------------ */
  --font-size-micro:      10px;  /* licence codes inside a chip. Nothing else. */
  --font-size-fine:       11px;  /* column heads, absence chips, identifiers */
  --font-size-small:      12px;  /* captions, photo credits, source citations */
  --font-size-data:       13px;  /* the table workhorse */
  --font-size-body:       15px;
  --font-size-body-lead:  17px;
  --font-size-title-sm:   20px;
  --font-size-title:      24px;
  --font-size-title-lg:   30px;
  --font-size-display-sm: 38px;
  --font-size-display:    48px;  /* the composite score, unornamented */

  /* --- 4.4 Type roles (font shorthand: weight size/leading family) ---------- */
  --type-page-title:    700 var(--font-size-title-lg)/1.14 var(--font-display);
  --type-section-title: 600 var(--font-size-title)/1.20   var(--font-display);
  --type-subhead:       600 var(--font-size-title-sm)/1.28 var(--font-display);
  --type-body-lead:     400 var(--font-size-body-lead)/1.55 var(--font-text);
  --type-body:          400 var(--font-size-body)/1.60    var(--font-text);
  --type-data:          400 var(--font-size-data)/1.40    var(--font-text);
  --type-data-strong:   600 var(--font-size-data)/1.40    var(--font-text);
  --type-column-head:   600 var(--font-size-fine)/1.30    var(--font-text);
  --type-caption:       400 var(--font-size-small)/1.45   var(--font-text);
  --type-credit:        400 var(--font-size-small)/1.50   var(--font-text);
  --type-eyebrow:       600 var(--font-size-fine)/1.20    var(--font-text);
  --type-code:          400 var(--font-size-small)/1.55   var(--font-mono);
  --type-identifier:    500 var(--font-size-fine)/1.20    var(--font-mono);
  --type-absence:       500 var(--font-size-fine)/1.20    var(--font-mono);
  --type-score-inline:  600 var(--font-size-body-lead)/1.10 var(--font-text);
  --type-score-display: 400 var(--font-size-display)/1.00 var(--font-text);
  /* v2 rework §2.4: the figure comes first (principle 1), so it gets its own
     size distinct from a table cell's --type-data-strong — big enough to read
     as the headline of a summary strip, not so big it needs a hero layout.
     Was proposed at 28/32 in v1; the owner's D3/D6 answers shrank the page
     enough (no score block, no confidence box) that 24/28 already reads as
     the largest thing on a car page without it. */
  --type-figure-lg:     600 24px/28px var(--font-text);
  --type-figure:        400 15px/20px var(--font-text);

  /* --- 4.5 Tracking --------------------------------------------------------
     Uppercase micro-type needs opening up; display numerals need closing. */
  --track-caps:     0.07em;
  --track-tight:    -0.012em;
  --track-display:  -0.022em;
  --track-normal:   0;

  /* --- 4.6 Space ----------------------------------------------------------- */
  --space-3xs:  2px;
  --space-2xs:  4px;
  --space-xs:   6px;
  --space-sm:   8px;
  --space-md:   12px;
  --space-lg:   16px;
  --space-xl:   20px;
  --space-2xl:  24px;
  --space-3xl:  32px;
  --space-4xl:  40px;
  --space-5xl:  56px;
  --space-6xl:  72px;

  /* Space roles */
  --gutter-page:    var(--space-2xl);
  --gutter-column:  var(--space-3xl);
  --pad-cell-x:     var(--space-md);
  --pad-cell-y:     var(--space-sm);
  --pad-sheet:      var(--space-lg);
  --stack-tight:    var(--space-xs);
  --stack-base:     var(--space-md);
  --stack-loose:    var(--space-2xl);
  --stack-section:  var(--space-5xl);

  /* --- 4.7 Measure --------------------------------------------------------- */
  --measure-text:   68ch;   /* running prose */
  --measure-note:   52ch;   /* captions, notices */
  --measure-page:   1160px;  /* widened on large screens — see below */

  /* --- 4.8 Line, radius, focus --------------------------------------------
     Radii are deliberately small. A 12px-radius card with a soft shadow is the
     dashboard trap; a reference work has edges. */
  --radius-flat:    0;
  --radius-chip:    2px;
  --radius-control: 3px;
  --radius-sheet:   4px;

  --border-hairline: 1px;
  --border-heavy:    2px;
  --border-marker:   3px;

  --edge-soft:  var(--border-hairline) solid var(--rule-soft);
  --edge:       var(--border-hairline) solid var(--rule);
  --edge-heavy: var(--border-heavy) solid var(--rule-heavy);

  --focus-color:  var(--accent);
  --focus-width:  2px;
  --focus-offset: 2px;

  /* --- 4.9 Completeness mark geometry --------------------------------------
     One census, two layouts. The 4x4 lattice (--mark-*) is the identity and the
     card/detail rendering; the linear tally (--tally-*) is the same thirteen
     cells unwrapped, for table rows where one DOM node per row matters. */
  --mark-cell:   4px;
  --mark-gap:    2px;
  --tally-tick:  3px;
  --tally-gap:   2px;
  --tally-height: 11px;
  --tally-stub:  3px;   /* an unscored factor is a stub, not a paler tick */

  /* --- 4.10 Photography ----------------------------------------------------
     Reserve the box: the layout must not reflow between the photo and no-photo
     cases, and Wikimedia is hotlinked so a 404 is routine, not exceptional. */
  --photo-aspect: 16 / 10;
  --photo-radius: var(--radius-chip);

  /* --- 4.11 Motion ---------------------------------------------------------
     Nothing here animates to persuade. Motion confirms an action and stops. */
  --duration-quick:      90ms;
  --duration-base:       160ms;
  --duration-considered: 280ms;
  --ease-standard: cubic-bezier(0.2, 0, 0.2, 1);
  --ease-enter:    cubic-bezier(0, 0, 0.2, 1);
  --ease-exit:     cubic-bezier(0.4, 0, 1, 1);
  --transition-control:
    background-color var(--duration-quick) var(--ease-standard),
    border-color     var(--duration-quick) var(--ease-standard),
    color            var(--duration-quick) var(--ease-standard);

  /* --- 4.12 Layers --------------------------------------------------------- */
  --layer-base:    0;
  --layer-sticky:  20;
  --layer-header:  30;
  --layer-overlay: 40;
  --layer-popover: 50;

  /* --- 4.13 Control height (v2 §2.5) ---------------------------------------
     ONE height for anything that is a control rather than a block of running
     text — a button, a one-line input, the browse summary — so a row mixing
     several of them lines up without each component guessing at padding.
     Two states, not a pointer media query: the breakpoint matches 14.'s 16px
     input floor exactly, because that is the same iOS/Android floor this one
     answers (Apple HIG and Material both call 44px the minimum tap target),
     and a control that grows on a touch laptop keyboard should agree with the
     input beside it that already reflowed there. */
  --control-h:       32px;
  --control-h-touch: 44px;

  /* --- 4.14 Print -----------------------------------------------------------
     Print forces a white ground and black ink NO MATTER which on-screen
     theme is active — a printed page burns the same toner whether the tab
     behind it was dark or light, and a dark-theme print inheriting --ink/
     --surface-raised would print light text on a dark fill. This is the one
     place the system names a literal colour instead of tracking the palette,
     which is also why it lives here rather than in any of the three themed
     palettes above: it is the same value in all of them, on purpose. */
  --print-surface: #ffffff;
  --print-ink:     #000000;
}

@media (prefers-reduced-motion: reduce) {
  :root {
    --duration-quick:      0ms;
    --duration-base:       0ms;
    --duration-considered: 0ms;
  }
}

@media (max-width: 768px) {
  :root { --control-h: var(--control-h-touch); }
}

/* Narrow viewports: tighten the page gutter and the score, leave everything
   else alone. The table is the thing that must survive, and it scrolls. */
@media (max-width: 640px) {
  :root {
    --gutter-page:       var(--space-md);
    --pad-cell-x:        var(--space-sm);
    --font-size-display: 38px;
  }
}
/* ============================= END tokens.css ============================= */

/* ========================== BEGIN components.css ========================== */
/* =============================================================================
   carstandings.com — components
   Version 1.0 · 13 September 2026
   Requires tokens.css. No framework, no build step, no JavaScript.

   Five components in here are constraint-bearing: they exist because the data
   has properties the design must not be able to lie about. Do not fork them.
     .absence   — an unknown, in one visual treatment with four copy variants
     .score     — a score that cannot be rendered without its market and year
     .cohort    — the market-year chip
     .photo + .credit — a photograph that cannot be rendered without its credit
     .factor    — a factor row that renders a value with a source, or a reason
   ========================================================================== */


/* =============================================================================
   0. BASE
   A thin base layer. If the host page already owns these, delete this block —
   nothing below section 1 depends on it.
   ========================================================================== */

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

html { -webkit-text-size-adjust: 100%; }

body {
  margin: 0;
  background: var(--surface-page);
  color: var(--ink);
  font: var(--type-body);
  font-variant-numeric: var(--numeric-proportional);
  -webkit-font-smoothing: antialiased;
  -moz-osx-font-smoothing: grayscale;
  text-rendering: optimizeLegibility;
}

::selection { background: var(--selection-surface); color: var(--selection-ink); }

h1, h2, h3, h4, h5, h6 { margin: 0; font-weight: 600; }
p, ul, ol, figure, table, pre { margin: 0; }
img, svg { vertical-align: middle; }
img { max-width: 100%; }

:focus-visible {
  outline: var(--focus-width) solid var(--focus-color);
  outline-offset: var(--focus-offset);
  border-radius: var(--radius-chip);
}

/* Any element carrying data-theme must paint its own ground, or it borrows the
   page's. Put .theme-scope on it. */
.theme-scope {
  background: var(--surface-page);
  color: var(--ink);
}


/* =============================================================================
   1. TYPOGRAPHIC ROLES
   ========================================================================== */

.page-title    { font: var(--type-page-title);    letter-spacing: var(--track-tight); }
.section-title { font: var(--type-section-title); letter-spacing: var(--track-tight); }
.subhead       { font: var(--type-subhead);       letter-spacing: var(--track-tight); }
.lead          { font: var(--type-body-lead);     color: var(--ink-secondary); max-width: var(--measure-text); }
.prose         { font: var(--type-body);          max-width: var(--measure-text); }
.prose + .prose { margin-top: var(--stack-base); }

.eyebrow {
  font: var(--type-eyebrow);
  letter-spacing: var(--track-caps);
  text-transform: uppercase;
  color: var(--ink-tertiary);
}

.caption {
  font: var(--type-caption);
  font-variant-numeric: var(--numeric-tabular);
  color: var(--ink-tertiary);
  max-width: var(--measure-note);
}

/* Publication line. Dated and past-tense: "As of 13 September 2026 we hold …"
   The date is PER MARKET, not per site: every row carries its own updated_at and
   markets are published hours apart — us and de were five hours apart on
   2026-09-13. A single site-wide "last updated" is a fiction; read the market's
   own timestamp. It is also the only freshness signal available until
   data_version ships. */
.meta {
  font: var(--type-caption);
  font-variant-numeric: var(--numeric-tabular);
  color: var(--ink-tertiary);
}

.identifier {
  font: var(--type-identifier);
  font-variant-numeric: var(--numeric-tabular);
  letter-spacing: var(--track-caps);
  text-transform: uppercase;
  color: var(--ink-secondary);
}

.num {
  font-variant-numeric: var(--numeric-tabular);
  text-align: right;
}

.code {
  display: block;
  overflow-x: auto;
  padding: var(--space-md) var(--space-lg);
  background: var(--surface-sunken);
  border: var(--edge);
  border-radius: var(--radius-control);
  font: var(--type-code);
  font-variant-numeric: var(--numeric-tabular);
  color: var(--ink);
  white-space: pre;
  tab-size: 2;
}

.code--inline {
  display: inline;
  color: var(--ink-secondary);  /* its own ink: inside a caption it inherited
                                   --ink-tertiary, 4.36:1 on the sunken chip
                                   (axe, the specimen's type table) */
  padding: 1px 5px;
  white-space: normal;
  overflow-x: visible;
  overflow-wrap: anywhere;  /* a URL or a long dotted path has no space to
                                wrap at; without this it forced the page
                                itself wider at 390px (found by
                                design_specimen.mjs's overflow check) */
  border-radius: var(--radius-chip);
}

/* A code sample with a visible language or endpoint label. The bar is part of
   the evidence, not chrome: an integrator has to know which endpoint a sample
   calls before they can trust the 200 it returns. */
.codeblock {
  border: var(--edge);
  border-radius: var(--radius-control);
  overflow: hidden;
  background: var(--surface-sunken);
}
.codeblock__bar {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--space-md);
  padding: var(--space-2xs) var(--space-md);
  border-bottom: var(--edge);
  background: var(--surface-raised);
  font: var(--type-identifier);
  letter-spacing: var(--track-caps);
  text-transform: uppercase;
  color: var(--ink-tertiary);
}
.codeblock__bar b { color: var(--ink); font-weight: 600; }
.codeblock > .code {
  border: 0;
  border-radius: 0;
  margin: 0;
}

.sr-only {
  position: absolute;
  width: 1px; height: 1px;
  padding: 0; margin: -1px;
  overflow: hidden;
  clip-path: inset(50%);
  white-space: nowrap;
  border: 0;
}


/* =============================================================================
   2. LINKS AND BUTTONS
   The accent appears here and nowhere else.
   ========================================================================== */

a, .link {
  color: var(--accent);
  text-decoration: underline;
  text-decoration-thickness: 1px;
  text-underline-offset: 0.18em;
  text-decoration-color: color-mix(in srgb, var(--accent) 40%, transparent);
  transition: var(--transition-control);
}
a:hover, .link:hover {
  color: var(--accent-hover);
  text-decoration-color: currentColor;
}
a:active, .link:active { color: var(--accent-active); }

/* A link that must not look like body prose — nav, breadcrumb, footer. */
.link--quiet {
  color: var(--ink-secondary);
  text-decoration: none;
}
.link--quiet:hover { color: var(--accent); text-decoration: underline; }

.btn {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: var(--space-xs);
  min-height: var(--control-h);
  padding: var(--space-sm) var(--space-lg);
  border: var(--border-hairline) solid transparent;
  border-radius: var(--radius-control);
  font: var(--type-data-strong);
  font-variant-numeric: var(--numeric-tabular);
  letter-spacing: var(--track-normal);
  text-decoration: none;
  cursor: pointer;
  white-space: nowrap;
  transition: var(--transition-control);
  -webkit-appearance: none;
  appearance: none;
}

.btn--primary {
  background: var(--accent);
  border-color: var(--accent);
  color: var(--accent-ink);
}
.btn--primary:hover { background: var(--accent-hover); border-color: var(--accent-hover); color: var(--accent-ink); text-decoration: none; }
.btn--primary:active { background: var(--accent-active); border-color: var(--accent-active); }

.btn--secondary {
  background: var(--surface-raised);
  border-color: var(--rule);
  color: var(--ink);
}
.btn--secondary:hover { background: var(--surface-hover); border-color: var(--rule-heavy); color: var(--ink); text-decoration: none; }

.btn--quiet {
  background: transparent;
  border-color: transparent;
  color: var(--ink-secondary);
  padding-left: var(--space-sm);
  padding-right: var(--space-sm);
}
.btn--quiet:hover { background: var(--surface-hover); color: var(--ink); text-decoration: none; }

.btn--sm {
  padding: var(--space-2xs) var(--space-sm);
  font: var(--type-column-head);
  font-variant-numeric: var(--numeric-tabular);
}

.btn--block { display: flex; width: 100%; }

/* [disabled] and [aria-disabled] look the same and are NOT the same.
   A [disabled] control leaves the tab order, and WCAG 1.4.3 exempts inactive
   components from contrast. An [aria-disabled] one stays focusable — a
   keyboard user lands on it — so it is held to 4.5:1 like any other text.
   Sharing one rule between them applied the exempt colour to the focusable
   case: --ink-quiet measures 2.48-3.45:1 against these surfaces. */
.btn[disabled] {
  cursor: not-allowed;
  background: var(--surface-sunken);
  border-color: var(--rule-soft);
  color: var(--ink-quiet);
}
.btn[disabled]:hover {
  background: var(--surface-sunken);
  border-color: var(--rule-soft);
  color: var(--ink-quiet);
}
.btn[aria-disabled="true"],
.btn[aria-disabled="true"]:hover {
  cursor: not-allowed;
  background: var(--surface-sunken);
  border-color: var(--rule-soft);
  /* --ink-secondary, not --ink-tertiary: tertiary is 4.36:1 on a sunken ground
     in the light theme, just under the bar. The "this is unavailable" signal
     comes from the cursor, the sunken fill and the aria state — not from text
     a keyboard user cannot read. */
  color: var(--ink-secondary);
}

.btn-row { display: flex; flex-wrap: wrap; gap: var(--space-sm); align-items: center; }


/* =============================================================================
   2b. LOGO LOCKUP
   The masthead lockup is the mark INLINED plus live HTML text, never
   <img src="logo.svg">. An <img> is a separate document: it resolves
   currentColor against its own svg:root rule, which follows the operating
   system only — so a page whose theme control is set to Dark on a light OS gets
   a dark-ink logo on a dark ground. Inline the mark and it inherits the page ink
   in every one of the three theme states.
   ========================================================================== */

.logo {
  display: inline-flex;
  align-items: center;
  gap: var(--space-sm);
  color: var(--ink);
  text-decoration: none;
}
.logo:hover { color: var(--ink); text-decoration: none; }
.logo__mark {
  flex: 0 0 auto;
  display: block;
  width: 22px;
  height: 22px;
  fill: currentColor;
}
.logo__word {
  font: 600 var(--font-size-body-lead)/1 var(--font-text);
  letter-spacing: var(--track-display);
}
.logo__tld { font-weight: 400; color: var(--ink-tertiary); }

.logo--lg { gap: var(--space-md); }
.logo--lg .logo__mark { width: 34px; height: 34px; }
.logo--lg .logo__word { font-size: var(--font-size-title); }


/* =============================================================================
   3. COHORT CHIP — the market-year, C2
   A score is a position inside one cohort. The cohort travels with it: on every
   card, in the URL, in every export row, in every API response, and here.
   ========================================================================== */

.cohort {
  display: inline-flex;
  align-items: baseline;
  gap: var(--space-2xs);
  padding: 2px var(--space-xs);
  border: var(--edge);
  border-radius: var(--radius-chip);
  background: var(--surface-raised);
  font: var(--type-identifier);
  font-variant-numeric: var(--numeric-tabular);
  color: var(--ink-secondary);
  white-space: nowrap;
}
.cohort__market { color: var(--ink); font-weight: 600; }
.cohort__year   { letter-spacing: var(--track-normal); }

/* The persistent anchor: stays in view while the leaderboard scrolls. */
.cohort--anchor {
  padding: var(--space-2xs) var(--space-sm);
  font-size: var(--font-size-small);
  border-color: var(--rule-heavy);
}
.cohort--bare {
  border: 0;
  background: none;
  padding: 0;
}


/* =============================================================================
   4. SCORE — C2
   Never rendered bare. .score__cohort is REQUIRED; the guard at the end of this
   block makes a missing one visible in review rather than in production.
   ========================================================================== */

.score {
  display: inline-flex;
  flex-direction: column;
  align-items: flex-start;
  gap: var(--space-3xs);
}

.score__figure {
  font: var(--type-score-display);
  font-variant-numeric: var(--numeric-tabular);
  letter-spacing: var(--track-display);
  color: var(--ink);
}

.score__cohort { display: inline-flex; align-items: center; gap: var(--space-xs); }

/* n of 13 factors, k sources. The strongest signal the row carries, and until
   now the one that was never rendered. */
.score__basis {
  font: var(--type-caption);
  font-variant-numeric: var(--numeric-tabular);
  color: var(--ink-tertiary);
  display: inline-flex;
  align-items: center;
  gap: var(--space-xs);
}

/* Table-cell variant: one line, figure and cohort side by side. */
.score--inline {
  flex-direction: row;
  align-items: baseline;
  gap: var(--space-xs);
}
.score--inline .score__figure {
  font: var(--type-score-inline);
  font-variant-numeric: var(--numeric-tabular);
  letter-spacing: var(--track-tight);
}

/* Not scored at all. Not a zero, not a dash. */
.score--absent .score__figure {
  font: var(--type-score-inline);
  font-variant-numeric: var(--numeric-tabular);
}

/* Dev guard, brief check 3: "grep templates for a score emitted without a market
   token. Expect zero." This makes the grep visual. Do not suppress it. */
.score:not(:has(.score__cohort))::after {
  content: "market-year missing";
  font: var(--type-absence);
  letter-spacing: var(--track-caps);
  text-transform: uppercase;
  color: var(--alert);
  background: var(--alert-surface);
  border: var(--border-hairline) solid var(--alert-rule);
  border-radius: var(--radius-chip);
  padding: 1px var(--space-xs);
}


/* =============================================================================
   5. COMPLETENESS — C4
   One census, two layouts. Both say the same thing: a scored factor is a full
   tick, an unscored factor is a stub, and a factor that cannot apply to this car
   is struck through. Never a colour, never a dot, never a green/amber/red
   anything — confidence is not quality.

   THE DENOMINATOR IS NOT ALWAYS 13, AND IT IS NOT ALWAYS PRESENT.
   pipeline/scoring.py:981 computes data_completeness as scored / applicable, and
   applicable_factor_count() (:284) drops fuel_efficiency for an ev or hydrogen
   car and for any car whose WLTP consumption was suppressed as incomparable. So
   a row can be scored out of twelve — 334 of 703 nl 2025 rows are.

   But the field is published unevenly, and WHICH markets carry it changes with
   every publish, so do not encode a market list anywhere, this comment included.
   Two rules that do not rot:

     const total = row.factors_applicable ?? 13;   // the only safe denominator
     // strike an --na cell ONLY when factors_applicable is present AND < 13

   Never infer the denominator, or a struck cell, from powertrain. Where the
   field is absent the row really is scored out of 13, so a powertrain-derived
   strike would put twelve cells beside a number that says thirteen — precisely
   the contradiction this mark exists to prevent.

   Dated observation, as evidence rather than as a rule — the deployed API on
   2026-09-13 returned factors_applicable for ca, nl and se, and null for us, cz
   and de, while the local pipeline output had computed it for all five sold
   markets. Production was mid-migration. Expect the set to have moved; expect
   the two rules above not to have.
   ========================================================================== */

/* 5a. The lattice. Thirteen cells in a 4x4 grid, laid out by CSS so a consumer
   emits exactly thirteen spans in factor order. This is the logo's geometry. */
.mark {
  display: inline-grid;
  grid-template-columns: repeat(4, var(--mark-cell));
  grid-template-rows: repeat(4, var(--mark-cell));
  gap: var(--mark-gap);
  color: var(--ink-secondary);
  vertical-align: -2px;
}
.mark--lg { --mark-cell: 8px; --mark-gap: 3px; }
.mark--xl { --mark-cell: 14px; --mark-gap: 5px; }

.mark__cell {
  background: currentColor;
  border-radius: 0.5px;
}
/* An unscored factor keeps its place and drops to a stub. */
.mark__cell--absent {
  background: linear-gradient(to top, currentColor 0 32%, transparent 32% 100%);
}
/* A factor that CANNOT apply to this car — an electric car burns no fuel — is
   struck through rather than stubbed. The orientation carries the difference:
   a horizontal bar at the foot of the cell is a gap we could close, a vertical
   rule through the middle is a cell that is not part of this car's census at
   all. Both survive greyscale, because neither is a colour. */
.mark__cell--na {
  background: linear-gradient(90deg, transparent 35%, currentColor 35% 65%, transparent 65%);
}
/* Gate it: strike a cell only when factors_applicable is present AND below 13.
   Never derive the struck cell from powertrain alone — in the 33 markets where
   the field is null the row is still scored out of 13, and a struck cell would
   contradict the fraction printed beside it. */

.mark__cell:nth-child(1)  { grid-area: 1 / 1; }
.mark__cell:nth-child(2)  { grid-area: 1 / 2; }
.mark__cell:nth-child(3)  { grid-area: 1 / 3; }
.mark__cell:nth-child(4)  { grid-area: 2 / 1; }
.mark__cell:nth-child(5)  { grid-area: 2 / 2; }
.mark__cell:nth-child(6)  { grid-area: 2 / 4; }
.mark__cell:nth-child(7)  { grid-area: 3 / 1; }
.mark__cell:nth-child(8)  { grid-area: 3 / 3; }
.mark__cell:nth-child(9)  { grid-area: 3 / 4; }
.mark__cell:nth-child(10) { grid-area: 4 / 1; }
.mark__cell:nth-child(11) { grid-area: 4 / 2; }
.mark__cell:nth-child(12) { grid-area: 4 / 3; }
.mark__cell:nth-child(13) { grid-area: 4 / 4; }

/* 5b. The tally. The same thirteen cells unwrapped into a line, drawn with two
   gradient layers so a 462-row leaderboard costs one DOM node per row.
   Set --n inline: style="--n:11". */
.tally {
  --n: 13;
  --total: 13;
  --tally-unit: calc(var(--tally-tick) + var(--tally-gap));
  display: inline-block;
  width: calc(var(--total) * var(--tally-unit) - var(--tally-gap));
  height: var(--tally-height);
  color: var(--ink-secondary);
  vertical-align: -1px;
  background-image:
    repeating-linear-gradient(90deg, currentColor 0 var(--tally-tick), transparent var(--tally-tick) var(--tally-unit)),
    repeating-linear-gradient(90deg, currentColor 0 var(--tally-tick), transparent var(--tally-tick) var(--tally-unit));
  background-size:
    calc(var(--n) * var(--tally-unit)) 100%,
    100% var(--tally-stub);
  background-position: left top, left bottom;
  background-repeat: no-repeat, no-repeat;
}

/* 5c. The row-level readout: tally plus the words. The number is not optional —
   the glyph is a summary of it, not a substitute for it. */
.completeness {
  display: inline-flex;
  align-items: center;
  gap: var(--space-xs);
  font: var(--type-caption);
  font-variant-numeric: var(--numeric-tabular);
  color: var(--ink-tertiary);
  white-space: nowrap;
}
.completeness__count { color: var(--ink-secondary); font-weight: 500; }


/* =============================================================================
   6. ABSENCE — C1
   ONE visual treatment. Four copy variants, carried as text. The cause goes in
   data-absence for tests and analytics and changes nothing visually: a reader
   should not have to learn a colour code to know a value is missing.
     data-absence="not-measured"    the source covers this car, carries no value
     data-absence="not-held-here"   no licensed source carries this field here
     data-absence="licence-refused" a source exists and refuses resale. Name it.
     data-absence="no-source"       no licensed source carries it in any market
     data-absence="not-applicable"  the car's figure does not mean what its cohort's
                                    does, so it is not scored. It is still a gap:
                                    it earns nothing (scoring.py composite_for)
     data-absence="not-in-payload"  the pipeline scored the factor but does not
                                    publish the measurement behind it — curb
                                    weight, warranty years, depreciation %,
                                    insurance $ on every car page
     data-absence="unknown"         we do not know why (never invent a default —
                                    see C4 problem 4)
   Deliberately NOT a low-contrast grey: grey parses as a disabled control.

   A MISSING KEY AND A NULL VALUE ARE THE SAME ABSENCE. The car object has no
   fixed schema, and the shape differs by market AND by artefact — always pin
   which one a field count came from. Counted 2026-09-13:
     deployed API row   us 67 keys · cz 64 · nl 57
     local build row    us 72 keys · cz 65 · nl 58   (the API strips five)
   And not the same keys: the us row carries epa_class, complaints_count and four
   NHTSA safety_*_rating fields; the cz row carries inspections,
   inspection_fail_rate and median_odometer_km. Outside nl and se, price_usd is
   not null — it is absent from the row.

   The sharpest case is factors_applicable itself, and it turns out not to be a
   property of the market at all:

     MEMBERSHIP TRACKS PUBLISH TIME. A row carries the key if and only if its own
     updated_at falls after the run in which the rule shipped. A market gains the
     field the first time it is republished; the migration moves through the
     markets in publish order.

   Measured across 21 markets on the deployed API, 2026-09-13 21:41Z: every row
   stamped 18:07:36Z or earlier lacked the key (id, jp, at, be, de, dk, es, fi,
   fr, gb, it, no, pl, pt, nz, cl); every row stamped 20:29:37Z or later carried
   it (us 13, ca 13, nl 12, se 12, cz 13). Clean split, no market on the wrong
   side. us itself crossed the front between two readings five minutes apart.

   Prefer that rule to any market list — it explains every reading, it is
   verifiable from the row you are holding, and it does not rot. But it describes
   a migration in PROGRESS: once every market has been republished the field is
   universal and the rule goes quiet. What survives either way is section 5's
   `?? 13`, because a row's shape can change between two requests in one session
   and a null check reads a key that is not there, yielding undefined.

   MEASURE MEMBERSHIP, NOT VALUE. `row.get("factors_applicable")` returns None
   for an absent key and for a null one, so it cannot tell you which shape you
   have. Every wrong claim made about this field during the design of this system
   — including two of mine — came from reading a value where the question was
   whether the key existed. Test `"factors_applicable" in row`.

   So never destructure a car row, and never let a missing key and a null value
   render differently. The reader is owed the reason for the gap, not the shape
   of the JSON that failed to carry it.
   ========================================================================== */

.absence {
  display: inline-flex;
  align-items: center;
  gap: var(--space-2xs);
  padding: 1px var(--space-xs);
  border: var(--border-hairline) solid var(--absence-rule);
  border-radius: var(--radius-chip);
  background-color: var(--absence-surface);
  background-image: var(--absence-hatch-dense);
  font: var(--type-absence);
  letter-spacing: var(--track-caps);
  text-transform: uppercase;
  color: var(--absence-ink);
  white-space: nowrap;
  cursor: help;
}

/* Fills a cell or a factor slot rather than sitting inline in prose. */
.absence--block {
  display: flex;
  width: 100%;
  padding: var(--space-xs) var(--space-sm);
  white-space: normal;
  text-transform: none;
  letter-spacing: var(--track-normal);
  font: var(--type-caption);
  color: var(--absence-ink);
  background-color: var(--absence-surface);
  background-image: var(--absence-hatch);
}

/* The named source that refused us. This is the most valuable string on the
   page and it is not fine print. */
.absence__source { font-weight: 600; }

/* The printed explanation that accompanies a disabled control, or an absence
   whose cause is not self-evident. Never a tooltip alone: a reason a reader has
   to hover to find is a reason most readers never see. .table__reason is this
   with a column-width cap. */
.reason,
.table__reason {
  display: block;
  margin-top: var(--space-3xs);
  font: var(--type-caption);
  font-size: var(--font-size-micro);
  letter-spacing: var(--track-normal);
  text-transform: none;
  color: var(--absence-ink);
  white-space: normal;
}

/* A whole panel with nothing in it. It WRAPS: .absence sets nowrap for a chip
   and this inherited it, so a sentence-long panel ("We hold no ranked car
   matching …") ran off the right edge of a phone as one line. */
.absence--panel {
  display: block;
  padding: var(--space-2xl);
  text-align: center;
  text-transform: none;
  letter-spacing: var(--track-normal);
  font: var(--type-body);
  color: var(--absence-ink);
  background-color: var(--absence-surface);
  background-image: var(--absence-hatch);
  border: var(--border-hairline) solid var(--absence-rule);
  border-radius: var(--radius-sheet);
  cursor: default;
  white-space: normal;
}


/* =============================================================================
   7. FACT AND VERIFIED ZERO — C1, fifth state
   A measured zero is the best news on the page. It is an affirmative fact with a
   source, and it must never render as an absence. No tick, no green: the
   distinction is typographic, so it survives greyscale (check 6).
   ========================================================================== */

.fact {
  display: flex;
  flex-direction: column;
  gap: 1px;
}
.fact__figure {
  font: var(--type-data-strong);
  font-variant-numeric: var(--numeric-tabular);
  color: var(--ink);
}
.fact__source {
  font: var(--type-caption);
  font-variant-numeric: var(--numeric-tabular);
  color: var(--ink-tertiary);
}

/* The verified zero. The leading rule is the marker: an ink bar, not a colour. */
.fact--verified {
  padding-left: var(--space-sm);
  border-left: var(--border-marker) solid var(--ink);
}
.fact--verified .fact__figure::after {
  content: "measured";
  margin-left: var(--space-xs);
  font: var(--type-identifier);
  letter-spacing: var(--track-caps);
  text-transform: uppercase;
  color: var(--ink-tertiary);
  vertical-align: 1px;
}


/* =============================================================================
   8. FACTOR ROW
   Either a value with its source named, or an absence with its reason. There is
   no third rendering, and there is no empty bar.
   ========================================================================== */

.factor {
  display: grid;
  grid-template-columns: minmax(9rem, 14rem) minmax(60px, 1fr) 4.5rem;
  align-items: center;
  gap: var(--space-md);
  padding: var(--space-sm) 0;
  border-bottom: var(--edge-soft);
}
.factor:last-child { border-bottom: 0; }

.factor__label {
  font: var(--type-data);
  color: var(--ink);
}
.factor__label small {
  display: block;
  font: var(--type-caption);
  color: var(--ink-tertiary);
}

.factor__track {
  position: relative;
  height: 10px;
  background: var(--surface-sunken);
  border: var(--border-hairline) solid var(--rule-soft);
  border-radius: var(--radius-chip);
  overflow: hidden;
}
/* Ink at varying length. No hue, no gradient, no "good" end of the scale. */
.factor__fill {
  display: block;
  height: 100%;
  width: calc(var(--v, 0) * 1%);
  background: var(--ink-secondary);
}

.factor__value {
  font: var(--type-data-strong);
  font-variant-numeric: var(--numeric-tabular);
  text-align: right;
  color: var(--ink);
}
.factor__source {
  grid-column: 1 / -1;
  margin-top: calc(var(--space-3xs) * -1);
  font: var(--type-caption);
  font-variant-numeric: var(--numeric-tabular);
  color: var(--ink-tertiary);
}

/* Absent: the bar is not drawn at all — an empty or part-filled track is exactly
   the "absence rendered as a number" failure — and the hatched chip takes its
   place at full width, carrying the reason. The row keeps its position in the
   thirteen: a missing row is indistinguishable from a factor that does not exist. */
.factor--absent { grid-template-columns: minmax(9rem, 14rem) 1fr; }
.factor--absent .factor__track { display: none; }
.factor--absent .factor__value { text-align: left; }
.factor--absent .factor__value .absence { max-width: 100%; white-space: normal; }


/* =============================================================================
   9. DATA TABLE
   Printed table craft: a heavy rule over the head, a hairline under it, hairlines
   between rows, a heavy rule closing the block. No zebra, no card, no shadow.
   ========================================================================== */

.table-frame {
  overflow-x: auto;
  -webkit-overflow-scrolling: touch;
  background: var(--surface-raised);
  border: var(--edge);
  border-radius: var(--radius-sheet);
}

.table {
  width: 100%;
  border-collapse: separate;  /* collapse drops borders off sticky heads in Chromium */
  border-spacing: 0;
  font: var(--type-data);
  font-variant-numeric: var(--numeric-tabular);
  color: var(--ink);
}

.table caption {
  caption-side: top;
  text-align: left;
  padding: var(--space-md) var(--pad-cell-x);
  border-bottom: var(--edge-soft);
  font: var(--type-caption);
  font-variant-numeric: var(--numeric-tabular);
  color: var(--ink-tertiary);
  max-width: none;
}
.table caption strong { color: var(--ink); font-weight: 600; }

.table thead th {
  position: sticky;
  top: 0;
  z-index: var(--layer-sticky);
  background: var(--surface-raised);
  padding: var(--space-sm) var(--pad-cell-x);
  border-top: var(--border-heavy) solid var(--rule-heavy);
  border-bottom: var(--edge);
  font: var(--type-column-head);
  letter-spacing: var(--track-caps);
  text-transform: uppercase;
  color: var(--ink-tertiary);
  text-align: left;
  vertical-align: bottom;
  white-space: nowrap;
}
.table thead th.num { text-align: right; }

.table tbody td {
  padding: var(--pad-cell-y) var(--pad-cell-x);
  border-bottom: var(--edge-soft);
  vertical-align: middle;
}
.table tbody tr:last-child td { border-bottom: var(--border-heavy) solid var(--rule-heavy); }
.table tbody tr:hover td { background: var(--surface-hover); }
.table tbody tr:focus-within td { background: var(--surface-hover); }

.table--dense { --pad-cell-y: var(--space-xs); }

.table__rank {
  font: var(--type-identifier);
  font-variant-numeric: var(--numeric-tabular);
  letter-spacing: var(--track-normal);
  color: var(--ink-tertiary);
  text-align: right;
  width: 3.5rem;
}
.table__name { font: var(--type-data-strong); }
.table__name a { color: var(--ink); text-decoration-color: var(--rule); }
.table__name a:hover { color: var(--accent); }
.table__sub {
  display: block;
  font: var(--type-caption);
  color: var(--ink-tertiary);
}

/* Sort control. Below MIN_FACTOR_COVERAGE the control is disabled AND the reason
   is printed under it — never a tooltip alone. */
.table__sort {
  display: inline-flex;
  align-items: center;
  gap: var(--space-2xs);
  padding: 0;
  border: 0;
  background: none;
  color: inherit;
  font: inherit;
  letter-spacing: inherit;
  text-transform: inherit;
  cursor: pointer;
}
.table__sort:hover { color: var(--ink); }
.table__sort[aria-sort="ascending"]::after  { content: "\2191"; color: var(--ink); }
.table__sort[aria-sort="descending"]::after { content: "\2193"; color: var(--ink); }
/* --ink-tertiary, not --ink-quiet: an aria-disabled control stays in the tab
   order, so WCAG's exemption for INACTIVE components does not cover it.
   Measured: --ink-quiet is 2.48-3.45:1 across the surfaces, --ink-tertiary
   4.36-5.75:1. Every other --ink-quiet use in this file is a genuinely
   disabled control or a placeholder, which are exempt. */
.table__sort[aria-disabled="true"] { cursor: not-allowed; color: var(--ink-secondary); }

.table__reason { max-width: 12rem; }

/* Rows excluded by a filter are reported, never lost silently. House style. */
.table__footnote {
  padding: var(--space-sm) var(--pad-cell-x);
  border-top: var(--edge-soft);
  font: var(--type-caption);
  font-variant-numeric: var(--numeric-tabular);
  color: var(--ink-tertiary);
}

.table__empty {
  padding: var(--space-4xl) var(--pad-cell-x);
  text-align: center;
}


/* =============================================================================
   9b. DATA TABLE (v2) — the market list
   `.table` above stays for the composite-market pages that still rank. The
   facts markets (D2/D3) print every row A-Z with no rank column, so WS4's
   list needed a table whose sort control is the ARIA-standard shape — a
   `<button>` inside a `<th aria-sort="…">`, not `.table__sort`'s bare
   button-with-class, so the same head both LOOKS sorted (the arrow) and IS
   sorted to a screen reader without a second attribute to keep in sync.
   Same printed-table craft as `.table`: one heavy rule top and bottom,
   hairlines between rows, tabular numerals throughout. Markup:
     <table class="data-table"><thead><tr>
       <th scope="col"><a class="row-link"|plain text></th>
       <th scope="col" aria-sort="descending"><button>Fuel economy</button></th>
     </tr></thead><tbody>…</tbody></table>
   ========================================================================== */

.data-table {
  width: 100%;
  border-collapse: separate;
  border-spacing: 0;
  font: var(--type-data);
  font-variant-numeric: var(--numeric-tabular);
  color: var(--ink);
}
.data-table th {
  padding: var(--space-sm) var(--pad-cell-x);
  border-top: var(--border-heavy) solid var(--rule-heavy);
  border-bottom: var(--edge);
  font: var(--type-column-head);
  letter-spacing: var(--track-caps);
  text-transform: uppercase;
  color: var(--ink-tertiary);
  text-align: left;
  vertical-align: bottom;
  white-space: nowrap;
}
.data-table td {
  padding: var(--pad-cell-y) var(--pad-cell-x);
  border-bottom: var(--edge-soft);
  vertical-align: middle;
}
.data-table tbody tr:last-child td { border-bottom: var(--border-heavy) solid var(--rule-heavy); }
.data-table tbody tr:hover td { background: var(--surface-hover); }
.data-table th.num, .data-table td.num { text-align: right; }

/* The sort control lives ON the th (aria-sort), so the button inside it can
   stay unstyled chrome — the arrow is drawn from the th's own state, once,
   rather than duplicated onto a class the button would also need. */
.data-table th[aria-sort] button {
  display: inline-flex;
  align-items: center;
  gap: var(--space-2xs);
  padding: 0;
  border: 0;
  background: none;
  font: inherit;
  letter-spacing: inherit;
  text-transform: inherit;
  color: inherit;
  cursor: pointer;
}
.data-table th[aria-sort] button:hover { color: var(--ink); }
.data-table th[aria-sort="ascending"] button::after  { content: "\2191"; color: var(--ink); }
.data-table th[aria-sort="descending"] button::after { content: "\2193"; color: var(--ink); }
/* A held sort (WS4 §3.3): the head is still a real button — pressing it opens
   the gate panel, it never becomes aria-disabled — so it is styled apart only
   by an icon, never by a colour that would read as "cannot act". */
.data-table th[data-sort-held] button::before {
  content: "";
  width: 10px;
  height: 10px;
  background: var(--held-bar);
  border-radius: var(--radius-chip);
}

/* The car name is the link — the whole row is not clickable (a row this wide
   would swallow the sort buttons above it), so the name has to look like a
   link, and it is one that survives greyscale (an underline, not a hue). */
.row-link {
  color: var(--ink);
  font: var(--type-data-strong);
  text-decoration-color: var(--rule);
}
.row-link:hover, .row-link:focus-visible { color: var(--accent); text-decoration-color: currentColor; }


/* =============================================================================
   10. CARD
   A sheet, not a panel: a hairline edge, a square-ish corner, no shadow.
   ========================================================================== */

.card {
  background: var(--surface-raised);
  border: var(--edge);
  border-radius: var(--radius-sheet);
  box-shadow: var(--shadow-flat);
}
.card__head {
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  gap: var(--space-md);
  padding: var(--space-md) var(--pad-sheet);
  border-bottom: var(--edge);
}
.card__title {
  font: var(--type-subhead);
  letter-spacing: var(--track-tight);
  color: var(--ink);
}
.card__body { padding: var(--pad-sheet); }
.card__foot {
  padding: var(--space-md) var(--pad-sheet);
  border-top: var(--edge-soft);
  background: var(--surface-page);
  border-radius: 0 0 var(--radius-sheet) var(--radius-sheet);
}
.card--flush .card__body { padding: 0; }
.card-grid {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(15rem, 1fr));
  gap: var(--space-lg);
}


/* =============================================================================
   11. BADGE
   Categorical only, and never chromatic. Powertrain is not a quality and must
   not borrow the accent, which means one thing: you can act here.
   ========================================================================== */

.badge {
  display: inline-flex;
  align-items: center;
  gap: var(--space-2xs);
  padding: 1px var(--space-xs);
  border: var(--border-hairline) solid var(--rule);
  border-radius: var(--radius-chip);
  background: transparent;
  font: var(--type-identifier);
  letter-spacing: var(--track-caps);
  text-transform: uppercase;
  color: var(--ink-secondary);
  white-space: nowrap;
}
.badge--solid {
  background: var(--ink);
  border-color: var(--ink);
  color: var(--ink-inverse);
}
.badge--licence {
  font-size: var(--font-size-micro);
  text-transform: none;
  letter-spacing: var(--track-normal);
  border-style: dashed;
}

/* Feature states, for pricing and the API index. The brief's hardest commercial
   rule is that no bullet may describe something that cannot be demonstrated
   today, so the three states are a component rather than a copy convention.
   "Forthcoming" borrows the absence register on purpose: a feature we have not
   built is an absence with a reason, and it should look like one. */
.badge--live {
  background: var(--ink);
  border-color: var(--ink);
  color: var(--ink-inverse);
}
.badge--forthcoming {
  border-color: var(--absence-rule);
  background-color: var(--absence-surface);
  background-image: var(--absence-hatch-dense);
  color: var(--absence-ink);
}
.badge--blocked {
  border-style: dashed;
  border-color: var(--notice-rule);
  background: var(--notice-surface);
  color: var(--notice);
}

.badge-row { display: inline-flex; flex-wrap: wrap; gap: var(--space-2xs); }


/* =============================================================================
   12. PHOTOGRAPH AND CREDIT — C3
   1,545 of 1,688 photographs require visible credit and 1,377 carry ShareAlike.
   Two rules the CSS is built to enforce:
     - The frame reserves its box, so the photo and no-photo cases do not reflow
       and a Wikimedia 404 costs nothing.
     - NOTHING is overlaid on the image. No position:absolute child exists in
       this component and none may be added: ShareAlike propagates to composites,
       so a score pill on a photo relicenses the card. Design around it.
   ========================================================================== */

.photo { display: block; margin: 0; }

.photo__frame {
  position: relative;
  aspect-ratio: var(--photo-aspect);
  overflow: hidden;
  background: var(--surface-sunken);
  border: var(--edge);
  border-radius: var(--photo-radius);
}
.photo__img {
  display: block;
  width: 100%;
  height: 100%;
  object-fit: cover;
}

/* Fourth state: about 26% of rows hold no photograph. A placeholder is correct. */
.photo__empty {
  display: flex;
  align-items: center;
  justify-content: center;
  width: 100%;
  height: 100%;
  background-color: var(--absence-surface);
  background-image: var(--absence-hatch);
  font: var(--type-absence);
  letter-spacing: var(--track-caps);
  text-transform: uppercase;
  color: var(--absence-ink);
  text-align: center;
  padding: var(--space-md);
}

/* The credit is evidence, in the same register as a per-factor source citation.
   Author, licence NAME LINKED TO THE DEED, Commons link, photo year. */
.credit {
  display: block;
  margin-top: var(--space-xs);
  padding-top: var(--space-xs);
  border-top: var(--edge-soft);
  font: var(--type-credit);
  font-variant-numeric: var(--numeric-tabular);
  color: var(--ink-secondary);
}
.credit__label {
  font: var(--type-identifier);
  letter-spacing: var(--track-caps);
  text-transform: uppercase;
  color: var(--ink-tertiary);
  margin-right: var(--space-xs);
}
.credit__licence { white-space: nowrap; }
.credit a { color: var(--accent); }
.credit .badge { vertical-align: 1px; margin-left: var(--space-2xs); }

/* Leaderboard thumbnail. Adding photography to a new surface creates the
   attribution obligation for every row at once — the credit ships with it. */
.photo--thumb { display: flex; align-items: center; gap: var(--space-sm); }
.photo--thumb .photo__frame { width: 76px; flex: 0 0 76px; }
.photo--thumb .credit {
  margin-top: 0;
  padding-top: 0;
  border-top: 0;
  font-size: var(--font-size-micro);
  line-height: 1.35;
}


/* =============================================================================
   13. NOTICE
   Where colour is allowed to appear on content: a licence obligation, a caveat,
   or something that went wrong. Never within one em of a number.
   ========================================================================== */

.notice {
  display: block;
  padding: var(--space-md) var(--space-lg);
  border: var(--edge);
  border-left: var(--border-marker) solid var(--rule-heavy);
  border-radius: var(--radius-chip);
  background: var(--surface-raised);
  font: var(--type-caption);
  color: var(--ink-secondary);
  max-width: var(--measure-text);
}
/* Header row of a page-level disclosure: title on the leading edge, status badge
   on the trailing edge, wrapping to two lines when narrow. .notice__title is
   frequently an <h2> here — it heads a real section — so nothing in this block
   may assume a <span>. */
.notice__bar {
  display: flex;
  flex-wrap: wrap;
  align-items: baseline;
  justify-content: space-between;
  gap: var(--space-xs) var(--space-md);
  margin: 0 0 var(--space-sm);
}
.notice__bar .notice__title { margin-bottom: 0; }

.notice__title {
  display: block;
  margin-bottom: var(--space-2xs);
  font: var(--type-eyebrow);
  letter-spacing: var(--track-caps);
  text-transform: uppercase;
  color: var(--ink);
}

.notice--licence {
  background: var(--notice-surface);
  border-color: var(--notice-rule);
  border-left-color: var(--notice);
  color: var(--ink);
}
.notice--licence .notice__title { color: var(--notice); }

.notice--alert {
  background: var(--alert-surface);
  border-color: var(--alert-rule);
  border-left-color: var(--alert);
  color: var(--ink);
}
.notice--alert .notice__title { color: var(--alert); }

/* v2 §3.4/§7.4 (D8, frozen): the Turnstile volume check, inline where the
   reader was already waiting — never a full-viewport modal. Same register as
   --notice (an obligation to clear, not a mistake). */
.notice--challenge {
  background: var(--notice-surface);
  border-color: var(--notice-rule);
  border-left-color: var(--notice);
  color: var(--ink);
}
.notice--challenge .notice__title { color: var(--notice); }

/* A real fault — ours, not the reader's absence of an account or a check to
   clear. §5.1 COPY.fault: "The problem is on our side." Same rule colour as
   --alert; the words, not another hue, say this one cannot be cleared by
   the reader. */
.notice--fault {
  background: var(--alert-surface);
  border-color: var(--alert-rule);
  border-left-color: var(--alert);
  color: var(--ink);
}
.notice--fault .notice__title { color: var(--alert); }

/* A page-level disclosure. The statement that scores do not compare across
   markets is the most important sentence on a market page and must not be set at
   caption size or held to the prose measure. */
.notice--lead {
  max-width: none;
  padding: var(--space-lg) var(--space-xl);
  font: var(--type-body);
  font-variant-numeric: var(--numeric-tabular);
  color: var(--ink);
}
.notice--lead .notice__title {
  font: var(--type-subhead);
  letter-spacing: var(--track-tight);
  text-transform: none;
  margin-bottom: var(--space-xs);
}

/* The standing caveats — running costs only, purchase price excluded,
   depreciation is per-market not per-model. Promoted out of the fine print. */
.notice--caveat {
  background: var(--surface-page);
  border-left-color: var(--rule-heavy);
}


/* =============================================================================
   14. FORM CONTROLS
   ========================================================================== */

.field { display: block; margin-bottom: var(--space-lg); }
.field__label {
  display: block;
  margin-bottom: var(--space-2xs);
  font: var(--type-column-head);
  letter-spacing: var(--track-caps);
  text-transform: uppercase;
  color: var(--ink-tertiary);
}
.field__hint {
  display: block;
  margin-top: var(--space-2xs);
  font: var(--type-caption);
  color: var(--ink-tertiary);
}
.field__error {
  display: block;
  margin-top: var(--space-2xs);
  font: var(--type-caption);
  color: var(--alert);
}

.input, .select, .textarea {
  display: block;
  width: 100%;
  padding: var(--space-sm) var(--space-md);
  border: var(--edge);
  border-radius: var(--radius-control);
  background-color: var(--surface-raised);
  color: var(--ink);
  font: var(--type-data);
  font-variant-numeric: var(--numeric-tabular);
  transition: var(--transition-control);
  -webkit-appearance: none;
  appearance: none;
}
.input::placeholder, .textarea::placeholder { color: var(--ink-quiet); }
.input:hover, .select:hover, .textarea:hover { border-color: var(--rule-heavy); }
.input[aria-invalid="true"] { border-color: var(--alert); }
.input[disabled], .select[disabled], .textarea[disabled] {
  background-color: var(--surface-sunken);
  color: var(--ink-quiet);
  cursor: not-allowed;
}
.textarea { min-height: 6rem; resize: vertical; }

.select {
  padding-right: var(--space-3xl);
  background-image: var(--icon-chevron);
  background-repeat: no-repeat;
  background-position: right var(--space-md) center;
  cursor: pointer;
}
.select[disabled] { cursor: not-allowed; }

.input--inline, .select--inline { display: inline-block; width: auto; }

.check {
  display: flex;
  align-items: flex-start;
  gap: var(--space-sm);
  padding: var(--space-2xs) 0;
  font: var(--type-data);
  color: var(--ink);
  cursor: pointer;
}
.check__box, .check__radio {
  flex: 0 0 auto;
  width: 16px;
  height: 16px;
  margin: 1px 0 0;
  border: var(--border-hairline) solid var(--rule-heavy);
  background: var(--surface-raised);
  cursor: pointer;
  transition: var(--transition-control);
  -webkit-appearance: none;
  appearance: none;
}
.check__box { border-radius: var(--radius-chip); }
.check__radio { border-radius: 50%; }
.check__box:checked {
  background-color: var(--ink);
  border-color: var(--ink);
  background-image: var(--icon-check);
  background-repeat: no-repeat;
  background-position: center;
}
.check__radio:checked {
  border-color: var(--ink);
  background-color: var(--surface-raised);
  background-image: radial-gradient(circle, var(--ink) 0 4px, transparent 4px);
  background-repeat: no-repeat;
  background-position: center;
}
.check__box[disabled], .check__radio[disabled] {
  border-color: var(--rule);
  background-color: var(--surface-sunken);
  cursor: not-allowed;
}
.check__text small {
  display: block;
  font: var(--type-caption);
  color: var(--ink-tertiary);
}

.fieldset {
  margin: 0;
  padding: var(--space-md) var(--space-lg) var(--space-lg);
  border: var(--edge);
  border-radius: var(--radius-control);
}
.fieldset > legend {
  padding: 0 var(--space-xs);
  font: var(--type-eyebrow);
  letter-spacing: var(--track-caps);
  text-transform: uppercase;
  color: var(--ink-tertiary);
}

/* Segmented control. The active segment is ink, not accent: choosing a view is
   not an action that needs advertising. */
.segmented {
  display: inline-flex;
  border: var(--edge);
  border-radius: var(--radius-control);
  overflow: hidden;
}
.segmented > button {
  padding: var(--space-2xs) var(--space-md);
  border: 0;
  border-right: var(--edge);
  background: var(--surface-raised);
  color: var(--ink-secondary);
  font: var(--type-column-head);
  letter-spacing: var(--track-caps);
  text-transform: uppercase;
  cursor: pointer;
  transition: var(--transition-control);
}
.segmented > button:last-child { border-right: 0; }
.segmented > button:hover { background: var(--surface-hover); color: var(--ink); }
.segmented > button[aria-pressed="true"] { background: var(--ink); color: var(--ink-inverse); }
.segmented > button[disabled] { color: var(--ink-quiet); cursor: not-allowed; }

/* A 16px FLOOR ON PHONES. iOS Safari zooms the whole page when a control it
   focuses is set under 16px, and does not zoom back on blur: a reader who
   tapped the 13px search field was left on a magnified header. Every text
   control, whether or not it carries a class, at every width up to the phone
   breakpoint the page furniture uses (3.18).
   !important, found necessary the hard way (critic, WS1 §2 item 2): a page
   rule with equal specificity but later in the cascade — `.header
   input[type="text"] { font: var(--type-data) }` (13px, 3.1) — sits after
   this block in the same stylesheet and was winning on source order alone,
   leaving #search-input and #list-held-sort at 13px on the live site even
   though the specimen (which has no such page rule to lose to) passed. A
   floor a later, equally-specific rule can silently beat is not a floor.
   Checkboxes and radios are not excluded: a box control's 13.33 px default
   failed the same check (the header's menu checkbox, .check__box), and a
   16 px box loses nothing — .check__box sizes itself in px, not em. */
@media (max-width: 768px) {
  .input, .select, .textarea,
  input, select, textarea {
    font-size: max(16px, 1em) !important;
  }
}


/* =============================================================================
   14b. HELD, SOURCE TAG, CRUMBS, BROWSE MENU, PLAN BADGE
   Added 2026-09-22 for the v2 UX rework (scratchpad/ux-rework-plan-v2.md
   §2.6). The class names are the contract the eight workstreams code against;
   the rules here are the register each component belongs to. Tokens only.
   ========================================================================== */

/* HELD — the fourth register. Absent means "no value exists"; held means "a
   value exists and a free account shows it". The two must not look alike, and
   held must never look like a number: a band, a bar where the figure would
   stand, the sentence, no digits (tests grep .held for /\d/). */
.held-row {
  background: var(--held-surface);
  color: var(--held-ink);
}
/* v3 (data-presentation-v3.md §3, C2): held is a flat bar, not a sunken band
   with a caption — "no text, no digits, width ≈ a value" (§8.2), on the
   --held token, and the markup carries the meaning in aria-label rather than
   in visible text: <span class="held" aria-label="held — free with an account">,
   left empty. color:transparent + overflow:hidden is not decoration for
   that empty span — it is what keeps a caller that has not yet migrated off
   the v2 text content (14b's original <span class="held">Free with an
   account</span>) from printing a sentence inside what must read as a bar
   either way, so the two conventions can co-exist while WS3/WS4 migrate the
   car and list pages figure by figure. */
.held {
  display: inline-block;
  width: 3.5rem;
  max-width: 70%;
  height: 0.7rem;
  border-radius: var(--radius-chip);
  background: var(--held);
  vertical-align: middle;
  overflow: hidden;
  color: transparent;
  white-space: nowrap;
}
.held a { color: var(--accent); }
/* Sized to the slot it stands in — a summary tile's numeral, a figure row's
   value, a table cell — so the reveal never changes the box the value lands
   in (§10 check 5: tile offsetHeight unchanged, CLS < 0.05). */
.tile .val .held { width: 4.25rem; height: 0.85rem; margin-top: var(--space-xs); }
.row .v .held, .summary__item .held { width: 3.25rem; }
.data-table td .held, td .held { width: 2.25rem; height: 0.55rem; }

/* SOURCE TAG AND POPOVER (D6). The publisher of a figure, on the figure's own
   line, as text: "EPA", "NHTSA", "NRCan", the make's name for a warranty.
   Without JS it is <a class="src" href="/sources/#epa">; with JS a <button
   aria-expanded> that opens the .src__pop beside it. Never tooltip-only.
   Markup: <span class="src__host"><a class="src">EPA</a>
             <div class="src__pop" hidden>…</div></span> */
.src {
  display: inline-flex;
  align-items: center;
  gap: var(--space-3xs);
  padding: 0 var(--space-2xs);
  border: var(--border-hairline) solid var(--rule);
  border-radius: var(--radius-chip);
  background: var(--surface-raised);
  font: var(--type-identifier);
  letter-spacing: var(--track-normal);
  text-transform: none;
  color: var(--ink-secondary);
  text-decoration: none;
  white-space: nowrap;
  cursor: pointer;
  transition: var(--transition-control);
}
.src:hover, .src:focus-visible { border-color: var(--rule-heavy); color: var(--ink); }
.src[aria-expanded="true"] { background: var(--ink); border-color: var(--ink); color: var(--ink-inverse); }
.src__host { position: relative; display: inline-block; }
.src__pop {
  position: absolute;
  z-index: var(--layer-popover);
  top: calc(100% + var(--space-2xs));
  left: 0;
  min-width: 16rem;
  /* Never wider than the viewport minus the page gutters, so it opens whole
     at 390px; --end flips it to the right edge for a tag near that edge. */
  max-width: min(24rem, calc(100vw - 2 * var(--gutter-page)));
  padding: var(--space-md) var(--space-lg);
  border: var(--edge);
  border-radius: var(--radius-sheet);
  background: var(--surface-overlay);
  box-shadow: var(--shadow-overlay);
  font: var(--type-caption);
  color: var(--ink);
  text-align: left;
  text-transform: none;
  letter-spacing: var(--track-normal);
  white-space: normal;
}
.src__pop--end { left: auto; right: 0; }
.src__pop[hidden] { display: none; }
.src__pop a { color: var(--accent); }
/* CLIPPING AT 390PX (critic, WS1 §2 item 4): a 16-24rem popover anchored
   `left:0`/`right:0` to an inline .src tag overflows one edge or the other
   on a 390px viewport whenever the tag is not itself near a page edge,
   because that width is wider than the room available on EITHER side of a
   mid-row anchor (the fuel-economy tag; the CO2 tag merely happened to sit
   close enough to the row's own right edge to fit). Anchoring to the tag
   stops being reliable once the box can be wider than half the viewport, so
   below 480px the popover becomes a sheet pinned to the page gutters
   instead of the tag — always on-screen, independent of where the tag that
   opened it sits; the tag's own aria-expanded state still says which one. */
@media (max-width: 480px) {
  .src__pop, .src__pop--end {
    position: fixed;
    left: var(--gutter-page);
    right: var(--gutter-page);
    top: auto;
    bottom: var(--gutter-page);
    width: auto;
    min-width: 0;
    max-width: none;
  }
}

/* BREADCRUMB. "United States › 2025 › Toyota › RAV4 Hybrid", each crumb a
   real path. Quiet: it is wayfinding, not a heading. */
.crumbs {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: var(--space-2xs);
  margin: 0;
  padding: 0;
  list-style: none;
  font: var(--type-caption);
  color: var(--ink-tertiary);
}
.crumbs li { display: inline-flex; align-items: center; gap: var(--space-2xs); }
.crumbs li + li::before { content: "›"; color: var(--ink-quiet); }
.crumbs a { color: var(--ink-secondary); text-decoration: none; }
.crumbs a:hover, .crumbs a:focus-visible { color: var(--accent); text-decoration: underline; }
.crumbs [aria-current="page"] { color: var(--ink); }

/* BROWSE MENU. The header's market-year control, as a <details> of real links
   baked at build so it works with no JS and no request; JS enhances it into
   the two selects. Markup:
     <details class="browse"><summary>Browse</summary>
       <div class="browse__menu">
         <div class="browse__group">United States</div><a href="/us/2025/">2025</a>…
       </div></details> */
details.browse { position: relative; }
details.browse > summary {
  list-style: none;
  display: inline-flex;
  align-items: center;
  gap: var(--space-2xs);
  padding: var(--space-2xs) var(--space-sm);
  border: var(--edge);
  border-radius: var(--radius-control);
  background: var(--surface-raised);
  font: var(--type-data);
  color: var(--ink);
  cursor: pointer;
  transition: var(--transition-control);
}
details.browse > summary::-webkit-details-marker { display: none; }
details.browse > summary::after {
  content: "";
  width: 10px;
  height: 7px;
  background: var(--icon-chevron) no-repeat center;
}
details.browse > summary:hover, details.browse[open] > summary { border-color: var(--rule-heavy); }
details.browse > .browse__menu {
  position: absolute;
  z-index: var(--layer-popover);
  top: calc(100% + var(--space-2xs));
  left: 0;
  min-width: 14rem;
  padding: var(--space-sm) 0;
  border: var(--edge);
  border-radius: var(--radius-sheet);
  background: var(--surface-overlay);
  box-shadow: var(--shadow-overlay);
}
.browse__group {
  padding: var(--space-2xs) var(--space-lg);
  font: var(--type-eyebrow);
  letter-spacing: var(--track-caps);
  text-transform: uppercase;
  /* --ink-secondary, not --ink-tertiary: on --surface-overlay (this menu's
     own popover ground, not --surface-raised) tertiary measures 4.42:1 in
     dark — under 4.5:1 (critic, WS1 §2 item 1, "United States"/"Canada" in
     the live header's Browse menu). Same fix as .code-inline below. */
  color: var(--ink-secondary);
}
.browse__menu a {
  display: block;
  padding: var(--space-2xs) var(--space-lg);
  font: var(--type-data);
  font-variant-numeric: var(--numeric-tabular);
  color: var(--ink);
  text-decoration: none;
}
.browse__menu a:hover, .browse__menu a:focus-visible { background: var(--surface-hover); }

/* PLAN BADGE (D13). "Best for automation" on the API card and nothing else;
   never "Most popular". Ink, not accent: a recommendation is not an action. */
.plan__badge {
  display: inline-flex;
  align-items: center;
  padding: 1px var(--space-xs);
  border: var(--border-hairline) solid var(--ink);
  border-radius: var(--radius-chip);
  background: var(--ink);
  color: var(--ink-inverse);
  font: var(--type-eyebrow);
  letter-spacing: var(--track-caps);
  text-transform: uppercase;
  white-space: nowrap;
}


/* =============================================================================
   14c. FIGURE ROW, SUMMARY STRIP, SOURCE LINE, ABSENCE LEGEND, GATE PANEL,
        METER CHIP
   Added 2026-09-22, v2 rework Phase 1 (WS1; plan §2.6-2.7, §5.1). Principle 1:
   "the figure comes first". These are the pieces a car page and a list page
   assemble a screen from — none of them is a whole page, and none renders a
   value the API withheld (that rule lives in the record, section 7 of the
   plan; these classes only style whatever HTML WS3/WS4 already decided to
   print).
   ========================================================================== */

/* FIGURE ROW. One label, one value, one unit, one publisher tag — "Fuel
   economy   39 mpg   EPA▾". Deliberately not a grid like `.factor`: a figure
   row has no bar to reserve a column for, so it wraps like text instead of
   truncating like a table. `.fig__num`/`.fig__unit` also work bare, inside a
   `.data-table` cell or `.summary__item`, wherever a value needs its unit
   set apart without pulling in the whole row. */
.fig {
  display: flex;
  flex-wrap: wrap;
  align-items: baseline;
  gap: var(--space-2xs) var(--space-sm);
  padding: var(--pad-cell-y) 0;
  border-bottom: var(--edge-soft);
}
.fig:last-child { border-bottom: 0; }
.fig__label { flex: 1 1 auto; font: var(--type-data); color: var(--ink); }
.fig__num {
  font: var(--type-figure);
  font-variant-numeric: var(--numeric-tabular);
  color: var(--ink);
}
.fig__unit { font: var(--type-caption); color: var(--ink-secondary); }
.fig__src { display: inline-flex; }

/* SUMMARY STRIP. 2-4 headline figures above the fold (WS3 acceptance: first
   .fig__num top < 520px at 1280 / < 700px at 390) — reuses `.fig__label` and
   `.fig__num` rather than inventing a second label/value pair, so the strip
   and the table agree pixel-for-pixel on what a label and a value look like. */
.summary {
  display: flex;
  flex-wrap: wrap;
  gap: var(--space-xl) var(--space-3xl);
  margin: var(--space-lg) 0 var(--space-xl);
}
.summary__item {
  display: flex;
  flex-direction: column;
  gap: var(--space-3xs);
  min-width: 0;
}
.summary__item .fig__label {
  font: var(--type-eyebrow);
  letter-spacing: var(--track-caps);
  text-transform: uppercase;
  color: var(--ink-tertiary);
}
.summary__item .fig__num { font: var(--type-figure-lg); }

/* SOURCE LINE. "Sources: EPA · NHTSA · Toyota — about these sources →", once
   under a figures table (§2.7) — never the per-row provenance prose it
   replaces (§8, root cause 3). */
.source-line { margin-top: var(--space-md); font: var(--type-caption); color: var(--ink-tertiary); }
.source-line a { color: var(--accent); }

/* ABSENCE LEGEND. The four causes, printed once per page (§2.7), each a real
   link to its /sources/ anchor — not a repeat of every row's own reason. */
.absence-legend {
  display: flex;
  flex-wrap: wrap;
  gap: var(--space-sm) var(--space-xl);
  margin-top: var(--space-lg);
  padding-top: var(--space-md);
  border-top: var(--edge-soft);
  font: var(--type-caption);
  color: var(--ink-tertiary);
}
.absence-legend__item { display: inline-flex; align-items: center; gap: var(--space-2xs); }
.absence-legend a { color: inherit; text-decoration-color: var(--rule); }
.absence-legend a:hover, .absence-legend a:focus-visible { color: var(--accent); text-decoration-color: currentColor; }

/* GATE PANEL. Counted and named (§3.4): "7 more figures for this car" · the
   list, in prose, of what they are · one primary action per state · a
   caption that never oversells what pressing it does. Sits on the held
   surface, not the raised one — it is asking about the SAME figures the held
   rows above it are holding, and the two must read as one register. */
.gate-panel {
  padding: var(--space-lg) var(--space-xl);
  border: var(--edge);
  border-radius: var(--radius-sheet);
  background: var(--held-surface);
}
.gate-panel__title {
  margin: 0 0 var(--space-xs);
  font: var(--type-subhead);
  color: var(--ink);
}
.gate-panel__body {
  margin: 0 0 var(--space-md);
  font: var(--type-body);
  color: var(--ink-secondary);
  max-width: var(--measure-text);
}
.gate-panel__actions { display: flex; flex-wrap: wrap; gap: var(--space-sm); margin-bottom: var(--space-sm); }
.gate-panel__caption { margin: 0; font: var(--type-caption); color: var(--ink-tertiary); }

/* METER CHIP (§2.7, §3.4). Appears only after a reveal — never guessed at
   before the metered read answers, because the cookie it reports on belongs
   to api.carstandings.com and the page cannot read it first. */
.meter {
  display: inline-flex;
  align-items: center;
  gap: var(--space-2xs);
  padding: 1px var(--space-sm);
  border: var(--border-hairline) solid var(--rule);
  border-radius: var(--radius-chip);
  font: var(--type-identifier);
  letter-spacing: var(--track-normal);
  text-transform: none;
  color: var(--ink-secondary);
}


/* =============================================================================
   14d. PLAN CARD, KEY / DOWNLOADS / USAGE PANELS, TOAST, SKELETON, CODE-INLINE
   Added 2026-09-22, v2 rework Phase 1 (WS1; plan §2.6, WS6's account and
   pricing surfaces). WS1 owns the rules; WS6 owns the markup and the copy
   that fills them (D9-D13).
   ========================================================================== */

/* PLAN CARD. `.plan__badge` (14b) sits inside it; `.plan--current` marks the
   reader's own plan (a heavier edge, not a colour — colour means "you can
   act", and the current plan's button is the one action on the page that is
   disabled). */
.plan {
  display: flex;
  flex-direction: column;
  gap: var(--space-md);
  padding: var(--space-xl);
  border: var(--edge);
  border-radius: var(--radius-sheet);
  background: var(--surface-raised);
}
.plan--current { border-width: var(--border-heavy); border-color: var(--ink); }
.plan__name { font: var(--type-subhead); color: var(--ink); }
.plan__price {
  font: var(--type-figure-lg);
  font-variant-numeric: var(--numeric-tabular);
  color: var(--ink);
}
.plan__body { font: var(--type-body); color: var(--ink-secondary); }

/* KEY PANEL. The API key's plaintext appears once (WS6 §3.10); every other
   visit shows only the prefix, so the panel has to look the same whether the
   value inside it is the whole key or three characters of it. */
.key-panel {
  padding: var(--space-lg);
  border: var(--edge);
  border-radius: var(--radius-sheet);
  background: var(--surface-raised);
}
.key-panel__value {
  display: inline-flex;
  align-items: center;
  gap: var(--space-sm);
  font: var(--type-code);
  font-variant-numeric: var(--numeric-tabular);
  color: var(--ink);
  word-break: break-all;
}
.key-panel__meta { margin-top: var(--space-2xs); font: var(--type-caption); color: var(--ink-tertiary); }

/* DOWNLOADS PANEL. Every launched market-year × format, one row each
   (D11: this is the only Data-tier download path — no key, no scripted
   access, so the row's own button is a real, session-authenticated fetch). */
.downloads {
  border: var(--edge);
  border-radius: var(--radius-sheet);
  background: var(--surface-raised);
}
.downloads__row {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  justify-content: space-between;
  gap: var(--space-sm) var(--space-md);
  padding: var(--space-sm) var(--space-lg);
  border-bottom: var(--edge-soft);
}
.downloads__row:last-child { border-bottom: 0; }
.downloads__name { font: var(--type-data-strong); color: var(--ink); }
.downloads__actions { display: flex; flex-wrap: wrap; gap: var(--space-2xs); }
.downloads__meta {
  padding: var(--space-sm) var(--space-lg);
  border-top: var(--edge);
  font: var(--type-caption);
  color: var(--ink-tertiary);
}

/* USAGE PANEL. A quota meter — API requests today, downloads today — drawn
   the same way `.factor__track`/`.factor__fill` draw a measured figure: ink
   at a length, no hue, because a quota is not a status to alarm over. */
.usage { display: flex; flex-direction: column; gap: var(--space-2xs); }
.usage__label {
  display: flex;
  justify-content: space-between;
  gap: var(--space-sm);
  font: var(--type-caption);
  font-variant-numeric: var(--numeric-tabular);
  color: var(--ink-tertiary);
}
.usage__track {
  position: relative;
  height: 6px;
  background: var(--surface-sunken);
  border: var(--border-hairline) solid var(--rule-soft);
  border-radius: var(--radius-chip);
  overflow: hidden;
}
.usage__fill { display: block; height: 100%; width: calc(var(--v, 0) * 1%); background: var(--ink-secondary); }

/* TOAST. One at a time, self-dismissing, announced through #live rather than
   role="alert" (the live region already exists on every page — a second
   announcer would double-speak it). Width-capped so a long message cannot
   force horizontal scroll at 390 (the popover uses the same guard). */
.toast {
  position: fixed;
  left: 50%;
  bottom: var(--space-xl);
  transform: translateX(-50%);
  z-index: var(--layer-overlay);
  max-width: calc(100vw - 2 * var(--gutter-page));
  padding: var(--space-sm) var(--space-lg);
  border: var(--edge);
  border-radius: var(--radius-control);
  background: var(--ink);
  color: var(--ink-inverse);
  font: var(--type-data);
  box-shadow: var(--shadow-overlay);
}

/* SKELETON. Fixed-height only (principle 6: nothing animates width, nothing
   moves after it appears) — a consumer sets the height inline or with a
   modifier for the row it stands in for; this rule never guesses at one. */
.skeleton {
  display: block;
  background: var(--surface-sunken);
  border-radius: var(--radius-chip);
}

/* CODE-INLINE. Mono, case-preserving — an API path or a key prefix is an
   identifier, not a word, and title-casing "GET /api/v1/car" would be wrong
   in a way ordinary prose-casing rules don't anticipate. */
.code-inline {
  padding: 1px var(--space-2xs);
  border-radius: var(--radius-chip);
  background: var(--surface-sunken);
  font: var(--type-code);
  text-transform: none;
  /* An explicit colour, not "whatever the caller's ink is": this chip sits
     on --surface-sunken, and inherited --ink-tertiary (the specimen's own
     .spec-note wraps it) measures 4.36:1 there — under 4.5:1 (critic, WS1 §2
     item 1). --ink-secondary is 9.3:1 on every surface this system defines,
     so the chip stays legible wherever it is dropped. */
  color: var(--ink-secondary);
}


/* =============================================================================
   14f. DATA PRESENTATION V3 — SIX-SLOT STRIP, REVEAL, QUESTION SECTIONS,
        FIGURE ROW, RECORD RAIL, NOTES, DATA-TABLE EXTENSIONS, CELL CARD,
        LIST GATE PANEL, TOOLBAR BUTTON AND COLUMN CHOOSER
   Added 2026-09-22 (WS1; scratchpad/data-presentation-v3.md §3's ten
   components, §8's visual system). The owner rejected the chip register on
   the car page and list ("sooo confusing... don't fill up everything with
   red flags" — scratchpad/owner-feedback-checks.md): this section is the
   CSS half of the replacement WS3 (car page) and WS4 (list) build their
   markup against, matching scratchpad/data-presentation-v3.html. --held
   (14b) is the only new token; everything else below is the existing
   palette, applied to new shapes (§8.1, "kept, unargued").

   WHAT IS DELIBERATELY ABSENT. No chip, no badge, no notice, no colour-coded
   warning class appears anywhere in this section, on either page these
   components serve — that omission IS the fix (§1.4, "nothing warns"; §8.1,
   "the chips were" the problem, not the system). .chip never existed in this
   codebase; .badge/.notice/.absence (14/14a and earlier) are UNCHANGED and
   stay exactly as they are for the composite-market pages and the app shell
   that still use them (pricing, account, EU/other rankings) — nothing here
   removes or restyles them. A car or list page built from THIS section's
   classes instead uses a bar (.held), a dash (.val.na/.v.na/.na), a letter
   mark (.data-table td .n) or a plain sentence, in the page's own ink. */

/* SIX-SLOT SUMMARY STRIP (C1). "A nutrition label: the rows never move" (§1,
   principle 1) — the same six slots, same order, on every car, so a buyer
   learns the layout once. min-height on .tile is what keeps the strip's
   height identical before and after the reveal fills it (§10 check 5: CLS
   < 0.05); the 2px heavy rule on top and the hairline rules the grid `gap`
   draws between tiles are §8.6's "nutrition label" cue. */
.six {
  display: grid;
  grid-template-columns: repeat(2, minmax(0, 1fr));
  gap: var(--border-hairline);
  background: var(--rule);
  border: var(--edge);
  border-top: var(--border-heavy) solid var(--rule-heavy);
  margin-top: var(--space-lg);
}
@media (min-width: 640px)  { .six { grid-template-columns: repeat(3, minmax(0, 1fr)); } }
@media (min-width: 1000px) { .six { grid-template-columns: repeat(6, minmax(0, 1fr)); } }
.tile {
  display: flex;
  flex-direction: column;
  min-width: 0;
  min-height: 7.9rem;
  padding: var(--space-md) var(--space-lg) var(--space-sm);
  background: var(--surface-raised);
}
.tile .lab { font: var(--type-caption); color: var(--ink-tertiary); }
.tile .val {
  display: flex;
  align-items: baseline;
  flex-wrap: wrap;
  gap: 0 var(--space-2xs);
  margin-top: var(--space-sm);
  min-height: 2.25rem;
  font: 600 clamp(1.6rem, 8vw, 2.125rem)/1.05 var(--font-text);
  letter-spacing: var(--track-tight);
  color: var(--ink);
}
@media (min-width: 1000px) { .tile .val { font-size: 2.25rem; } }
.tile .val .num { white-space: nowrap; }
/* Out of the numeral slot on purpose (§8.5): "$1,050" with "per year" in
   .sub, never "$1,050/yr" wrapping onto two lines inside .val. */
.tile .val .unit { font: 500 0.875rem/1 var(--font-text); color: var(--ink-secondary); white-space: nowrap; }
.tile .val.na { color: var(--ink-tertiary); font-weight: 500; }
.tile .sub { margin-top: var(--space-2xs); font: var(--type-caption); color: var(--ink-secondary); }
.tile .pub {
  margin-top: auto;
  padding-top: var(--space-sm);
  font: var(--type-identifier);
  letter-spacing: var(--track-normal);
  text-transform: none;
  color: var(--ink-tertiary);
  min-height: 1.2rem;      /* reserved even when a held tile prints no tag */
}

/* REFERENCE MARK. The "one interaction away" depth of provenance (§1.7): a
   superscript link into Notes and sources (C7), on tiles, rows and the
   record rail alike — never a badge, always text that scrolls to its note. */
sup.ref { font-size: 0.65em; line-height: 0; vertical-align: super; margin-left: var(--space-3xs); font-weight: 500; }
sup.ref a { color: var(--accent); text-decoration: none; }
sup.ref a:hover, sup.ref a:focus-visible { text-decoration: underline; }
.tile .val sup.ref { font-size: 0.75rem; vertical-align: 0.5em; margin-left: 1px; }

/* FADE ON REVEAL (§3, C1/C4/C5/C6). Values land in the slots they already
   occupy — no re-render, no reflow — with a considered opacity fade, never
   for a reader who asked for less motion (prefers-reduced-motion, 4.11). */
.fade { animation: cs-fade var(--duration-considered) var(--ease-standard); }
@keyframes cs-fade { from { opacity: 0; } to { opacity: 1; } }
@media (prefers-reduced-motion: reduce) { .fade { animation: none; } }

/* REVEAL (C3). "One sentence, one navy button, one meter line" (§1, principle
   6) — the page's only ask, directly under the strip. Collapses to the
   meter line alone once a signed-out reveal has answered (§4); cs.car never
   renders it at all for a signed-in reader. min-height here, not on .btn
   generally (6.), because this is specifically "the one button" §3's C3/C10
   sub-parts table calls out at 44px regardless of viewport — .btn elsewhere
   on the site (account, pricing) keeps the pointer-aware --control-h it
   already had. */
.reveal {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: var(--space-sm);
  padding: var(--space-xl) 0 var(--space-2xs);
  text-align: center;
}
.reveal .lead { margin: 0; font: var(--type-body); color: var(--ink-secondary); max-width: 44ch; }
.reveal .btn, .gate .btn { min-height: var(--control-h-touch); }
.reveal .meter { margin: 0; font: var(--type-caption); color: var(--ink-tertiary); }
.reveal .meter a { color: var(--accent); }

/* BELOW THE STRIP (§2.2): sections, the record rail, notes. Phone-first DOM
   order — reveal, sections, rail, notes (§2.1) — so nothing is duplicated
   for the two widths; the grid areas only re-seat the rail at >=1000px. */
.below { display: grid; grid-template-columns: minmax(0, 1fr); gap: 0 var(--space-3xl); }
@media (min-width: 1000px) {
  .below {
    grid-template-columns: minmax(0, 1fr) 300px;
    grid-template-areas: "a c" "b c" "d c";
  }
  .below > .reveal { grid-area: a; }
  .below > .qs     { grid-area: b; }
  .below > .notes  { grid-area: d; }
  .below > .rail   {
    grid-area: c;
    align-self: start;
    position: sticky;
    top: var(--space-lg);
    margin-top: var(--space-lg);
  }
}

/* QUESTION SECTIONS (C4). Headings named by the buyer's question (§1,
   principle 3), each carrying its key figure in .peek even closed — a
   reader never has to open a section to see the number, only to see the
   rest of the row. Open/closed itself is the native <details> attribute
   (WS3 sets it per viewport); this only styles the two states. */
.qs { margin-top: var(--space-lg); }
details.q { border-top: var(--edge); }
details.q:last-of-type { border-bottom: var(--edge); }
details.q > summary {
  display: flex;
  justify-content: space-between;
  align-items: baseline;
  gap: var(--space-md);
  padding: var(--space-md) 0;
  list-style: none;
  cursor: pointer;
  font: 600 1.0625rem/1.3 var(--font-display);
  color: var(--ink);
}
details.q > summary::-webkit-details-marker { display: none; }
details.q > summary > span:first-child { white-space: nowrap; }
details.q > summary .peek {
  font: var(--type-code);
  color: var(--ink-tertiary);
  text-align: right;
  max-width: 58%;
}
details.q > summary .peek::after { content: "  +"; color: var(--ink-quiet); }
/* Open: the peek's job is done (the row below now shows the number), so it
   is hidden rather than duplicated — color:transparent, not display:none,
   keeps the summary's own height (and so the section's) from jumping. */
details.q[open] > summary .peek { color: transparent; }
details.q[open] > summary .peek .held { visibility: hidden; }
details.q[open] > summary .peek::after { content: "  \2212"; color: var(--ink-quiet); }

/* FIGURE ROW (C5). Label / value+publisher (/ basis at >=1000) — two columns
   at phone width, the qualifier folded under the value as .row .v small;
   three at >=1000, where the same text becomes the Basis column instead of
   repeating it (§6: "the same text is the phone's one-tap note"). */
.rows { margin: 0 0 var(--space-md); }
.row {
  display: grid;
  grid-template-columns: minmax(0, 1fr) auto;
  gap: var(--space-3xs) var(--space-md);
  padding: var(--space-sm) 0;
  border-top: var(--edge-soft);
  align-items: baseline;
}
.row:first-child { border-top: 0; }
.row .k { font: var(--type-body); color: var(--ink-secondary); }
.row .v { font: var(--type-data-strong); text-align: right; white-space: nowrap; }
.row .v small { display: block; font: var(--type-caption); color: var(--ink-secondary); white-space: normal; }
.row .v .pub { display: block; font: var(--type-identifier); letter-spacing: var(--track-normal); text-transform: none; color: var(--ink-tertiary); }
.row .v.na { color: var(--ink-tertiary); font-weight: 400; }
.row .b { display: none; }
@media (min-width: 1000px) {
  .row { grid-template-columns: minmax(9.5rem, 0.75fr) auto minmax(0, 1.25fr); }
  .row .v small { display: none; }
  .row .v .pub { display: inline; margin-left: var(--space-sm); }
  .row .b { display: block; font: var(--type-caption); color: var(--ink-tertiary); }
}
/* Recall campaigns / complaint components (§3 sub-parts): a disclosure, not
   a table, so opening one never displaces the rows around it by much. */
details.sub { margin: 0 0 var(--space-md); font: var(--type-body); }
details.sub > summary { cursor: pointer; color: var(--accent); font: var(--type-caption); list-style: none; padding: var(--space-3xs) 0; }
details.sub > summary::-webkit-details-marker { display: none; }
details.sub > summary::before { content: "+ "; color: var(--ink-quiet); }
details.sub[open] > summary::before { content: "\2212 "; }
details.sub ul { margin: var(--space-xs) 0 0; padding-left: 0; list-style: none; }
details.sub li { padding: var(--space-xs) 0; border-top: var(--edge-soft); font: var(--type-body); color: var(--ink-secondary); }
details.sub li b { font-weight: 600; color: var(--ink); }
details.sub li .d { font: var(--type-code); color: var(--ink-tertiary); margin-right: var(--space-sm); }

/* THIS RECORD (C6). The analyst's rail (§6, §9): figures carried of figures
   published, publishers, dates, configuration, JSON/CSV/API/Compare, the
   exact GET, the three tiers in one sentence. Normal flow at phone width,
   after the sections and before .notes; sticky at >=1000 (.below, above). */
.rail { border-top: var(--border-heavy) solid var(--rule-heavy); margin-top: var(--space-2xl); padding-top: var(--space-md); font: var(--type-body); color: var(--ink-secondary); }
.rail h2 { font: var(--type-eyebrow); letter-spacing: var(--track-caps); text-transform: uppercase; color: var(--ink-tertiary); margin: 0 0 var(--space-md); }
.rail dl { margin: 0; display: grid; grid-template-columns: auto minmax(0, 1fr); gap: var(--space-3xs) var(--space-md); }
.rail dt { color: var(--ink-tertiary); }
.rail dd { margin: 0; color: var(--ink); }
.rail .links { margin: var(--space-md) 0 var(--space-sm); display: flex; flex-wrap: wrap; gap: var(--space-3xs) var(--space-lg); }
.rail .links a { color: var(--accent); }
.rail code {
  display: inline-block;
  margin-top: var(--space-sm);
  padding: var(--space-3xs) var(--space-xs);
  border-radius: var(--radius-control);
  background: var(--surface-sunken);
  font: var(--type-code);
  word-break: break-all;
}
.rail p { margin: var(--space-sm) 0 0; }
.rail .lic { font: var(--type-caption); color: var(--ink-tertiary); }

/* NOTES AND SOURCES (C7). One numbered note per figure — basis, configura-
   tion, date, corroboration — in the page's own ink (§1, principle 4:
   "nothing warns"). :target highlights the note a reference mark jumped to;
   .back returns focus to the figure the note came from. */
.notes { margin-top: var(--space-3xl); border-top: var(--border-heavy) solid var(--rule-heavy); padding-top: var(--space-md); }
.notes h2 { font: var(--type-eyebrow); letter-spacing: var(--track-caps); text-transform: uppercase; color: var(--ink-tertiary); margin: 0 0 var(--space-md); }
.notes ol { margin: 0; padding-left: var(--space-xl); font: var(--type-body); color: var(--ink-secondary); }
.notes li { padding: var(--space-3xs) var(--space-2xs); }
.notes li:target { background: var(--accent-surface); border-radius: var(--radius-control); }
.notes .back { margin-left: var(--space-2xs); font: var(--type-caption); color: var(--accent); text-decoration: none; }

/* DATA TABLE EXTENSIONS (C8), layered on the v2 .data-table (14c): sticky
   head and Car column, the held column-group and its one bar per row, the
   not-carrying band, the sort claim, the two-column legend, and the
   dense/fit modifiers WS4's list toolbar toggles. */
.tablewrap { overflow-x: auto; border: var(--edge); background: var(--surface-raised); -webkit-overflow-scrolling: touch; }
.data-table th { position: sticky; top: 0; background: var(--surface-raised); z-index: var(--layer-sticky); }
.data-table th:first-child, .data-table td:first-child { position: sticky; left: 0; background: var(--surface-raised); z-index: calc(var(--layer-sticky) + 1); }
.data-table th.grp {
  text-align: center;
  font-weight: 500;
  color: var(--ink-secondary);
  background: var(--surface-raised);
  white-space: normal;
}
.data-table th.grp a { color: var(--accent); font-weight: 400; }
.data-table td.grpcell { text-align: center; }
/* --ink-tertiary, not --ink-quiet: this dash IS a value (the absent one),
   same register as .tile .val.na and .row .v.na elsewhere in this section —
   and tokens.css says --ink-quiet outright ("FURNITURE ONLY... never a
   value"). At ~3.1:1 in both themes it also fails contrast outright. */
.data-table td.na { color: var(--ink-tertiary); }
/* The letter marks (§5, "a" through "k") — always beside a value, never in
   place of one; the legend below prints only the letters actually on screen. */
.data-table td .n { font: var(--type-identifier); font-size: 0.6875rem; letter-spacing: 0; text-transform: none; color: var(--accent); vertical-align: super; margin-left: 1px; }
.data-table tr.band td {
  /* --ink-secondary, not --ink-tertiary (same cause as critic WS1 §2 item 1's
     .code-inline): this band sits on --surface-sunken, where tertiary
     measures 4.36:1 light — under 4.5:1. --ink-secondary clears it. */
  background: var(--surface-sunken);
  color: var(--ink-secondary);
  font: var(--type-caption);
  text-align: left;
  position: static;
}
.data-table.dense th, .data-table.dense td { padding: var(--space-2xs) var(--space-sm); }
.data-table.dense .row-link small { display: none; }
@media (max-width: 719px) {
  /* table.fit (§2.3): signed out, the six held figures collapse into one
     group column with one bar per row, so the table fits a 390px screen
     without its own horizontal scroll (that is what .tablewrap is for once
     the reader is signed in, at the fuller column set). */
  .data-table.fit th, .data-table.fit td { padding: var(--space-sm) var(--space-2xs); }
  .data-table.fit th { white-space: normal; }
  .data-table.fit th.grp { min-width: 4.4rem; }
}
.claim { display: flex; flex-wrap: wrap; justify-content: space-between; gap: var(--space-3xs) var(--space-md); font: var(--type-caption); color: var(--ink-secondary); margin: 0 0 var(--space-sm); }
.claim a { color: var(--accent); }
.legend { font: var(--type-caption); color: var(--ink-secondary); margin-top: var(--space-md); columns: 1; column-gap: var(--space-2xl); }
@media (min-width: 720px) { .legend { columns: 2; } }
.legend span { display: block; break-inside: avoid; padding: var(--space-3xs) 0; }
.legend b { font-weight: 600; color: var(--ink); margin-right: var(--space-2xs); font-family: var(--font-mono); }
.after { margin-top: var(--space-md); font: var(--type-caption); color: var(--ink-tertiary); display: flex; flex-wrap: wrap; gap: var(--space-2xs) var(--space-lg); align-items: baseline; }
/* An explicit colour, not inherited --ink-tertiary (same cause as critic WS1
   §2 item 1's .code-inline): this chip sits on --surface-sunken, where
   tertiary measures 4.36:1 light — under 4.5:1. --ink-secondary clears it. */
.after code { font: var(--type-code); background: var(--surface-sunken); color: var(--ink-secondary); padding: 1px var(--space-2xs); border-radius: var(--radius-chip); word-break: break-all; }

/* CELL CARD (C9). One value's card on click: value / publisher / basis /
   configuration — the analyst's on-demand equivalent of the buyer's note,
   offered on a value, never on a held bar (§3 table: "not offered on a
   bar"). Positioned by its caller (WS4); sized and coloured here. */
.card {
  position: absolute;
  z-index: var(--layer-popover);
  max-width: min(20rem, calc(100vw - 2 * var(--gutter-page)));
  padding: var(--space-sm) var(--space-md);
  border: var(--edge);
  border-radius: var(--radius-control);
  background: var(--surface-overlay);
  box-shadow: var(--shadow-overlay);
  font: var(--type-body);
  color: var(--ink-secondary);
}
.card b { color: var(--ink); font-weight: 600; }
/* --ink-secondary, not --ink-tertiary (same cause as critic WS1 §2 item 1's
   .browse__menu and .code-inline): this card sits on --surface-overlay,
   where tertiary measures 4.42:1 in dark — under 4.5:1. --ink-secondary
   clears it (7.56:1 dark) without changing how the card reads in light. */
.card .pub { font: var(--type-identifier); letter-spacing: var(--track-normal); text-transform: none; color: var(--ink-secondary); }

/* LIST GATE PANEL (C10). Distinct from the car page's .gate-panel (14c): no
   title, one sentence carrying the coverage count, one button, "Back to
   A-Z" — replaces the table in place, the list's only button, appearing
   only after a held head is tapped signed out (§4). */
.gate {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: var(--space-sm);
  padding: var(--space-2xl) var(--space-xl);
  border: var(--edge);
  background: var(--surface-raised);
  text-align: center;
}
.gate p { margin: 0; font: var(--type-body); color: var(--ink-secondary); max-width: 52ch; }
.gate .meter { font: var(--type-caption); color: var(--ink-tertiary); }
.gate .meter a { color: var(--accent); }

/* TOOLBAR BUTTON AND COLUMN CHOOSER (§6, §11 workstream map: "the column
   chooser"). `.toolbar` itself is WS4's own (page CSS 3.6/3.10) and is not
   touched here; these are the pieces v3 adds inside it — a button styled
   like the toolbar's other controls (Dense, Columns) and the disclosure the
   Columns control opens, matching data-presentation-v3.html's `.tb` /
   `details.cols` / `.panel`. `.tb[aria-pressed]` is the one pressed-state
   colour this section allows: it is the OS-native toggle-button affordance
   (the accent surface, same token as a link would use), not a status. */
.tb {
  font: 500 0.8125rem/1 var(--font-text);
  color: var(--accent);
  background: var(--surface-raised);
  border: var(--edge);
  border-radius: var(--radius-control);
  padding: var(--space-xs) var(--space-sm);
  cursor: pointer;
}
.tb[aria-pressed="true"] { background: var(--accent-surface); border-color: var(--accent); }
.toolbar .sep { color: var(--rule); }
/* .toolbar .plan (the mockup's own name for the "(Data plan)"/"(API plan)"
   note beside the toolbar's CSV/API links) reuses a class this codebase
   already gives a full plan-CARD component (14d: flex column, padding,
   border, background, for the pricing and account pages) — collide it
   would, verbatim from the mockup, wrapping a few words of caption text in
   a bordered card. Reset here, not renamed, so WS4 can still copy the
   mockup's markup class-for-class without inheriting that box model; and
   --ink-tertiary, not the mockup's --ink-quiet, for the "(free account)"
   disabled-checkbox note below — tokens.css reserves quiet for furniture,
   never a reason a control is unavailable. */
.toolbar .plan { display: inline; padding: 0; border: 0; border-radius: 0; background: none; font: var(--type-caption); color: var(--ink-tertiary); }
details.cols { position: relative; }
details.cols > summary { list-style: none; }
details.cols > summary::-webkit-details-marker { display: none; }
details.cols .panel {
  position: absolute;
  z-index: var(--layer-popover);
  top: calc(100% + var(--space-xs));
  left: 0;
  min-width: 14.5rem;
  max-width: calc(100vw - 2 * var(--gutter-page));
  padding: var(--space-sm) var(--space-md);
  border: var(--edge);
  border-radius: var(--radius-control);
  background: var(--surface-raised);
  box-shadow: var(--shadow-overlay);
  font: var(--type-caption);
  color: var(--ink-secondary);
}
details.cols .panel label { display: flex; gap: var(--space-xs); align-items: center; padding: var(--space-3xs) 0; }
details.cols .panel .grp { font: var(--type-identifier); letter-spacing: var(--track-normal); color: var(--ink-tertiary); margin: var(--space-xs) 0 var(--space-3xs); }
details.cols .panel .grp:first-child { margin-top: 0; }
details.cols .panel .note { color: var(--ink-tertiary); }


/* =============================================================================
   15. UTILITIES
   ========================================================================== */

/* Visually hidden, present to assistive technology: the live region, the
   skip link until it is focused, a table caption that must be read but not
   seen. Not display:none, which hides it from everyone. */
.vh {
  position: absolute !important;
  width: 1px;
  height: 1px;
  margin: -1px;
  padding: 0;
  overflow: hidden;
  clip: rect(0 0 0 0);
  clip-path: inset(50%);
  white-space: nowrap;
  border: 0;
}

.stack > * + * { margin-top: var(--stack-base); }
.stack--tight > * + * { margin-top: var(--stack-tight); }
.stack--loose > * + * { margin-top: var(--stack-loose); }
.cluster { display: flex; flex-wrap: wrap; align-items: center; gap: var(--space-sm); }
.divider { height: 0; border: 0; border-top: var(--edge-soft); margin: var(--space-2xl) 0; }
.divider--heavy { border-top: var(--edge-heavy); }

/* Brief check 6: render the site in greyscale; no information is lost.
   Wrap anything in this to prove it in review. */
.greyscale-check { filter: grayscale(1); }

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

@media print {
  body { background: var(--print-surface); color: var(--print-ink); }
  .table thead th { position: static; }
  .btn, .segmented { display: none; }
  .credit, .absence, .notice { color: var(--print-ink); }
}
/* =========================== END components.css =========================== */

/* =============================================================================
   3. PAGE — carstandings.com SPA furniture
   Everything above this line is design/build/tokens.css and
   design/build/components.css, inlined byte-for-byte. Nothing below re-declares
   a colour: every value here resolves through a token, so all three theme
   states (light, dark, and the unstamped system default) are inherited rather
   than re-implemented.
   ========================================================================== */

/* --- 3.1 ONE VOCABULARY, NO ALIAS LAYER ----------------------------------
   The page used to carry its own palette — --ink-muted, --warm-white, --blue,
   --green, --border, --input-border … — declared once in :root and AGAIN
   inside a @media(prefers-color-scheme:dark) block. That is the failure the
   design teams already hit between themselves: two sets of names for one set
   of roles, kept in step by hand, with half the values existing only inside a
   media query so an explicit theme choice could never reach them.

   The first cut of this integration kept those names as one-line aliases onto
   the tokens. That worked, but it left twenty-one names that meant nothing on
   their own and invited the next person to add a twenty-second. Counting the
   survivors showed only four were still referenced anywhere — --ink-faint,
   --white, --blue and --input-border, eleven references between them — so all
   eleven were converted and the alias layer deleted.

   There is now exactly ONE vocabulary on this page: the tokens. Verify by
   grepping for any var() naming one of the retired roles — warm-white,
   ink-muted, ink-faint, blue, white, border, input-border, green, red,
   font-sans, shadow-card, warm-gray — which must find none, and with the
   undeclared-property sweep, which must find every var(--x) declared. The
   names are listed here WITHOUT their var() wrapper on purpose: a paragraph
   that quotes the pattern it describes makes the grep match its own
   documentation, which is how three checks went wrong in this repo in one
   afternoon. warm-gray, referenced four times and declared nowhere at all,
   went the same way as the rest.
   ------------------------------------------------------------------------ */

/* --- 3.2 BASE ------------------------------------------------------------ */
body {
  font: var(--type-body);
  font-variant-numeric: var(--numeric-proportional);
  background: var(--surface-page);
  color: var(--ink);
}
.mono {
  font-family: var(--font-mono);
  font-variant-numeric: var(--numeric-tabular);
}
.icon {
  width: 18px; height: 18px;
  stroke: currentColor; stroke-width: 2; fill: none;
  stroke-linecap: round; stroke-linejoin: round;
  display: inline-block; vertical-align: middle; flex-shrink: 0;
}
.icon-sm { width: 15px; height: 15px; }
.icon-lg { width: 22px; height: 22px; }
.hidden { display: none !important; }
/* SKIP LINK. Off-screen (.vh's own technique, not display:none, so a focus
   move actually lands somewhere) until it is the focused element, then a
   normal, visible, on-top control — the first Tab stop on every page. */
.skip-link {
  position: absolute; left: var(--space-sm); top: -100%;
  z-index: var(--layer-popover);
  padding: var(--space-xs) var(--space-md);
  background: var(--ink); color: var(--ink-inverse);
  border-radius: var(--radius-control);
  font: var(--type-data-strong);
  text-decoration: none;
  transition: top var(--duration-base) var(--ease-standard);
}
.skip-link:focus { top: var(--space-sm); }

/* --- 3.3 HEADER ---------------------------------------------------------- */
.header {
  position: sticky; top: 0; z-index: var(--layer-header);
  background: var(--surface-raised);
  /* The hairline is drawn INSIDE the box (an inset shadow), not as a border:
     a border added its 1 px to the 44 px control and 6 px + 6 px of padding
     and made the row 57 px — the owner's check is ONE row of at most 56 px
     at 390 (owner-feedback-checks 6). Same line, same colour, no height. */
  border-bottom: 0;
  box-shadow: inset 0 calc(-1 * var(--border-hairline)) 0 0 var(--rule);
  display: flex; align-items: center; justify-content: space-between;
  gap: var(--space-md);
  padding: 0 var(--gutter-page);
  min-height: 56px;
}
.header__controls { display: flex; align-items: center; gap: var(--space-sm); }
/* The mobile menu toggle (page CSS §3.18 makes it visible and wires the
   checkbox to .header__controls). Hidden at desktop widths by default: at
   ≥769px .header__controls is always shown inline and needs no toggle at all,
   checked or not. */
.header__menu-toggle {
  display: none;
  align-items: center; justify-content: center;
  width: var(--control-h, 32px); height: var(--control-h, 32px);
  border: var(--edge); border-radius: var(--radius-control);
  background: var(--surface-raised); color: var(--ink);
  cursor: pointer;
}
.header__link {
  font: var(--type-data);
  color: var(--ink); text-decoration: none;
  padding: var(--space-2xs) var(--space-2xs);
  white-space: nowrap;
}
.header__link:hover, .header__link:focus-visible { color: var(--accent); text-decoration: underline; }
.header__search-input { width: 220px; }
.header select {
  font: var(--type-data);
  font-variant-numeric: var(--numeric-tabular);
  padding: var(--space-2xs) var(--space-3xl) var(--space-2xs) var(--space-sm);
  border: var(--edge); border-radius: var(--radius-control);
  background-color: var(--surface-raised);
  background-image: var(--icon-chevron);
  background-repeat: no-repeat;
  background-position: right var(--space-sm) center;
  color: var(--ink); cursor: pointer;
  -webkit-appearance: none; appearance: none;
  /* FIXED WIDTH, NOT CONTENT-SIZED (header CLS 0). populateSelectors fills
     this control's OPTIONS asynchronously — an empty "Markets…" placeholder,
     later real country names — and a select sized to its own longest option
     grows the instant that answer arrives, shoving Pricing/Developers/Sign
     in sideways out from under a reader's cursor. Measured: this repo's
     longest launched-market option is "United States"; #year-select only
     ever holds four digits. Truncating with an ellipsis costs nothing here
     — the value is still in the accessible name and in .value. */
  width: 9rem; max-width: 9rem;
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
#year-select { width: 5rem; max-width: 5rem; }
.header input[type="text"] {
  font: var(--type-data);
  border: var(--edge); border-radius: var(--radius-control);
  background: var(--surface-raised); color: var(--ink);
}
.ticker {
  background: var(--surface-page);
  border-bottom: var(--edge-soft);
  font: var(--type-caption);
  font-variant-numeric: var(--numeric-tabular);
  color: var(--ink-tertiary);
  padding: var(--space-2xs) var(--gutter-page);
  text-align: center;
}

/* --- 3.4 THEME CONTROL ---------------------------------------------------
   Three states, because the palette has three. "System" is the UNSTAMPED
   state — it removes data-theme rather than writing a third value, so the
   media query in tokens.css §2 is what decides. */
.theme-ctl { display: inline-flex; }
.theme-ctl > button {
  padding: var(--space-3xs) var(--space-xs);
  min-width: 30px;
  font: var(--type-column-head);
  letter-spacing: var(--track-caps);
  text-transform: uppercase;
}

/* --- 3.5 SECTIONS AND HERO ----------------------------------------------- */
.section { max-width: 680px; margin: 0 auto; padding: var(--space-4xl) var(--gutter-page); }
.section--alt {
  background: var(--surface-page);
  border-block: var(--edge-soft);
  max-width: none;
  padding-left: var(--gutter-page); padding-right: var(--gutter-page);
}
.section--alt-inner { max-width: 680px; margin: 0 auto; }
.section h2 { font: var(--type-section-title); letter-spacing: var(--track-tight); margin-bottom: var(--space-md); }
.section p  { font: var(--type-body); color: var(--ink-secondary); }
.data-sources { display: flex; flex-wrap: wrap; gap: var(--space-2xs); margin-top: var(--space-md); }
/* Required licence acknowledgements (licences.ATTRIBUTION_REQUIRED). Quiet, but
   present: an obligation rendered invisible is an obligation unmet. */
.attribution { font: var(--type-caption); color: var(--ink-tertiary); }
.attribution__item { display: block; }

.hero { text-align: left; padding: var(--space-5xl) var(--gutter-page) var(--space-3xl); max-width: var(--measure-page); margin: 0 auto; }
.hero h1 { font: var(--type-page-title); font-size: var(--font-size-display-sm); letter-spacing: var(--track-display); margin-bottom: var(--space-md); }
.hero > p { font: var(--type-body-lead); color: var(--ink-secondary); max-width: var(--measure-text); margin-bottom: var(--space-3xl); }
.selector-card { max-width: 420px; }
.selector-card label { display: block; font: var(--type-column-head); letter-spacing: var(--track-caps); text-transform: uppercase; color: var(--ink-tertiary); margin-bottom: var(--space-2xs); }
.selector-card select { margin-bottom: var(--space-lg); }
/* THE MARKET-YEAR TILES (v2 rework §3.2; critic pass 13, WS4 item 16): baked
   by generate_site.market_tiles_html between the market-tiles markers, one
   link per launched market-year with its car count — the front page's
   primary way in, and the no-JS reader's only one. Tokens only; a tile is a
   44 px touch target. */
.mkt-tiles { display: grid; grid-template-columns: repeat(auto-fit, minmax(18rem, 1fr)); gap: var(--space-lg); margin: var(--space-xl) 0 var(--space-2xl); }
.mkt-tiles__market { font: var(--type-column-head); letter-spacing: var(--track-caps); text-transform: uppercase; color: var(--ink-tertiary); margin: 0 0 var(--space-2xs); }
.mkt-tiles__years { display: flex; flex-wrap: wrap; gap: var(--space-2xs); }
.mkt-tile { display: inline-flex; align-items: center; gap: var(--space-2xs); min-height: var(--control-h-touch); padding: var(--space-3xs) var(--space-sm); border: var(--edge); border-radius: var(--radius-control); background: var(--surface-raised); color: var(--ink); text-decoration: none; font: var(--type-data-strong); transition: var(--transition-control); }
.mkt-tile:hover, .mkt-tile:focus-visible { border-color: var(--ink); text-decoration: none; }
.mkt-tile__count { font: var(--type-caption); color: var(--ink-secondary); }

/* --- 3.6 FILTER BAR ------------------------------------------------------ */
.filter-bar {
  position: sticky; top: 56px; z-index: var(--layer-sticky);
  background: var(--surface-raised);
  border-bottom: var(--edge);
  padding: var(--space-sm) var(--gutter-page);
  display: flex; align-items: center; gap: var(--space-sm); flex-wrap: wrap;
}
.filter-bar__context { margin-right: auto; display: inline-flex; align-items: center; gap: var(--space-sm); }
.filter-bar select {
  font: var(--type-column-head);
  letter-spacing: var(--track-normal);
  padding: var(--space-2xs) var(--space-2xl) var(--space-2xs) var(--space-sm);
  border: var(--edge); border-radius: var(--radius-control);
  background-color: var(--surface-raised);
  background-image: var(--icon-chevron);
  background-repeat: no-repeat;
  background-position: right var(--space-xs) center;
  color: var(--ink); cursor: pointer;
  -webkit-appearance: none; appearance: none;
}
/* --ink-quiet is 3.1:1 and tokens.css marks it FURNITURE ONLY — never a
   value, never a reason string, and a disabled control's own label is
   still text a reader has to read. --ink-tertiary is 5.4:1. */
.filter-bar select:disabled { color: var(--ink-tertiary); cursor: not-allowed; }

/* --- 3.7 RANKING ROWS ----------------------------------------------------
   A leaderboard row, not a card: a hairline between rows, a heavy rule closing
   the block, and no hue anywhere. Rank, name, score, completeness. */
.ranking-list { max-width: var(--measure-page); margin: 0 auto; padding: var(--space-lg) var(--gutter-page); }
.car-row {
  padding: var(--pad-cell-y) 0;
  border-bottom: var(--edge-soft);
  cursor: pointer;
}
.car-row:last-of-type { border-bottom: var(--edge-heavy); }
.car-row:hover { background: var(--surface-hover); }
.car-row__top { display: flex; align-items: center; gap: var(--space-md); }
.car-row__rank {
  font: var(--type-identifier);
  font-variant-numeric: var(--numeric-tabular);
  letter-spacing: var(--track-normal);
  color: var(--ink-tertiary);
  min-width: 3rem; text-align: right;
}
.car-row__name { flex: 1; font: var(--type-data-strong); display: flex; align-items: baseline; flex-wrap: wrap; gap: var(--space-xs); }
.car-row__meta { margin-top: var(--space-3xs); margin-left: calc(3rem + var(--space-md)); display: flex; align-items: center; flex-wrap: wrap; gap: var(--space-md); }
.car-row__expand { display: none; margin-top: var(--space-md); margin-left: calc(3rem + var(--space-md)); }
.car-row.expanded .car-row__expand { display: block; }
/* .car-row--locked is GONE (2026-09-17). It painted the rows past FREE_LIMIT as
   a greyed "Ranked N — in the full standings" tile. Browsing is free: there is
   no locked row to paint. */

/* --- 3.7b A FIGURE IN A LIST -------------------------------------------- */
/* A list in a facts market is ordered by ONE measured quantity, and the row
   prints that quantity in ITS OWN UNIT. It is deliberately not .score: a score
   is a position inside a cohort and components.css makes a .score without a
   .cohort child scream; a figure is a measurement and carries a unit and a
   publisher instead. Nothing here is out of 100 and nothing is a verdict. */
.figure { display: inline-flex; align-items: baseline; gap: var(--space-2xs); white-space: nowrap; }
.figure__value { font: var(--type-data-strong); font-variant-numeric: tabular-nums; letter-spacing: var(--track-tight); }
.figure__unit { font: var(--type-caption); color: var(--ink-tertiary); }
.figure__source { display: block; font: var(--type-caption); color: var(--ink-tertiary); }
/* A figure row has NO rank cell, so its meta and expanded panel must not keep
   the 3rem indent that used to sit under one — they would hang off nothing. */
.car-row--figure .car-row__meta,
.car-row--figure .car-row__expand { margin-left: 0; }
.car-row--unlisted .car-row__name { color: var(--ink-secondary); }

.proscons { display: flex; gap: var(--space-2xl); margin-top: var(--space-md); flex-wrap: wrap; }
.proscons__col { flex: 1; min-width: 200px; }
.proscons__title { font: var(--type-eyebrow); letter-spacing: var(--track-caps); text-transform: uppercase; color: var(--ink-tertiary); margin-bottom: var(--space-2xs); display: flex; align-items: center; gap: var(--space-2xs); }
.proscons__item { font: var(--type-caption); color: var(--ink-secondary); padding: var(--space-3xs) 0; }

/* --- 3.7c THE MARKET LIST — DATA TABLE, TOOLBAR, SORT GROUPS -------------
   v2 rework (§2.6) names .data-table, th[aria-sort], .row-link, .toolbar,
   .sort-groups/.sort-group/.sort-btn, .no-score, .source-line and
   .absence-legend as WS1 Phase 1 components; none of the eight workstreams'
   worktrees carries that build yet. THIS IS A THIN FALLBACK — cs.list.enhance
   and generate_site.generate_figures_page (WS4) need the classes they emit to
   render as SOMETHING sane now rather than as unstyled HTML, so this section
   holds the minimum rule set, on the same tokens as everywhere else, and is
   meant to be deleted the moment WS1's own components.css build lands with
   the real ones under these same names. */
.data-table { width: 100%; max-width: var(--measure-page); margin: var(--space-2xs) auto var(--space-lg); border-collapse: collapse; }
.data-table th, .data-table td { padding: var(--pad-cell-y) var(--space-sm); text-align: left; border-bottom: var(--edge-soft); }
.data-table thead tr { border-bottom: var(--edge-heavy); }
.data-table th.num, .data-table td.num { text-align: right; font-variant-numeric: var(--numeric-tabular); }
.data-table td.num { font-family: var(--font-mono); }
.data-table .sort-btn { background: none; border: 0; padding: 0; font: inherit; font-weight: 600; color: inherit; cursor: pointer; }
/* The sort arrow is drawn ONCE, by the design system's own
   `.data-table th[aria-sort="…"] button::after` (components.css): aria-sort
   lives on the <th> — the column header, the only role the attribute is
   valid on (v3 §10.15) — not on the button inside it. */
.row-link { color: var(--ink); font: var(--type-data-strong); text-decoration: none; }
.row-link:hover, .row-link:focus-visible { text-decoration: underline; }
.toolbar { display: flex; flex-wrap: wrap; align-items: center; column-gap: var(--space-md); row-gap: var(--space-2xs); max-width: var(--measure-page); margin: 0 auto; padding: var(--space-2xs) var(--gutter-page); }
.toolbar .filter-group { display: flex; flex-wrap: wrap; gap: var(--space-xs); }
.sort-groups { display: flex; flex-wrap: wrap; gap: var(--space-lg); margin-left: auto; }
.sort-group { display: flex; align-items: center; flex-wrap: wrap; gap: var(--space-2xs); }
.sort-group--held .sort-group__label { color: var(--held-ink); }
.sort-group__label { font: var(--type-eyebrow); letter-spacing: var(--track-caps); text-transform: uppercase; color: var(--ink-tertiary); margin-right: var(--space-2xs); }
.sort-btn {
  border: var(--border-hairline) solid var(--rule-soft); border-radius: var(--radius-chip);
  background: var(--surface-raised); color: var(--ink); padding: var(--space-3xs) var(--space-sm);
  font: var(--type-data); cursor: pointer;
}
th[aria-sort]:not([aria-sort="none"]) .sort-btn { border-color: var(--ink); font-weight: 600; }
.sort-btn--held { background: var(--held-surface); color: var(--held-ink); }
.no-score { max-width: var(--measure-page); margin: var(--space-3xs) auto var(--space-xs); padding: 0 var(--gutter-page); font: var(--type-caption); color: var(--ink-secondary); }
.source-line { max-width: var(--measure-page); margin: var(--space-sm) auto; font: var(--type-caption); color: var(--ink-tertiary); }
.absence-legend { display: flex; flex-wrap: wrap; gap: var(--space-md); max-width: var(--measure-page); margin: var(--space-lg) auto; font: var(--type-caption); color: var(--ink-tertiary); }
.absence-legend a { color: inherit; }
/* .gate-panel is cs.gate's contract (WS3), used here for the held-sort gate
   cs.list.enhance draws when a signed-out reader orders by a held figure. */
.gate-panel { max-width: var(--measure-text); margin: var(--space-xl) auto; padding: var(--space-lg); background: var(--surface-sunken); border-left: var(--border-marker) solid var(--rule-heavy); }
/* Phone: the toolbar's filter row and its two sort groups each scroll
   horizontally rather than wrapping onto five or six lines — the difference
   between a first data row a phone reaches at 450px and one buried past
   900px. */
@media (max-width: 640px) {
  .toolbar { align-items: stretch; }
  /* The three filters wrap onto two short rows rather than scrolling
     sideways: a strip clipped the body-type select at the viewport's edge
     ("All bod…" at 390) with nothing to say more was there. The search
     takes its own line; the two selects share the next and shrink to fit. */
  .toolbar .filter-group { flex-wrap: wrap; }
  .toolbar .filter-group > [data-filter="q"] { flex: 1 1 100%; min-width: 0; }
  .toolbar .filter-group > select { flex: 1 1 0; min-width: 0; }
  .sort-groups { flex-wrap: nowrap; overflow-x: auto; margin-left: 0; -webkit-overflow-scrolling: touch; }
  .sort-group { flex: none; flex-wrap: nowrap; }
}
#page-market { padding-bottom: var(--space-3xl); }
#page-market > h1 { font: var(--type-page-title); letter-spacing: var(--track-tight); max-width: var(--measure-page); margin: var(--space-xs) auto var(--space-3xs); padding: 0 var(--gutter-page); }
#page-market > .crumbs { max-width: var(--measure-page); margin: var(--space-sm) auto 0; padding: 0 var(--gutter-page); }

/* --- 3.7d THE v3 LIST — VIEW TOOLBAR, CLAIM, GROUPED HELD COLUMN, CELL
   CARD, DENSE, FIT (data-presentation-v3.md §2.3/§2.4, §3, §11 WS4 rows).
   Additive to 3.7c's fallback table rules above; kept compact (small
   paddings, one filter row, one view-toolbar row) because the WS4
   acceptance measures the first data row's top at both widths. */
.toolbar--view { column-gap: var(--space-sm); }
.btn-tb {
  border: var(--border-hairline) solid var(--rule-soft); border-radius: var(--radius-chip);
  background: var(--surface-raised); color: var(--ink); padding: var(--space-3xs) var(--space-sm);
  font: var(--type-data); cursor: pointer;
}
.btn-tb[aria-pressed="true"] { border-color: var(--ink); font-weight: 600; }
.toolbar__sep { color: var(--rule); }
.toolbar__plan { font: var(--type-caption); color: var(--ink-tertiary); }
/* The column chooser (§3 C-set, §11): a disclosure, not a modal — closes on
   its own summary re-click, no JS needed for open/close. */
details.cols { position: relative; }
details.cols > summary { list-style: none; }
details.cols > summary::-webkit-details-marker { display: none; }
.cols__panel {
  /* The popover layer (components.css's own `details.cols .panel` rule), not
     the sticky one: on --layer-sticky the table's sticky heads and first
     column, later in the DOM, painted over the open panel and took its clicks. */
  position: absolute; top: calc(100% + 4px); left: 0; z-index: var(--layer-popover);
  background: var(--surface-raised); border: var(--edge); border-radius: var(--radius-sheet);
  padding: var(--space-sm); min-width: 200px; box-shadow: var(--shadow-overlay);
}
.cols__group { font: var(--type-eyebrow); letter-spacing: var(--track-caps); text-transform: uppercase; color: var(--ink-tertiary); margin: var(--space-xs) 0 var(--space-2xs); }
.cols__group:first-child { margin-top: 0; }
.cols__panel label { display: flex; align-items: center; gap: var(--space-2xs); font: var(--type-data); padding: var(--space-3xs) 0; white-space: nowrap; }

/* The claim line (D3, §6): what the order IS, the disclosure link, and the
   "Back to A–Z" that appears the moment the order is not A–Z — either from a
   public sort's own band or from the held-sort gate (cs.list.enhance). */
.claim { display: flex; flex-wrap: wrap; justify-content: space-between; gap: var(--space-3xs) var(--space-md); max-width: var(--measure-page); margin: var(--space-3xs) auto var(--space-2xs); padding: 0 var(--gutter-page); font: var(--type-caption); color: var(--ink-secondary); }
.claim a { color: var(--accent); }

/* The grouped "with a free account" column (C8): one head, one flat bar per
   row — never a value, never a chip. th.grp holds the <select> itself. */
th.grp { text-align: center; font-weight: 500; color: var(--ink-secondary); background: var(--surface-raised); white-space: normal; }
th.grp select { max-width: 100%; }
td.grpcell { text-align: center; }
td.num.na { color: var(--ink-tertiary); }

/* The not-carrying band (§6): rows without the sorted figure, set apart by
   one row of caption text rather than interleaved blanks. */
tr.band td { background: var(--surface-page); color: var(--ink-tertiary); font: var(--type-caption); text-align: left; }

/* Dense (§2.3/§11): halves the row padding and drops the Car cell's type
   sub-line — the two things that make a dense table read as a spreadsheet. */
.data-table.dense th, .data-table.dense td { padding: var(--space-3xs) var(--space-sm); font-size: 0.85em; }
.data-table.dense td.car small { display: none; }
/* The optional "Type" column duplicates the Car cell's own sub-line, so the
   sub-line is dropped the moment the column is switched on (§11 chooser). */
.data-table.show-type-col td.car small { display: none; }

/* The cell card (C9): value · publisher, one tap away — this is where a list
   cell's provenance lives in v3, off all 433 rows (see _facts_row_html). */
td[data-card] { cursor: pointer; }
.card {
  position: absolute; z-index: var(--layer-sticky); background: var(--surface-raised);
  border: var(--edge); border-radius: var(--radius-control); box-shadow: var(--shadow-overlay);
  padding: var(--space-sm) var(--space-md); font: var(--type-body); color: var(--ink-secondary);
  max-width: min(320px, calc(100vw - 32px));
}
.card b { color: var(--ink); }
.card .pub { display: block; font: var(--type-caption); font-family: var(--font-mono); color: var(--ink-tertiary); margin-top: var(--space-3xs); }

/* The after-line (§2.3 point 6, §6): the market-wide price note, the unit
   rule, and the exact GET — printed, not merely documented. */
.after-line { display: flex; flex-wrap: wrap; gap: var(--space-3xs) var(--space-md); max-width: var(--measure-page); margin: var(--space-sm) auto; padding: 0 var(--gutter-page); font: var(--type-caption); color: var(--ink-tertiary); }
.after-line code { font-family: var(--font-mono); }

@media (max-width: 719px) {
  /* table-layout: fixed FORCES the table into the viewport (WS4 acceptance:
     no horizontal overflow at 390) — without it, a browser sizes every
     column to its longest UNWRAPPED cell first and only then applies
     white-space, so the table grows past the frame regardless of wrapping
     rules. Fixed layout shares the frame's own width among the columns, and
     wrapping (already set below) does the rest. */
  table.data-table.fit { table-layout: fixed; width: 100%; }
  table.data-table.fit th, table.data-table.fit td { padding: var(--space-2xs) var(--space-2xs); white-space: normal; overflow-wrap: break-word; }
  table.data-table.fit td.car { white-space: normal; }
  th.grp { min-width: 64px; }
}

/* --- 3.8 (WAS: PAYWALL) -------------------------------------------------- */
/* DELETED 2026-09-17, with the browsing paywall it painted: .paywall-card,
   .paywall-card h3/p, .paywall-card__alt, .sticky-cta, .sticky-cta a,
   .sticky-cta__close, and the .market-unavailable override that hid the two of
   them on an unlaunched market. Every car page and every figure table in every
   launched market is free and unlimited, so there is nothing left to lock, to
   truncate, or to pitch over. What is sold is the API and the bulk data, and
   that is sold on the pricing page — not on top of the reading surface.
   The sign-in, account and checkout plumbing is untouched. */

/* --- 3.9 DETAIL / MODEL PAGE --------------------------------------------- */
.detail { max-width: 760px; margin: 0 auto; padding: var(--space-2xl) var(--gutter-page); }
.detail__back { font: var(--type-caption); margin-bottom: var(--space-lg); display: inline-flex; align-items: center; gap: var(--space-2xs); }
.detail__name { font: var(--type-page-title); letter-spacing: var(--track-tight); margin-bottom: var(--space-2xs); }
.detail__year { font: var(--type-caption); color: var(--ink-tertiary); margin-bottom: var(--space-xl); display: flex; align-items: center; gap: var(--space-sm); flex-wrap: wrap; }
.detail__score-box {
  background: var(--surface-raised);
  border: var(--edge);
  border-radius: var(--radius-sheet);
  padding: var(--space-xl);
  margin-bottom: var(--space-2xl);
}
.detail__section { margin-bottom: var(--space-3xl); }
.detail__section h2 { font: var(--type-subhead); letter-spacing: var(--track-tight); margin-bottom: var(--space-md); }
.detail__section h3 { font: var(--type-data-strong); margin-bottom: var(--space-xs); }
.detail__section > p { font: var(--type-body); color: var(--ink-secondary); }

/* v2 rework (2026-09-22; ux-rework-plan-v2.md §3.9, WS3): the facts car
   page's own components — the identity line, the summary strip, the
   figures table and its held/absent rows, the gate panel, the fuel line
   and the Sources line. `.held`, `.held-row`, `.src`, `.src__pop`,
   `.crumbs` and `.absence` are WS1's (14b, Phase 0); everything below is
   this page's own, tokens only, alongside `.detail`/`.detail__section`
   above which it reuses rather than duplicates. */
.detail__identity { font: var(--type-caption); color: var(--ink-tertiary); margin-bottom: var(--space-lg); }
.summary { display: flex; flex-wrap: wrap; gap: var(--space-xl); margin-bottom: var(--space-2xl); }
.summary__item { font: var(--type-score-display); font-size: var(--font-size-title); }
table.figures { width: 100%; border-collapse: collapse; margin-bottom: var(--space-md); }
table.figures td { padding: var(--space-sm) 0; border-bottom: var(--edge-soft); vertical-align: baseline; }
table.figures tr:last-child td { border-bottom: 0; }
.fig__label { font: var(--type-data-strong); padding-right: var(--space-lg); white-space: nowrap; }
.fig__num { font-family: var(--font-mono); font-variant-numeric: var(--numeric-tabular); text-align: right; white-space: nowrap; }
.fig__unit { font-family: var(--font-text); font-weight: 400; color: var(--ink-secondary); }
/* At phone width a nowrap label beside a nowrap "39 mpg (US gallons) EPA"
   pushed the table past the viewport (scrollWidth 399 at 390); both may wrap. */
@media (max-width: 768px) { table.figures .fig__label, table.figures .fig__num { white-space: normal; } }
.absence-row { display: flex; flex-wrap: wrap; align-items: baseline; gap: var(--space-sm); padding: var(--space-sm) 0; border-bottom: var(--edge-soft); }
.absence-row:last-child { border-bottom: 0; }
.absence__why { font: var(--type-caption); color: var(--ink-tertiary); flex-basis: 100%; }
.gate-panel { background: var(--surface-raised); border: var(--edge); border-radius: var(--radius-sheet); padding: var(--space-xl); margin: var(--space-xl) 0; }
.gate-panel h2 { font: var(--type-subhead); margin-bottom: var(--space-sm); }
.gate-panel p { font: var(--type-body); color: var(--ink-secondary); margin-bottom: var(--space-md); }
.notice__caption { font: var(--type-caption); color: var(--ink-tertiary); margin-top: var(--space-sm); }
.fuel-line, .source-line { font: var(--type-data); color: var(--ink-secondary); margin: var(--space-md) 0; }
.fuel-line__caption { display: block; font: var(--type-caption); color: var(--ink-tertiary); margin-top: var(--space-2xs); }
.meter { font: var(--type-caption); color: var(--ink-tertiary); margin: var(--space-sm) 0; }
.detail__api-link { font: var(--type-data); margin-top: var(--space-xl); }

/* v3 (2026-09-22; scratchpad/data-presentation-v3.md §3, §8, §11; WS3): the
   car page as a statistical abstract — six fixed slots, one action, five
   sections named by the buyer's question, the "This record" rail, numbered
   notes. Sizes are the mockup's measured ones (§9, §10); colours and faces
   are tokens only. Every rule is scoped under `article.car` so it cannot
   restyle WS1's generic `.held` (the list's held chip, with its ::before
   bar and caption face), `.btn`, `.meter` (a chip) or `.lead`; where the
   spec's component set lands the same class names in components.css these
   page rules agree with them. `--held` is the spec's new token (WS1); until
   it lands the bar falls back to --held-bar.

   NO REFLOW ON REVEAL (§4). A tile always carries all five slots — label,
   value, two sub-lines, publisher — and each keeps its height whether
   empty, held or filled: the value slot's min-height fits the numeral, a
   sub-line's one line, the publisher slot's one tag, and the tile's own
   min-height fits all of them. The one line that wraps at every tile
   width is the crash tile's "frontal 4 · side 5 · rollover 5", so that
   tile alone is a line taller from the start; the row it sits in is that
   tall before the tap, and no row changes height after it. */
article.car { max-width: var(--measure-page); margin: 0 auto; padding: var(--space-lg) var(--gutter-page) var(--space-3xl); }
article.car .crumbs { margin: 0; }
article.car h1.car { font-family: var(--font-display); font-weight: 600; font-size: 28px; line-height: 1.15; letter-spacing: var(--track-tight); margin: var(--space-xs) 0 0; }
article.car h1.car .pt { font-weight: 500; color: var(--ink-secondary); }
article.car .ident { margin: var(--space-2xs) 0 0; font: 400 14.5px/1.45 var(--font-text); color: var(--ink-secondary); }
@media (min-width: 720px) { article.car h1.car { font-size: 40px; } article.car .ident { font-size: 15.5px; } }

/* C1 the summary strip: six slots, same order on every car */
article.car .six { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 1px; background: var(--rule); border: 1px solid var(--rule); border-top: 2px solid var(--rule-heavy); margin: var(--space-lg) 0 0; padding: 0; }
@media (min-width: 640px) { article.car .six { grid-template-columns: repeat(3, minmax(0, 1fr)); } }
@media (min-width: 1000px) { article.car .six { grid-template-columns: repeat(6, minmax(0, 1fr)); } }
article.car .tile { background: var(--surface-raised); padding: 10px 12px 8px; min-height: 148px; display: flex; flex-direction: column; min-width: 0; }
article.car .tile[data-slot="crash_rating"] { min-height: 164px; }
article.car .tile .lab { font: 400 12.5px/1.25 var(--font-text); color: var(--ink-tertiary); min-height: 16px; }
article.car .tile .val { font: 600 clamp(26px, 8.5vw, 34px)/1.05 var(--font-text); font-variant-numeric: var(--numeric-tabular); letter-spacing: -.015em; margin-top: 8px; color: var(--ink); min-height: 36px; display: flex; align-items: baseline; flex-wrap: wrap; gap: 0 5px; }
article.car .tile .val .num { white-space: nowrap; }
article.car .tile .val .unit { font: 500 14px/1 var(--font-text); color: var(--ink-secondary); letter-spacing: 0; white-space: nowrap; }
article.car .tile .val.na { color: var(--ink-tertiary); font-weight: 500; }
article.car .tile .sub { font: 400 12.5px/1.3 var(--font-text); color: var(--ink-secondary); margin-top: 5px; min-height: 1.3em; }
article.car .tile .pub-slot { margin-top: auto; padding-top: 8px; font: 500 11px/1 var(--font-mono); color: var(--ink-tertiary); letter-spacing: .02em; min-height: 19px; }
article.car .pub { font: 500 11px/1 var(--font-mono); color: var(--ink-tertiary); letter-spacing: .02em; text-decoration: none; }
article.car a.pub:hover, article.car a.pub:focus-visible { color: var(--accent); text-decoration: underline; }
@media (min-width: 1000px) { article.car .tile .val { font-size: 36px; } }
article.car sup.ref { font-size: .62em; line-height: 0; vertical-align: super; margin-left: 2px; font-weight: 500; letter-spacing: 0; }
article.car sup.ref a { color: var(--accent); text-decoration: none; }
article.car .tile .val sup.ref { font-size: 12px; font-weight: 500; margin-left: 1px; vertical-align: 6px; }

/* C2 the held register: a flat bar the width a value would take — no colour, no text, no digits */
article.car .held { display: inline-block; width: 64px; max-width: 70%; height: 12px; border-radius: 2px; background: var(--held, var(--held-bar)); vertical-align: middle; font: 0/0 var(--font-text); color: transparent; gap: 0; white-space: normal; }
article.car .held::before { content: none; }
article.car .tile .val .held { width: 72px; height: 14px; margin-top: 10px; }
article.car .row .v .held { width: 56px; }
article.car .fade { animation: cs-fade .3s ease-out; }
@keyframes cs-fade { from { opacity: 0; } to { opacity: 1; } }
@media (prefers-reduced-motion: reduce) { article.car .fade { animation: none; } }

/* C3, RETIRED AS AN ACTION (access-model-v2.md §1/§2/§6, 2026-09-22). There is
   no reveal control any more — cs.car.enhance removes [data-reveal] and
   blanks [data-reveal-panel] the moment it runs, before deciding anything
   from a click. What lands in .reveal now is read-only: nothing (anonymous,
   paid), the "{left} of 5 car pages left today" counter (free, either side
   of the quota), or the counter plus ONE dismissible upgrade panel (free,
   over quota). The rules below still apply to whatever the static page (not
   yet migrated off _v3_reveal_html) paints before JS runs, and to the
   .meter counter line cs.gate.setCounter draws in its place. */
article.car .reveal { display: flex; flex-direction: column; align-items: center; gap: 8px; padding: 16px 0 4px; text-align: center; }
article.car .reveal .lead { margin: 0; font: 400 14.5px/1.45 var(--font-text); color: var(--ink-secondary); max-width: 44ch; }
article.car .reveal .btn { min-height: var(--control-h-touch); padding: 14px 22px; font: 600 15px/1 var(--font-text); border-radius: 4px; }
article.car .reveal .meter { display: block; margin: 0; padding: 0; border: 0; border-radius: 0; background: transparent; font: 400 13px/1.45 var(--font-text); color: var(--ink-tertiary); }
article.car .reveal .meter a { color: var(--accent); }

/* LOCKED ROW (access-model-v2.md §1/§6). Not .held — .held stays the flat,
   textless bar (WS1's contract, tests/test_site_contracts.py) for a value
   that MIGHT still arrive un-narrated. A locked row has already been
   narrated: the reader knows why (no account, or an account over its daily
   quota) and where to act, so it reads as a short link, ink-tertiary like
   every other secondary line on this page — never a warning colour, because
   needing an account is not an error. cs.gate.paintLocks replaces each
   .held span with one of these; the row's own label, unit and publisher
   slots are untouched. */
article.car .lock { display: inline-flex; align-items: center; gap: 4px; font: 400 13px/1.3 var(--font-text); color: var(--ink-tertiary); text-decoration: none; white-space: nowrap; }
article.car .lock:hover, article.car .lock:focus-visible { color: var(--accent); text-decoration: underline; }
article.car .tile .val .lock { margin-top: 10px; font-size: 14px; }
/* critic-3 F1: the lock text ran straight into its unit and publisher
   ("…accountmpgEPA") — the textless bar it replaced needed no gap. A row
   keeps name, unit and publisher (access-model-v2.md §2) with the gaps a
   value has; a tile's one value line has no room for the unit beside the
   lock text, and its label already names the figure, so the unit waits
   for the number (the tile's height is the same either way). */
article.car .row .v .lock + .unit, article.car .row .v .lock + .pub { margin-left: 6px; }
article.car .row .v .unit + .pub { margin-left: var(--space-sm); }
article.car .tile .val .lock + .unit { display: none; }
/* C6 rail, critic-2 N2 / critic-3 F6: the page says in one line that it
   scores nothing, with the sources page's explanation one tap away. */
article.car .rail .noscore { margin: 10px 0 0; font: 400 13px/1.45 var(--font-text); color: var(--ink-tertiary); }
article.car .rail .noscore a { color: var(--accent); text-decoration: none; }
article.car .rail .noscore a:hover, article.car .rail .noscore a:focus-visible { text-decoration: underline; }
/* Legal pages (critic-3 F7): the cookie and licence tables were wider than
   a phone (428 px at 390). The table scrolls inside its own box; the page
   never scrolls sideways. */
.legal__table { display: block; max-width: 100%; overflow-x: auto; border-collapse: collapse; font-size: 0.9375rem; line-height: 1.45; }
.legal__table th, .legal__table td { padding: var(--space-xs) var(--space-sm); border-bottom: var(--edge); text-align: left; vertical-align: top; min-width: 8rem; }
.legal__table th { font-weight: 600; }

/* TEASER LINE (access-model-v2.md §6): the ★ line cs.car._paintTeaserLine
   inserts after the identity line on a Top-10 nameplate's page — informational,
   not a gate, so it reads as ink-secondary prose rather than a badge. */
article.car .teaser-line { margin: var(--space-2xs) 0 0; font: 400 13px/1.4 var(--font-text); color: var(--ink-secondary); }

/* ==========================================================================
   BRAND INDEX and MODEL LIST (access-model-v2 §3) — the browse pages that
   replaced the A–Z data table: /{cc}/{yyyy}/ lists every make with its model
   count; /{cc}/{yyyy}/{make}/ lists that make's models with body, powertrain
   and the public set's fuel economy. The same printed craft as the table they
   replace: one heavy rule at the top, hairlines between rows, tabular
   numerals — no card, no button, no chip. Markup (pipeline/generate_site.py
   generate_brand_index_page / generate_model_list_page), as selectors:
     main.brand-index  > .toolbar[data-list-toolbar] input[data-filter="q"]
                       > ul.brand-list[data-list] > li.brand-row[data-name]
                         > a > .brand-name + .brand-count        ("Toyota", "21 models")
     main.model-list   > ul.model-list-rows[data-model-list] > li.model-row
                         > a > .model-name (.teaser-mark ★ first) + .model-body + .model-pt + .model-fe
   ========================================================================== */
main.brand-index, main.model-list { max-width: var(--measure-page); margin: 0 auto; padding: var(--space-lg) var(--gutter-page) var(--space-3xl); }
main.brand-index h1, main.model-list h1 { font: var(--type-page-title); letter-spacing: var(--track-tight); margin: var(--space-sm) 0 var(--space-xs); }
main.brand-index .toolbar { padding-left: 0; padding-right: 0; margin: var(--space-md) 0 0; }
main.brand-index .toolbar input[type="search"] { flex: 1 1 100%; min-width: 0; max-width: 32ch; }
.brand-list, .model-list-rows { list-style: none; margin: var(--space-md) 0 0; padding: 0; border-top: 2px solid var(--rule-heavy); }
.brand-row, .model-row { margin: 0; padding: 0; border-bottom: 1px solid var(--rule-soft); }
.brand-row > a, .model-row > a { display: grid; align-items: baseline; column-gap: var(--space-md); row-gap: var(--space-3xs); padding: var(--space-sm) 0; color: var(--ink); text-decoration: none; }
.brand-row > a, .model-row > a { grid-template-columns: minmax(0, 1fr) auto; }
.brand-row > a:hover .brand-name, .brand-row > a:focus-visible .brand-name,
.model-row > a:hover .model-name, .model-row > a:focus-visible .model-name { color: var(--accent); text-decoration: underline; }
.brand-name, .model-name { font: 600 var(--font-size-body)/1.35 var(--font-display); }
.brand-count, .model-body, .model-pt, .model-fe { font: var(--type-caption); color: var(--ink-tertiary); font-variant-numeric: tabular-nums; }
.model-body, .model-pt { grid-column: 1; }
.model-fe { grid-column: 2; grid-row: 1; text-align: right; color: var(--ink-secondary); white-space: nowrap; }
@media (min-width: 640px) {
  /* a fixed last column: each row is its own grid, so an `auto` column
     would let "119 MPGe" push one row's columns out of line with the rest */
  .model-row > a { grid-template-columns: minmax(0, 2fr) minmax(0, 1fr) minmax(0, 1fr) 7.5em; }
  .model-body, .model-pt { grid-column: auto; }
  .model-fe { grid-column: auto; grid-row: auto; }
}
.model-row .teaser-mark { color: var(--ink-secondary); margin-right: var(--space-3xs); }

/* MOST POPULAR (access-model-v2.md §3/§6): the home page's teaser-nameplate
   strip, directly after the market-year tiles. Reuses the model-list row
   markup above wholesale (.model-list-rows > .model-row > .model-name /
   .model-body / .model-pt / .model-fe / .teaser-mark) — one hairline-and-
   tabular-numerals block for both surfaces — so only the wrapper and the
   heading below it are new. Markup (pipeline/generate_site.py
   most_popular_html):
     section.most-popular[data-most-popular] > h2.section-title
                                              + p.most-popular__sub
                                              + ul.model-list-rows > li.model-row (as above)
   Tokens only, so light and dark both fall out of the existing palette. */
.most-popular { margin-top: var(--space-xl); }
.most-popular__sub { font: var(--type-caption); color: var(--ink-secondary); margin: var(--space-3xs) 0 0; max-width: var(--measure-text); }


/* UPGRADE PANEL DISMISS (§6: "Not now" is also the ✕). Reuses .gate-panel's
   own tokens (14c) rather than a new surface; the ✕ is a second way to reach
   the same action the "Not now" button already offers, positioned so a thumb
   at 390px does not have to find the text button to leave the panel alone. */
article.car [data-upgrade-panel] { position: relative; margin-top: 8px; text-align: left; }
article.car .gate-panel__dismiss { position: absolute; top: var(--space-sm); right: var(--space-sm); width: 28px; height: 28px; border: 0; background: transparent; color: var(--ink-tertiary); font-size: 18px; line-height: 1; cursor: pointer; }
article.car .gate-panel__dismiss:hover, article.car .gate-panel__dismiss:focus-visible { color: var(--ink); }

/* below the strip: sections left, the record rail right at ≥ 1000 */
article.car .below { display: grid; grid-template-columns: minmax(0, 1fr); gap: 0 40px; }
article.car .rail { border-top: 2px solid var(--rule-heavy); margin-top: 22px; padding-top: 10px; font: 400 13.5px/1.5 var(--font-text); color: var(--ink-secondary); }
@media (min-width: 1000px) {
  article.car .below { grid-template-columns: minmax(0, 1fr) 300px; grid-template-areas: "a c" "b c" "d c"; }
  article.car .reveal { grid-area: a; }
  article.car .qs { grid-area: b; }
  article.car .notes { grid-area: d; }
  article.car .rail { grid-area: c; align-self: start; position: sticky; top: 16px; margin-top: 18px; }
}

/* C4 sections named by the buyer's question */
article.car .qs { margin-top: 18px; }
article.car details.q { border-top: 1px solid var(--rule); }
article.car details.q:last-of-type { border-bottom: 1px solid var(--rule); }
article.car details.q > summary { list-style: none; display: flex; justify-content: space-between; align-items: baseline; gap: 12px; padding: 13px 0; cursor: pointer; font: 600 17px/1.3 var(--font-display); }
article.car details.q > summary::-webkit-details-marker { display: none; }
article.car details.q > summary > span:first-child { white-space: nowrap; }
article.car details.q > summary .peek { font: 400 13px/1.3 var(--font-mono); color: var(--ink-tertiary); text-align: right; max-width: 58%; }
article.car details.q > summary .peek::after { content: "  +"; color: var(--ink-quiet); }
article.car details.q[open] > summary .peek { color: transparent; }
article.car details.q[open] > summary .peek .held { visibility: hidden; }
article.car details.q[open] > summary .peek::after { content: "  −"; color: var(--ink-quiet); }
article.car .rows { margin: 0 0 14px; }
/* C5 the figure row: label / value + publisher (/ basis at ≥ 1000) */
article.car .row { display: grid; grid-template-columns: minmax(0, 1fr) auto; gap: 2px 14px; padding: 8px 0; border-top: 1px solid var(--rule-soft); align-items: baseline; }
article.car .row:first-child { border-top: 0; padding-top: 2px; }
article.car .row .k { color: var(--ink-secondary); font: 400 14.5px/1.35 var(--font-text); }
article.car .row .v { text-align: right; font: 400 15.5px/1.35 var(--font-text); font-variant-numeric: var(--numeric-tabular); white-space: nowrap; }
article.car .row .v small { display: block; color: var(--ink-secondary); font-size: 12.5px; white-space: normal; }
article.car .row .v .pub { display: block; line-height: 1.5; }
article.car .row .v.na { color: var(--ink-tertiary); }
article.car .row .b { display: none; }
@media (min-width: 1000px) {
  article.car .row { grid-template-columns: minmax(150px, .75fr) auto minmax(0, 1.25fr); }
  article.car .row .v small { display: none; }
  article.car .row .v .pub { display: inline; margin-left: 8px; }
  article.car .row .b { display: block; font: 400 12.5px/1.4 var(--font-text); color: var(--ink-tertiary); }
}
article.car .stars { letter-spacing: .06em; font-size: 15px; }
article.car details.sub { margin: 0 0 12px; font-size: 14px; }
article.car details.sub summary { cursor: pointer; color: var(--accent); font-size: 13.5px; list-style: none; padding: 2px 0; }
article.car details.sub summary::-webkit-details-marker { display: none; }
article.car details.sub summary::before { content: "+ "; color: var(--ink-quiet); }
article.car details.sub[open] summary::before { content: "− "; }
article.car details.sub .subbody { margin: 0; padding: 0; }
article.car details.sub ul { margin: 6px 0 0; padding-left: 0; list-style: none; }
article.car details.sub li { padding: 7px 0; border-top: 1px solid var(--rule-soft); font-size: 13.5px; line-height: 1.4; color: var(--ink-secondary); }
article.car details.sub li b { font-weight: 600; color: var(--ink); }
article.car details.sub li .d { font: 400 12px var(--font-mono); color: var(--ink-tertiary); margin-right: 8px; }
article.car .comp { display: grid; grid-template-columns: minmax(0, 1fr) auto; gap: 2px 12px; font-size: 13.5px; color: var(--ink-secondary); margin: 6px 0 4px; }
article.car .comp span:nth-child(even) { text-align: right; }
article.car details.sub .more { margin: 6px 0 0; font-size: 13px; }

/* C6 This record */
article.car .rail h2 { font: 600 12px/1 var(--font-text); letter-spacing: .08em; text-transform: uppercase; color: var(--ink-tertiary); margin: 0 0 10px; }
article.car .rail dl { margin: 0; display: grid; grid-template-columns: auto minmax(0, 1fr); gap: 4px 12px; }
article.car .rail dt { color: var(--ink-tertiary); }
article.car .rail dd { margin: 0; color: var(--ink); overflow-wrap: anywhere; }
article.car .rail .links { margin: 10px 0 6px; display: flex; flex-wrap: wrap; gap: 4px 14px; }
article.car .rail code { font: 400 12px/1.5 var(--font-mono); background: var(--surface-sunken); padding: 2px 5px; border-radius: 3px; word-break: break-all; display: inline-block; margin-top: 6px; text-transform: none; }
article.car .rail p { margin: 8px 0 0; }
article.car .rail .lic { font-size: 12.5px; color: var(--ink-tertiary); }

/* C7 notes and sources */
article.car .notes { margin-top: 26px; border-top: 2px solid var(--rule-heavy); padding-top: 12px; }
article.car .notes h2 { font: 600 12px/1 var(--font-text); letter-spacing: .08em; text-transform: uppercase; color: var(--ink-tertiary); margin: 0 0 10px; }
article.car .notes ol { margin: 0; padding-left: 22px; font: 400 13.5px/1.5 var(--font-text); color: var(--ink-secondary); }
article.car .notes li { padding: 3px 4px; }
article.car .notes li:target { background: var(--accent-surface); border-radius: 3px; }
article.car .notes .back { color: var(--accent); text-decoration: none; margin-left: 6px; font-size: 12px; }
article.car .notes .source-line { max-width: none; margin: 10px 0 0; }

/* --- 3.10 LISTS ---------------------------------------------------------- */
.issue-item { padding: var(--space-md) 0; border-bottom: var(--edge-soft); display: flex; align-items: flex-start; gap: var(--space-sm); }
.issue-item:last-child { border-bottom: 0; }
.issue-item__body { flex: 1; }
.issue-item__title { font: var(--type-data-strong); }
.issue-item__meta { font: var(--type-caption); font-variant-numeric: var(--numeric-tabular); color: var(--ink-tertiary); margin-top: var(--space-3xs); }
.issue-item__summary { font: var(--type-caption); color: var(--ink-secondary); margin-top: var(--space-2xs); }

.recall-item { padding: var(--space-md) 0; border-bottom: var(--edge-soft); }
.recall-item:last-child { border-bottom: 0; }
.recall-item__header { font: var(--type-data-strong); display: flex; align-items: center; gap: var(--space-xs); flex-wrap: wrap; }
.recall-item__summary { font: var(--type-caption); color: var(--ink-secondary); margin-top: var(--space-2xs); }
.recall-item__remedy { font: var(--type-caption); color: var(--ink-secondary); margin-top: var(--space-2xs); }

.reasons-list { list-style: none; padding: 0; margin: 0; }
.reasons-list li { padding: var(--space-sm) 0; font: var(--type-data); display: flex; align-items: flex-start; gap: var(--space-sm); border-bottom: var(--edge-soft); }
.reasons-list li:last-child { border-bottom: 0; }
.reasons-list li .icon { color: var(--ink-tertiary); }

.checklist { list-style: none; padding: 0; margin: 0; }
.checklist li { padding: var(--space-sm) 0; font: var(--type-data); display: flex; align-items: flex-start; gap: var(--space-sm); border-bottom: var(--edge-soft); }
.checklist li:last-child { border-bottom: 0; }
.checklist li .icon { flex-shrink: 0; margin-top: 2px; color: var(--ink-tertiary); }
.checklist--urgent .icon { color: var(--ink); }

/* --- 3.11 PRICING -------------------------------------------------------- */
/* The account panel's status line when it is a sentence with a date
   ("Changes to Data on 2026-10-03", "Cancels on …"): the plan copy as
   written, not the eyebrow's caps. */
.notice__title--sentence { text-transform: none; letter-spacing: var(--track-tight); }
.pricing { max-width: var(--measure-page); margin: 0 auto; padding: var(--space-5xl) var(--gutter-page); }
.pricing h1 { font: var(--type-page-title); letter-spacing: var(--track-tight); margin-bottom: var(--space-xs); }
.pricing__subhead { font: var(--type-body-lead); color: var(--ink-secondary); margin-bottom: var(--space-4xl); }
.tier-grid {
  /* THREE CARDS SPANNING THE CONTAINER (v2 UX rework §5.3, WS6). This was
     `repeat(auto-fit, minmax(190px, 1fr))`, which does not mean "three equal
     columns" — with three items in a ~1100px container it creates as many
     190px+ tracks as fit (five or six), the three cards occupy the first
     three, and the rest of the row sits empty. `repeat(3, …)` is exactly
     three tracks, always, so the cards fill the row at every width down to
     the 768px breakpoint below (3.18, WS2), which drops to one column. */
  display: grid; grid-template-columns: repeat(3, minmax(0, 1fr)); gap: var(--space-lg); margin-bottom: var(--space-3xl);
}
.tier-card {
  border: var(--edge); border-radius: var(--radius-sheet);
  padding: var(--pad-sheet); background: var(--surface-raised);
  display: flex; flex-direction: column;
}
.tier-card--popular { border-color: var(--rule-heavy); border-width: var(--border-heavy); padding: calc(var(--pad-sheet) - 1px); }
.tier-card--global  { border-color: var(--rule-heavy); background: var(--surface-page); }
/* THE ACCOUNT'S OWN PLAN (D10, WS6). Ink, not accent: like .plan__badge,
   marking the current plan is information, not an action to take. */
.tier-card.plan--current { border-color: var(--ink); border-width: var(--border-heavy); padding: calc(var(--pad-sheet) - 1px); }
.tier-name { font: var(--type-eyebrow); letter-spacing: var(--track-caps); text-transform: uppercase; color: var(--ink-tertiary); margin-bottom: var(--space-sm); }
.tier-price { font: var(--type-score-inline); font-size: var(--font-size-title-lg); font-variant-numeric: var(--numeric-tabular); letter-spacing: var(--track-display); margin-bottom: var(--space-2xs); }
.tier-price .period { font: var(--type-caption); color: var(--ink-tertiary); }
.tier-desc { font: var(--type-caption); color: var(--ink-tertiary); margin-bottom: var(--space-lg); }
.tier-features { list-style: none; padding: 0; margin: 0; font: var(--type-data); color: var(--ink-secondary); }
.tier-features li { padding: var(--space-3xs) 0; }
.tier-btn { margin-top: auto; width: 100%; }
/* Why a plan cannot be bought yet, or which markets it reaches. Secondary ink,
   not tertiary: it is the only explanation next to a disabled button, and it
   has to be read. */
.tier-avail { font: var(--type-caption); color: var(--ink-secondary); margin: var(--space-md) 0 var(--space-sm); }

/* --- 3.12 COMPARE -------------------------------------------------------- */
.compare { max-width: var(--measure-page); margin: 0 auto; padding: var(--space-2xl) var(--gutter-page); }
.compare__grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(260px, 1fr)); gap: var(--space-lg); margin-top: var(--space-lg); }

/* --- 3.13 GUIDE ---------------------------------------------------------- */
.guide { max-width: 760px; margin: 0 auto; padding: var(--space-2xl) var(--gutter-page); }
.guide__verdict {
  background: var(--surface-raised);
  border: var(--edge);
  border-left: var(--border-marker) solid var(--rule-heavy);
  border-radius: var(--radius-sheet);
  padding: var(--space-xl); margin-bottom: var(--space-3xl);
}
.guide__verdict h2 { font: var(--type-subhead); letter-spacing: var(--track-tight); margin-bottom: var(--space-sm); display: flex; align-items: center; gap: var(--space-sm); flex-wrap: wrap; }
.guide__verdict p { font: var(--type-body); color: var(--ink-secondary); }
.guide__section { margin-bottom: var(--space-3xl); }
.guide__section h2 { font: var(--type-subhead); letter-spacing: var(--track-tight); margin-bottom: var(--space-md); }

/* --- 3.14 FOOTER --------------------------------------------------------- */
/* The .footer class this replaced painted the one-off <footer> that used to
   live only inside #page-home (deleted, v2 rework §5.0); .site-footer paints
   the one footer every page now shares (extracted verbatim by
   generate_site.extract_shell()). */
.site-footer {
  max-width: var(--measure-page); margin: 0 auto;
  padding: var(--space-2xl) var(--gutter-page);
  border-top: var(--edge-heavy);
  display: flex; flex-wrap: wrap; gap: var(--space-2xl);
  font: var(--type-caption);
}
.site-footer__col { display: flex; flex-direction: column; gap: var(--space-3xs); min-width: 8rem; }
.site-footer__heading {
  font: var(--type-eyebrow); letter-spacing: var(--track-caps); text-transform: uppercase;
  color: var(--ink-tertiary); margin: 0;
}
.site-footer a { color: var(--ink-secondary); text-decoration: none; }
.site-footer a:hover, .site-footer a:focus-visible { color: var(--accent); text-decoration: underline; }
/* The obligation the licence register attaches to every Canadian row (v2
   rework §3.8, §7.5): verbatim, on its own line, wide enough to read as a
   sentence rather than a fifth column. */
.site-footer__attribution {
  flex-basis: 100%; margin-top: var(--space-md);
  padding-top: var(--space-md); border-top: var(--edge-soft);
  color: var(--ink-tertiary);
}

/* --- 3.15 MARKET PICKER --------------------------------------------------
   A finite plan cannot be sold without naming its markets. */
.mkt-overlay {
  position: fixed; inset: 0;
  background: color-mix(in srgb, var(--ink) 55%, transparent);
  display: flex; align-items: center; justify-content: center;
  padding: var(--space-xl); z-index: var(--layer-popover);
}
.mkt-panel {
  background: var(--surface-overlay); color: var(--ink);
  border: var(--edge); border-radius: var(--radius-sheet);
  box-shadow: var(--shadow-overlay);
  width: min(680px, 100%); max-height: 86vh;
  display: flex; flex-direction: column; overflow: hidden;
}
.mkt-head { padding: var(--space-xl) var(--space-xl) var(--space-md); border-bottom: var(--edge); }
.mkt-head h3 { font: var(--type-subhead); letter-spacing: var(--track-tight); margin: 0 0 var(--space-2xs); }
.mkt-head p { margin: 0; font: var(--type-caption); color: var(--ink-secondary); }
.mkt-grid {
  display: grid; grid-template-columns: repeat(auto-fill, minmax(200px, 1fr));
  gap: var(--space-3xs); padding: var(--space-lg) var(--space-xl); overflow-y: auto;
}
.mkt-opt {
  display: flex; align-items: center; gap: var(--space-sm);
  padding: var(--space-xs) var(--space-sm);
  border: var(--border-hairline) solid transparent;
  border-radius: var(--radius-chip);
  cursor: pointer; font: var(--type-data);
}
.mkt-opt:hover { background: var(--surface-hover); }
.mkt-opt input { flex: none; margin: 0; }
.mkt-opt[data-full="1"] { color: var(--ink-tertiary); cursor: not-allowed; }
.mkt-opt .mkt-n { margin-left: auto; font: var(--type-caption); font-variant-numeric: var(--numeric-tabular); color: var(--ink-tertiary); }
.mkt-foot { display: flex; align-items: center; gap: var(--space-md); padding: var(--space-md) var(--space-xl); border-top: var(--edge); background: var(--surface-page); }
.mkt-count { font: var(--type-data); font-variant-numeric: var(--numeric-tabular); color: var(--ink-secondary); }
.mkt-foot .mkt-actions { margin-left: auto; display: flex; gap: var(--space-sm); }

/* --- 3.16 SEARCH DROPDOWN ------------------------------------------------ */
#search-results {
  position: fixed; top: 60px; right: var(--gutter-page);
  width: 340px; max-height: 400px; overflow-y: auto;
  background: var(--surface-overlay);
  border: var(--edge); border-radius: var(--radius-sheet);
  box-shadow: var(--shadow-overlay);
  z-index: var(--layer-popover);
  padding: var(--space-2xs);
}
#search-results a { display: flex; align-items: center; gap: var(--space-sm); padding: var(--space-sm) var(--space-md); border-radius: var(--radius-chip); text-decoration: none; color: var(--ink); }
#search-results a:hover { background: var(--surface-hover); }

/* --- 3.17 STATE PANELS --------------------------------------------------- */
.state-msg { padding: var(--space-4xl) var(--gutter-page); text-align: center; }
.state-msg__title { font: var(--type-subhead); letter-spacing: var(--track-tight); color: var(--ink); margin-bottom: var(--space-sm); }
.state-msg p { font: var(--type-body); color: var(--ink-secondary); max-width: var(--measure-note); margin: 0 auto var(--space-lg); }

/* --- 3.18 NARROW VIEWPORTS ----------------------------------------------- */
@media (max-width: 768px) {
  /* THE PHONE ROW (v2 rework §3.5): "logo · search icon · menu · Sign in;
     56px; no ticker" — one row, not the wrapped multi-row header this used to
     be (flex-wrap: wrap here is exactly what made the old header taller than
     56px at 390 and fail the "one row" acceptance check). Browse, the market
     selects, the search field, Pricing, Developers and the theme control all
     move into .header__controls, which .header__menu-check's native
     checked/unchecked state shows or hides — no JS involved, so it works
     identically with JS disabled. */
  .header { flex-wrap: nowrap; padding: var(--space-xs) var(--gutter-page); position: relative; }
  .header__menu-toggle { display: inline-flex; }
  .header__controls { display: none; }
  #header-menu-check:checked ~ .header__controls {
    display: flex; flex-direction: column; align-items: stretch;
    gap: var(--space-sm);
    position: absolute; top: 100%; left: 0; right: 0;
    background: var(--surface-overlay);
    border-bottom: var(--edge);
    box-shadow: var(--shadow-overlay);
    padding: var(--space-md) var(--gutter-page);
    z-index: var(--layer-popover);
  }
  #header-controls .header__search, #header-controls .header__search-input { width: 100%; }
  /* 16px, not --font-size-small: this selector outranks components.css §14's
     phone floor, and a 12px select is exactly what makes iOS zoom the header. */
  .header select { font-size: max(16px, 1em); }
  .filter-bar { position: static; gap: var(--space-xs); }
  .car-row__meta, .car-row__expand { margin-left: 0; }
  .hero h1 { font-size: var(--font-size-title-lg); }
  .detail, .guide, .ranking-list, .pricing, .compare { padding-left: var(--gutter-page); padding-right: var(--gutter-page); }
  .compare__grid, .tier-grid { grid-template-columns: 1fr; }
  #search-results { width: auto; right: var(--gutter-page); left: var(--gutter-page); }
  .factor { grid-template-columns: minmax(7rem, 1fr) 3.5rem; }
  .factor .factor__track { grid-column: 1 / -1; }
  .factor--absent { grid-template-columns: 1fr; }
}
/* `hidden` MUST WIN OVER ANY display RULE.
   Shipped 2026-09-19 and broke the whole site for the time it was live:
   `#turnstile-host { display: grid }` is an ID selector, so it beat the UA
   stylesheet's `[hidden] { display: none }`, and a fixed full-viewport scrim at
   z-index 9000 sat over every page swallowing every click. The element
   reported `hidden === true` the whole time — the attribute was set, it just
   did not mean anything.
   One global rule, because the next component to toggle `el.hidden` will have
   the same accident otherwise. */
/* THE PAGE GROWS WITH THE WINDOW; THE PROSE DOES NOT.
   `--measure-page` was a flat 1160px, so a 2560px monitor showed the content
   on 45% of the screen and gave the rest to margin — a table of figures with
   room to breathe squeezed into half a window.
   Only the PAGE measure moves. Running prose keeps `--measure-text` (68ch) and
   captions keep `--measure-note`, because a line of text 2,000 pixels wide is
   unreadable however much room there is for it. What gains the space is what
   benefits from it: the ranking list, the figure tables, the pricing grid. */
@media (min-width: 1500px) { :root { --measure-page: 1400px; } }
@media (min-width: 1900px) { :root { --measure-page: 1680px; } }
@media (min-width: 2400px) { :root { --measure-page: 2000px; } }

[hidden] { display: none !important; }

/* THE CHALLENGE NOTICE (v2 §2.7, §3.4 — the full-viewport modal is retired).
   `#turnstile-host { position: fixed; inset: 0; z-index: 9000 }` covered the
   WHOLE PAGE the instant any request was challenged — elementFromPoint at
   the header's own Sign-in control returned this element, not Sign-in, for
   as long as the dialog was up. Fixed positioning is not the fix for "the
   challenge can interrupt any view": inline is. `cs.challenge.noticeHTML()`
   (WS5) returns a `.notice--challenge` the CALLER inserts wherever their own
   request failed — a search dropdown, a held-sort gate panel — and
   `#turnstile-host` is nothing but the reserved mount INSIDE that markup, so
   it has no size or position of its own to fix anything WITH any more.
   `.notice--challenge`'s look is a placeholder here (WS5's region is this
   page-CSS section, not the shared tokens/components block); WS1 owns the
   real `.notice` variant set per the component inventory and may consolidate
   it there. */
.notice--challenge {
  max-width: none;
  color: var(--ink);
}
.notice--challenge .notice__title {
  color: var(--ink);
  text-transform: none;
  font: var(--type-subhead);
  letter-spacing: var(--track-tight);
}
.notice--challenge .notice__actions {
  display: flex; flex-wrap: wrap; align-items: center;
  gap: var(--space-sm) var(--space-md);
  margin-top: var(--space-md);
}
#turnstile-host { display: none; }
/* Reserved at Turnstile's own compact widget height (65px) the MOMENT "Do
   the check" is pressed — before the iframe itself has loaded — so the
   notice grows once, to its final size, rather than again a moment later
   when the widget's content arrives (the acceptance's "no layout shift"). */
#turnstile-host:not([hidden]) {
  display: flex; align-items: center; justify-content: center;
  min-height: 65px;
  margin-top: var(--space-md);
}
  

/* ── The figures table: label, value, basis ─────────────────────────────── */
.detail__measured { margin: var(--space-lg) 0 var(--space-xl); }
.figure-row {
  display: grid;
  grid-template-columns: minmax(9rem, 14rem) minmax(6rem, auto) 1fr;
  gap: var(--space-md);
  align-items: baseline;
  padding: var(--pad-cell-y) 0;
  border-bottom: var(--edge-soft);
}
.figure-row:last-child { border-bottom: 0; }
.figure-row__label { font-weight: 500; }
.figure-row__value {
  font-family: var(--font-mono);
  font-variant-numeric: var(--numeric-tabular);
  white-space: nowrap;
}
/* The basis is the smaller half of the claim, never the headline — it has to
   be readable beside the figure without competing with it. */
.figure-row__basis {
  font: var(--type-data);
  color: var(--absence-ink);
}
/* A figure this car does not carry. The row stays: an absent figure with its
   reason is part of the product, not a gap in it. */
.figure-row--absent .figure-row__value { color: var(--absence-ink); font-family: inherit; }

/* A figure held back until the reader has an account. Deliberately quiet —
   it states what exists without shouting, and carries no value to blur. */
.figure-row--gated { background: var(--surface-sunken); }
.figure-row__value--locked {
  font-family: inherit;
  font-size: var(--font-size-small);
  color: var(--absence-ink);
  white-space: normal;
}
.figure-gate {
  margin: var(--space-md) 0 0;
  padding: var(--space-md);
  background: var(--surface-sunken);
  border-left: 3px solid var(--rule-soft);
  font-size: var(--font-size-small);
  color: var(--absence-ink);
}

/* ── Running costs, sources, confidence ─────────────────────────────────── */
.tco-table { width: 100%; border-collapse: collapse; margin: var(--space-md) 0; }
.tco-table th, .tco-table td {
  text-align: left;
  padding: var(--space-sm) var(--space-md) var(--space-sm) 0;
  border-bottom: var(--edge-soft);
}
.tco-table th { font-size: var(--font-size-small); font-weight: 600; }
.tco-val {
  font-family: var(--font-mono);
  font-variant-numeric: var(--numeric-tabular);
  text-align: right;
  white-space: nowrap;
}
.tco-total td { font-weight: 600; border-bottom: 0; }
.source-list { margin: var(--space-sm) 0 0; padding-left: var(--space-lg); font-size: var(--font-size-small); }
.source-list li { margin-bottom: var(--space-2xs); }
.detail__confidence { font-size: var(--font-size-small); color: var(--absence-ink); }

/* ── Header ─────────────────────────────────────────────────────────────── */
.header__logo { display: inline-flex; align-items: center; gap: var(--space-xs);
                text-decoration: none; color: var(--ink); font-weight: 600; }
.logo-mark {
  display: inline-grid; place-items: center;
  width: 1.6em; height: 1.6em;
  background: var(--ink); color: var(--surface-page);
  font-size: var(--font-size-small); font-weight: 700; letter-spacing: .02em;
}

/* ── The market landing page: one row per car ───────────────────────────── */
/* THE TWELVE PAGES THE SITE'S OWN LINKS POINT AT. The first version of this
   stylesheet covered car pages only, because the test that checked coverage
   rendered only car pages — so all twelve market landing pages shipped with
   these classes undefined and their powertrain pills as bare text (critic pass
   10, E1). */
.car-row__link { color: inherit; text-decoration: none; display: block; }
.car-row__link:hover .car-row__name { text-decoration: underline; }
.car-row__figures {
  font-family: var(--font-mono);
  font-variant-numeric: var(--numeric-tabular);
  color: var(--absence-ink);
  font-size: var(--font-size-small);
}
.car-row__factor { font: var(--type-data); color: var(--absence-ink); margin-top: var(--space-2xs); }

/* The powertrain, as a word beside the name. Colour is never the only carrier:
   the pill always says what it is. */
.pill {
  display: inline-block;
  padding: 1px var(--space-xs);
  border: var(--border-hairline) solid var(--rule-soft);
  border-radius: 999px;
  font-size: var(--font-size-fine);
  letter-spacing: .02em;
  vertical-align: baseline;
  background: var(--surface-sunken);
  color: var(--absence-ink);
}
/* v2 rework, WS1 (2026-09-22): powertrain is a category, not a quality — the
   same rule components.css §11 (BADGE) already states for this exact case
   ("must not borrow the accent, which means one thing: you can act here").
   Five hand-picked hues here made the opposite claim by accident: EV read as
   good (green), diesel as a warning (amber-brown). One neutral pill for
   every powertrain now — the word is the only thing that says which one. */
.pill--ev, .pill--phev, .pill--hybrid, .pill--hydrogen, .pill--diesel, .pill--gas {
  background: var(--surface-sunken);
  color: var(--absence-ink);
}

/* What a list claims, and what it covers, under the list itself. */
.scale-list { margin: var(--space-sm) 0 var(--space-lg); padding-left: var(--space-lg);
              font-size: var(--font-size-small); }
.scale-list li { margin-bottom: var(--space-2xs); }
/* Only 399 pages carry one — all of them us/2024 — which is why sampling three
   OTHER market-years could not see that it had no rule. */
.coverage-note { font: var(--type-data); color: var(--absence-ink); margin: var(--space-sm) 0; }

/* A phone shows the figure above its basis rather than three cramped columns. */
@media (max-width: 640px) {
  .figure-row { grid-template-columns: 1fr auto; gap: var(--space-xs) var(--space-sm); }
  .figure-row__basis { grid-column: 1 / -1; }
}
/* ── Confidence dots and the notes beside a recall or complaint list ─────── */
/* The dot is a confidence signal beside a score, and a red one also marks a
   locked row. It must read at a glance without colour alone carrying the
   meaning — the word is always beside it. */
.dot {
  display: inline-block;
  width: .55em; height: .55em;
  border-radius: 50%;
  background: var(--rule-soft);
  margin-right: var(--space-2xs);
  vertical-align: baseline;
}
/* v2 rework, WS1 (2026-09-22): a confidence signal is data, and principle 9
   ("colour means you can act; data carries no colour") applies to it exactly
   as it does to the pill above. Distinguished by ink strength and, for the
   lowest tier, an outline rather than a fill — a shape difference, so it
   still reads apart from the others in greyscale (check 6), the same
   guarantee section 7 (FACT) already gives the verified-zero marker. */
.dot--green  { background: var(--ink); }
.dot--yellow { background: var(--ink-secondary); }
.dot--red    { background: var(--surface-raised); border: var(--border-hairline) solid var(--ink-secondary); }

/* The sentence that says what a list of components or campaigns covers, and
   at what scope. Quieter than the list it explains. */
.issue-note {
  margin: var(--space-sm) 0 0;
  font-size: var(--font-size-small);
  color: var(--absence-ink);
}
