/* ============================================================================
   dccannabisdata — shared design system
   ----------------------------------------------------------------------------
   EXTRACTED VERBATIM from web/index.html's first <style> block on 2026-08-15,
   which is the reference implementation of DESIGN.md. It is now one shared file
   instead of four hand-maintained copies: index.html, maryland.html,
   compare.html and dispensaries.html each carried their own near-copy, and they
   had already drifted (maryland.html's copy is 90 lines shorter).

   Everything below the "GENERATOR ADDITIONS" rule is new and belongs to the
   template system. Nothing above it may be edited here without editing
   DESIGN.md first — the tokens ARE the spec.
   ========================================================================== */
/* Light values. --faint / --link are tuned to clear 4.5:1 against the WORST
     surface they land on (--paper, the darkest light surface), not against white. */
  :root{
    color-scheme:light dark;
    --paper:#f4f6f2; --card:#ffffff; --card-2:#fbfcfa;
    --ink:#17211b; --ink-soft:#33413a; --muted:#5c6b62; --faint:#67726a;
    --line:#dde4dd; --line-strong:#cbd4cc;
    --accent:#2f6248; --accent-bright:#3f8a60; --accent-soft:#e6f0e9;
    --link:#2f6248; --accent-ink:#ffffff;
    /* Part-to-whole segments: solid, >=15 L* apart, each with its own ink so the
       in-bar label always clears 4.5:1. Never opacity — that blends to the page. */
    --seg-1:#1c4531; --seg-1-ink:#ffffff;
    --seg-2:#357453; --seg-2-ink:#ffffff;
    --seg-3:#8fc9a9; --seg-3-ink:#12261c;
    --red:#b3282d; --red-soft:#f5e4e4;
    /* Diverging (signed change). Blue<->orange, never red/green; both clear 4.5:1 on --paper. */
    --up:#0072b2; --down:#936600;
    /* Categorical series: six of them, because six is the chart cap the
       compare page is designed around.
       THE RULE IS DESIGN.md's AND THIS COMMENT DOES NOT GET TO REWRITE IT.
       DESIGN.md, Categorical series (amended 2026-08-23, Chris ruled): "Every
       co-drawn pair of series colors must differ by >=10 L* AND sit >=40
       degrees apart in hue, in both themes, with the 3:1 non-text floor
       intact." The old "adjacent >=15 L*" wording was proved arithmetically
       unsatisfiable at six co-drawn series (six pairwise-15 colors need 75 L*
       of span; 3:1 against these surfaces leaves ~56); the arithmetic is
       recorded in DESIGN.md beside the amendment.
       MEASURED, this theme (build.py:palette_warnings recomputes it every run):
         L*        0.9  45.2  56.2  34.0  11.9  22.9   (--series-1 .. -6)
         hue deg   206    52   150   316   100     5
         min pairwise dL* 10.9 (>=10) . min pairwise hue gap 48deg (>=40)
         vs --track / --card / --paper                 -> min 3.03:1, floor met
       Hues are fixed per series and shared with the dark block (dark lifts
       lightness, preserves hue). --series-1 sits near-black BY ARITHMETIC:
       six 10-L* steps under the L* 56.5 track ceiling leave the bottom rung
       at ~1. Identity is carried by hue plus three non-colour channels that
       always render: the legend swatch and name, the endpoint value label,
       and the visually-hidden data table. */
    --track:#e9ece9; --series-1:#000407; --series-2:#7a6b03; --series-3:#089b51;
    --series-4:#82316c; --series-5:#102406; --series-6:#6d140c;
  }
  @media (prefers-color-scheme: dark){
    :root{
      --paper:#0e1310; --card:#161d18; --card-2:#1a221c;
      --ink:#e8eee9; --ink-soft:#c3cdc6; --muted:#93a399; --faint:#7a8d7f;
      --line:#26302a; --line-strong:#334037;
      --accent:#4e9e73; --accent-bright:#6fc294; --accent-soft:#182620;
      --link:#6fc294; --accent-ink:#0e1310;
      --seg-1:#2f6248; --seg-1-ink:#ffffff;
      --seg-2:#4e9e73; --seg-2-ink:#0e1310;
      --seg-3:#9ad3b4; --seg-3-ink:#0e1310;
      --red:#e07276; --red-soft:#2a1a1b;
      --up:#56b4e9; --down:#e69f00;
      /* Categorical series, dark. Same rule as the light block (DESIGN.md,
         amended 2026-08-23, Chris ruled: every co-drawn pair >=10 L* AND
         >=40deg of hue apart, 3:1 floor intact), same hues per series,
         lifted lightness - written out per theme because the measurement
         differs.
         MEASURED, this theme (build.py:palette_warnings recomputes it):
           L*        68.6  99.2  89.0  78.8  58.3  48.2   (--series-1 .. -6)
           hue deg    205    53   149   317   101     5
           min pairwise dL* 10.1 (>=10) . min pairwise hue gap 47deg (>=40)
           vs --track / --card / --paper                -> min 3.05:1, floor met
         The dark window is L* [47.8, 100] once every stroke must clear 3:1
         against --track, so six 10-L* steps use nearly all of it: --series-6
         sits just above the floor and --series-2 is near-white BY ARITHMETIC,
         the mirror of the light block's near-black --series-1. */
      --track:#222b25; --series-1:#6daedd; --series-2:#fefdf5; --series-3:#b8ebd1;
      --series-4:#e3b6d6; --series-5:#649a4a; --series-6:#b6554c;
    }
  }
  :root[data-theme="light"]{
    color-scheme:light;
    --paper:#f4f6f2; --card:#ffffff; --card-2:#fbfcfa;
    --ink:#17211b; --ink-soft:#33413a; --muted:#5c6b62; --faint:#67726a;
    --line:#dde4dd; --line-strong:#cbd4cc;
    --accent:#2f6248; --accent-bright:#3f8a60; --accent-soft:#e6f0e9;
    --link:#2f6248; --accent-ink:#ffffff;
    --seg-1:#1c4531; --seg-1-ink:#ffffff;
    --seg-2:#357453; --seg-2-ink:#ffffff;
    --seg-3:#8fc9a9; --seg-3-ink:#12261c;
    --red:#b3282d; --red-soft:#f5e4e4;
    /* Diverging (signed change). Blue<->orange, never red/green; both clear 4.5:1 on --paper. */
    --up:#0072b2; --down:#936600;
    /* Categorical series: six of them, because six is the chart cap the
       compare page is designed around.
       THE RULE IS DESIGN.md's AND THIS COMMENT DOES NOT GET TO REWRITE IT.
       DESIGN.md, Categorical series (amended 2026-08-23, Chris ruled): "Every
       co-drawn pair of series colors must differ by >=10 L* AND sit >=40
       degrees apart in hue, in both themes, with the 3:1 non-text floor
       intact." The old "adjacent >=15 L*" wording was proved arithmetically
       unsatisfiable at six co-drawn series (six pairwise-15 colors need 75 L*
       of span; 3:1 against these surfaces leaves ~56); the arithmetic is
       recorded in DESIGN.md beside the amendment.
       MEASURED, this theme (build.py:palette_warnings recomputes it every run):
         L*        0.9  45.2  56.2  34.0  11.9  22.9   (--series-1 .. -6)
         hue deg   206    52   150   316   100     5
         min pairwise dL* 10.9 (>=10) . min pairwise hue gap 48deg (>=40)
         vs --track / --card / --paper                 -> min 3.03:1, floor met
       Hues are fixed per series and shared with the dark block (dark lifts
       lightness, preserves hue). --series-1 sits near-black BY ARITHMETIC:
       six 10-L* steps under the L* 56.5 track ceiling leave the bottom rung
       at ~1. Identity is carried by hue plus three non-colour channels that
       always render: the legend swatch and name, the endpoint value label,
       and the visually-hidden data table. */
    --track:#e9ece9; --series-1:#000407; --series-2:#7a6b03; --series-3:#089b51;
    --series-4:#82316c; --series-5:#102406; --series-6:#6d140c;
  }
  :root[data-theme="dark"]{
    color-scheme:dark;
    --paper:#0e1310; --card:#161d18; --card-2:#1a221c;
    --ink:#e8eee9; --ink-soft:#c3cdc6; --muted:#93a399; --faint:#7a8d7f;
    --line:#26302a; --line-strong:#334037;
    --accent:#4e9e73; --accent-bright:#6fc294; --accent-soft:#182620;
    --link:#6fc294; --accent-ink:#0e1310;
    --seg-1:#2f6248; --seg-1-ink:#ffffff;
    --seg-2:#4e9e73; --seg-2-ink:#0e1310;
    --seg-3:#9ad3b4; --seg-3-ink:#0e1310;
    --red:#e07276; --red-soft:#2a1a1b;
    --up:#56b4e9; --down:#e69f00;
    /* Categorical series, dark. Same rule as the light block (DESIGN.md,
       amended 2026-08-23, Chris ruled: every co-drawn pair >=10 L* AND
       >=40deg of hue apart, 3:1 floor intact), same hues per series,
       lifted lightness - written out per theme because the measurement
       differs.
       MEASURED, this theme (build.py:palette_warnings recomputes it):
         L*        68.6  99.2  89.0  78.8  58.3  48.2   (--series-1 .. -6)
         hue deg    205    53   149   317   101     5
         min pairwise dL* 10.1 (>=10) . min pairwise hue gap 47deg (>=40)
         vs --track / --card / --paper                -> min 3.05:1, floor met
       The dark window is L* [47.8, 100] once every stroke must clear 3:1
       against --track, so six 10-L* steps use nearly all of it: --series-6
       sits just above the floor and --series-2 is near-white BY ARITHMETIC,
       the mirror of the light block's near-black --series-1. */
    --track:#222b25; --series-1:#6daedd; --series-2:#fefdf5; --series-3:#b8ebd1;
    --series-4:#e3b6d6; --series-5:#649a4a; --series-6:#b6554c;
  }

  /* Radius scale (theme-independent) — DESIGN.md Radius & Elevation */
  :root{--r-sm:6px;--r-md:10px;--r-lg:14px;--r-pill:999px}

  *{box-sizing:border-box}
  /* Controls carry their own pressed/selected states; the UA tap flash fights them. */
  html{-webkit-tap-highlight-color:transparent}
  html{-webkit-text-size-adjust:100%}
  body{margin:0;background:var(--paper);color:var(--ink);
    font-family:-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,Helvetica,Arial,sans-serif;
    line-height:1.55;-webkit-font-smoothing:antialiased}
  .wrap{max-width:1080px;margin:0 auto;padding:0 20px}
  /* The states index is a DIRECTORY of 48 jurisdictions, not a reading page:
     at 1080 its card grids ran three-up with a short prose measure beside a lot
     of nothing, which DESIGN.md's Density section and its one aesthetic
     reference (Stripe Dashboard) both rule against. It gets its own container
     width; the state pages keep 1080, where the measure governs. In the
     critical block on purpose - overriding a container width from the linked
     sheet would reflow the page after first paint. */
  body.idx .wrap{max-width:1320px}
  /* Compare is the same kind of surface: up to six jurisdictions side by side,
     a picker of 28 and a matrix as wide as the selection. Same reasoning, same
     width, and in the critical block for the same reason. */
  body.cmp .wrap{max-width:1320px}
  /* critical:end -- everything ABOVE this line is inlined into every generated
     page's <head> by build.py, and this file is still linked for the rest.

     Why: on Playwright's emulated-mobile WebKit (iPhone 14 profile), a page
     whose <head> contains NO <script src> after the stylesheet never applied
     the `body` rule at all - background stayed transparent, colour stayed
     black, and every custom property failed to inherit past :root. The state
     pages passed only because their deferred scripts happened to force the
     sheet to be applied first. Measured 2026-08-15: index.html and all 18
     stubs failed, dc.html and maryland.html did not.

     Whether that is a real iOS Safari behaviour or a port artifact, the fix is
     the one DESIGN.md already asks for: the ground must not depend on a
     round trip. The theme boot script sets data-theme BEFORE the stylesheet
     "so no page ever flashes the wrong surface"; a render-blocking external
     sheet is exactly that round trip. Inlining ~2KB of tokens removes it.

     Do not move this marker without re-running the staging a11y gate on the
     four WebKit projects. */
  .mono{font-family:ui-monospace,"SF Mono","Cascadia Code",Consolas,"Liberation Mono",monospace;font-variant-numeric:tabular-nums}
  .tnum{font-variant-numeric:tabular-nums}
  /* Available to assistive tech, removed from the visual layout. Used for the
     chart's data table and the filter status announcements. */
  .visually-hidden{position:absolute;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}
  /* Slides in on focus via transform, not `top` — layout properties must not be
     animated (see DESIGN.md Motion Policy). */
  .skip-link{position:absolute;left:8px;top:8px;z-index:60;background:var(--card);
    color:var(--ink);border:1px solid var(--accent);border-radius:var(--r-md);padding:10px 14px;
    font-size:14px;text-decoration:none;transform:translateY(-300%)}
  .skip-link:focus{transform:translateY(0)}
  @media (prefers-reduced-motion: no-preference){
    .skip-link{transition:transform .15s ease-out}
  }

  header.mast{border-bottom:2px solid var(--ink);background:var(--card-2)}
  /* Site nav. One row, identical on every page: it sits directly under the
     brand bar so its position never depends on how tall a page's title is,
     and it never wraps, so its shape never changes either. Both of those used
     to shift between pages, which reads as the bar jumping around. */
  .tabs{display:flex;gap:2px;margin:0 -20px;padding:0 20px;overflow-x:auto;
    scrollbar-width:none;-ms-overflow-style:none;border-bottom:1px solid var(--line)}
  .tabs::-webkit-scrollbar{display:none}
  .tab{flex:none;display:inline-flex;align-items:center;min-height:44px;padding:0 12px;
    font-size:13.5px;font-weight:600;color:var(--muted);text-decoration:none;
    white-space:nowrap;border-bottom:2px solid transparent}
  .tab:hover{color:var(--ink)}
  .tab:focus-visible{outline:2px solid var(--accent);outline-offset:-2px}
  .tab[aria-current="page"]{color:var(--ink);border-bottom-color:var(--accent)}
  /* Fixed height: the nav sits directly beneath, so anything that changes
     this row's height moves the nav between pages. The badge truncates
     rather than wrapping for the same reason. */
  .mast-top{display:flex;align-items:center;justify-content:flex-end;gap:12px;padding:10px 0;min-height:52px}
  .mast-top .badge{margin-right:auto;white-space:nowrap}
  .mast-top .priv,.mast-top .theme-btn{flex:none}
  /* Below this the row cannot hold all three without wrapping, and a
     wrapping brand row is what used to shove the nav down the page. */
  @media(max-width:480px){.mast-top .badge{display:none}}
  .badge{display:inline-flex;align-items:center;gap:8px;font-size:11px;letter-spacing:.12em;text-transform:uppercase;color:var(--muted);font-weight:600}
  .dot{width:7px;height:7px;border-radius:50%;background:var(--red)}
  .priv{display:inline-flex;align-items:center;gap:6px;font-size:11px;letter-spacing:.1em;text-transform:uppercase;color:var(--red);border:1px solid var(--red);border-radius:var(--r-pill);padding:4px 8px;font-weight:700}
  .theme-btn{background:transparent;border:1px solid var(--line-strong);color:var(--muted);
    border-radius:var(--r-md);padding:0 12px;min-height:32px;font-size:12px;cursor:pointer;
    display:inline-flex;align-items:center;gap:6px;touch-action:manipulation}
  .theme-btn:hover{color:var(--ink);border-color:var(--muted)}
  /* Same accent ring the tabs and selects use — the toggle is reachable by keyboard
     and must show it in the site's own focus style, not the UA's. */
  .theme-btn:focus-visible{outline:2px solid var(--accent);outline-offset:2px}
  /* Touch floor. Compact shrinks the visual control, never the hit area —
     the same rule .preset already follows. */
  @media (pointer:coarse){.theme-btn{min-height:44px;padding:0 14px}}
  .mast-main{padding:24px 0 0}
  h1.title{font-family:Georgia,serif;font-weight:700;margin:0;font-size:clamp(28px,5vw,46px);line-height:1.04;letter-spacing:-.01em;text-wrap:balance}
  .title .thin{color:var(--muted);font-weight:400;font-style:italic}
  .dateline{display:flex;flex-wrap:wrap;gap:8px 20px;margin-top:16px;font-size:13px;color:var(--muted)}
  .dateline b{color:var(--ink-soft);font-weight:600}
  .kicker{font-size:12px;letter-spacing:.16em;text-transform:uppercase;color:var(--accent);font-weight:700;margin-bottom:12px}

  /* Controls */
  .controls{position:sticky;top:0;z-index:20;background:color-mix(in srgb,var(--paper) 88%, transparent);backdrop-filter:blur(8px);border-bottom:1px solid var(--line)}
  .ctrl-inner{display:flex;flex-wrap:wrap;align-items:center;gap:12px 20px;padding:12px 0}

  /* Mobile: the full bar is ~192px once chips wrap at 44px touch height — far too
     much to pin. Instead pin a ~48px row that always says WHICH range you are
     looking at, and expand to the real controls on demand. Desktop is unchanged. */
  .ctrl-summary{display:none}
  .ctrl-current{font-size:13.5px;font-weight:700;color:var(--ink);font-variant-numeric:tabular-nums;line-height:1.25}
  .ctrl-current small{display:block;font-size:11px;font-weight:600;color:var(--faint);
    letter-spacing:.09em;text-transform:uppercase}
  .ctrl-toggle{flex:none;background:transparent;border:1px solid var(--line-strong);color:var(--ink);
    border-radius:var(--r-pill);padding:0 14px;min-height:36px;font-size:12.5px;font-weight:600;
    cursor:pointer;display:inline-flex;align-items:center;gap:6px}
  .ctrl-toggle:hover{border-color:var(--muted)}
  .ctrl-toggle .caret{font-size:10px;transition:none}
  @media(max-width:640px){
    .ctrl-summary{display:flex;align-items:center;justify-content:space-between;gap:12px;
      padding:6px 0;min-height:48px}
    .ctrl-inner{display:none;padding-bottom:14px}
    .controls[data-open="true"] .ctrl-inner{display:flex}
    .ctrl-toggle{min-height:44px}
  }
  .field{display:flex;align-items:center;gap:8px;font-size:12.5px;color:var(--muted)}
  .field label{text-transform:uppercase;letter-spacing:.07em;font-size:11px;font-weight:700;color:var(--faint)}
  select{font-family:inherit;font-size:13px;color:var(--ink);background:var(--card);border:1px solid var(--line-strong);border-radius:var(--r-md);padding:0 28px 0 10px;min-height:32px;cursor:pointer;
    -webkit-appearance:none;appearance:none;background-image:linear-gradient(45deg,transparent 50%,var(--muted) 50%),linear-gradient(135deg,var(--muted) 50%,transparent 50%);
    background-position:calc(100% - 14px) 52%,calc(100% - 9px) 52%;background-size:5px 5px,5px 5px;background-repeat:no-repeat}
  select:focus-visible{outline:2px solid var(--accent);outline-offset:1px}
  /* The coarse-pointer bump was applied control by control - .theme-btn,
     .preset, .statepick select, details.basis>summary all have one - and the
     RANGE selects, which are the page's primary control, were the ones missed.
     They rendered 13px/32px under real touch emulation against DESIGN.md's two
     hard rules: form controls are >=16px on mobile (14px triggers iOS
     zoom-on-focus) and touch targets are >=44px. Neither gate could see it -
     axe does not check the iOS zoom rule, and the QC touch measurement sampled
     the jurisdiction picker, which already had the bump. Applied to `select`
     generally so the next one added inherits it instead of being missed too. */
  @media (pointer:coarse){select{min-height:44px;font-size:16px}}
  .presets{display:flex;flex-wrap:wrap;gap:6px}
  .preset{font-size:11.5px;border:1px solid var(--line-strong);background:transparent;color:var(--muted);
    border-radius:var(--r-pill);padding:0 12px;min-height:32px;cursor:pointer;
    display:inline-flex;align-items:center}
  /* >=44px hit area on touch without inflating the visual chip */
  @media (pointer:coarse){.preset{min-height:44px;padding:0 16px}}
  .preset:hover{color:var(--ink);border-color:var(--muted)}
  .preset.active{background:var(--accent);border-color:var(--accent);color:var(--accent-ink)}
  /* A preset the selection's unit cannot honour is DISABLED AND SAYS WHY,
     the same wall .pick.off puts up in the jurisdiction picker two controls
     away. The reason is text, not a title attribute: a tooltip is
     unreachable by touch and by keyboard, and a disabled button is not
     focusable, so aria-describedby alone would never be announced. */
  .preset-off{display:inline-flex;align-items:center;gap:6px;flex-wrap:wrap;max-width:100%}
  .preset.off,.preset:disabled{color:var(--faint);border-style:dashed;cursor:not-allowed}
  .preset.off:hover{color:var(--faint);border-color:var(--line-strong)}
  .presetwhy{font-size:11px;color:var(--faint);max-width:34ch;line-height:1.35}
  /* The roll-up note sits under the whole control bar rather than inside
     it: it describes what the controls DID, and it is absent whenever they
     did nothing. */
  .rollup-note{margin:8px 0 0;font-size:12px;line-height:1.45;color:var(--muted);
               border-left:2px solid var(--line-strong);padding-left:10px;max-width:78ch}
  .coverage{margin-left:auto;font-size:11px;color:var(--faint)}
  .coverage b{color:var(--muted)}

  section{padding:28px 0}
  .sec-head{display:flex;flex-wrap:wrap;align-items:baseline;justify-content:space-between;
    gap:4px 12px;margin-bottom:6px;border-bottom:1px solid var(--line);padding-bottom:10px}
  /* V8: below this the heading and its note collide — stack them instead. */
  @media(max-width:560px){.sec-head{flex-direction:column;align-items:flex-start}
    .sec-note{text-align:left}}
  h2.sec{font-family:Georgia,serif;font-size:22px;margin:0;font-weight:700;letter-spacing:-.01em}
  .sec-note{font-size:12px;color:var(--faint);text-align:right}
  .lead{color:var(--muted);font-size:14.5px;max-width:66ch;margin:14px 0 22px}

  /* In-page anchor targets (the FAQ links to #trend) must clear the sticky
     controls bar, or the jump lands the heading underneath it. */
  main section{scroll-margin-top:64px}

  /* FAQ: plain questions and answers, one panel. Answers carry live figures
     (baked into the HTML for non-rendering crawlers, refreshed by JS). */
  #faq .qa+.qa{margin-top:16px;padding-top:16px;border-top:1px solid var(--line)}
  #faq h3{margin:0 0 6px;text-wrap:balance}
  #faq .qa p{margin:0;color:var(--muted);font-size:14.5px;max-width:72ch}
  /* Desktop: a single 72ch column leaves half the panel empty — go 2×2. */
  @media(min-width:881px){
    #faq .panel{display:grid;grid-template-columns:1fr 1fr;gap:24px 40px}
    #faq .qa+.qa{margin-top:0;padding-top:0;border-top:0}
  }

  .kpis{display:grid;grid-template-columns:repeat(3,1fr);gap:16px}
  @media(max-width:760px){.kpis{grid-template-columns:repeat(2,1fr)}}
  @media(max-width:460px){.kpis{grid-template-columns:1fr}}
  .kpi{background:var(--card);border:1px solid var(--line);border-radius:var(--r-lg);padding:16px;position:relative;overflow:hidden}
  .kpi::before{content:"";position:absolute;left:0;top:0;bottom:0;width:3px;background:var(--accent);opacity:.85}
  .kpi.empty::before{background:var(--line-strong)}
  .kpi .label{font-size:11.5px;letter-spacing:.06em;text-transform:uppercase;color:var(--muted);font-weight:600}
  .kpi .val{font-size:31px;font-weight:700;margin:8px 0 2px;letter-spacing:-.02em}
  .kpi.empty .val{color:var(--faint);font-weight:600;font-size:26px}
  .kpi .sub{font-size:12.5px;color:var(--faint)}
  .chg{display:inline-flex;align-items:center;gap:4px;font-size:12.5px;font-weight:600;margin-top:8px}
  .chg.up{color:var(--up)} .chg.down{color:var(--down)} .chg.flat{color:var(--faint)}
  .chg .arw{font-size:11px}

  .grid2{display:grid;grid-template-columns:1fr 1fr;gap:16px}
  @media(max-width:820px){.grid2{grid-template-columns:1fr}}
  .panel{background:var(--card);border:1px solid var(--line);border-radius:var(--r-lg);padding:20px}
  .panel h3{margin:0 0 2px;font-size:15px;font-weight:700;letter-spacing:-.01em}
  .panel .cap{font-size:12px;color:var(--faint);margin-bottom:14px}
  .chart{width:100%;height:auto;display:block;overflow:visible}
  .chart-note{margin:12px 0 0;padding-top:12px;border-top:1px solid var(--line);
    font-size:12px;line-height:1.6;color:var(--muted);max-width:78ch}
  .chart-note b{color:var(--ink-soft)}
  .chart-note + .chart-note{border-top:none;padding-top:0;margin-top:8px}
  .chart-legend{display:flex;flex-wrap:wrap;align-items:baseline;gap:4px 18px;margin:0 0 10px;
    font-size:13px;color:var(--ink-soft);min-height:40px}
  .chart-legend span{display:inline-flex;align-items:center;gap:8px}
  .chart-legend i{width:18px;height:3px;border-radius:2px;flex:none}
  .chart-legend .rt-month{font-weight:700;color:var(--ink);font-variant-numeric:tabular-nums;
    min-width:8ch}
  .chart-legend .rt-val{font-variant-numeric:tabular-nums;font-weight:600;color:var(--ink)}
  .chart-legend .rt-hint{color:var(--faint);font-size:11.5px}
  /* Horizontal drag scrubs; vertical still scrolls the page. */
  .scrub-layer{cursor:crosshair;touch-action:pan-y}
  /* Tapping to scrub focuses the chart; only show a ring for real keyboard
     focus. outline:none is safe here because :focus-visible replaces it. */
  #lineChart:focus{outline:none}
  #lineChart:focus-visible{outline:2px solid var(--accent);outline-offset:2px}

  .hbars{display:flex;flex-direction:column;gap:8px;list-style:none;margin:0;padding:0}
  .hbar{display:grid;grid-template-columns:120px 1fr auto;align-items:center;gap:10px;
    min-height:24px;font-size:12.5px}
  .hbar .nm{color:var(--ink-soft)}
  .track{height:16px;background:var(--track);border-radius:var(--r-sm);overflow:hidden}
  /* display:block is REQUIRED — .fill is a <span> inside .track, and width/height
     do not apply to non-replaced inline elements. Without it every bar is 0x0. */
  .fill{display:block;height:100%;background:var(--accent);border-radius:var(--r-sm);width:0;transform-origin:left center}
  .fill.red{background:var(--red)}
  .hbar .amt{color:var(--muted);font-size:12px;min-width:64px;text-align:right}
  @media(max-width:480px){.hbar{grid-template-columns:96px 1fr auto}}

  .split{display:flex;height:34px;border-radius:var(--r-sm);overflow:hidden;border:1px solid var(--line);margin-bottom:12px}
  .split>div{display:flex;align-items:center;justify-content:center;font-size:12px;font-weight:600;min-width:0;width:0}
  .kv{display:flex;justify-content:space-between;font-size:13px;padding:6px 0;border-top:1px solid var(--line)}
  .kv:first-child{border-top:none}
  .kv .k{color:var(--muted)} .kv .v{font-weight:600}

  .segbar{display:flex;height:30px;border-radius:var(--r-sm);overflow:hidden;border:1px solid var(--line)}
  .segbar>div{display:flex;align-items:center;justify-content:center;font-size:11.5px;font-weight:600;min-width:0;width:0}
  .seg-legend{display:grid;grid-template-columns:1fr 1fr 1fr;gap:10px;margin-top:14px}
  @media(max-width:560px){.seg-legend{grid-template-columns:1fr}}
  .seg-item{border-left:3px solid var(--accent);padding-left:10px}
  .seg-item .sv{font-size:19px;font-weight:700}
  .seg-item .sl{font-size:11.5px;color:var(--muted);text-transform:uppercase;letter-spacing:.05em}
  .seg-item .sd{font-size:12px;color:var(--faint)}

  .enf-grid{display:grid;grid-template-columns:1.1fr .9fr;gap:16px}
  @media(max-width:820px){.enf-grid{grid-template-columns:1fr}}
  .stat-row{display:grid;grid-template-columns:repeat(2,1fr);gap:12px;margin-bottom:16px}
  .ministat{background:var(--card-2);border:1px solid var(--line);border-radius:var(--r-md);padding:12px 14px}
  .ministat .v{font-size:24px;font-weight:700}
  .ministat .l{font-size:11.5px;color:var(--muted)}
  .closed{list-style:none;padding:0;margin:8px 0 0}
  .closed li{display:flex;justify-content:space-between;gap:10px;padding:8px 0;border-top:1px solid var(--line);font-size:13px}
  .closed li:first-child{border-top:none}
  .closed .addr{color:var(--faint);font-size:12px}
  .closed .nm{font-weight:600}

  table.mini{width:100%;border-collapse:collapse;font-size:12.5px}
  table.mini th{text-align:left;color:var(--muted);font-weight:600;font-size:11px;text-transform:uppercase;letter-spacing:.05em;padding:6px 8px;border-bottom:1px solid var(--line)}
  table.mini td{padding:8px;border-bottom:1px solid var(--line)}
  table.mini td.n{text-align:right}

  .nodata{border:1px dashed var(--line-strong);border-radius:var(--r-lg);padding:24px 20px;text-align:center;color:var(--faint);font-size:13.5px;background:var(--card-2)}
  .nodata b{color:var(--muted)}
  .hidden{display:none}

  /* CLS guard. #main is `hidden` until data.json resolves, so without this the
     footer renders just below the masthead and is then shoved ~430px down when
     content inflates — a 0.49 layout shift, ~5x the 0.1 budget. Reserving a
     viewport of height keeps the footer BELOW the fold until the swap, so the
     inflation happens off-screen and contributes nothing to CLS. Held in the
     error state too: dropping it there would just move the shift, not remove it. */
  #loadState{min-height:100svh}

  footer{border-top:2px solid var(--ink);margin-top:20px;padding:32px 0 56px;color:var(--muted);font-size:12.5px}
  footer a{color:var(--link)}
  .method{background:var(--card-2);border:1px solid var(--line);border-radius:var(--r-lg);padding:16px 20px;margin-bottom:20px}
  .method h4{margin:0 0 8px;font-size:13px;color:var(--ink)}
  .method p{margin:6px 0;max-width:78ch}
  .surge{display:inline-block;background:var(--accent-soft);color:var(--accent);border-radius:var(--r-sm);padding:2px 8px;font-size:11px;font-weight:700}

  .gridln{stroke:var(--line)} .axtx{fill:var(--faint);font-size:11px;font-family:ui-monospace,monospace}
  .annot{fill:var(--muted);font-size:11px}.annotln{stroke:var(--line-strong);stroke-dasharray:3 3}
  /* The absence-band label is 11px uppercase and tracked, which DESIGN.md
     admits ONLY at >=4.5:1, and it sits on the --track band rather than on the
     panel ground - a lighter surface in light theme than any other .annot has
     under it. --muted was the weakest text in the set there. --ink-soft is the
     same family, one step darker, and is what the residual segment already
     uses when it prints on --track. Marker annotations keep --muted: they sit
     on the panel ground, not on a band. */
  .absband-label{fill:var(--ink-soft)}
  /* Regional context is subordinate to the data: fainter, finer, dotted. */
  .rgnln{stroke:var(--faint);stroke-dasharray:2 4;opacity:.75}
  .rgntx{fill:var(--faint);font-size:11px}

  /* ---- Arrests section (renderer: arrests.js · rationale: ARRESTS_SECTION_PLAN.md)
     A flagged year is signalled three ways — a dashed segment, a shaded band and
     the always-visible note under the chart — so the caveat never rests on colour
     or on one cue a reader might miss. */
  .arr-line{fill:none;stroke:var(--series-1);stroke-width:2.5;stroke-linejoin:round;stroke-linecap:round}
  .arr-line.is-broken{stroke-dasharray:5 4}
  .arr-dot{fill:var(--series-1)}
  .arr-dot.is-broken{fill:var(--card);stroke:var(--series-1);stroke-width:1.6}
  /* A different source entirely: hollow, and never joined to the line. */
  .arr-dot-alt{fill:none;stroke:var(--muted);stroke-width:1.6;stroke-dasharray:2.5 2}
  .arr-band{fill:var(--line);opacity:.55}
  .arr-band-edge{stroke:var(--line-strong);stroke-dasharray:3 3}
  .arr-band-tx{fill:var(--muted);font-size:11px}
  /* Legal milestones are subordinate to the data: fainter, finer, dotted. They
     carry their own classes so this whole block is identical on both pages. */
  .arr-mln{stroke:var(--faint);stroke-dasharray:2 4;opacity:.75}
  /* Halo in the panel colour so a label stays legible where the series line
     runs behind it — without it a plunging line reads as a strikethrough. */
  .arr-mtx{fill:var(--faint);font-size:11px;stroke:var(--card);stroke-width:3;
    paint-order:stroke fill;stroke-linejoin:round}
  .arr-gapln{stroke:var(--line-strong);stroke-dasharray:2 4;opacity:.8}
  .arr-val{fill:var(--ink);font-size:12px;font-weight:700;font-variant-numeric:tabular-nums;
    stroke:var(--card);stroke-width:3.5;paint-order:stroke fill;stroke-linejoin:round}
  .arr-val.is-alt{fill:var(--muted);font-weight:600}
  .arr-grid{display:grid;grid-template-columns:1fr 1fr;gap:16px;margin-top:16px}
  @media(max-width:820px){.arr-grid{grid-template-columns:1fr}}
  .arr-fine{margin:10px 0 0;font-size:11.5px;color:var(--faint);font-variant-numeric:tabular-nums}
  .arr-facts{display:grid;grid-template-columns:1fr 1fr;gap:16px}
  @media(max-width:560px){.arr-facts{grid-template-columns:1fr}}
  .arr-fact{border-left:3px solid var(--series-1);padding-left:10px}
  .arr-fact .fv{font-size:22px;font-weight:700;line-height:1.15;font-variant-numeric:tabular-nums}
  .arr-fact .fl{font-size:12.5px;color:var(--ink-soft);margin-top:2px}
  .arr-fact .fs{font-size:11.5px;color:var(--faint)}
  .arr-total{font-size:30px;font-weight:700;line-height:1.1}
  .arr-relief{list-style:none;margin:12px 0 0;padding:0}
  .arr-relief li{display:flex;justify-content:space-between;gap:12px;padding:7px 0;
    border-top:1px solid var(--line);font-size:13px}
  .arr-relief li:first-child{border-top:none}
  #arrSources{margin-top:16px;font-size:11.5px;color:var(--faint);line-height:1.7}
  #arrSources p{margin:0 0 4px;max-width:78ch}
  #arrSources a{color:var(--link)}
  .arr-panel{margin-top:16px}
  /* DESIGN.md: --accent is a brand/interactive affordance and never encodes
     data. These bars are data, so they take the same series colour as the
     line above them — which also keeps the section visually of a piece. */
  .arr-grid .fill{background:var(--series-1)}
  /* Reveal via transform (compositor-only) at 180ms — DESIGN.md forbids animating
     layout properties and caps duration at 200ms. Was: width, 550ms. */
  .segbar>div,.split>div{transform-origin:left center}
  @media (prefers-reduced-motion: no-preference){
    .fill,.segbar>div,.split>div{transition:transform .18s ease-out}
  }

  /* ---- Motion vocabulary (DESIGN.md · Motion Policy) ------------------------
     Hand-ported from Motion-Primitives / Magic UI concepts — no runtime, no
     library. html.mo is set in <head> only when the reader has NOT asked for
     reduced motion (and IntersectionObserver exists). Base styles keep every
     element visible, so no-JS, reduced-motion and pre-JS paint all read the
     finished page. transform/opacity only. */
  @media (prefers-reduced-motion: no-preference){
    /* Reading-depth hairline. Scroll-linked position, not a timed animation. */
    #progress{position:fixed;top:0;left:0;width:100%;height:2px;z-index:80;
      pointer-events:none;background:var(--accent-bright);
      transform-origin:0 50%;transform:scaleX(var(--sp,0))}
    @keyframes mo-rise{from{opacity:0;transform:translateY(12px)}to{opacity:1;transform:none}}
    /* Masthead settles in on arrival: kicker, then headline word-by-word, then meta. */
    html.mo .mast .kicker{animation:mo-rise .5s cubic-bezier(.2,.7,.3,1) both}
    html.mo .mast .dateline{animation:mo-rise .5s cubic-bezier(.2,.7,.3,1) .34s both}
    html.mo .title .mo-w{display:inline-block;animation:mo-rise .55s cubic-bezier(.2,.7,.3,1) both;
      animation-delay:calc(80ms + var(--wi,0)*45ms)}
    /* Scroll reveal: hidden only once JS has tagged the element .mo, revealed by
       the observer as .mo-in. Containers keep .mo-in across re-renders, so a
       filter change never replays the entrance. */
    html.mo .mo{opacity:0;transform:translateY(14px)}
    html.mo .mo-in{opacity:1;transform:none;
      transition:opacity .5s cubic-bezier(.2,.7,.3,1),transform .5s cubic-bezier(.2,.7,.3,1);
      transition-delay:var(--mo-d,0ms)}
    /* KPI tiles stagger under their container's reveal. */
    html.mo .kpis.mo .kpi{opacity:0;transform:translateY(14px)}
    html.mo .kpis.mo-in .kpi{opacity:1;transform:none;
      transition:opacity .5s cubic-bezier(.2,.7,.3,1),transform .5s cubic-bezier(.2,.7,.3,1)}
    html.mo .kpis.mo-in .kpi:nth-child(2){transition-delay:60ms}
    html.mo .kpis.mo-in .kpi:nth-child(3){transition-delay:120ms}
    html.mo .kpis.mo-in .kpi:nth-child(4){transition-delay:180ms}
    html.mo .kpis.mo-in .kpi:nth-child(5){transition-delay:240ms}
    html.mo .kpis.mo-in .kpi:nth-child(6){transition-delay:300ms}
  }

/* ============================================================================
   GENERATOR ADDITIONS - components the four hand-written pages never needed
   ----------------------------------------------------------------------------
   Every value below comes off the DESIGN.md ramps: type from the 9-step scale
   with an 11px floor, spacing on the 4px base unit, radii from the four-step
   scale, separation by 1px hairline and never a shadow. No new colour token is
   invented - these all reuse the palette above.
   ========================================================================== */

/* ---- state switcher -------------------------------------------------------
   46 jurisdictions cannot be tabs. DESIGN.md: "Prefer native elements over
   hand-rolled ARIA ... Do not hand-roll a combobox." A native select with
   optgroups is one 44px control that holds every jurisdiction, gets the OS
   picker on touch, and needs no keyboard code of its own. */
/* min-width:0 in three places, because a native <select> sizes itself to its
   WIDEST OPTION and "Eastern Band of Cherokee Indians (Qualla Boundary)" is
   79px wider than a 390px viewport. Flex items default to min-width:auto,
   so without these the overflow propagates all the way to <html> and the whole
   page scrolls sideways. Measured, not guessed: scrollWidth 469 vs 390. */
.statepick{display:flex;align-items:center;gap:8px;flex:0 1 auto;min-width:0}
.statepick label{flex:none;font-size:11px;letter-spacing:.09em;text-transform:uppercase;
  color:var(--faint);font-weight:700}
.statepick select{min-height:32px;max-width:15rem;min-width:0;flex:1 1 auto;
  text-overflow:ellipsis}
@media (pointer:coarse){.statepick select{min-height:44px;font-size:16px}}
/* Below this the brand row cannot hold the picker AND the theme button. The
   picker takes its own row rather than wrapping the row above it, because a
   wrapping brand row is what shoves the nav down the page (see .mast-top). */
@media(max-width:640px){
  .mast-top{flex-wrap:wrap}
  .statepick{order:3;width:100%;padding-bottom:8px}
  .statepick select{max-width:none;width:100%}
}

/* ---- provenance chips: vendor data, basis flags, staleness ----------------
   These are claims about how far the data can be trusted, so they sit next to
   the figure they qualify and never in a footnote nobody scrolls to. */
.flags{display:flex;flex-wrap:wrap;gap:6px;margin:12px 0 0}
/* In the masthead the flags are the last line of the title block, so they take
   the dateline's rhythm rather than floating between the masthead and the
   sticky filter bar - where they read as filter chips, which they are not.
   `.mast-main` has no bottom padding of its own (the dateline used to be last
   and carried its own margin), so the flags must supply it or the masthead's
   2px bottom rule slices them in half. */
.mast-main .flags{margin:12px 0 0;padding-bottom:18px}
.flag{display:inline-flex;align-items:center;gap:6px;font-size:11px;
  letter-spacing:.06em;text-transform:uppercase;font-weight:700;
  border:1px solid var(--line-strong);border-radius:var(--r-pill);
  padding:3px 10px;color:var(--muted);background:var(--card-2)}
.flag b{font-weight:700;color:var(--ink-soft);letter-spacing:0;text-transform:none}
/* Vendor data is the one flag that must be impossible to miss (DECISIONS Q2):
   it is not the state speaking. Carried by ink AND by the word itself, never
   by colour alone. */
.flag.vendor{border-color:var(--red);color:var(--red)}
.flag.stale{border-color:var(--down);color:var(--down)}

/* A basis flag opens a plain details element, not a tooltip: DESIGN.md prefers
   a native expandable over a hand-rolled tooltip, and the content is several
   paragraphs, which no tooltip should hold. */
details.basis{margin:12px 0 0;border:1px solid var(--line);border-radius:var(--r-lg);
  background:var(--card-2)}
details.basis>summary{cursor:pointer;list-style:none;padding:10px 16px;
  font-size:12px;letter-spacing:.06em;text-transform:uppercase;font-weight:700;
  color:var(--muted);display:flex;align-items:center;gap:8px;min-height:40px}
details.basis>summary::-webkit-details-marker{display:none}
details.basis>summary::after{content:"+";margin-left:auto;font-size:14px;color:var(--faint)}
details.basis[open]>summary::after{content:"\2212"}
details.basis>summary:hover{color:var(--ink)}
details.basis>summary:focus-visible{outline:2px solid var(--accent);outline-offset:-2px}
@media (pointer:coarse){details.basis>summary{min-height:44px}}
.basis-body{padding:0 16px 16px;font-size:13px;color:var(--muted);line-height:1.6}
.basis-body dl{display:grid;grid-template-columns:auto 1fr;gap:6px 16px;margin:0 0 12px}
@media(max-width:480px){
  .basis-body dl{grid-template-columns:1fr;gap:2px 0}
  .basis-body dd{margin:0 0 8px}
}
.basis-body dt{font-size:11px;letter-spacing:.06em;text-transform:uppercase;
  font-weight:700;color:var(--faint)}
.basis-body dd{margin:0;color:var(--ink-soft);font-size:13px}
.basis-body q{font-style:italic;color:var(--ink-soft)}
.basis-body p{margin:0 0 8px;max-width:78ch}
.basis-body p:last-child{margin-bottom:0}

/* ---- era note: one panel for a run of same-reason gaps --------------------
   Maryland grew 29% taller at 390px when 29 adult-use zeroes became declared
   gaps and every one of them rendered its own explanation. Twenty-nine copies
   of one sentence is not more honest than one copy that names the run; it is
   the same sentence, 29 times, pushing the reader a third of a page further
   from the data. */
.eranote{border:1px dashed var(--line-strong);border-left:3px solid var(--line-strong);
  border-radius:var(--r-md);background:var(--card-2);padding:12px 16px;margin:0 0 16px;
  font-size:13px;color:var(--muted);line-height:1.6}
.eranote b{color:var(--ink-soft)}
.eranote .count{font-variant-numeric:tabular-nums}
/* The verbatim token the source printed, where there was one. It is evidence,
   so it is reproduced exactly rather than paraphrased. */
.eranote code{font-family:ui-monospace,"SF Mono",Consolas,monospace;font-size:12px;
  background:var(--track);border-radius:var(--r-sm);padding:1px 5px;color:var(--ink-soft)}
/* Several source sentences under one run. Collapsing a run to one panel is the
   density win; collapsing the SENTENCES to a generic reason word was a candour
   loss - "not published yet" in place of four notes each naming the sheet and
   the month it begins. One panel, one line per source. */
.eranote-src{margin:8px 0 0;padding:0 0 0 20px;list-style:disc}
/* 12px = DESIGN.md --text-xs (captions, chart annotations). Written as a raw
   px like the rest of this sheet: the type ramp is not tokenised here, and a
   var() whose fallback always fires implies a token system that does not
   exist. */
.eranote-src li{margin:0 0 4px;font-size:12px}
.eranote-src li:last-child{margin-bottom:0}

/* ---- quarterly grain ------------------------------------------------------
   DECISIONS Q4: a quarterly series is never interpolated into months, so it is
   never drawn on a monthly axis either. It gets its own section, its own
   heading and its own period labels. */
.grain-tag{display:inline-flex;align-items:center;font-size:11px;font-weight:700;
  letter-spacing:.09em;text-transform:uppercase;color:var(--accent);
  border:1px solid var(--accent);border-radius:var(--r-pill);padding:2px 8px;
  margin-left:8px;vertical-align:middle}

/* ---- states index ---------------------------------------------------------
   Two tiers, told apart by CHROME rather than by colour: a full page is a card
   with a border, a stub is a plain row. */
.tierhead{display:flex;flex-wrap:wrap;align-items:baseline;gap:6px 12px;margin:0 0 4px}
.tierhead h2{font-family:Georgia,serif;font-size:22px;margin:0;font-weight:700;
  letter-spacing:-.01em}
.tiercount{font-size:12px;color:var(--faint);font-variant-numeric:tabular-nums}
.tierlead{color:var(--muted);font-size:14.5px;max-width:72ch;margin:8px 0 18px}

/* auto-FIT, not a fixed column count and not auto-fill. A fixed 3 left the
   two-card "Live pages" grid with an empty third of the row at 1440 and capped
   the 28-card grid at three columns on a 1280px measure; auto-fill would keep
   the empty tracks. auto-fit collapses them, so a short tier fills its row and
   a long one gets as many columns as the width honestly holds - five at 1440,
   three at 880, one on a phone, with no breakpoint list to keep in sync. */
.stgrid{display:grid;grid-template-columns:repeat(auto-fit,minmax(240px,1fr));
  gap:12px;list-style:none;margin:0;padding:0}
.stcard{position:relative;background:var(--card);border:1px solid var(--line);
  border-radius:var(--r-lg);padding:16px;display:flex;flex-direction:column;gap:6px}
/* A tier with only two entries stretches them across the row, and a 500px-wide
   stacked card is the sparseness complaint in miniature. Wide cards lay out as
   dense rows instead: name left, meta and tags right, one line.

   They also carry more weight than the 28 announced-but-unbuilt cards below
   them. Two thin rows above a dense block of twenty-eight inverted the
   hierarchy - the pages that actually work read as subordinate to the ones that
   do not exist. Weight comes from the type ramp and the border system, not from
   colour or a shadow: --text-lg (panel titles) and the strong hairline, with
   full-panel padding per Density. */
.stgrid--live .stcard{padding:20px;border-color:var(--line-strong)}
.stgrid--live .stcard a.stname{font-size:20px}
@media(min-width:760px){
  .stgrid--live .stcard{flex-direction:row;flex-wrap:wrap;align-items:baseline;
    justify-content:space-between;column-gap:16px}
  .stgrid--live .stcard .sttags{margin-top:0}
}
.stcard a.stname{font-size:16px;font-weight:700;color:var(--ink);text-decoration:none;
  letter-spacing:-.01em}
/* Whole-card target without nesting interactive elements: the link is the only
   focusable thing and its ::after covers the card. */
.stcard a.stname::after{content:"";position:absolute;inset:0;border-radius:var(--r-lg)}
.stcard a.stname:hover{color:var(--link)}
.stcard a.stname:focus-visible{outline:none}
.stcard:focus-within{outline:2px solid var(--accent);outline-offset:2px}
.stmeta{font-size:12px;color:var(--faint);font-variant-numeric:tabular-nums}
.stmeta b{color:var(--muted);font-weight:600}
.sttags{display:flex;flex-wrap:wrap;gap:4px;margin-top:2px}
.sttag{font-size:11px;letter-spacing:.05em;text-transform:uppercase;font-weight:700;
  color:var(--muted);border:1px solid var(--line-strong);border-radius:var(--r-pill);
  padding:1px 8px}
.sttag.vendor{color:var(--red);border-color:var(--red)}

.stublist{list-style:none;margin:0;padding:0;border-top:1px solid var(--line)}
.stub-row{display:grid;grid-template-columns:minmax(9rem,14rem) 1fr;gap:4px 20px;
  padding:14px 0;border-bottom:1px solid var(--line);align-items:baseline}
@media(max-width:640px){.stub-row{grid-template-columns:1fr;gap:4px}}
.stub-row .stname{font-size:14px;font-weight:700;color:var(--ink);text-decoration:none}
.stub-row .stname:hover{color:var(--link)}
.stub-row .stwhy{font-size:13px;color:var(--muted);max-width:78ch;margin:0}
/* Eighteen rows in one column is a long scroll past a lot of empty right-hand
   side on a wide screen. Two columns of nine, each row still name + reason. */
@media(min-width:1100px){
  .stublist{display:grid;grid-template-columns:1fr 1fr;column-gap:40px;
    border-top:0}
  .stublist .stub-row{border-top:1px solid var(--line);border-bottom:0}
}
/* The index's "How a jurisdiction gets classified" panel: four short Q/A
   blocks that stacked into a single 74ch column. Two-up on a wide screen. */
@media(min-width:900px){
  .qagrid{display:grid;grid-template-columns:1fr 1fr;gap:4px 32px}
}

/* ---- stub page ----------------------------------------------------------- */
.stub-status{display:grid;grid-template-columns:repeat(2,1fr);gap:16px;margin:0 0 24px}
@media(max-width:560px){.stub-status{grid-template-columns:1fr}}
.stub-absent{border:1px dashed var(--line-strong);border-radius:var(--r-lg);
  background:var(--card-2);padding:20px;margin:0 0 20px}
.stub-absent h3{margin:0 0 8px;font-size:15px;font-weight:700}
.stub-absent p{margin:0 0 10px;color:var(--muted);font-size:14px;max-width:78ch}
.stub-absent p:last-child{margin-bottom:0}
.stub-absent ul{margin:0;padding-left:20px;color:var(--muted);font-size:14px}
.stub-absent li{margin:0 0 6px;max-width:76ch}
.trigger{border-left:3px solid var(--accent);background:var(--accent-soft);
  border-radius:0 var(--r-md) var(--r-md) 0;padding:12px 16px;margin:0 0 20px}
.trigger .tl{font-size:11px;letter-spacing:.09em;text-transform:uppercase;
  font-weight:700;color:var(--accent);margin:0 0 4px}
.trigger p{margin:0;font-size:14px;color:var(--ink-soft);max-width:78ch}
.trigger p+p,.trigger ul+p{margin-top:8px}
.trigger ul{margin:8px 0 0;padding-left:20px}
.trigger li{margin:0 0 8px;font-size:14px;color:var(--ink-soft);max-width:78ch}

/* ---- faq & method page --------------------------------------------------- */
/* The citation list under "This method is not ours". A separate panel below
   the method Q/A grid; the cite itself carries the ink, the gloss is muted. */
#citations{margin-top:16px}
.citelead{margin:0 0 12px;font-size:14px;color:var(--muted);max-width:78ch}
.cites{list-style:none;margin:0;padding:0}
.cites li{border-top:1px solid var(--line);padding:12px 0;font-size:14px;
  color:var(--muted);max-width:82ch}
.cites li:last-child{padding-bottom:0}
.cites b{color:var(--ink);font-weight:600}

/* ---- provenance tables (sources, integrity, open questions) -------------- */
.prov{width:100%;border-collapse:collapse;font-size:13px}
.prov caption{text-align:left;font-size:11px;letter-spacing:.06em;text-transform:uppercase;
  font-weight:700;color:var(--faint);padding-bottom:8px}
.prov th{text-align:left;color:var(--muted);font-weight:600;font-size:11px;
  text-transform:uppercase;letter-spacing:.05em;padding:6px 10px 6px 0;
  border-bottom:1px solid var(--line);white-space:nowrap}
.prov td{padding:10px 10px 10px 0;border-bottom:1px solid var(--line);
  color:var(--ink-soft);vertical-align:top}
.prov td.wrap{min-width:22ch}
/* A table cannot reflow to one column, so on a phone it scrolls inside its own
   box rather than pushing the page sideways. */
.provwrap{overflow-x:auto;-webkit-overflow-scrolling:touch}

/* ---- section-level "not reported" ---------------------------------------- */
.nodata .why{display:block;margin:8px auto 0;font-size:12.5px;color:var(--faint);
  max-width:66ch}

/* ---- the staleness banner -------------------------------------------------
   .nodata is centred, which is right for the short "not reported for this
   range" notice it was built for - a label sitting in an empty panel. The
   staleness banner reuses the treatment and is not that: it is two or three
   sentences of prose above the range bar, and Nevada renders TWO of them
   stacked. Centred multi-line prose gives every line a different left edge, so
   the one block on the page whose whole job is to be believed was the only
   block not aligned with the page. Left-aligned here, and only here: the empty
   -panel notices keep the centred treatment they were designed for. The `.why`
   reset below is a precaution, not an observed necessity: no shipped page puts
   a .why inside the stale banner today, so the rule is unexercised and no gate
   watches it. If a banner ever carries one, margin:auto would re-centre it. */
#staleBanner .nodata{text-align:left;max-width:80ch}
#staleBanner .nodata .why{margin-left:0;margin-right:0}

/* ==========================================================================
   COMPARE ENGINE v2
   --------------------------------------------------------------------------
   Everything below belongs to compare.html. It reuses the existing panel,
   tile, chart and provenance systems rather than introducing a second visual
   language; what is new here is only the vocabulary the comparison itself
   needs - a marker, an exclusion list, an as-of stamp and a picker.
   ========================================================================== */

/* ---- the comparability marker -------------------------------------------
   Never colour alone (DESIGN.md: no colour-only status encoding). The word is
   the status; the left rule is a second, redundant channel; the sentence is
   the reason. All three come from the state files at render. */
.cmp-marker{display:flex;flex-wrap:wrap;gap:4px 12px;align-items:baseline;
  margin:14px 0 4px;padding:10px 14px;font-size:13px;line-height:1.6;
  background:var(--card-2);border:1px solid var(--line);border-left:3px solid var(--line-strong);
  border-radius:var(--r-md)}
.cmp-marker b{font-size:11.5px;letter-spacing:.06em;text-transform:uppercase;
  color:var(--ink);white-space:nowrap}
.cmp-marker span{color:var(--muted);max-width:82ch}
.cmp-marker[data-like="yes"]{border-left-color:var(--series-3)}
.cmp-marker[data-like="partial"]{border-left-color:var(--series-2)}
.cmp-marker[data-like="no"]{border-left-color:var(--series-6)}
.marker-src{margin:6px 0 0;font-size:11.5px;color:var(--faint);max-width:82ch}

/* ---- excluded jurisdictions ---------------------------------------------
   A shorter chart with no explanation is indistinguishable from a chart
   nobody thought about, so every selected jurisdiction that is NOT on the
   axis is named here with the reason its own file gives. */
.excluded{margin:16px 0 0;padding:14px 16px;border:1px dashed var(--line-strong);
  border-radius:var(--r-lg);background:var(--card-2)}
.excluded h4{margin:0 0 4px;font-size:11.5px;letter-spacing:.06em;text-transform:uppercase;color:var(--muted)}
.excluded > p{margin:0 0 10px;font-size:12px;color:var(--faint);max-width:78ch}
.excluded ul{margin:0;padding-left:18px}
.excluded li{font-size:13px;color:var(--ink-soft);line-height:1.6;margin-bottom:8px;max-width:82ch}
.excluded li b{color:var(--ink)}
.xwhy{display:block;color:var(--faint);font-size:12px;margin-top:2px}

.inherited{margin:16px 0 0;padding:14px 16px;border:1px solid var(--line);
  border-radius:var(--r-lg);background:var(--card-2)}
.inherited h4{margin:0 0 4px;font-size:11.5px;letter-spacing:.06em;text-transform:uppercase;color:var(--muted)}
.inherited > p{margin:0 0 10px;font-size:12px;color:var(--faint);max-width:78ch}
.inherited ul{margin:0;padding-left:18px}
.inherited li{font-size:12.5px;color:var(--ink-soft);line-height:1.6;margin-bottom:6px;max-width:82ch}

/* ---- as-of stamps (the ragged latest-month rule, made visible) ----------- */
.asof{display:flex;flex-wrap:wrap;gap:6px 16px;align-items:center;margin:12px 0 0;
  padding-top:12px;border-top:1px solid var(--line);font-size:12px;color:var(--muted)}
.asof-lab{font-size:11px;letter-spacing:.06em;text-transform:uppercase;color:var(--faint);font-weight:700}
.asof-item{display:inline-flex;align-items:center;gap:6px}
.asof-item b{color:var(--ink);font-variant-numeric:tabular-nums}
.asof-common{display:block;max-width:82ch;line-height:1.6}
.asof-common b{color:var(--ink)}
/* The common-month control declined: the stamp says so and the per-jurisdiction
   row follows it, so the two read as one statement rather than two. */
.asof-declined{border-top:1px solid var(--line);padding-top:12px;margin-bottom:0}
.asof-declined + .asof{border-top:0;padding-top:6px;margin-top:0}
/* Coverage is listed per declared grain, so a market that publishes at two
   cadences gets two lines rather than one em-dash. */
.covline{display:block;font-variant-numeric:tabular-nums}
.covg{color:var(--faint);font-weight:700;font-size:11px;text-transform:uppercase;letter-spacing:.05em}
.swatch{width:10px;height:10px;border-radius:2px;flex:none;display:inline-block}

/* ---- comparison KPI tiles ------------------------------------------------ */
.kpi.cmp::before{background:var(--sc)}
.kpi.cmp .label{color:var(--ink-soft)}
.tsub{display:block;margin-top:4px;color:var(--muted);font-variant-numeric:tabular-nums}

/* ---- per-jurisdiction channel / measure rows ----------------------------- */
.ch-panel h3{display:flex;align-items:center;gap:8px}
.ch-row{display:grid;grid-template-columns:1fr auto;gap:2px 12px;align-items:baseline;
  padding:10px 0;border-top:1px solid var(--line)}
.ch-row .sl{font-size:12.5px;color:var(--ink-soft);display:flex;align-items:center;gap:8px}
.ch-row .sv{font-size:19px;font-weight:700;text-align:right;font-variant-numeric:tabular-nums}
.ch-row .sv.na{color:var(--faint);font-weight:600}
.ch-row .sd{grid-column:1 / -1;font-size:12px;color:var(--faint);line-height:1.5;max-width:70ch}
.panel-note{margin-top:16px;max-width:82ch}

/* ---- segmented controls -------------------------------------------------- */
.flabel{text-transform:uppercase;letter-spacing:.07em;font-size:11px;font-weight:700;color:var(--faint)}
.segctl{display:inline-flex;border:1px solid var(--line-strong);border-radius:var(--r-pill);overflow:hidden}
.segctl button{font-family:inherit;font-size:11.5px;border:0;background:transparent;color:var(--muted);
  padding:0 12px;min-height:32px;cursor:pointer}
.segctl button + button{border-left:1px solid var(--line-strong)}
.segctl button:hover{color:var(--ink)}
.segctl button.active{background:var(--accent);color:var(--accent-ink)}
.segctl button:focus-visible{outline:2px solid var(--accent);outline-offset:-2px}
@media (pointer:coarse){.segctl button{min-height:44px;padding:0 16px}}

/* ---- jurisdiction picker -------------------------------------------------
   A native <details> over native checkboxes. Scales to 28 rows by wrapping
   into columns rather than by growing a scroll region nobody can see the
   bottom of, and the cap disables the rest rather than truncating silently. */
.jpick{border:1px solid var(--line-strong);border-radius:var(--r-md);background:var(--card)}
.jpick>summary{display:flex;align-items:center;gap:8px;flex-wrap:wrap;
  padding:0 12px;min-height:32px;cursor:pointer;font-size:12.5px;color:var(--ink-soft);
  list-style:none}
.jpick>summary::-webkit-details-marker{display:none}
.jpick>summary::after{content:"\25BE";color:var(--muted);font-size:10px;margin-left:auto}
.jpick[open]>summary::after{content:"\25B4"}
.jpick>summary:focus-visible{outline:2px solid var(--accent);outline-offset:1px}
@media (pointer:coarse){.jpick>summary{min-height:44px}}
.jp-lab{text-transform:uppercase;letter-spacing:.07em;font-size:11px;font-weight:700;color:var(--faint)}
.jp-cur{font-weight:700;color:var(--ink)}
.jp-cnt{font-size:11px;color:var(--faint)}
.jp-body{padding:12px;border-top:1px solid var(--line)}
.jp-body fieldset{border:0;margin:0;padding:0}
.jp-note{margin:10px 0 0;font-size:11.5px;color:var(--faint);max-width:70ch}
.picklist{display:grid;grid-template-columns:repeat(auto-fit,minmax(190px,1fr));gap:2px 14px}
.pick{display:flex;align-items:center;gap:8px;font-size:13px;color:var(--ink-soft);
  min-height:32px;cursor:pointer}
@media (pointer:coarse){.pick{min-height:44px}}
.pick input{width:16px;height:16px;flex:none;accent-color:var(--accent)}
.pick.off{color:var(--faint);cursor:not-allowed}
.pickwhy{font-size:11px;color:var(--faint)}

/* A tabbable SVG needs a visible focus ring. The live stylesheet only gave one
   to #lineChart by id, so every generated chart fell back to the UA default -
   which several themes render invisibly against --card. */
.chart:focus-visible{outline:2px solid var(--accent);outline-offset:2px}

/* ---- compare: two-column intro and reference blocks ----------------------
   Prose measures are unchanged (DESIGN.md Density: compact tightens chrome,
   never running text). What changes is that two ~70ch columns sit side by
   side in a 1320px container instead of one column with an empty half. */
@media(min-width:1100px){
  body.cmp .cmp-intro{display:grid;grid-template-columns:1fr 1fr;gap:8px 32px;align-items:start}
  body.cmp .cmp-intro > :only-child{grid-column:1 / -1}
  body.cmp .cmp-intro .lead{margin-top:14px}
  body.cmp .inherited ul,body.cmp .excluded ul{columns:2;column-gap:32px}
  body.cmp .inherited li,body.cmp .excluded li{break-inside:avoid}
}
/* Tiles fill the row whatever the selection size. A fixed three-column grid
   left a hole at two jurisdictions and wrapped awkwardly at four; auto-fit
   replaces two hand-maintained breakpoints with none, and is the same fix the
   index took in QC round 1. */
body.cmp .kpis{grid-template-columns:repeat(auto-fit,minmax(240px,1fr))}
@media(max-width:460px){body.cmp .kpis{grid-template-columns:1fr}}
.cavlist{margin:16px 0 0;padding:14px 16px;border:1px solid var(--line);
  border-radius:var(--r-lg);background:var(--card-2)}
.cavlist h4{margin:0 0 8px;font-size:11.5px;letter-spacing:.06em;text-transform:uppercase;color:var(--muted)}
.cavlist ul{margin:0;padding-left:18px}
.cavlist li{font-size:12.5px;color:var(--ink-soft);line-height:1.6;margin-bottom:8px;max-width:82ch}
.cavlist li b{color:var(--ink)}
@media(min-width:1100px){body.cmp .cavlist ul{columns:2;column-gap:32px}
  body.cmp .cavlist li{break-inside:avoid}}

/* The compare control bar carries three segmented controls and the picker, so
   it stands 105px tall at desktop against the state pages' ~64px. The shared
   `scroll-margin-top:64px` therefore dropped an in-page anchor 41px UNDER the
   sticky bar, hiding the section heading it was aimed at. Measured, not
   guessed. Mobile keeps the shared value: there the bar collapses to a ~48px
   summary row. */
@media(min-width:760px){body.cmp main section{scroll-margin-top:120px}}
/* Chart notes are prose at a 78ch measure; two of them stacked in a 1280px
   panel left the right half of every chart panel empty. They go side by side
   as WHOLE notes - `column-count` on the paragraphs themselves would split one
   paragraph down two columns, which is harder to read than the empty space it
   was fixing. */
.notegrid{margin-top:12px;padding-top:12px;border-top:1px solid var(--line)}
.notegrid .chart-note{border-top:none;padding-top:0;margin:0}
@media(min-width:1100px){
  body.cmp .notegrid{display:grid;grid-template-columns:1fr 1fr;gap:8px 32px;align-items:start}
}
/* A horizontally scrollable region needs keyboard access (axe
   `scrollable-region-focusable`, serious). `.provwrap` had `overflow-x:auto`
   and no tab stop, so a keyboard user could not reach the right-hand columns
   of any provenance table on any page. It went unseen because the existing
   audit runs with the disclosures CLOSED, and axe does not audit inside a
   closed <details> - the compare spec opens them all first, which is what
   surfaced it. */
.provwrap:focus-visible{outline:2px solid var(--accent);outline-offset:2px}

/* ---- metric definitions (Chris, 2026-08-23 staging review; pre-launch) ----
   The info control beside every metric label, and the ONE popover that shows
   the state file's own definition prose (statepage.js: defFor / DEFS). Real
   button, disclosure semantics; hover+focus on desktop, tap on mobile. The
   popover is position:fixed and body-mounted so no .kpi{overflow:hidden} or
   revealed-section transform can clip or re-anchor it; statepage.js flips and
   clamps it inside the viewport and qc/defs_probe.mjs asserts that at all
   four estate widths. Colours are existing tokens only: the glyph is
   var(--muted), which the .kpi .label beside it already uses at 11.5px, and
   separation is a 1px hairline, never a shadow (DESIGN.md). */
.def-btn{display:inline-flex;align-items:center;justify-content:center;
  width:16px;height:16px;margin-left:6px;padding:0;vertical-align:-2px;
  border:1px solid var(--line-strong);border-radius:var(--r-pill);
  background:transparent;color:var(--muted);cursor:pointer;position:relative;
  font:italic 700 10.5px/1 Georgia,'Times New Roman',serif;text-transform:none;flex:none}
.def-btn::after{content:"";position:absolute;inset:-4px}  /* 24px touch target */
.def-btn:hover,.def-btn[aria-expanded="true"]{color:var(--ink);border-color:var(--ink-soft)}
.def-btn:focus-visible{outline:2px solid var(--accent);outline-offset:2px}
.panel h3 .def-btn,.sec-head .def-btn{vertical-align:1px}
.defpop{position:fixed;z-index:70;background:var(--card);color:var(--ink-soft);
  border:1px solid var(--ink-soft);border-radius:var(--r-md);padding:12px 14px;
  overflow-y:auto;overflow-wrap:break-word;font-size:12.5px;line-height:1.55;
  text-transform:none;letter-spacing:normal}
.defpop:focus-visible{outline:2px solid var(--accent);outline-offset:2px}
.defpop[hidden]{display:none}
.defpop p{margin:0 0 8px}
.defpop .defent>p:last-child{margin-bottom:0}
.defent+.defent{border-top:1px solid var(--line);margin-top:10px;padding-top:10px}
.defent-name{display:block;font-size:11px;font-weight:700;letter-spacing:.06em;
  text-transform:uppercase;color:var(--muted);margin-bottom:4px}
.defmeta{font-size:11.5px;color:var(--muted)}
.defmeta b{font-size:10.5px;letter-spacing:.05em;text-transform:uppercase;
  color:var(--ink-soft);margin-right:2px}
/* THE 44PX TOUCH FLOOR IS THE ELEMENT'S OWN BOX, NOT A HALO (staging-a11y
   "every control meets the touch floor where the pointer is coarse" measures
   getBoundingClientRect on every button - the ::after halo above cannot
   satisfy it, and the 16px control failed that gate on all four mobile
   projects the first full-matrix run). On a coarse pointer the button
   becomes a 44x44 box, absolutely positioned WITHOUT offsets so it anchors
   at its own static inline position - the label's line box neither grows nor
   reflows - and the visible 18px ring is drawn by ::before, centred in the
   hit box where the fine-pointer border was. Fine pointers keep the 16px
   control: a 44px invisible hover target beside a tile label would open the
   popover from hovers meant for the text. */
@media (pointer:coarse){
  .def-btn{position:absolute;width:44px;height:44px;margin:-15px 0 0 -10px;
    border:none;background:transparent}
  .def-btn::after{content:none}
  .def-btn::before{content:"";position:absolute;inset:13px;
    border:1px solid var(--line-strong);border-radius:var(--r-pill)}
  .def-btn:hover::before,.def-btn[aria-expanded="true"]::before{border-color:var(--ink-soft)}
}
/* Motion policy: appear-in only under the reader's no-preference, and nothing
   moves - opacity alone, 120ms. Base state is fully visible. */
@media (prefers-reduced-motion: no-preference){
  .defpop{animation:defpop-in .12s ease-out}
  @keyframes defpop-in{from{opacity:0}to{opacity:1}}
}

/* ============================================================================
   FLAGSHIP MAP - index.html only (Chris's ruling, 2026-08-23)
   ============================================================================
   The structure it styles, generated by sitegen/usmap.py:

     .usmap-shell            the section: value prop, proof strip, figure, freshness
       .usmap-stage          grid: [ map box | card slot ]
         .usmap-box          position:relative - the svg plus the callout chips
           svg.usmap-svg     51 shapes, drawn from <defs> by <use>
           a.usmap-chip      the nine jurisdictions too small to hit on the map
         .usmap-cardslot     one card per jurisdiction, stacked, one visible

   THE COLOURS ARE MEASURED, NOT CHOSEN. build.py:map_palette_problems()
   recomputes all of it on every run and REFUSES the build if it stops being
   true, the same way palette_problems() does for the chart series:

     * each of the three DATA fills clears 3:1 against every surface the map is
       drawn on in its own theme (--paper, --card, --card-2), because a fill
       that carries meaning is a graphical object under WCAG 1.4.11;
     * every pair of them is >= 15 L* apart, so the three classes survive
       greyscale and print;
     * colour is never the only channel. "No usable public data" also carries a
       diagonal hatch, "Not in the 2026 sweep" a dotted outline, and every
       shape's accessible name states its class in words.

     MEASURED, light:  full #143c28 L* 22.1 | caveat #2b6a4a L* 40.1
                       absent #738c7f L* 56.0
                       worst-vs-surface 11.31 / 5.92 / 3.34 ; min pair dL* 15.9
     MEASURED, dark:   full #a5d7be L* 82.0 | caveat #4ea67a L* 61.9
                       absent #607168 L* 46.0
                       worst-vs-surface 10.13 / 5.47 / 3.15 ; min pair dL* 15.9

   --map-outside is NOT one of the three data fills and is not held to the 3:1
   floor: it is the ground under a jurisdiction the sweep did not cover, and
   the mark that carries its meaning is the dashed outline, not the fill. It
   still had to move, though. It started at #151c18, one step off the dark
   theme's --card #161d18 - and since the ocean IS --card showing through, the
   eight out-of-scope states read as holes in the coastline rather than as
   land with nothing published. Both themes now sit a visible step off the
   card they are drawn on.

   The two themes run in OPPOSITE directions - light's "full" is the darkest of
   the three and dark's is the lightest - for the same arithmetic reason
   --series-1 is near-black in light and near-white in dark: the 3:1 floor
   against a near-white page puts the whole usable window below L* 57, and
   against a near-black one puts it above L* 44.

   "Absent" is deliberately DESATURATED rather than merely darker. A dim green
   would have read as a weaker version of the same claim; a grey-green reads as
   the absence it is, while still clearing the contrast floor.
   ========================================================================== */
:root{
  --map-full:#143c28; --map-caveat:#2b6a4a; --map-absent:#738c7f;
  --map-outside:#e2e7e1; --map-edge:#ffffff; --map-outline:#c2ccc4;
  --map-absent-chip:#7d9689;
  --map-hatch:#31473b;
}
@media (prefers-color-scheme: dark){
  :root{
    --map-full:#a5d7be; --map-caveat:#4ea67a; --map-absent:#607168;
    --map-outside:#232d27; --map-edge:#0e1310; --map-outline:#3b4a41;
    --map-absent-chip:#52625a;
    --map-hatch:#c3cdc6;
  }
}
:root[data-theme="light"]{
  --map-full:#143c28; --map-caveat:#2b6a4a; --map-absent:#738c7f;
  --map-outside:#e2e7e1; --map-edge:#ffffff; --map-outline:#c2ccc4;
  --map-absent-chip:#7d9689;
  --map-hatch:#31473b;
}
:root[data-theme="dark"]{
  --map-full:#a5d7be; --map-caveat:#4ea67a; --map-absent:#607168;
  --map-outside:#232d27; --map-edge:#0e1310; --map-outline:#3b4a41;
    --map-absent-chip:#52625a;
  --map-hatch:#c3cdc6;
}

.usmap-shell{margin-top:2px}
.usmap-vp{margin:0 0 16px;max-width:62ch;font-size:19px;line-height:1.45;
  color:var(--ink);font-weight:500}
.usmap-proof{list-style:none;display:flex;flex-wrap:wrap;gap:6px 16px;
  margin:0 0 18px;padding:0;font-family:ui-monospace,SFMono-Regular,Menlo,Consolas,monospace;
  font-size:11.5px;font-weight:600;letter-spacing:.09em;text-transform:uppercase;
  line-height:1.6;color:var(--muted)}
.usmap-proof b{color:var(--ink);font-size:13px;font-variant-numeric:tabular-nums}
.usmap-proof li+li{position:relative;padding-left:17px}
.usmap-proof li+li::before{content:"";position:absolute;left:0;top:.72em;
  width:9px;height:1px;background:var(--line-strong)}
.usmap-proof a{color:var(--link)}

.usmap{margin:0;padding:16px;background:var(--card);
  border:1px solid var(--line-strong);border-radius:var(--r-lg)}
.usmap-stage{display:grid;grid-template-columns:minmax(0,1fr) 300px;gap:18px;
  align-items:start}
.usmap-box{position:relative;min-width:0}
.usmap-svg{display:block;width:100%;height:auto}

/* ---- the shapes ---------------------------------------------------------
   `--on` is set to 1 by the per-jurisdiction rules the build generates into
   the page, and `--edge` comes with it. Nothing else toggles: one generated
   declaration block, and this sheet decides what "on" looks like for a shape,
   a leader line and a card separately. */
.usmap-land{fill:var(--map-outside);stroke:var(--edge,var(--map-edge));
  stroke-width:calc(0.9px + var(--on,0) * 2.6px);stroke-linejoin:round;
  pointer-events:all}
.usmap-land[data-cls="full"]{fill:var(--map-full)}
.usmap-land[data-cls="caveat"]{fill:var(--map-caveat)}
.usmap-land[data-cls="compare"]{fill:var(--map-caveat)}
.usmap-land[data-cls="absent"]{fill:url(#usmapHatch)}
.usmap-land[data-cls="outside"]{fill:var(--map-outside);
  stroke:var(--edge,var(--map-outline));stroke-dasharray:2.4 2.6}
.usmap-hatchbg{fill:var(--map-absent)}
.usmap-hatch{stroke:var(--map-hatch);stroke-width:1.5;stroke-opacity:.55}

/* THE TARGET IS THE SHAPE, EXACTLY - see usmap.py:_state_markup. A widened
   transparent hit band was tried and removed: SVG hit-testing takes the
   topmost element and the shapes are emitted in postal order, so Wisconsin's
   band sat over Michigan's interior and answered for it. The shapes tile the
   plane without overlapping, so as drawn there is exactly one answer for every
   point. The nine jurisdictions too small to aim at have chips instead. */
.usmap-st{cursor:pointer}
.usmap-svg a.usmap-st:focus{outline:none}
.usmap-svg a.usmap-st:focus-visible .usmap-land{stroke:var(--accent-bright);
  stroke-width:3.4px}

.usmap-lead{stroke:var(--edge,var(--map-outline));stroke-width:1;
  stroke-opacity:calc(.45 + var(--on,0) * .55)}

/* ---- callout chips ------------------------------------------------------
   The nine jurisdictions whose projected bounding box is under 40 viewBox
   units on a side (DC 3.1 through Maryland 32.7; the next one up is Tennessee
   at 42.9). They are the ONLY control for those jurisdictions - their shapes
   on the map are paint, not links - so there is one tab stop each and no
   duplicate in a screen reader's links list.

   Real HTML at a real 44px, positioned by percentage of the svg box. An SVG
   rect would have scaled with the map and been about 18px at 390px wide. */
.usmap-chip{position:absolute;left:var(--cx);top:var(--cy);
  transform:translate(-50%,-50%);
  display:flex;align-items:center;justify-content:center;
  min-width:44px;min-height:44px;padding:0 4px;
  border:1px solid var(--line-strong);border-radius:var(--r-sm);
  font-family:ui-monospace,SFMono-Regular,Menlo,Consolas,monospace;
  font-size:12px;font-weight:700;letter-spacing:.06em;text-decoration:none}
.usmap-chip[data-cls="full"]{background:var(--map-full)}
.usmap-chip[data-cls="caveat"],.usmap-chip[data-cls="compare"]{background:var(--map-caveat)}
/* THE CHIP DOES NOT USE THE MAP'S "ABSENT" FILL, and it is the only class
   where they differ. --map-absent has to clear 3:1 against the card it is
   drawn on AND stay 15 L* from the other two fills, which pins it mid-grey -
   and at mid-grey NO ink clears 4.5:1 behind 12px bold type. Measured on
   #607168: near-black ink 3.63, near-white 4.39, and the axe gate on
   /index.html named exactly that (the New Hampshire chip, 3.62:1) across four
   projects. The two constraints genuinely conflict, so the CONTROL takes a
   fill chosen for its text - same hue family, one step further from the ink -
   while the MAP MARK keeps the measured ordinal ramp. Everything else uses
   the map fill unchanged. */
.usmap-chip[data-cls="absent"]{background:var(--map-absent-chip)}
.usmap-chip[data-cls="outside"]{background:var(--map-outside);
  border-color:var(--map-outline)}
/* Ink per fill, so the two letters clear 4.5:1 on every class in both themes:
   the light theme's fills run dark and the dark theme's run light, so the
   label follows the fill rather than the page. */
:root .usmap-chip[data-cls="full"],:root .usmap-chip[data-cls="caveat"],
:root .usmap-chip[data-cls="compare"]{color:#ffffff}
:root .usmap-chip[data-cls="absent"],:root .usmap-chip[data-cls="outside"]{color:#17211b}
@media (prefers-color-scheme: dark){
  :root .usmap-chip[data-cls="full"],:root .usmap-chip[data-cls="caveat"],
  :root .usmap-chip[data-cls="compare"]{color:#0e1310}
  :root .usmap-chip[data-cls="absent"],:root .usmap-chip[data-cls="outside"]{color:#e8eee9}
}
:root[data-theme="light"] .usmap-chip[data-cls="full"],
:root[data-theme="light"] .usmap-chip[data-cls="caveat"],
:root[data-theme="light"] .usmap-chip[data-cls="compare"]{color:#ffffff}
:root[data-theme="light"] .usmap-chip[data-cls="absent"],
:root[data-theme="light"] .usmap-chip[data-cls="outside"]{color:#17211b}
:root[data-theme="dark"] .usmap-chip[data-cls="full"],
:root[data-theme="dark"] .usmap-chip[data-cls="caveat"],
:root[data-theme="dark"] .usmap-chip[data-cls="compare"]{color:#0e1310}
:root[data-theme="dark"] .usmap-chip[data-cls="absent"],
:root[data-theme="dark"] .usmap-chip[data-cls="outside"]{color:#e8eee9}
.usmap-chip:hover{border-color:var(--ink)}
.usmap-chip:focus-visible{outline:2px solid var(--accent);outline-offset:2px}

/* ---- the card ----------------------------------------------------------- */
/* ALL CARDS IN ONE GRID CELL. Every card is placed at 1/1, so they stack and
   the row's height is the height of the TALLEST of them - the slot sizes
   itself to its worst case instead of being told a number.

   It was told a number first (min-height:268px), and the probe's FIT arm
   found 156 overflows across the four widths: Montana's card ran 90px past
   the bottom at 1920, and the failures moved around as the column width
   changed, which is what a magic number does. Nothing here can overflow now,
   at any width, in any theme, whatever a state file's prose does next. */
.usmap-cardslot{display:grid;border:1px solid var(--line);
  border-radius:var(--r-lg);background:var(--card-2);padding:16px}
.usmap-card{grid-area:1/1;opacity:var(--on,0);pointer-events:none}
.usmap-card--idle{opacity:1}
/* The trigger must match usmap.hover_css exactly. The nine callout states have
   their landmass OUTSIDE any .usmap-st, so keying this on .usmap-st alone left
   the idle panel visible underneath their card - two blocks of prose stacked on
   top of each other, which is worse than the no-hover bug it followed. */
.usmap-stage:has(:is(.usmap-st,.usmap-land-callout):is(:hover,:focus)) .usmap-card--idle{opacity:0}
.usmap-cname{margin:0 0 6px;font-family:Georgia,serif;font-size:19px;
  font-weight:700;line-height:1.2;color:var(--ink)}
.usmap-ccls{margin:0 0 12px;font-family:ui-monospace,SFMono-Regular,Menlo,Consolas,monospace;
  font-size:10.5px;font-weight:700;letter-spacing:.11em;text-transform:uppercase;
  color:var(--muted)}
.usmap-cfig{margin:0;font-size:30px;font-weight:700;line-height:1.05;
  color:var(--ink);letter-spacing:-.01em}
.usmap-cfield{margin:4px 0 12px;font-size:13px;line-height:1.4;color:var(--ink-soft)}
.usmap-cper{display:block;font-family:ui-monospace,SFMono-Regular,Menlo,Consolas,monospace;
  font-size:11px;letter-spacing:.06em;text-transform:uppercase;color:var(--muted)}
.usmap-cflag{margin:0 0 8px;display:inline-block;background:var(--accent-soft);
  color:var(--accent);border-radius:var(--r-sm);padding:2px 8px;font-size:11px;
  font-weight:700}
.usmap-cline{margin:0;font-size:13px;line-height:1.5;color:var(--ink-soft)}
.usmap-cgo{margin:10px 0 0;font-size:12.5px;font-weight:700;color:var(--link)}
.usmap-cgo::after{content:" \2192"}

/* ---- legend, territories, caption, freshness ---------------------------- */
.usmap-legend{list-style:none;display:flex;flex-wrap:wrap;gap:6px 18px;
  margin:16px 0 0;padding:14px 0 0;border-top:1px solid var(--line);
  font-size:12.5px;color:var(--ink-soft)}
.usmap-key{display:flex;align-items:center;gap:8px}
.usmap-key b{font-variant-numeric:tabular-nums;color:var(--ink)}
.usmap-sw{width:15px;height:15px;border-radius:3px;flex:none;
  border:1px solid var(--map-edge);background:var(--map-outside)}
.usmap-key[data-cls="full"] .usmap-sw{background:var(--map-full)}
.usmap-key[data-cls="caveat"] .usmap-sw,
.usmap-key[data-cls="compare"] .usmap-sw{background:var(--map-caveat)}
.usmap-key[data-cls="absent"] .usmap-sw{background:var(--map-absent);
  background-image:repeating-linear-gradient(45deg,transparent 0 3px,
    var(--map-hatch) 3px 4.5px)}
.usmap-key[data-cls="outside"] .usmap-sw{border:1px dashed var(--map-outline)}

.usmap-others{margin:16px 0 0;padding:14px 0 0;border-top:1px solid var(--line)}
.usmap-others h3{margin:0 0 2px;font-size:13px;color:var(--ink)}
.usmap-others>p{margin:0 0 10px;font-size:12.5px;color:var(--muted)}
.usmap-others ul{list-style:none;display:flex;flex-wrap:wrap;gap:8px;margin:0;padding:0}
.usmap-orow{display:flex;flex-direction:column;gap:1px;min-height:44px;
  justify-content:center;padding:6px 12px;text-decoration:none;
  border:1px solid var(--line-strong);border-radius:var(--r-md);
  background:var(--card-2)}
.usmap-orow b{font-size:13px;color:var(--ink)}
.usmap-orow span{font-family:ui-monospace,SFMono-Regular,Menlo,Consolas,monospace;
  font-size:10px;letter-spacing:.09em;text-transform:uppercase;color:var(--muted)}
.usmap-orow:hover{border-color:var(--ink-soft)}
.usmap-orow:focus-visible{outline:2px solid var(--accent);outline-offset:2px}

.usmap-figcap{margin:16px 0 0;padding:14px 0 0;border-top:1px solid var(--line);
  font-size:12.5px;line-height:1.55;color:var(--muted)}
.usmap-figcap p{margin:0 0 8px;max-width:86ch}
.usmap-figcap p:last-child{margin-bottom:0}
.usmap-figcap b{color:var(--ink-soft)}
.usmap-credit{font-size:11.5px}

.usmap-fresh{margin:22px 0 0}
.usmap-fresh h3{margin:0 0 2px;font-family:Georgia,serif;font-size:17px;color:var(--ink)}
.usmap-fresh>p{margin:0 0 12px;font-size:12.5px;color:var(--muted);max-width:80ch}
.usmap-fresh ul{list-style:none;display:grid;
  grid-template-columns:repeat(auto-fit,minmax(180px,1fr));gap:8px;margin:0;padding:0}
.usmap-fresh a{display:flex;flex-direction:column;gap:2px;min-height:44px;
  justify-content:center;padding:10px 14px;text-decoration:none;
  border:1px solid var(--line);border-left:3px solid var(--accent);
  border-radius:var(--r-md);background:var(--card)}
.usmap-fresh a:hover{border-color:var(--line-strong);border-left-color:var(--accent-bright)}
.usmap-fresh a:focus-visible{outline:2px solid var(--accent);outline-offset:2px}
.usmap-fresh b{font-size:14px;color:var(--ink)}
.usmap-fper{font-family:ui-monospace,SFMono-Regular,Menlo,Consolas,monospace;
  font-size:11px;letter-spacing:.05em;text-transform:uppercase;color:var(--ink-soft)}
.usmap-fage{font-size:11.5px;color:var(--muted);font-variant-numeric:tabular-nums}

/* ---- narrow: the card drops below the map, the chips become a row -------
   1160px IS ARITHMETIC, not a round number. The chips are 44px tall and sit 54
   viewBox units apart down a 942-unit-wide box, so they stop fitting inside
   the map's own height once 54 * (map width / 942) drops below 44 - i.e. once
   the map column is under ~768px. Add the 300px card column, the 18px gap and
   the padding and that is a ~1160px viewport; below it the nine chips would
   overlap each other and the northern ones would collide with Maine.

   So below 1160 the card drops under the map and the chips leave absolute
   positioning to flow as a wrapped row, keeping their 44px targets and their
   single tab stop. The leader lines are hidden with them, because a line
   pointing at nothing is worse than no line. */
@media (max-width:1160px){
  .usmap-stage{grid-template-columns:minmax(0,1fr)}
  /* THE CARD GOES ABOVE THE MAP HERE, and that is not a stylistic preference.
     Stacked in source order the card sits below the map AND below the flowed
     chip row, which on a phone put it off the bottom of the screen: the first
     tap on Florida opened a card the reader could not see, which is the whole
     interaction failing silently. Visual order only - the DOM is untouched, so
     the tab order still runs through the map and nothing needs script. */
  .usmap-cardslot{order:-1}
  .usmap-box{display:flex;flex-wrap:wrap;gap:6px;overflow:hidden}
  /* RECLAIM THE CHIP GUTTER. The viewBox carries ~10% of dead width on its
     right for the callout column, and once the chips have flowed below the map
     that column is empty - at 390px it was costing the map 20px of height it
     could not spare. The svg is widened past its box by exactly that fraction
     (Maine's east coast ends at 89.2% of the frame) and the empty remainder is
     clipped. Nothing is cut off but ocean, and the map gains ~11% in both
     dimensions for three declarations and no change to a coordinate. */
  .usmap-svg{flex:0 0 112%;width:112%;max-width:none;margin-right:-12%}
  .usmap-chip{position:static;transform:none}
  .usmap-lead{display:none}
  .usmap-vp{font-size:17px}
}
@media (max-width:520px){
  .usmap{padding:12px}
  .usmap-cfig{font-size:26px}
}

/* ---- motion (DESIGN.md - Motion Policy) ---------------------------------
   ONE entrance, once, and only for a reader who has not asked for less: the
   51 shapes fade up in geographic order 5ms apart, finishing inside the
   550ms entrance cap. This is the sanctioned "map dot pop-in" generalised from
   dots to shapes - the same idea the finder uses, on the same gate.

   Base state is the FINISHED state, so reduced motion, no-JS, and the frames
   before html.mo is set all paint the completed map. Opacity only: the shapes
   never move, so nothing here can fight a hover state or shift layout.

   The card and the shapes keep their 120-140ms feedback transitions in the
   no-preference path; those are state changes, not entrance, and DESIGN.md
   keeps them. Under reduce they are switched off with everything else. */
@media (prefers-reduced-motion: no-preference){
  .usmap-card{transition:opacity .12s ease-out}
  .usmap-land{transition:fill .14s ease-out,stroke-width .14s ease-out}
  @keyframes usmap-in{from{opacity:0}to{opacity:1}}
  /* THE ARITHMETIC IS THE CAP. 51 shapes at a 5ms stagger put the last one's
     delay at 250ms, and 250 + 280 = 530ms, inside DESIGN.md's 550ms entrance
     cap with 20ms to spare. The first version used a 9ms stagger and a 420ms
     duration and ran 870ms - and every still frame in the review set passed it,
     because a still cannot see a duration. qc/usmap_motion.mjs measures the
     real timings out of getAnimations() and is what caught it; if the shape
     count or the stagger changes, that is what will catch it again. */
  html.mo .usmap-land{animation:usmap-in .28s cubic-bezier(.2,.7,.3,1) both;
    animation-delay:calc(var(--i,0) * 5ms)}
  html.mo .usmap-chip,html.mo .usmap-lead{
    animation:usmap-in .28s cubic-bezier(.2,.7,.3,1) both;animation-delay:.25s}
}

/* ---- dispensary directory ------------------------------------------------
   Cards for the six jurisdictions, then the on-demand records table. Uses the
   existing card/line/accent tokens rather than introducing a second palette. */
.dsp-grid{display:grid;grid-template-columns:repeat(auto-fit,minmax(320px,1fr));
  gap:16px;margin:28px 0}
.dsp-card{background:var(--card);border:1px solid var(--line);
  border-radius:var(--r-lg);padding:18px 20px;display:flex;flex-direction:column;gap:10px}
.dsp-card header{display:flex;align-items:baseline;justify-content:space-between;gap:12px}
.dsp-card h3{margin:0;font-size:17px}
.dsp-n{font-size:12px;color:var(--faint);white-space:nowrap}
.dsp-status{margin:0;font-size:13.5px;color:var(--ink-soft);
  display:flex;flex-wrap:wrap;gap:4px 10px;align-items:baseline}
.dsp-status b{color:var(--ink)}
/* Not-published is stated, never implied by an empty cell or a dash. */
.dsp-nostatus{color:var(--muted);font-style:italic}
.dsp-opened{display:block;width:100%;color:var(--ink-soft)}
.dsp-op{margin:0;font-size:13px;color:var(--muted)}
.dsp-caveat{margin:0;font-size:12.5px;color:var(--muted);max-width:60ch}
.dsp-src{margin:0;font-size:12px;color:var(--faint)}
.dsp-open{font-family:inherit;font-size:13px;margin-top:auto;align-self:flex-start;
  background:transparent;color:var(--link);border:1px solid var(--line-strong);
  border-radius:var(--r-pill);padding:0 14px;min-height:36px;cursor:pointer}
.dsp-open:hover{border-color:var(--accent);color:var(--accent-bright)}
.dsp-open[disabled]{opacity:.6;cursor:progress}

.dsp-table{margin:28px 0;background:var(--card);border:1px solid var(--line);
  border-radius:var(--r-lg);padding:18px 20px}
.dsp-thead{display:flex;flex-wrap:wrap;gap:10px 16px;align-items:center;margin-bottom:12px}
.dsp-thead h3{margin:0;font-size:17px;flex:0 0 auto}
#dsp-q{flex:1 1 260px;font-family:inherit;font-size:13px;min-height:36px;
  padding:0 12px;background:var(--card-2);color:var(--ink);
  border:1px solid var(--line-strong);border-radius:var(--r-pill)}
.dsp-count{font-size:12px;color:var(--faint);white-space:nowrap}
.dsp-cap{margin:0 0 10px;font-size:12.5px;color:var(--muted)}
.dsp-err{margin:0;font-size:13.5px;color:var(--down)}
/* WIDE CONTENT SCROLLS INSIDE ITS OWN BOX - the page body must never scroll
   sideways on a phone. */
.dsp-t{width:100%;border-collapse:collapse;font-size:13px;display:block;overflow-x:auto}
.dsp-t th{text-align:left;font-size:11px;text-transform:uppercase;
  letter-spacing:.06em;color:var(--faint);font-weight:700;
  padding:0 14px 8px 0;border-bottom:1px solid var(--line-strong);white-space:nowrap}
.dsp-t td{padding:9px 14px 9px 0;border-bottom:1px solid var(--line);
  vertical-align:top;color:var(--ink-soft)}
.dsp-t tbody tr:hover td{background:var(--card-2)}
.dsp-ent{display:block;font-size:11.5px;color:var(--faint)}
.dsp-lic{font-variant-numeric:tabular-nums;color:var(--muted);white-space:nowrap}
.dsp-unk{color:var(--muted);font-style:italic}
.dsp-none{color:var(--muted);padding:16px 0}
.dsp-active{color:var(--accent-bright)}
.dsp-inactive{color:var(--down)}
.dsp-pending,.dsp-other{color:var(--ink-soft)}
.dsp-op2{display:block;font-size:11.5px;color:var(--faint)}

/* Cross-link from a state page to its rows in the dispensary directory. Sits
   between the last data section and provenance - after the figures, before the
   caveats about them. */
.dsp-xlink{margin:28px 0 0;padding:14px 18px;background:var(--card);
  border:1px solid var(--line);border-radius:var(--r-lg);
  font-size:13.5px;color:var(--muted)}
.dsp-xlink a{font-weight:700;color:var(--link);text-decoration:none}
.dsp-xlink a:hover{color:var(--accent-bright);text-decoration:underline}
.dsp-xlink b{color:var(--ink)}

/* ---- publication notices -------------------------------------------------
   PORTED from the legacy DC register with its decision intact: a full-width
   band that reads as site infrastructure, sitting with sourcing and
   corrections rather than as a promotional card. Deliberately not a modal,
   popup or scroll interstitial - the site is open and signup is a
   convenience. */
.subs{background:var(--accent-soft);border-top:1px solid var(--line);
  border-bottom:1px solid var(--line);padding:32px 16px;margin:40px 0 0}
.subs-grid{max-width:1120px;margin:0 auto;display:grid;gap:20px}
.subs h4{font-family:Georgia,"Times New Roman",serif;font-size:22px;
  line-height:1.2;font-weight:700;margin:0 0 6px}
/* 14.5px matches .lead / .qa p / .tierlead - the size this site
   already uses for public explanatory prose. 13px is the ramp's
   "dense table cells" size and reads as chrome, which this is not. */
.subs-copy p{margin:0;color:var(--muted);font-size:14.5px}
.subs-lab{margin:0 0 6px;font-size:11px;text-transform:uppercase;
  /* --muted, NOT --faint. At 11px on the band's --accent-soft ground, --faint
     measures 4.30:1 light / 4.45:1 dark - under the 4.5:1 that DESIGN.md makes
     the CONDITION of using 11px at all. --muted is 4.82 / 5.94, and is already
     what .label (this site's other uppercase micro-label) uses. Axe caught
     this; reviewing the screenshot did not. */
  letter-spacing:.07em;font-weight:700;color:var(--muted)}
.subs-note{margin:8px 0 0;font-size:14.5px;color:var(--muted);max-width:64ch}
.subs-feedurl{margin:10px 0 0;overflow-wrap:anywhere}
.subs-feedurl a{color:var(--link);text-decoration:none}
.subs-feedurl a:hover{text-decoration:underline}
.subs-feedurl code{font-size:14px}
/* The push control is injected only where it can work; until then this is
   hidden and the feed stands alone. */
.subs-push{margin:14px 0 0;display:flex;flex-wrap:wrap;gap:10px 14px;align-items:center}
.subs-push button{font-family:inherit;font-size:16px;min-height:44px;
  padding:0 18px;cursor:pointer;background:transparent;color:var(--link);
  border:1px solid var(--line-strong);border-radius:var(--r-pill)}
.subs-push button:hover:not([disabled]){border-color:var(--accent);color:var(--accent-bright)}
.subs-push button[aria-pressed="true"]{border-color:var(--accent);color:var(--accent-bright)}
.subs-push button[disabled]{opacity:.6;cursor:not-allowed}
.push-msg{font-size:14px;color:var(--muted)}
@media(min-width:900px){
  .subs{padding:40px 24px}
  .subs-grid{grid-template-columns:minmax(240px,1fr) 2fr;gap:40px;align-items:start}
}

/* Licence line in the footer. --muted, not --faint: this is running prose at
   the size .lead/.qa p use, and --faint measured under 4.5:1 on the band
   ground earlier today. Sits with sourcing and corrections because that is
   what it is - a statement about the data, not a promotion. */
.lic{margin:10px 0 0;font-size:14.5px;color:var(--muted);max-width:72ch}
/* UNDERLINED BY DEFAULT, and it must stay that way. This link sits inside a
   sentence, so axe's link-in-text-block applies: a link in running prose may
   not be distinguished by colour alone unless it clears 3:1 against the text
   around it. --link on --muted measured 1.23:1, and the gate failed on all
   four mobile projects. Underline is the fix; do not "tidy" it away. */
.lic a{color:var(--link);text-decoration:underline;text-underline-offset:2px}
.lic a:hover,.lic a:focus-visible{text-decoration-thickness:2px}

/* Alcohol comparison panel. The scale note is load-bearing rather than
   decorative: it is where the page says this axis is never shared with the
   cannabis charts, so it sits with the chart and not in the notes below. */
.alc-scale{margin:10px 0 0;font-size:14.5px;color:var(--muted);max-width:72ch}
.chart-legend .lg-note{color:var(--faint);font-size:11.5px;font-weight:400}
