/* ==========================================================================
   Counter — dashboard styles

   DESIGN NOTE
   -----------
   This is an operator's instrument panel for a network of sites, not a
   marketing dashboard. The reference points are plotting charts and signals
   monitoring: a pale chart ground, ink linework, tabular numerals, hairline
   rules, and almost no chrome.

   The one rule that governs the palette: AMBER MEANS LIVE. Nothing that is
   not happening right now is ever amber. Historical data is ink and slate;
   a hit that arrived seconds ago is amber and cools back to ink as it ages.
   Colour encodes recency, so "what is happening now" is readable without
   reading a single timestamp. Do not spend amber on anything else — the
   moment it becomes a generic accent, the signal is gone.

   No build step, no CDN, no webfonts. System stacks only; the typographic
   personality comes from treatment (tabular monospace at display sizes,
   wide-tracked uppercase micro-labels) rather than from a purchased face.
   ========================================================================== */

/* --------------------------------------------------------------- tokens */

:root {
  /* Design-system scales. Use these for new work; component-specific sizes
     remain named where their meaning is more useful than a scale step. */
  --space-0: 0;
  --space-1: 4px;
  --space-2: 8px;
  --space-3: 12px;
  --space-4: 16px;
  --space-5: 20px;
  --space-6: 24px;
  --space-7: 28px;
  --space-8: 32px;
  --space-10: 40px;
  --space-12: 48px;
  --space-16: 64px;

  /* IMP-0099. The WCAG 2.5.5/2.5.8 touch-target floor, named once. Every
     interactive control in the dashboard — .btn, select, .danger, the shared
     form-control rule below, the map/pager buttons — was already built to
     this exact figure; this only gives the repeated literal a name so a
     future control can be written against the rule rather than against the
     number. Applied here at the two shared control rules (this file's
     `select, .btn` and the `:is(.account-form, ...)` input block); the
     remaining literal `44px`s elsewhere in this file are the same constant,
     not yet retrofitted — see docs/design/DESIGN_SYSTEM.md. */
  --control-height: 44px;

  --text-xs: 10px;
  /* IMP-0221. The step between --text-xs and --text-sm, named for the same
     reason --control-height was: nineteen rules already used the bare 11px,
     across the two most-read regions of the product (the dashboard's .delta,
     .stale-label, .panel-body h3, .posture-tile span; the auth and landing
     surfaces' .auth-field small, .auth-check, .landing-kicker,
     .landing-canada-note). A figure used nineteen times is a scale step in
     everything but name, and a name lets the next rule be written against the
     step rather than against the number.

     Its role is annotation: secondary text sitting beside a primary value —
     the delta beside a metric, the staleness caption beside a panel, the hint
     beside a field. It is NOT the uppercase micro-label; that is `.label` at
     --text-xs. Naming a role rather than extending the t-shirt sequence
     follows --text-ui (13px), which sits between sm and base for the same
     reason. */
  --text-note: 11px;
  --text-brand-micro: 9px;
  --text-map-detail: 10.5px;
  --text-live-row: 11.5px;
  --text-compact-value: 12.5px;
  --text-chart-tick: 9.5px;
  --text-sm: 12px;
  --text-ui: 13px;
  --text-base: 14px;
  --text-lg: 16px;
  --text-xl: 18px;
  --text-2xl: 24px;

  /* IMP-0225. The page title, named once per shell. There were five clamps
     for one role and three of them sat inside the SAME wide shell:
     .help-hero at 62px, .tool-head at 68px, and .demo-head at 58px with a 5vw
     slope instead of 6vw, so /demo drifted away from its neighbours mid-resize
     rather than only at the ends. None of those differences was chosen; they
     are the residue of hero blocks written on different days.

     Two values because there are two shells (see the layout section of
     DESIGN_SYSTEM.md): the wide .help-shell carries a 1120px column, the
     document .legal-shell a 780px measure, and a title proportionate to one
     is not proportionate to the other. The dashboard's .account-hero and the
     landing/auth surfaces keep their own scale — those are different
     surfaces, not the same role at a different size. */
  --text-title: clamp(34px, 6vw, 62px);
  --text-title-doc: clamp(34px, 6vw, 58px);

  /* Custom properties cannot be interpolated into media-query conditions.
     These document the responsive contract; queries below use the same
     literal values: wide 1080px, compact 900px, narrow 760px, phone 480px. */
  --breakpoint-wide: 1080px;
  --breakpoint-compact: 900px;
  --breakpoint-narrow: 760px;
  --breakpoint-phone: 480px;

  --motion-fast: 150ms;
  --motion-base: 160ms;
  --motion-slow: 240ms;

  --radius-brand-sm: 8px;
  --radius-brand-mark: 9px;
  --radius-brand-control: 10px;
  --radius-brand-grid: 15px;
  --radius-brand-panel: 18px;
  --radius-brand-card: 20px;
  --radius-brand-halo: 32px;

  /* Chart paper */
  --paper:        #eef2f5;
  --paper-raised: #f8fafb;
  --paper-sunk:   #e4eaef;

  /* Ink.

     BUG-0051: --ink-faint was #8ba0ad, which measures 2.41:1 on --paper,
     2.60:1 on --paper-raised and 2.24:1 on the --paper-sunk rail — well under
     the WCAG AA 4.5:1 floor for normal text. It is not a decorative token:
     it colours .label, table th, .delta, td.n.muted, .note, .empty and the
     chart ticks, so the smallest type in the product was also the least
     legible, and the rail's headings sat on the worst background of the three.
     #536b7a measures 4.97 / 5.35 / 4.61 on the same three papers. The quiet
     of the label now comes from its size, tracking and weight rather than
     from being too pale to read. Amber (--signal) is not an option here: it
     means "happening right now" and nothing else. */
  --ink:      #16303f;
  --ink-soft: #5a7183;
  --ink-faint:#536b7a;

  /* Structure */
  --rule:  #cbd8e1;
  --grid:  #dde7ed;

  /* The basemap reads as a MAP, the way StatCounter's does: warm land against
     blue water, with the coastline carried by the colour change rather than by
     a stroke. Sampled off their visitor map — land #e8e5d8, water #94c1e1 —
     then pulled back a little, because theirs is a photographic tile raster
     and this is a flat vector that sits inside a page. */
  --land:        #e6e2d6;
  --ocean:       #b9d5ea;
  --border-line: #b6b1a4;

  /* Map dots. Red/coral is absent from the beige-and-blue basemap, so even a
     compact circle remains legible without the visual bulk of a teardrop.
     --signal still owns "just arrived"; this owns "a visitor is here". */
  --map-dot:      #b93d3c;
  --map-dot-edge: rgba(255, 255, 255, 0.9);
  --canada:   #c73531;

  /* Data series — deliberately not a brand colour, just legible against ink */
  --series:      #2e6b7e;
  --series-soft: rgba(46, 107, 126, 0.14);

  /* LIVE. Reserved. See the note above. */
  --signal:      #cf6a1f;
  --signal-soft: rgba(207, 106, 31, 0.13);

  /* Audit #81 (BUG-0079). --signal itself measures only 3.2531:1 for
     #cf6a1f on --paper (#eef2f5) — under the WCAG AA 4.5:1 floor for the
     .live-pill's 12px text — but it must not change: CLAUDE.md reserves it
     everywhere else for "happening right now", and diluting it site-wide to
     fix one selector is exactly the mistake that note warns against. This
     token exists ONLY for .live-pill's text and is not for reuse elsewhere.
     #9c4e17 is the same hue/saturation family as --signal (24.8°/74.3% vs
     25.6°/73.9%), just darker, so the pill still reads amber/urgent — its
     border and dot keep the brighter --signal. Measures 5.3061:1 on
     --paper, comfortably over 4.5. Dark mode needs no separate shade (see
     below); this is the light-mode value. */
  --live-pill-ink: #9c4e17;

  --up:   #2f6b4f;
  --down: #9c3b2e;
  /* IMP-0222. The focus halo for the risk-tiered fields, matching the alpha of
     --series-soft below rather than inventing a second weight. It exists
     because .risk-action/.risk-inline inputs carry a --down border as their
     risk grammar, so borrowing --series-soft for their focus ring would put a
     selection colour around a destructive-action field. Same shape as
     --series-soft/--signal-soft: the base hue at low alpha, nothing else. */
  --down-soft: rgba(156, 59, 46, 0.14);

  /* Fixed brand surfaces and primary actions. Unlike the chart-paper tokens,
     these do not change with the user's colour scheme. */
  --surface-brand: #0d1c24;
  --surface-brand-raised: #13242e;
  --surface-brand-sunk: #0a171d;
  --on-brand: #f4f8fb;
  --brand-series: #63aac0;
  --cta-from: #78e7d7;
  --cta-to: #45bdd3;
  --cta-ink: #05131c;
  --cta-shadow: rgba(69, 189, 211, 0.16);

  /* Traffic intensity. A single red/coral family reads as one dataset while
     the increasing depth remains distinct from --series (selection) and
     --signal (live recency). */
  --traffic-1: #c44b46;
  --traffic-2: #b93d3c;
  --traffic-3: #a92e39;
  --traffic-4: #84243a;

  --display: ui-monospace, "SF Mono", "Cascadia Mono", "JetBrains Mono",
             "Roboto Mono", Menlo, Consolas, "Liberation Mono", monospace;
  --sans: system-ui, -apple-system, "Segoe UI", Roboto, "Helvetica Neue",
          Arial, sans-serif;

  --rail-width: 232px;
  --gutter: 28px;
  --radius: 2px;
}

@media (prefers-color-scheme: dark) {
  :root {
    --paper:        #0d1c24;
    --paper-raised: #13242e;
    --paper-sunk:   #0a171d;
    --ink:      #dae7ee;
    --ink-soft: #7d96a5;
    /* BUG-0051, dark half. #57707f measured 3.33:1 on --paper, 3.05:1 on the
       raised panels every table header sits inside and 3.49:1 on --paper-sunk;
       #758e9c measures 5.05 / 4.63 / 5.29. Still dimmer than --ink-soft above
       it, so the two-step ink hierarchy survives the lift. */
    --ink-faint:#758e9c;
    --rule:  #23404f;
    --grid:  #1a3140;
    /* Same relationship, dimmed: land still warmer than the water it sits in,
       both dark enough that the dots carry the page. */
    --land:        #3a3a34;
    --ocean:       #16303f;
    --border-line: #55524a;
    --series:      #63aac0;
    --series-soft: rgba(99, 170, 192, 0.16);
    --map-dot:      #ff6967;
    --map-dot-edge: rgba(4, 14, 20, 0.88);
    --signal:      #ef8f3f;
    --signal-soft: rgba(239, 143, 63, 0.15);
    /* Audit #81 (BUG-0079), dark half. Dark-mode --signal (#ef8f3f) already
       measures 7.1738:1 on dark --paper (#0d1c24) — comfortably over the
       4.5:1 the light-mode override above exists for — so .live-pill's text
       can stay on the same amber as its border/dot here; no separate shade
       is needed in this theme. */
    --live-pill-ink: var(--signal);
    --up:   #5aa37c;
    --down: #d0705f;
    --down-soft: rgba(208, 112, 95, 0.16);
    --traffic-1: #ffaaa0;
    --traffic-2: #ff867a;
    --traffic-3: #ff6967;
    --traffic-4: #ff5268;
  }
}

/* ----------------------------------------------------------------- base */

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

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

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

body {
  margin: 0;
  background: var(--paper);
  color: var(--ink);
  font-family: var(--sans);
  font-size: var(--text-base);
  line-height: 1.5;
  -webkit-font-smoothing: antialiased;
}

/* Audit #94 fixes BUG-0092. Anonymous pages are one public product, not a
   light/dark dashboard skin wrapped around whichever template happens to be
   open. Scope the landing page's fixed teal brand family at body level for
   every signed-out response; landing/auth can still refine their own local
   composition, while help, legal, trust, demo, share and action pages inherit
   the same papers, inks, rules, data colours and browser colour scheme. */
body.public-brand-surface {
  color-scheme: dark;
  --paper: var(--surface-brand);
  --paper-raised: var(--surface-brand-raised);
  --paper-sunk: var(--surface-brand-sunk);
  --ink: var(--on-brand);
  --ink-soft: #afc2d1;
  --ink-faint: #7892a6;
  --rule: rgba(151, 184, 207, 0.2);
  --grid: rgba(151, 184, 207, 0.1);
  --land: #3a3a34;
  --ocean: #16303f;
  --border-line: #55524a;
  --series: #55dbc9;
  --series-soft: rgba(85, 219, 201, 0.16);
  --map-dot: #ff6967;
  --map-dot-edge: rgba(4, 14, 20, 0.88);
  --signal: #ffab3d;
  --signal-soft: rgba(255, 171, 61, 0.15);
  --live-pill-ink: var(--signal);
  --up: #62d6a2;
  --down: #d0705f;
  --traffic-1: #ffaaa0;
  --traffic-2: #ff867a;
  --traffic-3: #ff6967;
  --traffic-4: #ff5268;
  min-height: 100vh;
  background:
    radial-gradient(circle at 78% 8%, rgba(52, 119, 150, 0.2), transparent 28rem),
    radial-gradient(circle at 18% 34%, rgba(85, 219, 201, 0.07), transparent 24rem),
    linear-gradient(90deg, transparent 49.9%, rgba(151, 184, 207, 0.06) 50%, transparent 50.1%),
    var(--paper);
}

a { color: inherit; text-decoration: none; }
a:hover { text-decoration: underline; text-underline-offset: 2px; }

/* BUG-0042. --series, not --signal. Amber is reserved for things
   happening right now, and a focus ring is the single most frequently drawn
   accent in the interface — every Tab press painted "live" onto whatever the
   keyboard happened to be resting on. A colour that means everything means
   nothing, and it is the arrivals feed and the presence dot that pay. */
:focus-visible {
  outline: 2px solid var(--series);
  outline-offset: 2px;
}

/* Every numeral in this interface is tabular. Live figures update in place,
   and proportional digits make them jitter as the values change. */
.num, .metric-value, table td.n, .feed-time {
  font-family: var(--display);
  font-variant-numeric: tabular-nums;
  font-feature-settings: "tnum" 1;
}

/* The recurring micro-label: wide-tracked, uppercase, quiet. */
.label {
  font-family: var(--sans);
  font-size: var(--text-xs);
  font-weight: 600;
  letter-spacing: 0.09em;
  text-transform: uppercase;
  color: var(--ink-faint);
}

.text-button {
  appearance: none;
  border: 0;
  background: none;
  padding: 0;
  cursor: pointer;
}

.skip-link {
  position: fixed; left: 12px; top: -80px; z-index: 100;
  padding: 11px 14px; background: var(--ink); color: var(--paper-raised);
}
.skip-link:focus { top: 12px; }
.utility-nav {
  position: fixed; right: 14px; bottom: 14px; z-index: 30;
  display: flex; align-items: center; gap: 7px; flex-wrap: wrap;
  padding: 7px; background: color-mix(in srgb, var(--paper-raised) 94%, transparent);
  border: 1px solid var(--rule); box-shadow: 0 8px 30px rgba(9, 30, 42, 0.12);
}
.utility-nav a { text-decoration: none; }
.utility-nav form { display: contents; }
/* IMP-0157. aria-current="true" marks the EN/FR link matching the resolved
   locale, the same visual treatment [aria-current="page"] already gives the
   active section link. */
.utility-nav [aria-current="page"],
.utility-nav [aria-current="true"] { border-color: var(--series); font-weight: 700; }

/* --------------------------------------------------------------- layout */

.shell {
  display: grid;
  grid-template-columns: var(--rail-width) minmax(0, 1fr);
  min-height: 100vh;
}

/* The network rail. This service exists to watch many sites at once, so the
   whole network stays on screen rather than hiding behind a dropdown. */
.rail {
  background: var(--paper-sunk);
  border-right: 1px solid var(--rule);
  padding: var(--space-5) 0;
  display: flex;
  flex-direction: column;
  gap: var(--space-1);
}

.rail-brand {
  display: block;
  font-family: var(--display);
  font-size: 13px;
  font-weight: 700;
  letter-spacing: 0.18em;
  text-transform: uppercase;
  padding: 0 var(--space-5) 18px;
  color: var(--ink);
}
.rail-brand:hover { color: var(--ink); text-decoration: none; }

.rail-brand small {
  display: block;
  font-family: var(--sans);
  font-size: 10px;
  font-weight: 500;
  letter-spacing: 0.08em;
  color: var(--ink-faint);
  margin-top: 3px;
}

.rail-heading { padding: 14px var(--space-5) 6px; }

/* Audit #74 (BUG-0068): --ink-soft measures 4.1979:1 for #5a7183 on the
   --paper-sunk (#e4eaef) the rail sits on — under the WCAG AA 4.5:1 floor for
   13px normal-weight text, same shape of bug as BUG-0051 (see the --ink-faint
   note in :root above). --ink-soft itself stays as-is: it is used elsewhere
   at sizes/contexts that already pass, and darkening the token would move
   every one of those, not just this rail. --ink-faint already measures
   4.6148:1 here (it is the token BUG-0051 darkened, and is coincidentally
   already used two rules down for .site-link .count), and 5.2945:1 for its
   dark-mode value (#758e9c) against dark-mode --paper-sunk (#0a171d) — dark
   mode was never broken (--ink-soft dark measures 5.8774:1 there already;
   axe only runs light per tests/browser/conftest.py), so this scopes the fix
   to the one selector rather than the variable. */
.site-link {
  display: flex;
  align-items: baseline;
  gap: 9px;
  padding: 7px var(--space-5);
  border-left: 2px solid transparent;
  color: var(--ink-faint);
  font-size: 13px;
}

.site-link:hover { background: var(--paper); text-decoration: none; color: var(--ink); }

/* BUG-0043. --series, not --signal. "Which site am I looking at"
   is navigation state, not liveness, and this marker sits inches from the
   presence dot that uses amber to mean exactly one thing. Two amber marks
   side by side, one meaning "selected" and one meaning "live", is how the
   reader stops trusting either. It also makes the .live-pill comment below
   true again — that really is now the only always-amber element. */
.site-link[aria-current="page"] {
  background: var(--paper);
  border-left-color: var(--series);
  color: var(--ink);
  font-weight: 600;
}

/* Presence dot: amber when the site has traffic in the last five minutes. */
.presence {
  width: 6px;
  height: 6px;
  border-radius: 50%;
  background: var(--ink-faint);
  flex: none;
  align-self: center;
}
.presence.is-live { background: var(--signal); }

.site-link .count {
  margin-left: auto;
  font-family: var(--display);
  font-variant-numeric: tabular-nums;
  font-size: 12px;
  color: var(--ink-faint);
}

.main { min-width: 0; display: flex; flex-direction: column; }
.account-main { min-height: 100vh; }
.account-content { width: min(1080px, 100%); margin: 0 auto; padding-bottom: 110px; }
.account-hero { padding: 42px 0 var(--space-6); max-width: 760px; }
.account-hero h1 { font-size: clamp(36px, 6vw, 68px); line-height: 1; margin: 7px 0 var(--space-4); }
.account-security-link { display: inline-flex; text-decoration: none; margin-bottom: 18px; }
.account-section {
  margin: 18px 0; padding: var(--space-6); background: var(--paper-raised);
  border: 1px solid var(--rule);
}
.account-section-head { margin-bottom: var(--space-5); }
.account-section-head h2 { margin: 3px 0 0; font-size: clamp(22px, 3vw, var(--space-8)); }
.operator-main { display: block; min-height: 100vh; }
.operator-content { width: min(1280px, calc(100% - var(--space-10))); margin: 0 auto; padding: 0 0 110px; }
.operator-hero {
  display: flex; justify-content: space-between; align-items: end; gap: var(--space-6);
  padding: 42px 0 var(--space-6);
}
.operator-hero > div { max-width: 700px; }
.operator-hero h1 { font-size: clamp(36px, 6vw, 68px); line-height: 1; margin: 7px 0 var(--space-4); }
.operator-ranges { display: flex; flex-wrap: wrap; gap: 6px; }
.operator-ranges .btn[aria-current="page"] { color: var(--paper-raised); background: var(--ink); border-color: var(--ink); }
.operator-metrics { grid-template-columns: repeat(4, minmax(0, 1fr)); margin-bottom: 18px; }
.operator-attention-grid { display: grid; grid-template-columns: repeat(4, minmax(0, 1fr)); gap: 10px; }
.operator-attention-item { display: grid; gap: var(--space-1); padding: var(--space-3); background: var(--paper); border-left: 3px solid var(--up); }
.operator-attention-item.is-warn { border-left-color: var(--down); }
.operator-attention-item strong { font: 700 24px/1 var(--display); }
.operator-attention-item span { color: var(--ink-soft); font-size: 12px; }
.operator-chart { margin: 18px 0; }
.operator-ranked-grid { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 18px; }
.operator-ranked-grid .account-section { min-width: 0; margin: 0; }
.operator-table-wrap { max-width: 100%; overflow-x: auto; }
.operator-table-wrap table { min-width: 620px; }
.operator-table-wrap td small { display: block; margin-top: 3px; color: var(--ink-faint); }
.operator-actions { min-width: 220px; }
.operator-actions > summary { cursor: pointer; font-weight: 650; }
.operator-actions[open] > summary { margin-bottom: var(--space-2); }
.operator-audit > summary { cursor: pointer; display: grid; gap: var(--space-1); }
.operator-audit > summary strong { font-size: 20px; }
@media (max-width: 800px) {
  .operator-content { width: min(100% - 24px, 1280px); }
  .operator-hero { display: grid; align-items: start; }
  .operator-metrics, .operator-attention-grid { grid-template-columns: repeat(2, minmax(0, 1fr)); }
  .operator-ranked-grid { grid-template-columns: 1fr; }
}
.usage-grid { display: grid; grid-template-columns: repeat(3, minmax(0, 1fr)); gap: var(--space-3); }
.usage-card { padding: 18px; background: var(--paper); border-top: 3px solid var(--series); }
.usage-card strong { display: block; font: 700 clamp(28px, 4vw, 44px)/1 var(--display); }
.usage-card span { display: block; color: var(--ink-soft); margin-top: 7px; }
.usage-card p { font-size: 13px; color: var(--ink-soft); margin-bottom: 0; }
.account-form { display: grid; gap: 14px; max-width: 680px; }
.account-form label { display: grid; gap: 6px; }
.account-form .btn { justify-self: start; }
.account-disclosure { margin-top: var(--space-6); border-top: 1px solid var(--rule); padding-top: 18px; }
.account-disclosure summary { cursor: pointer; font-weight: 700; margin-bottom: 18px; }
.account-danger { border-color: color-mix(in srgb, var(--down) 55%, var(--rule)); }
.account-danger .auth-eyebrow { color: var(--down); }
/* IMP-0093. The plain-language purge phases on the deletion preview page —
   an ordered list because the phases run in that order, not a bulleted one. */
.account-disclosure-list { margin: 0; padding-left: var(--space-5); display: grid; gap: var(--space-2); }
.account-disclosure-list li { color: var(--ink-soft); }
/* IMP-0093. Deletion receipts on the account page: small, secondary — this
   is a look-up-later record, not something that competes with the sections
   above it for attention. */
.receipt-list { display: grid; gap: 10px; margin-top: 14px; }
.receipt-row {
  display: flex; flex-wrap: wrap; gap: 6px 14px; align-items: baseline;
  padding: 10px 0; border-top: 1px solid var(--rule); font-size: 13px;
}
.receipt-row .receipt-state { color: var(--ink-soft); }
/* IMP-0137. A row of related action buttons (rotate a key, exclude this
   browser, run a test) below a panel's explanatory copy — used across
   account/site/migrate settings, previously left with no rule of its own so
   each call site improvised its own inline top margin. */
.settings-actions { display: flex; flex-wrap: wrap; align-items: center; gap: var(--space-2); margin: 14px 0 0; }
/* IMP-0137. The operator-console purge button is the one destructive action
   in this table that is not paired with a confirmation step, so it gets its
   own full button treatment (matching select, .btn below in box model) in the
   --down accent rather than the plain suspend/reactivate .btn look. */
.danger {
  font-family: var(--sans);
  font-size: 13px;
  font-weight: 650;
  color: var(--paper-raised);
  background: var(--down);
  border: 1px solid var(--down);
  border-radius: var(--radius);
  padding: 5px 9px;
  cursor: pointer;
  min-height: 44px;
  min-width: 44px;
  display: inline-flex;
  align-items: center;
  justify-content: center;
}
.danger:hover { background: color-mix(in srgb, var(--down) 82%, black); border-color: color-mix(in srgb, var(--down) 82%, black); }
.account-security form { display: grid; gap: var(--space-3); max-width: 680px; margin-bottom: 26px; }
.account-security form label { display: grid; gap: 6px; }
.account-security h2 { margin-top: 30px; padding-top: var(--space-5); border-top: 1px solid var(--rule); }
/* IMP-0097. Risk-tiered, password-gated actions — one visual grammar for
   every action that already re-proves identity inline before it runs (a
   current password, sometimes a second factor, sometimes a fifteen-minute-
   old sign-in) but never looked different from an ordinary settings form.
   Reuses --down/.danger (IMP-0137) rather than a third accent colour: amber
   (--signal) is reserved for live recency and --map-dot red for map markers only
   (CLAUDE.md).

   Two densities of the same idea. .risk-action is the full card for a
   standalone form (security.html). .risk-inline compresses the same accent
   and tag to fit an operator-console table cell, where several rows of it
   sit stacked in one column. */
.risk-action {
  border: 1px solid color-mix(in srgb, var(--down) 35%, var(--rule));
  border-left: 3px solid var(--down);
  background: var(--paper);
  padding: 18px var(--space-5);
  margin: 0 0 22px;
  display: grid;
  gap: 10px;
  max-width: 680px;
}
.risk-action:last-child { margin-bottom: 0; }
.risk-action h3 { margin: 0; font-size: var(--space-4); }
.risk-action .hint { margin: 0; color: var(--ink-soft); font-size: 13px; }
/* The severe modifier is for an action that ends the account's own
   authentication factor or, on the operator console, the account itself —
   a thicker accent rather than a second colour, so the hierarchy still reads
   in a colour-blind-safe way (width, not hue, carries the extra tier). */
.risk-action-severe { border-left-width: 5px; }

.risk-inline {
  border-left: 3px solid var(--down);
  background: var(--paper);
  padding: var(--space-2) 10px;
  margin-bottom: var(--space-2);
  display: grid;
  gap: 6px;
  max-width: 260px;
}
.risk-inline:last-child { margin-bottom: 0; }
.risk-inline-severe { border-left-width: 5px; }

.risk-tag {
  margin: 0;
  font-family: var(--display);
  font-size: 10px;
  font-weight: 800;
  letter-spacing: 0.08em;
  text-transform: uppercase;
  color: var(--down);
}

.risk-action input:not([type="hidden"]),
.risk-inline input:not([type="hidden"]) {
  width: 100%;
  padding: 6px 9px;
  font-family: var(--sans);
  font-size: 13px;
  color: var(--ink);
  background: var(--paper-raised);
  border: 1px solid var(--rule);
  border-radius: var(--radius);
}
/* IMP-0222. These used to be `outline: none` with only the border recoloured.
   That put the weakest focus indicator in the product on its highest-stakes
   field: .risk-action/.risk-inline inputs are the password re-entry on disable
   MFA, delete account, delete site and the operator purge. Every other input
   in the interface already gets a 3px halo on focus; a 1px border recolour is
   a thin thing to meet WCAG 2.4.11 with. Same 3px geometry as the shared form
   control rule, in --down-soft so the risk grammar survives. */
.risk-action input:focus, .risk-inline input:focus {
  border-color: var(--down);
  outline: 3px solid var(--down-soft);
  outline-offset: 0;
}
.risk-inline .confirm-label { display: grid; gap: var(--space-1); font-size: var(--space-3); color: var(--ink-soft); }
.risk-inline .confirm-label code { font-family: var(--display); font-size: var(--text-note); color: var(--ink); }

/* Security posture summary (security.html). Same tile shape as .usage-grid/
   .usage-card (account.html's quota tiles), but the top border reads status
   rather than a fixed series colour — --up for "in a good state", --down for
   "needs attention", the same two tokens a numeric .delta already uses, not
   a new hue. */
.posture-grid { display: grid; grid-template-columns: repeat(4, minmax(0, 1fr)); gap: var(--space-3); }
.posture-tile { padding: var(--space-4); background: var(--paper); border-top: 3px solid var(--ink-faint); }
.posture-tile.is-ok { border-top-color: var(--up); }
.posture-tile.is-warn { border-top-color: var(--down); }
.posture-tile strong { display: block; font: 700 18px/1.3 var(--display); word-break: break-word; }
.posture-tile span {
  display: block; color: var(--ink-soft); margin-top: 6px;
  font-size: var(--text-note); text-transform: uppercase; letter-spacing: 0.05em;
}
.posture-tile p { font-size: var(--space-3); color: var(--ink-soft); margin: var(--space-2) 0 0; }
/* IMP-0168. One row per signed-in device on security.html — same list shape
   as account.html's .receipt-list/.receipt-row (a bounded feed of small,
   flush-left rows), not a table: entries here are heterogeneous height once
   a revoke button is involved, which a table cell handles awkwardly. */
.session-list { display: grid; gap: 10px; margin: 14px 0; max-width: 680px; }
.session-row {
  display: flex; flex-wrap: wrap; align-items: baseline; gap: var(--space-1) 14px;
  padding: 10px 0; border-top: 1px solid var(--rule); font-size: 13px;
}
.session-row.is-current { border-top-color: var(--up); }
.session-row-main { display: flex; align-items: baseline; gap: var(--space-2); flex: 1 1 100%; }
.session-row .hint { margin: 0; flex: 1 1 100%; }
.session-row form { margin: 0; }

/* The [hidden] attribute only sets `display: none` through the user-agent
   stylesheet, which any class-based `display` rule outranks. Without this,
   every element the JavaScript hides with `node.hidden = true` stays on
   screen — which is exactly how the live pill ended up reading "0 now".
   Declared once, globally, so the trap cannot be re-sprung per component. */
[hidden] { display: none !important; }

/* --------------------------------------------------------------- header */

.topbar {
  display: flex;
  align-items: center;
  gap: 18px;
  padding: 18px var(--gutter) 0;
  /* Wrap rather than overflow: on a phone the title plus the live pill plus
     the range select do not fit on one line, and an un-wrapped flex row
     pushes the range control off the right edge where it cannot be reached. */
  flex-wrap: wrap;
}

.topbar h1 {
  margin: 0;
  font-size: 19px;
  font-weight: 650;
  letter-spacing: -0.01em;
}

.topbar .domain {
  font-family: var(--display);
  font-size: 12px;
  color: var(--ink-faint);
}

.topbar-right { margin-left: auto; display: flex; align-items: center; gap: 10px; }

.range-control,
.custom-range {
  display: flex;
  align-items: center;
  flex-wrap: wrap;
  gap: var(--space-2);
}

.range-control > .label { margin-right: -4px; }

.range-compare,
.custom-range label {
  display: inline-flex;
  align-items: center;
  gap: 6px;
  color: var(--ink-muted);
  font-family: var(--display);
  font-size: var(--text-note);
}

.range-compare input { margin: 0; }

.custom-range {
  flex-basis: 100%;
  justify-content: flex-end;
}

/* IMP-0223. analytics_range_control puts these two date fields in one row with
   a .btn and a select (see dashboard/_macros.html), and they used to disagree
   with both on three properties at once: 36px tall, a 4px corner and a --grid
   border, against 44px / var(--radius) / --rule beside them. The height was
   the one that mattered beyond looks — IMP-0099 named --control-height because
   every interactive control in the dashboard was already built to it, and
   these two were the exception, shipping below the WCAG 2.5.5/2.5.8 target
   floor flush against two controls that met it. */
.custom-range input[type="date"] {
  min-height: var(--control-height);
  padding: 6px var(--space-2);
  border: 1px solid var(--rule);
  border-radius: var(--radius);
  color: var(--ink);
  background: var(--paper);
  font: inherit;
}

.range-error {
  flex-basis: 100%;
  color: var(--down);
  text-align: right;
  font-size: 12px;
}

/* The live pill. The only always-amber element on the page — it is by
   definition about now.

   Audit #81 (BUG-0079): the text used plain --signal, which is only
   3.2531:1 on --paper in light mode — under WCAG AA's 4.5:1 floor for 12px
   text. Border and dot stay --signal (its 3.25:1 already clears the 3:1
   non-text floor, and neither is text), only the text colour switches to
   --live-pill-ink — see the tokens' own comments in :root above for why
   that is a scoped token rather than a change to --signal. */
.live-pill {
  display: inline-flex;
  align-items: center;
  gap: 7px;
  padding: var(--space-1) 10px;
  border: 1px solid var(--signal);
  border-radius: 999px;
  color: var(--live-pill-ink);
  font-family: var(--display);
  font-size: 12px;
  font-variant-numeric: tabular-nums;
}

.live-pill .dot {
  width: 6px; height: 6px;
  border-radius: 50%;
  background: var(--signal);
  animation: pulse 2.4s ease-in-out infinite;
}

@keyframes pulse {
  0%, 100% { opacity: 1; }
  50%      { opacity: 0.25; }
}

select, .btn {
  font-family: var(--sans);
  font-size: var(--text-ui);
  color: var(--ink);
  background: var(--paper-raised);
  border: 1px solid var(--rule);
  border-radius: var(--radius);
  padding: var(--space-2) var(--space-3);
  cursor: pointer;
  min-height: var(--control-height);
  min-width: var(--control-height);
  display: inline-flex;
  align-items: center;
  justify-content: center;
}

.btn:hover { border-color: var(--ink-soft); text-decoration: none; }

.btn-primary {
  color: var(--cta-ink);
  background: linear-gradient(135deg, var(--cta-from), var(--cta-to));
  border-color: transparent;
  box-shadow: 0 10px 30px var(--cta-shadow);
  font-weight: 800;
}
.btn-primary:hover { border-color: transparent; filter: brightness(1.04); }

.btn-danger,
.btn.danger {
  color: var(--paper-raised);
  background: var(--down);
  border-color: var(--down);
  font-weight: 700;
}
.btn-danger:hover,
.btn.danger:hover {
  color: var(--paper-raised);
  background: color-mix(in srgb, var(--down) 82%, black);
  border-color: color-mix(in srgb, var(--down) 82%, black);
}

/* Dashboard-side control contract. Auth cards intentionally use the denser
   branded variant below, but both families share tokens, focus treatment and
   the .btn modifier vocabulary. */
:is(.account-form, .account-security form, .form-stack, .goal-form)
  :is(input:not([type="hidden"]):not([type="checkbox"]):not([type="radio"]), textarea, select) {
  width: 100%;
  min-height: var(--control-height);
  padding: var(--space-2) var(--space-3);
  font: inherit;
  color: var(--ink);
  background: var(--paper);
  border: 1px solid var(--rule);
  border-radius: var(--radius);
}

:is(.account-form, .account-security form, .form-stack, .goal-form)
  :is(input:not([type="hidden"]):not([type="checkbox"]):not([type="radio"]), textarea, select):focus {
  border-color: var(--series);
  outline: 3px solid var(--series-soft);
  outline-offset: 0;
}

:is(input[type="checkbox"], input[type="radio"]) {
  inline-size: 18px;
  block-size: 18px;
  margin: 0;
  accent-color: var(--series);
}

/* Small, composable layout utilities replace repeated static style=
   attributes. Dynamic visualization coordinates remain inline by design. */
.span-all { grid-column: 1 / -1; }
.span-two { grid-column: span 2; }
.content.content-tight { padding-top: 22px; }
.panel-note { margin-top: 0; color: var(--ink-soft); }
.note.note-pull-up { margin-top: -10px; }
.note.note-flush { margin-top: 0; }
.goal-form.form-compact { margin-top: var(--space-4); }
.note.form-error { grid-column: 1 / -1; margin: 0; color: var(--down); }
.note.field-row,
.field.field-row { grid-column: 1 / -1; margin: 0; }
.section-heading,
.panel-body h3.section-heading { margin-top: var(--space-6); }
.note.reveal-note { margin-top: var(--space-4); }
.section-subheading,
.panel-body h3.section-subheading {
  margin: var(--space-5) 0 var(--space-2);
  font-size: var(--text-base);
}
.button-offset { margin-top: 10px; }
.choice-offset { margin-left: var(--space-3); }
.integration-logo { height: 20px; vertical-align: middle; }
.risk-tag-current { color: var(--up); }
.auth-card.auth-card-compact { max-width: 460px; }
.auth-card.auth-card-wide { max-width: 620px; }
.auth-card .auth-compact-title { margin: 0 0 var(--space-2); font-size: var(--text-lg); }
.auth-compact-copy { margin: 0 0 var(--space-5); color: var(--ink-soft); font-size: var(--text-ui); }
.auth-compact-note { color: var(--ink-soft); font-size: var(--text-sm); }

/* IMP-0105. Generic disabled/pending treatment — before this, a disabled
   .btn was pixel-identical to an enabled one except for not responding to
   clicks, so a revoke/delete in flight looked exactly like one that hadn't
   been pressed yet. The pager's own [disabled] rule stays local to it. */
.btn[disabled] { opacity: 0.5; cursor: default; pointer-events: none; }
.btn[disabled]:hover { border-color: var(--rule); }

/* Same treatment, but for a trigger that isn't a .btn — the goal-delete
   link is a plain <a class="label">, which has no [disabled] to hook. */
.is-pending { opacity: 0.5; cursor: default; pointer-events: none; }

/* ----------------------------------------------------------------- tabs */

.tabs {
  display: flex;
  gap: 0;
  padding: 0 var(--gutter);
  border-bottom: 1px solid var(--rule);
  margin-top: 14px;
  overflow-x: auto;
  scrollbar-width: none;
}
.tabs::-webkit-scrollbar { display: none; }

.tab {
  padding: 9px 14px;
  min-height: 44px;
  display: inline-flex;
  align-items: center;
  font-size: 13px;
  color: var(--ink-soft);
  border-bottom: 2px solid transparent;
  margin-bottom: -1px;
  white-space: nowrap;
}

/* IMP-0124. The active cross-panel drill-down filter, and its clear control.
   Sits between the tab strip and the content so it visibly scopes everything
   below it, on every tab, the same way the range control above the tabs
   already scopes the whole page. */
.segment-filter {
  display: flex;
  align-items: center;
  flex-wrap: wrap;
  gap: 10px;
  padding: 10px var(--gutter);
  background: var(--paper-raised);
  border-bottom: 1px solid var(--rule);
  font-size: 13px;
}
.segment-filter[hidden] { display: none; }

button,
input[type="submit"],
input[type="button"] {
  min-height: 44px;
}

.help-shell {
  width: min(1120px, calc(100% - 32px));
  margin: 0 auto;
  padding: 34px 0 80px;
}
.help-hero { max-width: 760px; margin-bottom: var(--space-8); }
.help-hero h1 { font-size: var(--text-title); line-height: 1.02; }
.help-search { display: grid; gap: var(--space-2); margin-top: 26px; font-weight: 650; }
.help-search input { min-height: 50px; font-size: var(--space-4); padding: 10px 14px; }
.help-grid { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 18px; }
.help-grid article {
  min-width: 0;
  padding: var(--space-6);
  border: 1px solid var(--rule);
  border-radius: var(--radius);
  background: var(--paper-raised);
}
.help-grid article h2 { margin-top: var(--space-1); }
.help-grid article code { overflow-wrap: anywhere; }
.share-report-meta { margin-bottom: 22px; }
.share-range-nav { display: flex; flex-wrap: wrap; gap: var(--space-2); }
/* IMP-0152. --series, not --signal — a selected range preset is not "live",
   and the utility nav (above) already uses --series for the same kind of
   selected-state marker. */
.share-range-nav [aria-current="page"] {
  border-color: var(--series);
  box-shadow: inset 0 -2px var(--series);
}
.share-report-footer {
  display: flex;
  justify-content: space-between;
  gap: var(--space-4);
  padding: 18px var(--gutter);
  color: var(--ink-faint);
  font-size: 12px;
}
.share-report { max-width: 1100px; margin: 0 auto; padding-bottom: 70px; }
/* The DOCUMENT presentation: long-form prose read top to bottom, held to a
   ~780px measure on a full-viewport ground. /terms, /privacy, /acceptable-use,
   /status, /product-principles, /metrics-glossary, /accessibility and
   /data-retention all use it. The wide .help-shell is its counterpart, for
   pages whose content is a grid or a tool (.help-grid, the badge builder, the
   demo readout) and would break at this measure.

   IMP-0137 consolidated the geometry but left two vocabularies for it: the
   legal templates said `.legal`/`.legal article` while the newer pages said
   `.legal-shell`/`.legal-card`, comma-joined into one rule. IMP-0226 retired
   the first pair — same presentation, one name, so a new document page has
   only one thing to reach for. */
.legal-shell {
  min-height: 100vh; padding: clamp(var(--space-6), 6vw, 74px) var(--space-5);
  background:
    linear-gradient(90deg, transparent 49.9%, color-mix(in srgb, var(--rule) 32%, transparent) 50%, transparent 50.1%),
    var(--paper);
}
.legal-card {
  width: min(780px, 100%); margin: 0 auto; padding: clamp(var(--space-7), 6vw, 68px);
  background: var(--paper-raised); border: 1px solid var(--rule);
}
.legal-header { margin-bottom: 46px; }
.legal-header .auth-mark { margin-bottom: 46px; }
.legal-card h1 { font-size: var(--text-title-doc); line-height: 1.02; margin: 0 0 10px; }
.legal-card h2 { margin-top: 34px; padding-top: 18px; border-top: 1px solid var(--rule); }
.legal-card p { line-height: 1.72; }
.legal-version { color: var(--ink-soft); font-family: var(--display); }
/* IMP-0167. status.html's incident-history table: the global `table` rule
   (below, "tables") is tuned for the dashboard's dense data grids — 10px
   uppercase headers, tight cell padding — which reads cramped next to a
   .legal-card's spacious clamp(28px, 6vw, 68px) padding and 1.72 paragraph
   line-height. Same border/rule colours so it still reads as one table
   language, just with room to breathe. */
.legal-table { width: 100%; border-collapse: collapse; margin: 18px 0; }
.legal-table th {
  text-align: left;
  font-family: var(--sans);
  font-size: var(--text-note);
  font-weight: 600;
  letter-spacing: 0.08em;
  text-transform: uppercase;
  color: var(--ink-faint);
  padding: 0 var(--space-4) 10px 0;
  border-bottom: 1px solid var(--rule);
}
.legal-table td { padding: var(--space-3) var(--space-4) var(--space-3) 0; border-bottom: 1px solid var(--grid); line-height: 1.5; }
.legal-table tr:last-child td { border-bottom: 0; }
/* IMP-0122 time-zone chooser. Radios stack rather than sit inline: each option
   carries a resolved zone name after it, and those wrap badly side by side. */
.tz-choice {
  display: flex;
  gap: 10px;
  align-items: baseline;
  padding: var(--space-2) 0;
}
.account-form label.tz-choice { display: flex; align-items: center; }
.tz-choice input { flex: none; }
.tz-choice small { color: var(--ink-faint); font-weight: 400; }
.tz-note {
  margin: 6px 0 14px;
  color: var(--ink-faint);
  font-size: 13px;
  line-height: 1.55;
}

/* The public network directory. One column of plain entries: the list is read
   top to bottom to find a property, not scanned as a grid of cards. */
.network-sites-list {
  display: grid;
  gap: 18px;
  width: min(820px, 100%);
  margin: 0;
  padding: 0;
  list-style: none;
}
.network-sites-list li {
  padding-bottom: 18px;
  border-bottom: 1px solid var(--rule);
  line-height: 1.62;
  color: var(--ink-soft);
}
.network-sites-list li:last-child { padding-bottom: 0; border-bottom: 0; }
.network-sites-list a { font-weight: 700; }

/* IMP-0224. demo.html wears .help-shell. There used to be a .demo-shell here
   whose rule body was byte-identical to it — the same width, margin and
   padding — which is exactly the drift DESIGN_SYSTEM.md's layout section warns
   against: reach for the shell that already matches the page's job before
   writing a new top-level wrapper. The .demo-* rules below are content, not a
   shell, and were never nested inside it. */
.demo-head {
  display: flex;
  flex-wrap: wrap;
  align-items: end;
  gap: 18px;
  margin-bottom: 18px;
}
.demo-head > div { flex: 1 1 480px; }
.demo-head h1 { font-size: var(--text-title); line-height: 1.02; }
.demo-range { display: grid; gap: 5px; color: var(--ink-soft); font-size: var(--space-3); }
.demo-state { color: var(--ink-soft); }
.demo-chart { width: 100%; min-width: 0; color: var(--series); }
.demo-callout {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--space-6);
  margin-top: 26px;
  padding: var(--space-7);
  background: var(--surface-brand);
  color: var(--on-brand);
  border-radius: var(--radius);
}
.demo-callout h2 { margin-bottom: 0; }
/* A fixed brand island uses the fixed brand-series token in both schemes. */
.demo-callout .auth-eyebrow { color: var(--brand-series); }
/* IMP-0226. badge_builder.html wears .help-shell. There used to be a
   .tool-shell here doing the same job with four different numbers —
   min(1080px, 100% - 40px) and 38px/70px padding against .help-shell's
   min(1120px, 100% - 32px) and 34px/80px — so the badge builder was 40px
   narrower than /help and nobody had chosen that. The .tool-* rules below
   are content, not a shell, and were never nested inside it. */
.tool-head { max-width: 760px; margin: 52px 0 30px; }
.tool-head h1 { font-size: var(--text-title); line-height: 1.02; }
.form-stack { display: grid; gap: 18px; }
.form-stack label { display: grid; gap: 7px; }
.badge-preview {
  min-height: 96px; display: grid; place-items: center; margin-bottom: 14px;
  background: var(--paper-sunk); border: 1px dashed var(--rule);
}
.badge-preview img { image-rendering: auto; }
.badge-code { width: 100%; margin: 7px 0 var(--space-3); resize: vertical; }
.public-nav {
  display: flex; align-items: center; justify-content: space-between;
  gap: var(--space-6); padding-bottom: var(--space-5); border-bottom: 1px solid var(--rule);
}
.public-nav-links {
  display: flex; align-items: center; justify-content: flex-end;
  gap: var(--space-4); flex-wrap: wrap;
}
.public-nav-links a { font-size: 13px; font-weight: 650; }
.public-nav-links .btn { text-decoration: none; }
/* IMP-0157. Same treatment as the utility-nav's EN/FR pair: underline the
   link matching the resolved locale. */
.public-nav-links [aria-current="true"] { text-decoration: underline; }

/* IMP-0218. The public page footer. Mirrors .public-nav's grammar on purpose —
   same mark, same 13px/650 link weight, same rule colour — so a content page
   is bookended rather than ending in a second, unrelated component. The rule
   sits on top here where the nav's sits underneath, which is the only
   difference between them.

   Collapses to a single column at the narrow breakpoint with the rest of the
   responsive block below; it is not repeated there because the grid falls back
   to `display: grid` with one implicit column on its own. */
.public-footer {
  display: grid;
  grid-template-columns: auto 1fr;
  gap: var(--space-2) var(--space-6);
  align-items: center;
  margin-top: var(--space-16);
  padding-top: var(--space-6);
  border-top: 1px solid var(--rule);
}
.public-footer nav {
  grid-column: 1 / -1;
  display: flex;
  flex-wrap: wrap;
  gap: var(--space-4);
  margin-top: var(--space-2);
  font-size: var(--text-ui);
  font-weight: 650;
}

@media print {
  .utility-nav,
  .share-range-nav { display: none !important; }
  body { background: #fff; color: #000; }
  .panel,
  .readout,
  .hero { break-inside: avoid; box-shadow: none; }
  .share-report-footer { border-top: 1px solid #bbb; }
  a { color: inherit; text-decoration: none; }
}
.tab:hover { color: var(--ink); text-decoration: none; }
.tab[aria-current="page"] {
  color: var(--ink);
  font-weight: 600;
  border-bottom-color: var(--ink);
}

/* IMP-0150. .utility-nav is fixed to the viewport's bottom-right (see its
   own rule above), and a plain var(--gutter) bottom edge is not enough
   clearance — the dock visibly sits on top of the last table row(s) at
   scroll-end on every page that uses this class (site, network, settings).
   .account-content already solved this with padding-bottom: 110px; reused
   verbatim here rather than a new number, both so the two agree and because
   — cascade note — .account-content's own 110px is a class *combined* with
   this one (`class="content account-content"`), and since this rule and
   that one share specificity (0,1,0), whichever is declared later in this
   file wins the padding-bottom property outright, not just for elements
   with only one class. Declaring the same value here means it no longer
   matters which of the two comes last. */
.content { padding: var(--gutter); padding-bottom: 110px; flex: 1; }

/* ------------------------------------------------- hero: map + arrivals */

/* The thesis of the overview. Most analytics products bury geography in a
   tab; for a visitor counter, "someone in Osaka is reading this right now"
   is the whole feeling, so it opens the page. */
.hero {
  display: grid;
  grid-template-columns: minmax(0, 1fr) 290px;
  gap: 1px;
  background: var(--rule);
  border: 1px solid var(--rule);
  border-radius: var(--radius);
  margin-bottom: 26px;
}

/* The Map tab, where the map is the content rather than the backdrop. Giving
   it the full width is also the only correct way to make it taller: the
   viewport's height is derived from its width by the 2:1 above. */
.hero.is-wide { grid-template-columns: minmax(0, 1fr); }

.hero > * { background: var(--paper-raised); }

.map-panel { padding: var(--space-4) 18px 10px; min-width: 0; }

/* Title left, state right — the same head grammar as .panel-head, so the map
   reads as a titled region rather than a picture with a caption. The state is
   live copy: which sites are showing, how many cities, what the zoom is
   scoped to. */
.map-head {
  display: flex;
  align-items: baseline;
  gap: var(--space-3);
  margin-bottom: 6px;
}

.map-head h2 { margin: 0; font-size: 13px; font-weight: 650; }

/* Second child onward, NOT `.label` — the share page's head is two labels with
   no heading, and keying on the class put `margin-left:auto` on the title as
   well, which let the first item absorb the free space and shunted the whole
   head to the right edge. */
.map-head > * + * { margin-left: auto; text-align: right; }

/* overflow:hidden is the backstop for BUG-0013 — nothing positioned against
   the map may escape its box onto the page. */
.map-wrap { position: relative; overflow: hidden; }

/* The window onto the map.
   THE 2:1 IS LOAD-BEARING — DO NOT GIVE THIS A DIFFERENT ASPECT RATIO.
   The basemap's viewBox is 1000x500 and it is drawn with
   preserveAspectRatio="xMidYMid meet", so in any box that is not 2:1 the SVG
   letterboxes itself and draws smaller than its element. The dot layer is
   positioned in percentages of this box, so the moment the two disagree every
   dot slides off the coastline by up to half the letterbox. Want a bigger
   map? Give it more width (see .hero.is-wide) — the height follows. */
.map-viewport {
  position: relative;
  overflow: hidden;
  aspect-ratio: 2 / 1;
  cursor: grab;
  touch-action: pan-y;   /* one finger still scrolls the page; see map.js */
  /* The water. The basemap draws land only, so everything the coastlines do
     not cover is ocean — which is what makes this read as a map rather than as
     a diagram of some continents. */
  background: var(--ocean);
}

.map-viewport:focus-visible {
  outline: 2px solid var(--series);
  outline-offset: -2px;
}

.map-viewport.is-dragging { cursor: grabbing; }

/* Everything that moves. One transform, written by map.js.
   Deliberately NOT will-change:transform. Promoting this to its own
   compositor layer makes the browser rasterise the basemap once at 1x and
   then scale that bitmap, so at 16x every coastline is a soft grey smear
   instead of a vector edge. Re-rasterising per zoom is the correct trade:
   the map is a few hundred paths, and a blurry map is not a map. */
.map-canvas {
  position: absolute;
  inset: 0;
  transform-origin: 0 0;
}

.worldmap {
  display: block;
  width: 100%;
  height: 100%;
  color: var(--land);
}

/* Land is a quiet ground, not the subject. The dots are the subject. */
.worldmap-land { fill: currentColor; }

/* The 50m countries replace the 110m silhouette rather than sitting on top of
   it — the same land drawn twice at two resolutions fringes every coastline. */
.map-viewport.has-detail .worldmap-land { display: none; }

/* vector-effect keeps every stroke a hairline on screen instead of thickening
   with the transform — without it a 16x view is all border and no country.
   IT MUST BE SET ON THE PATHS, NOT THE GROUP: fill and stroke inherit, but
   vector-effect does not, so a rule on the <g> silently does nothing. */
.worldmap-countries path,
.worldmap-states path,
.worldmap-graticule path { vector-effect: non-scaling-stroke; }

/* Borders are drawn IN a border colour now, not knocked out in the panel
   background. Knocking them out only works while the land sits on the panel;
   over water it drew white canals down every frontier. */
.worldmap-countries {
  fill: currentColor;
  stroke: var(--border-line);
  stroke-width: 0.6px;
  stroke-linejoin: round;
}

.worldmap-states {
  fill: none;
  stroke: var(--border-line);
  stroke-width: 0.5px;
  stroke-linejoin: round;
  opacity: 0;
  transition: opacity 140ms linear;
}

/* has-states means "fetched"; show-states means "zoomed in far enough to
   want them". Both are needed — see the note in map.js. */
.map-viewport.has-states.show-states .worldmap-states { opacity: 0.7; }

/* Latitude/longitude every 30°. Under the land, and quiet enough to read as
   graph paper rather than as data. */
.worldmap-graticule {
  fill: none;
  stroke: var(--grid);
  stroke-width: 1px;
  opacity: 0;
  transition: opacity 160ms linear;
}

.map-viewport.is-zoomed .worldmap-graticule { opacity: 0.7; }

/* The tile layer. Sits under everything else inside .map-canvas, so the one
   transform that pans and zooms the map carries the tiles along with the dots
   for free — no per-frame repositioning. */
/* Sibling of .map-canvas, NOT a child of it — see the long note in map.js.
   Positioned in screen pixels and repositioned per frame, because a tile
   inside a 30,000x transform has a sub-pixel layout box and rasterises to
   nothing. Inserted before the canvas so the dots paint on top. */
.map-tiles {
  position: absolute;
  inset: 0;
  overflow: hidden;
  pointer-events: none;
}

.map-tile {
  position: absolute;
  display: block;
  /* The browser resamples while a gesture is mid-flight and before the next
     zoom level's tiles arrive; smooth beats the blocky nearest-neighbour some
     engines pick for upscaled images. */
  image-rendering: auto;
}

/* Once real tiles are down, the vendored silhouette underneath them is just a
   beige shape showing through every gap while they load. */
.map-viewport.has-tiles .worldmap { display: none; }

.map-dots,
.map-labels {
  position: absolute;
  inset: 0;
  pointer-events: none;
}

/* IMP-0251. Every map surface uses the same centred circular marker. Dots
   leave substantially more geography visible than teardrops, remain coherent
   at the compact sizes a busy map needs, and keep the data coordinate in the
   visual centre while the canvas counter-scales during zoom. The light edge
   separates the small colour mark from both land and water without inflating
   its encoded diameter. */
.map-dots .dot {
  position: absolute;
  background: var(--map-dot);
  border-radius: 50%;
  /* --dot-k is 1/zoom, set by map.js on the canvas. Without it every dot
     would inflate with the map and a zoomed view would be one solid blob. */
  transform: translate(-50%, -50%) scale(var(--dot-k, 1));
  transform-origin: 50% 50%;
  box-shadow:
    0 0 0 1px var(--map-dot-edge),
    0 1px 3px rgba(20, 33, 43, 0.38);
  /* Solid. Over photographic tiles a translucent mark picks up whatever
     coastline is behind it and stops reading as one object. */
  opacity: 1;
}

/* IMP-0251. The red/coral sequence adds a second traffic cue without spending
   amber, which remains exclusive to live arrivals. */
.map-dots .dot.traffic-tier-1 { background: var(--traffic-1); }
.map-dots .dot.traffic-tier-2 { background: var(--traffic-2); }
.map-dots .dot.traffic-tier-3 { background: var(--traffic-3); }
.map-dots .dot.traffic-tier-4 { background: var(--traffic-4); }

/* Clustering remains a shape/weight modifier, not a separate series colour. */
.map-dots .dot.is-cluster { filter: saturate(1.15) brightness(0.82); }

/* IMP-0103 / IMP-0251. Activating a place-list row highlights its dot with a
   high-contrast ring as well as a colour change, so selection does not depend
   on hue alone. The ring is temporary interaction chrome, not data magnitude. */
.map-dots .dot.is-selected {
  background: var(--series);
  z-index: 2;
  transform: translate(-50%, -50%) scale(calc(var(--dot-k, 1) * 1.25));
  box-shadow:
    0 0 0 2px var(--paper-raised),
    0 0 0 4px var(--series);
  filter: none;
}

.map-dots .dot.is-cluster.is-selected {
  filter: none;
}

.map-traffic-legend {
  display: flex;
  align-items: center;
  justify-content: flex-end;
  gap: 6px;
  padding: 7px 10px 0;
  color: var(--ink-faint);
  font: 600 10px/1 var(--sans);
}

.map-traffic-swatch {
  width: 9px;
  height: 9px;
  border-radius: 50%;
}

.map-traffic-swatch.tier-1 { background: var(--traffic-1); }
.map-traffic-swatch.tier-2 { background: var(--traffic-2); }
.map-traffic-swatch.tier-3 { background: var(--traffic-3); }
.map-traffic-swatch.tier-4 { background: var(--traffic-4); }

/* BUG-0052. The dot layer above is pointer-events: none, which also suppresses
   the browser's native tooltip on a dot's title attribute. In the dashboard
   that costs nothing — map.js hit-tests on the viewport's own mousemove and
   draws .map-tip itself, and letting a dot swallow the pointer would only risk
   a native tooltip fighting the custom one. The public share report is the
   opposite case: templates/share/view.html is a flat, JavaScript-free render
   whose dots carry title="City — visitors" and nothing else, so with the layer
   inert every dot there was an anonymous red mark with no way to find out which
   city it marked. Re-enabled for that view only, keyed on .map-wrap, which is
   the share view's container — the dashboard's .map-viewport/.map-canvas never
   matches. */
.map-wrap .map-dots .dot {
  pointer-events: auto;
  /* The public report intentionally ships no JavaScript, so it cannot expose
     the persistent dashboard control. Match that control's compact default. */
  transform: translate(-50%, -50%) scale(0.65);
}

/* The rest of the network, while one site's footprint is in focus.
   Weight, not hue: the same series colour dropped to a ground. Recolouring it
   would invent a second series, and the one colour with a meaning free to take
   is amber — which is spoken for and would read as "these cities are live".
   Uniform and small by design (see GHOST_PX in map.js): this layer says where
   the network is, and the layer on top says how much. */
.map-dots .dot.is-muted {
  opacity: 0.32;
  box-shadow: none;
}

@media (prefers-color-scheme: dark) {
  /* A pale dot on a dark ground loses more to a low alpha than a dark dot on
     paper does. Same apparent weight, different number. */
  .map-dots .dot.is-muted { opacity: 0.42; }
}

/* Screen space, not canvas space — see the note in site.html. map.js writes
   left/top in pixels whenever the transform moves. */
.map-label {
  position: absolute;
  transform: translateY(-50%);
  font: 600 11px/1 var(--sans);
  color: var(--ink);
  white-space: nowrap;
  /* A halo rather than a plate: the label has to stay readable over land,
     over water and over another country's fill without boxing the map in. */
  text-shadow:
     0  1px 2px var(--paper-raised),  0 -1px 2px var(--paper-raised),
     1px  0   2px var(--paper-raised), -1px  0   2px var(--paper-raised),
     1px  1px 0   var(--paper-raised), -1px  1px 0   var(--paper-raised),
     1px -1px 0   var(--paper-raised), -1px -1px 0   var(--paper-raised);
}

/* -------------------------------------------------------- map: chrome */

/* ZOOM BUTTONS ARE BACK, deliberately, and the note that used to sit here
   arguing against them is gone. It said a control cluster "just covers the
   part of the world it is sitting on" — true, and irrelevant next to the fact
   that a map with no visible controls reads as a picture. Every map anyone
   uses has +/- in a corner; StatCounter's has exactly this stack, bottom
   right, and people go looking for it. The wheel and drag still work, and the
   keyboard still gets arrows, +/- and 0. */

.map-controls {
  position: absolute;
  right: 10px;
  bottom: 10px;
  z-index: 3;
  display: flex;
  flex-direction: column;
  border: 1px solid var(--rule);
  border-radius: var(--radius);
  overflow: hidden;
  box-shadow: 0 1px 4px rgb(0 0 0 / 0.18);
}

/* IMP-0251. Visible dot diameter is reader-controlled independently of zoom.
   This pair sits immediately left of the zoom stack so the two operations do
   not masquerade as one another; the dot preview makes the target explicit
   even before the title/accessible name is read. */
.map-size-controls {
  position: absolute;
  right: 47px;
  bottom: 10px;
  z-index: 3;
  display: flex;
  border: 1px solid var(--rule);
  border-radius: var(--radius);
  overflow: hidden;
  box-shadow: 0 1px 4px rgb(0 0 0 / 0.18);
}

.map-btn {
  width: 27px;
  height: 27px;
  padding: 0;
  display: flex;
  align-items: center;
  justify-content: center;
  background: var(--paper-raised);
  color: var(--ink);
  border: 0;
  border-radius: 0;
  font: 600 15px/1 var(--sans);
  cursor: pointer;
}

.map-btn + .map-btn { border-top: 1px solid var(--rule); }
.map-btn:hover { background: var(--paper-sunk); }
.map-btn:focus-visible { outline: 2px solid var(--series); outline-offset: -2px; }
.map-btn[disabled] { color: var(--ink-faint); cursor: default; background: var(--paper-raised); }

.map-size-controls .map-btn { width: 34px; }
.map-size-controls .map-btn + .map-btn {
  border-top: 0;
  border-left: 1px solid var(--rule);
}

.map-size-symbol { display: flex; align-items: center; gap: 3px; }
.map-size-dot {
  display: block;
  flex: none;
  border-radius: 50%;
  background: currentColor;
}
.map-size-dot.is-small { width: 5px; height: 5px; }
.map-size-dot.is-large { width: 8px; height: 8px; }

/* Reset sits under the two zoom keys, as the third key in the same stack. */
.map-btn svg { width: 13px; height: 13px; fill: none; stroke: currentColor; stroke-width: 1.8; }

/* Expand, top right — the corner everything else puts it in. */
.map-expand {
  position: absolute;
  top: 10px;
  right: 10px;
  z-index: 3;
  border: 1px solid var(--rule);
  border-radius: var(--radius);
  box-shadow: 0 1px 4px rgb(0 0 0 / 0.18);
}

/* Expanded. NOT stretched to the window — the dot layer is positioned in
   percentages of this box, so breaking the 2:1 slides every dot off its city
   (see the note on .map-viewport). So it becomes the largest 2:1 that fits,
   centred over a backdrop. */
.map-viewport.is-full {
  position: fixed;
  top: 50%;
  left: 50%;
  transform: translate(-50%, -50%);
  width: min(96vw, 186vh);
  height: auto;
  z-index: 61;
  box-shadow: 0 8px 40px rgb(0 0 0 / 0.35);
}

.map-backdrop {
  position: fixed;
  inset: 0;
  z-index: 60;
  background: rgb(8 22 30 / 0.72);
}

/* Required by the tile licence, and it has to stay legible over both water and
   land at every zoom, so it gets a plate rather than a halo. */
.map-credit {
  position: absolute;
  left: 0;
  bottom: 0;
  z-index: 3;
  padding: 2px 6px;
  background: rgb(255 255 255 / 0.72);
  color: #29323a;
  font: 400 10px/1.4 var(--sans);
  border-top-right-radius: var(--radius);
}

.map-credit a { color: inherit; text-decoration: underline; }

/* Stacked, not a sentence: place, then figures, then whatever the page adds.
   One long line made the eye read prose to reach a number, and left nowhere to
   put a third fact — which the network map needs, to say who is behind a dot. */
.map-tip {
  position: absolute;
  /* IMP-0251. A tooltip is the answer to an explicit hover/selection, so it
     paints over map chrome (z-index:3), not underneath controls that can hide
     the city and counts on a short mobile map. pointer-events remains none,
     so the controls underneath are still operable while the tip is visible. */
  z-index: 4;
  padding: 6px 9px;
  background: var(--paper-raised);
  border: 1px solid var(--rule);
  border-radius: var(--radius);
  font: 500 11px/1.45 var(--sans);
  color: var(--ink);
  white-space: nowrap;
  pointer-events: none;
  box-shadow: 0 2px 8px rgb(0 0 0 / 0.12);
}

.map-tip span { display: block; }
.map-tip-place { font-weight: 650; }

.map-tip-counts {
  font-family: var(--display);
  font-variant-numeric: tabular-nums;
  font-size: var(--text-map-detail);
  color: var(--ink-soft);
}

/* Ruled off rather than just spaced: the line above is this city's own totals,
   the line below is how they split. */
.map-tip-detail {
  margin-top: var(--space-1);
  padding-top: var(--space-1);
  border-top: 1px solid var(--grid);
  font-size: var(--text-map-detail);
  color: var(--ink-soft);
}

/* IMP-0103. The keyboard/screen-reader counterpart to the map: the same
   cities, as a real sortable table of focusable rows, appended by map.js
   under the legend so it reads as part of the same card. */
.map-places {
  margin-top: 14px;
  padding-top: 10px;
  border-top: 1px solid var(--rule);
}

.map-places h3 { margin: 0 0 var(--space-2); font-size: 13px; font-weight: 650; }

/* The network overview can carry hundreds of cities. Keep the complete,
   sortable table available without letting it become several viewports of
   mandatory scrolling between the map and the traffic trend. Native details
   supplies the disclosure state; this is only its compact dashboard skin. */
.map-places.is-collapsible { padding-top: 0; }

.map-places-summary {
  display: flex;
  align-items: center;
  gap: var(--space-2);
  min-height: var(--control-height);
  padding: 8px 2px 0;
  cursor: pointer;
  list-style: none;
}

.map-places-summary::-webkit-details-marker { display: none; }

.map-places-summary::before {
  content: "";
  width: 7px;
  height: 7px;
  flex: none;
  border-right: 1.5px solid currentColor;
  border-bottom: 1.5px solid currentColor;
  transform: rotate(-45deg);
  transition: transform 120ms ease;
}

.map-places.is-collapsible[open] > .map-places-summary::before {
  transform: rotate(45deg);
}

.map-places-summary:focus-visible,
.map-places.is-collapsible:focus-visible {
  outline: 2px solid var(--series);
  outline-offset: 2px;
}

.map-places-title { font-size: 13px; font-weight: 650; }
.map-places-count { margin-left: auto; }
.map-places-content { padding-top: var(--space-2); }

/* The skip-map link map.js prepends before the viewport reuses .skip-link
   as-is (base.html's own "Skip to main content" link) rather than inventing
   a second fixed/hidden-until-focused pattern for the identical job. */

.map-place-btn {
  appearance: none;
  background: none;
  border: 0;
  padding: 0;
  margin: 0;
  font: inherit;
  color: inherit;
  text-align: left;
  cursor: pointer;
}

.map-place-btn:hover { color: var(--series); text-decoration: underline; }

/* The row's half of "activating a place highlights its dot" — the marker's
   half is .map-dots .dot.is-selected above. */
.map-place-btn[aria-pressed="true"] { color: var(--series); font-weight: 650; }

.map-places-note { margin: var(--space-2) 0 0; }

/* A ranked row that the map can frame, and whose own click/keydown still
   carries that meaning (the visitor-map panel's "recent visits" list, where
   there is no second interactive control inside the row). Audit #76
   (BUG-0075): the Countries/Regions/Cities panels used to set this same
   class and role="button" on their <tr> too, but once IMP-0124 put a real
   <button> inside that row as well, a role="button" ancestor around a
   genuine interactive descendant was a WCAG 4.1.2 nested-interactive
   violation — see dashboard.js's renderRows decorate. Those rows are plain
   <tr>s now; the equivalent affordance is .row-fly-btn below. */
tr.is-mappable { cursor: zoom-in; }
tr.is-mappable:hover .bar-label,
tr.is-mappable:focus-visible .bar-label { color: var(--series); }

/* A hit that just landed uses the same circle as every other map state, with
   amber reserved for recency. Its soft outer ring expands the interaction
   cue without making the data mark itself larger, then both cool back to the
   resting dot treatment over the same 20s as the arrivals feed. */
.map-dots .dot.is-fresh {
  background: var(--signal);
  box-shadow:
    0 0 0 1px var(--map-dot-edge),
    0 0 0 4px var(--signal-soft);
  animation: land 20s linear forwards;
}

@keyframes land {
  0% {
    background: var(--signal);
    box-shadow:
      0 0 0 1px var(--map-dot-edge),
      0 0 0 4px var(--signal-soft);
  }
  100% {
    background: var(--map-dot);
    box-shadow:
      0 0 0 1px var(--map-dot-edge),
      0 1px 3px rgba(20, 33, 43, 0.38);
  }
}

/* -------------------------------------------------- signature: arrivals */

/* The one bold element. A ticker of hits as they land, newest at the top,
   each cooling from amber to ink over 20 seconds so age is legible at a
   glance without a "3s ago" column. */
.arrivals { display: flex; flex-direction: column; min-height: 0; }

.arrivals-head {
  padding: 14px var(--space-4) var(--space-2);
  border-bottom: 1px solid var(--rule);
  display: flex;
  align-items: baseline;
  justify-content: space-between;
}

.feed {
  list-style: none;
  margin: 0;
  padding: var(--space-1) 0;
  overflow-y: auto;
  max-height: 340px;
  font-family: var(--display);
  font-size: var(--text-live-row);
}

/* BUG-0041. No animation on the base row. The cooling belongs to
   .is-entering alone, which the JS applies only once the feed is primed —
   i.e. to hits that genuinely just landed, never to the ~50 backfilled rows
   painted on load. Animating every row meant a page refresh lit the whole
   feed amber and cooled it, announcing "fifty visitors just arrived" about
   traffic that was hours old. That is the recency indicator lying, which is
   worse than not having one. */
.feed li {
  display: flex;
  gap: 9px;
  padding: 3px var(--space-4);
  color: var(--ink-soft);
  white-space: nowrap;
}

@keyframes cool {
  0%   { color: var(--signal); background: var(--signal-soft); }
  12%  { background: transparent; }
  100% { color: var(--ink-soft); background: transparent; }
}

.feed li.is-entering { animation: slide-in 220ms ease-out, cool 20s linear forwards; }

@keyframes slide-in {
  from { opacity: 0; transform: translateY(-6px); }
  to   { opacity: 1; transform: none; }
}

.feed-time { flex: none; opacity: 0.75; }
.feed-path { overflow: hidden; text-overflow: ellipsis; }
.feed-geo { margin-left: auto; flex: none; opacity: 0.8; }

.feed-empty { padding: 22px var(--space-4); color: var(--ink-faint); font-size: var(--space-3); }

/* ----------------------------------------------------- signature: reach */

/* The network map's companion, and the thing that makes a combined map worth
   drawing at all. A per-site map already answers "where are my visitors"; only
   the network view can answer "which property owns which part of the world",
   and a map whose dots are all the same colour cannot answer it either.

   So the panel is the legend AND the control: one row per site, and pointing at
   a row isolates that site's cities on the map while the rest of the network
   drops to a ground behind it (.dot.is-muted). Sweeping the list plays each
   property's footprint over the shape of the whole.

   Deliberately NOT a colour key. Twelve sites is twelve hues, none of them
   distinguishable at 4px, all of them competing with the one colour in this
   interface that carries meaning. One footprint at a time, in the series
   colour, over a ghost, says the same thing with nothing spent. */
/* THE MAP SETS THE HEIGHT OF THIS ROW, NEVER THIS COLUMN.
   The hero is a grid, so the row is as tall as its tallest child. The map's
   height is derived from its width by the 2:1, and this column's is driven by
   how many sites and shared cities there are — the two only agree at one
   window width. Left to grow, this column won the row at ordinary laptop
   widths and the map panel got a half-panel of white space under the map.
   So the lists below are given `flex-basis: 0` and `min-height: 0`: they
   contribute nothing to the column's minimum height, then expand into whatever
   the map leaves and scroll past that. */
/* BUG-0074. The map, not this column, decides how tall the hero row is.
   .hero is a grid, so the row is as tall as its tallest child. The map's
   height is derived from its width by the load-bearing 2:1, while this
   column's came from however many sites and shared cities there happened to
   be — the two agreed at exactly one window width, and below roughly 1500px
   this column won and left a band of empty panel under the map.

   `height: 0` is what fixes it: a grid item with a definite height
   contributes nothing to an auto row's size, so the row collapses onto the
   map. `min-height: 100%` then resolves against that now-definite row height
   and stretches this column back to fill it. The order matters — take the
   height away first, then claim the result. */
.reach {
  display: flex;
  flex-direction: column;
  height: 0;
  min-height: 100%;
  overflow: hidden;
}

/* display:flex above beats the [hidden] default, and network.js hides this
   whole column when there is only one site to talk about. */
.reach[hidden] { display: none; }

.reach-head {
  padding: 14px var(--space-4) var(--space-2);
  border-bottom: 1px solid var(--rule);
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  gap: 10px;
}

/* Shrinkable, but it gets what it needs before Shared ground does — the site
   roster is the point of the panel and is short; the city list is the filler. */
.reach-list {
  list-style: none;
  margin: 0;
  padding: 2px 0;
  flex: 0 1 auto;
  min-height: 0;
  max-height: 296px;
  overflow-y: auto;
}

/* A button because it does something, and because the isolate has to be
   reachable from the keyboard — focus isolates, exactly as hover does. */
.reach-row {
  position: relative;
  display: block;
  width: 100%;
  padding: 7px var(--space-4) var(--space-2);
  background: none;
  border: 0;
  border-radius: 0;
  color: inherit;
  font: inherit;
  text-align: left;
  cursor: pointer;
}

.reach-row:focus-visible { outline: 2px solid var(--series); outline-offset: -2px; }

/* Share of the network's located visitors.
   NOT the bar-behind-the-label the ranked tables use. Behind a two-line row a
   part-width block stops reading as a measure and starts reading as a
   half-highlighted row — it looks like a selection, which is a state this panel
   genuinely has and needs to keep for itself. So the share is stated explicitly
   instead: a rule at the foot of the row, filled to its share of the leader. */
.reach-meter {
  display: block;
  height: 2px;
  margin-top: 6px;
  background: var(--grid);
}

.reach-meter i {
  display: block;
  height: 100%;
  background: var(--series);
  opacity: 0.55;
}

.reach-line { display: flex; align-items: center; gap: 7px; }

.reach-name {
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
  font-size: var(--text-compact-value);
  font-weight: 600;
}

.reach-visitors {
  margin-left: auto;
  padding-left: 10px;
  font-size: var(--text-live-row);
  /* Audit #113 / BUG-0113. A hovered/focused row uses --paper-sunk. At this
     small size --ink-soft is only 4.19:1 there; --ink-faint is 4.61:1 while
     retaining the intended secondary emphasis on the normal panel surface. */
  color: var(--ink-faint);
}

.reach-sub { display: block; margin-top: 2px; padding-left: 13px; }

/* The row background is reserved for state, which is why the share is a rule
   and not a block behind the text. */
.reach-row:hover,
.reach-row:focus-visible { background: var(--paper-sunk); }
.reach-row:hover .reach-name,
.reach-row:focus-visible .reach-name { color: var(--series); }
.reach-row:hover .reach-meter i,
.reach-row:focus-visible .reach-meter i { opacity: 1; }

/* Locked on. The rail marks the current site the same way, so a held selection
   looks the same wherever it is held. */
.reach-row[aria-pressed="true"] { box-shadow: inset 2px 0 0 var(--series); }
.reach-row[aria-pressed="true"] .reach-name { color: var(--series); }
.reach-row[aria-pressed="true"] .reach-meter i { opacity: 1; }

/* ------------------------------------------------------- shared ground */

/* Where the network doubles up. The reach rows above say what each property
   covers; this says where they land on top of each other, which is the other
   half of the same question and the thing an operator acts on — two sites
   competing for one city is a decision, one site alone in a city is not.

   It also does honest work for the layout: the map is 2:1 and half a page tall,
   so the column beside it is tall and narrow. Left at one short list it was
   four hundred pixels of empty panel. */
/* Takes the column's slack rather than a fixed height: the map is 2:1, so how
   tall this column is depends on how wide the window is, and any cap picked
   here is wrong at some width — too short and it scrolls with rows to spare
   below, too tall and it pushes the footer off. */
.shared {
  display: flex;
  flex-direction: column;
  /* basis 0, so a long city list cannot push the column past the map */
  flex: 1 1 0;
  min-height: 0;
  border-top: 1px solid var(--rule);
}

.shared-head {
  padding: var(--space-3) var(--space-4) 7px;
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  gap: 10px;
}

.shared-list {
  list-style: none;
  margin: 0;
  padding: 0 0 6px;
  flex: 1 1 0;
  min-height: 0;
  overflow-y: auto;
  font-size: 12px;
}

/* Framing a city is the same gesture the ranked tables offer, so it gets the
   same cursor. */
.shared-row {
  display: flex;
  align-items: baseline;
  gap: var(--space-2);
  width: 100%;
  padding: 3px var(--space-4);
  background: none;
  border: 0;
  color: var(--ink-soft);
  font: inherit;
  text-align: left;
  cursor: zoom-in;
}

.shared-row:hover { background: var(--paper-sunk); color: var(--ink); }
.shared-row:focus-visible { outline: 2px solid var(--series); outline-offset: -2px; }

.shared-city {
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

/* How many of the network's sites are in this city. The count is the ranking
   key, so it is the figure that gets the emphasis, not the visitor total. */
.shared-sites {
  margin-left: auto;
  flex: none;
  font-family: var(--display);
  font-variant-numeric: tabular-nums;
  font-size: var(--text-note);
  color: var(--ink);
}

.shared-visitors {
  flex: none;
  width: 46px;
  text-align: right;
  font-family: var(--display);
  font-variant-numeric: tabular-nums;
  font-size: var(--text-note);
  color: var(--ink-faint);
}

/* Not a metric tile. It is one sentence, because overlap is a fact about the
   network rather than a figure to track. */
.reach-foot {
  margin-top: auto;
  padding: 10px var(--space-4) var(--space-3);
  border-top: 1px solid var(--rule);
  color: var(--ink-faint);
  font-size: var(--text-live-row);
  line-height: 1.4;
}

.reach-empty { padding: 22px var(--space-4); color: var(--ink-faint); font-size: var(--space-3); }

/* IMP-0145. Same distinction .empty.is-error draws for a failed panel vs.
   an empty one — reach-empty is the <li>-based counterpart, needed because
   .reach-list is a <ul> and .empty's <div> wrapper is not a valid child of
   one. */
.reach-empty.is-error { color: var(--down); }
.reach-empty .retry-btn { margin-top: 10px; }

/* ------------------------------------------------------ metric readout */

/* An instrument readout, not a row of cards. Values share one baseline so
   the eye can scan them as a single line of telemetry. */
.readout {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(126px, 1fr));
  gap: 1px;
  background: var(--rule);
  border: 1px solid var(--rule);
  border-radius: var(--radius);
  margin-bottom: 26px;
}

.metric {
  background: var(--paper-raised);
  padding: 14px var(--space-4) 13px;
}

.metric-value {
  font-size: 25px;
  font-weight: 600;
  line-height: 1.1;
  letter-spacing: -0.02em;
  display: block;
}

.metric .label { margin-top: 5px; display: block; }

.delta {
  font-family: var(--display);
  font-size: var(--text-note);
  margin-top: 3px;
  display: block;
  color: var(--ink-faint);
}
.delta.up   { color: var(--up); }
.delta.down { color: var(--down); }

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

.panel {
  background: var(--paper-raised);
  border: 1px solid var(--rule);
  border-radius: var(--radius);
  margin-bottom: 22px;
}

.panel-head {
  display: flex;
  align-items: baseline;
  gap: var(--space-3);
  padding: 13px var(--space-4);
  border-bottom: 1px solid var(--rule);
}

.panel-head h2 { margin: 0; font-size: 13px; font-weight: 650; }
.panel-head .spacer { margin-left: auto; }

/* The network chart has two kinds of context: what is plotted and how the
   buckets are cut. Keep those together at the left, with the metric switch
   and comparison key as one control group at the right. This also gives the
   phone layout two coherent groups to wrap instead of five fragments fighting
   for one line. */
.network-chart-title { display: grid; gap: 2px; }
.network-chart-controls {
  display: flex;
  align-items: center;
  justify-content: flex-end;
  flex-wrap: wrap;
  gap: var(--space-3);
  margin-left: auto;
}
.panel-body {
  padding: 14px var(--space-4);
  overflow-x: auto;
  -webkit-overflow-scrolling: touch;
}
/* Audit #73 (BUG-0069): dashboard.js/network.js/monitoring.js's
   UI.markScrollable and the two static tables in share/view.html give a
   .panel-body that actually holds a table tabindex="0" so its horizontal
   scroll (forced at the 720px breakpoint below) is keyboard-reachable, not
   just draggable. The browser's default focus ring on a plain <div> is easy
   to miss against chart paper, so this makes it explicit rather than relying
   on the UA default. */
.panel-body[tabindex]:focus-visible {
  outline: 2px solid var(--series);
  outline-offset: -2px;
}

/* IMP-0094. One row per finding — same border-top-between-rows shape as
   .receipt-list/.receipt-row (account.html's deletion receipts), the closest
   existing "small list of self-contained items" panel in this codebase. */
.insight-item { padding: var(--space-3) 0; border-top: 1px solid var(--rule); }
.insight-item:first-child { padding-top: 0; border-top: 0; }
.insight-head { display: flex; align-items: baseline; gap: 10px; flex-wrap: wrap; }
/* Reuses .delta's own up/down colouring (see the readout above this panel)
   rather than a second good/bad colour pair — the same two tokens, the same
   meaning, wherever a number in this dashboard needs to read as better or
   worse. Unlike .delta this is the finding's own headline, not a small
   caption under one, so it does not inherit .delta's block/margin-top. */
.insight-head strong.delta { display: inline; margin-top: 0; font-size: 13px; color: var(--ink); }
.insight-head strong.delta.up   { color: var(--up); }
.insight-head strong.delta.down { color: var(--down); }
.insight-narrative { margin: 6px 0 0; font-size: 13px; line-height: 1.5; color: var(--ink-soft); }
.insight-item .settings-actions { margin-top: 10px; }
.insight-item [data-insight-feedback][aria-pressed="true"] {
  border-color: var(--series);
  color: var(--series);
}
.visitor-log-more {
  display: block;
  min-height: 44px;
  margin: 18px auto 0;
}
.session-link {
  min-height: 44px;
  padding: 0;
  border: 0;
  color: var(--series);
  background: transparent;
  text-decoration: underline;
  cursor: pointer;
}
.session-detail[hidden] { display: none; }

/* IMP-0080. A goal name button, same treatment as .session-link — a text
   button styled as a link, because a <tr> full of <td>s is not a place a
   real <a> can carry the click without breaking table semantics elsewhere on
   this row (the Remove action already relies on the row being plain cells). */
.goal-link {
  min-height: 44px;
  padding: 0;
  border: 0;
  color: var(--series);
  background: transparent;
  text-decoration: underline;
  cursor: pointer;
  font: inherit;
  text-align: left;
}
.goal-detail[hidden] { display: none; }

/* IMP-0124. A ranked row's drill-down control — narrows the whole page (see
   .segment-filter above) to this row's value. Same text-button treatment as
   .session-link/.goal-link and for the same reason: a real <button> because a
   <tr> of plain <td>s is not a place an <a> can carry the click without
   breaking table semantics elsewhere on the row. For the geo dimensions that
   also get .row-fly-btn below, the two are siblings in the row, not one
   nested inside the other — see Audit #76 (BUG-0075) at .row-fly-btn. */
.row-filter-btn {
  min-height: 44px;
  padding: 0;
  border: 0;
  color: var(--series);
  background: transparent;
  text-decoration: underline;
  cursor: pointer;
  font: inherit;
  font-size: 12px;
}
.row-filter-btn:hover,
.row-filter-btn:focus-visible { color: var(--ink); }

/* Audit #76 (BUG-0075): the Countries/Regions/Cities panels' "show this on
   the map" control. Used to be the ranked <tr> itself carrying role="button"
   (IMP-0019); once IMP-0124 added .row-filter-btn as a second control inside
   that same row, a role="button" ancestor around a real interactive
   descendant became a WCAG 4.1.2 nested-interactive violation (axe:
   "Element has focusable descendants"). Now a real, sibling <button> instead
   — same text-button treatment as .row-filter-btn, one column over, so
   neither control is ever an ancestor of the other. */
.row-fly-btn {
  min-height: 44px;
  padding: 0;
  border: 0;
  color: var(--ink-faint);
  background: transparent;
  text-decoration: underline;
  cursor: pointer;
  font: inherit;
  font-size: 12px;
}
.row-fly-btn:hover,
.row-fly-btn:focus-visible { color: var(--series); }

/* A sub-heading inside a panel that already has its own <h2> — same
   micro-label treatment panel-head h2 uses, one size down, so "Landing
   pages" reads as a subsection rather than a second panel title. */
.panel-body h3 {
  margin: 22px 0 6px;
  font-family: var(--sans);
  font-size: var(--text-note);
  font-weight: 650;
  letter-spacing: 0.07em;
  text-transform: uppercase;
  color: var(--ink-faint);
}
.session-summary {
  display: grid;
  grid-template-columns: max-content minmax(0, 1fr);
  gap: 6px 18px;
  margin: 0 0 22px;
}
.session-summary dt { color: var(--ink-faint); }
.session-summary dd { margin: 0; overflow-wrap: anywhere; }
.session-journey { display: grid; gap: 1px; margin: 0; padding-left: var(--space-7); }
.session-journey li {
  display: flex;
  justify-content: space-between;
  gap: 18px;
  padding: 10px var(--space-3);
  background: var(--paper);
}

.grid-2 {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(320px, 1fr));
  gap: 22px;
  /* Panels size to their content. Without this, grid stretches every panel in
     a row to match the tallest, so an 8-row table next to a 15-row one gets
     half a screen of dead space under it. */
  align-items: start;
}

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

.chart { width: 100%; height: 200px; display: block; overflow: visible; }
.chart .axis   { stroke: var(--grid); stroke-width: 1; }
.chart .series-line { fill: none; stroke: var(--series); stroke-width: 1.75; }
.chart .series-area { fill: var(--series-soft); }
.chart .comparison-line {
  fill: none;
  stroke: var(--ink-faint);
  stroke-width: 1.5;
  stroke-dasharray: 5 4;
}
.chart .tick   { font-family: var(--display); font-size: var(--text-chart-tick); fill: var(--ink-faint); }
.chart .marker { fill: var(--series); }

/* IMP-0100. The y-axis grid renderChart now draws — .grid-line is the
   fainter, everywhere-else-but-the-baseline sibling of .axis, which stays
   the one solid line at zero. */
.chart .grid-line { stroke: var(--grid); stroke-width: 1; }

/* The point a mouse hover or a keyboard focus on the accessible table's
   rows is currently inspecting (see shared.js's _inspectChartPoint) — an
   open ring rather than .marker's solid fill so it never reads as a second
   permanent peak marker.

   IMP-0208: cx/cy transition rather than snap, so moving from point to point
   (drag the mouse, arrow through the table) reads as the dot travelling the
   line instead of teleporting along it. This is the first thing in this
   chart that moves rather than being redrawn outright, so it is also the
   first thing here the sitewide reduced-motion rule below actually has
   something to flatten — no extra gating needed here beyond that existing
   `@media (prefers-reduced-motion: reduce)` block, which already forces every
   transition-duration on the page to 0.01ms. */
.chart .hover-point {
  fill: var(--paper);
  stroke: var(--series);
  stroke-width: 2;
  pointer-events: none;
  transition: cx 150ms ease, cy 150ms ease;
}
.chart .hover-point.is-hidden { display: none; }

/* IMP-0208. Point-in-time event markers (a goal changed, a deploy shipped) —
   see shared.js's renderChart `annotations` parameter. --ink-soft rather
   than --series (already the data line/dots) or --signal (reserved for "this
   is happening right now" recency, see CLAUDE.md) — an annotation is neither
   live nor part of the measured series, just a dated fact placed against it,
   and a third colour on this small a chart would compete with both. */
.chart .chart-annotation-line {
  stroke: var(--ink-soft);
  stroke-width: 1;
  stroke-dasharray: 2 3;
  opacity: 0.6;
}
.chart .chart-annotation {
  fill: var(--paper-raised);
  stroke: var(--ink-soft);
  stroke-width: 1.5;
  cursor: pointer;
}
.chart .chart-annotation:hover,
.chart .chart-annotation:focus-visible {
  fill: var(--ink-soft);
}
.chart .chart-annotation:focus-visible {
  outline: 2px solid var(--series);
  outline-offset: 1px;
}

.chart-key {
  display: inline-flex;
  align-items: center;
  gap: 5px;
  color: var(--ink-faint);
  font-family: var(--display);
  font-size: 10px;
}

.chart-key-line {
  width: 18px;
  border-top: 1.5px dashed var(--ink-faint);
}
.chart-key-line.chart-key-current {
  border-top-style: solid;
  border-top-color: var(--series);
}

/* IMP-0100. renderChart's hover/focus tip, positioned absolutely against
   the chart's own .panel-body — same look as the map's .map-tip, sized for
   a shorter, two-or-three-line fact rather than a place name. */
.panel-body:has(.chart) { position: relative; }

.chart-tip {
  position: absolute;
  z-index: 2;
  padding: 6px 9px;
  background: var(--paper-raised);
  border: 1px solid var(--rule);
  border-radius: var(--radius);
  font: 500 11px/1.45 var(--sans);
  color: var(--ink);
  white-space: nowrap;
  pointer-events: none;
  box-shadow: 0 2px 8px rgb(0 0 0 / 0.12);
}
.chart-tip span { display: block; }
.chart-tip-period { font-weight: 650; }
.chart-tip-value,
.chart-tip-compare {
  font-family: var(--display);
  font-variant-numeric: tabular-nums;
  font-size: var(--text-map-detail);
  color: var(--ink-soft);
}

/* The accessible-table disclosure — a plain .btn, and the table it reveals.
   Both sit under the chart inside the same .panel-body renderChart found
   the <svg> in. */
.chart-table-toggle { margin-top: 10px; }
.chart-table-wrap {
  margin-top: 10px;
  max-height: 360px;
  overflow-y: auto;
}
.chart-table-wrap[hidden] { display: none; }

.chart-row-btn {
  appearance: none;
  background: none;
  border: 0;
  padding: 0;
  margin: 0;
  font: inherit;
  color: inherit;
  text-align: left;
  cursor: pointer;
}
.chart-row-btn:hover,
.chart-row-btn:focus-visible { color: var(--series); }
.chart-row-btn[aria-pressed="true"] { color: var(--series); font-weight: 650; }

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

/* Ranked lists carry an inline bar behind the label. The bar is the
   comparison; the number is the detail. */
table { width: 100%; border-collapse: collapse; font-size: 13px; }

table th {
  text-align: left;
  font-family: var(--sans);
  font-size: 10px;
  font-weight: 600;
  letter-spacing: 0.09em;
  text-transform: uppercase;
  color: var(--ink-faint);
  padding: 0 var(--space-2) 7px 0;
  border-bottom: 1px solid var(--rule);
}

table td {
  padding: 6px var(--space-2) 6px 0;
  border-bottom: 1px solid var(--grid);
  vertical-align: middle;
}

table tr:last-child td { border-bottom: 0; }
table td.n { text-align: right; white-space: nowrap; width: 1%; padding-left: 14px; }
table td.n.muted { color: var(--ink-faint); }

/* IMP-0097. table td above is deliberately vertical-align: middle for the
   short, uniform single-line rows every other table in this product has.
   The operator console's Action column is not that any more: a .risk-inline
   card (suspend, and a taller one for purge, sometimes both stacked) makes
   that one cell far taller than "acct_…"/an email/"active". Middle-aligned
   against that height read as the account id floating in blank space partway
   down its own row instead of level with the actions beside it. Scoped to
   tables that actually contain the tall card, so every other table on the
   site keeps the vertical centering it was set up for. */
table:has(.risk-inline) td { vertical-align: top; }

/* IMP-0077. A ranked panel's own filter box + accessible match count, sat
   above the table it filters. Sits in every table-search markup, even a
   3-row panel — a quiet input that does nothing is cheaper than one panel
   having the control and its neighbour not. */
.table-search {
  display: flex;
  align-items: center;
  gap: 10px;
  margin-bottom: 10px;
}

.table-search input[type="search"] {
  flex: 1 1 auto;
  min-width: 0;
  padding: 6px 9px;
  font-family: var(--sans);
  font-size: 13px;
  color: var(--ink);
  background: var(--paper);
  border: 1px solid var(--rule);
  border-radius: var(--radius);
}

/* IMP-0222. Was `outline: none` with only the border recoloured — the same
   thin indicator the risk fields carried. Matches the shared form control
   rule instead. */
.table-search input[type="search"]:focus {
  border-color: var(--series);
  outline: 3px solid var(--series-soft);
  outline-offset: 0;
}

/* A sortable column header. Same type scale as a plain <th> — the button
   resets to inherit it rather than the browser's own button chrome. */
.sort-btn {
  appearance: none;
  background: none;
  border: 0;
  padding: 0;
  margin: 0;
  font: inherit;
  color: inherit;
  letter-spacing: inherit;
  text-transform: inherit;
  cursor: pointer;
}

.sort-btn:hover,
.sort-btn:focus-visible { color: var(--ink); }

th[aria-sort] { white-space: nowrap; }

/* A caret only once a direction is known — "none" stays unmarked so a table
   nobody has sorted yet doesn't imply an order it isn't in. */
th[aria-sort="ascending"] .sort-btn::after { content: " \2191"; }
th[aria-sort="descending"] .sort-btn::after { content: " \2193"; }

.bar-cell { position: relative; min-width: 0; }

.bar {
  position: absolute;
  left: 0; top: 50%;
  transform: translateY(-50%);
  height: 17px;
  background: var(--series-soft);
  border-radius: 1px;
  z-index: 0;
}

.bar-label {
  position: relative;
  z-index: 1;
  display: block;
  padding: 0 6px;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

/* The site name is a link beside a plain-text domain on the same translucent
   traffic bar. Colour alone is not enough to distinguish those two readings
   (WCAG 1.4.1 / axe link-in-text-block), so keep a restrained underline at
   rest and strengthen it for intentional interaction. */
.bar-label > a {
  text-decoration-line: underline;
  text-decoration-thickness: 1px;
  text-decoration-color: var(--ink-faint);
  text-underline-offset: 2px;
}

.bar-label > a:hover,
.bar-label > a:focus-visible {
  color: var(--series);
  text-decoration-color: currentColor;
  text-decoration-thickness: 2px;
}

/* IMP-0155. The network table's per-site domain subtitle (network.js's
   renderLabel) — a .bar-label can sit over .bar at any width up to 100%, so
   the worst-case background behind this text is --series-soft composited
   over --paper-raised at full bar width, not --paper-raised alone. In light
   mode that composite is #dce6ea (0.14 alpha of --series #2e6b7e over
   --paper-raised #f8fafb); plain --ink-faint (#536b7a) measures only
   4.4111:1 there — under the WCAG AA 4.5:1 floor for 12px text, despite
   clearing every plain-paper background BUG-0051 tuned it for. #4b606e (90%
   of --ink-faint's own RGB, same hue) measures 5.1758:1 on that same
   composite. This used to be an inline style in network.js; moved here to
   follow this file's own convention (a scoped colour rule, not
   element.style) once the fix needed to differ from --ink-faint anyway. */
.site-domain { color: #4b606e; }

@media (prefers-color-scheme: dark) {
  /* Dark mode's own --ink-faint (#758e9c) fares worse here, not better: the
     composite of dark --series-soft (0.16 alpha of #63aac0) over dark
     --paper-raised (#13242e) is #203945, and #758e9c on #203945 measures
     3.5240:1 — well under AA, because a light "faint" ink needs *more*
     luminance headroom on a background this dark, and --ink-faint dark was
     only tuned against the flat --paper/--paper-raised/--paper-sunk trio.
     #96a9b4 (24% of the way from --ink-faint dark to white) measures
     4.9821:1 on the same composite. */
  .site-domain { color: #96a9b4; }
}

.empty {
  padding: 26px var(--space-1);
  color: var(--ink-faint);
  font-size: 13px;
  text-align: center;
}

/* IMP-0105. A failed panel is shaped like .empty but must read as a
   problem, not as "nothing here yet" — the two used to be visually
   identical. */
.empty.is-error { color: var(--down); }
.empty .retry-btn { margin-top: 10px; }

/* Skeleton bars: a shape, not a spinner. The eye reads "this is where the
   table will be" without a moving element competing with --signal amber,
   which is reserved elsewhere on this page for "happening right now". */
.skeleton-rows {
  display: flex;
  flex-direction: column;
  gap: var(--space-2);
  padding: 6px 0;
}

.skeleton-row {
  height: 14px;
  border-radius: var(--radius);
  background: linear-gradient(
    90deg,
    var(--paper-raised) 25%,
    var(--rule) 37%,
    var(--paper-raised) 63%
  );
  background-size: 400% 100%;
  animation: skeleton-shimmer 1.6s ease-in-out infinite;
}

.skeleton-row:nth-child(2) { width: 92%; }
.skeleton-row:nth-child(3) { width: 78%; }
.skeleton-row:nth-child(4) { width: 85%; }
.skeleton-row:nth-child(5) { width: 65%; }

@keyframes skeleton-shimmer {
  0%   { background-position: 100% 50%; }
  100% { background-position: 0% 50%; }
}

/* A caption next to a panel that kept its last-good data on screen rather
   than blanking it after a background refresh failed. */
.stale-label {
  display: block;
  margin-top: 6px;
  font-size: var(--text-note);
  color: var(--ink-faint);
  text-align: center;
}

.stale-label.is-stale { color: var(--down); }

/* Toasts: a transient confirmation after a completed action, never
   --signal amber — that colour means "happening right now" and a toast is
   reporting something already finished. Inverted relative to the page
   background in both themes, since --ink/--paper-raised are the same pair
   the page itself uses, just swapped, so contrast holds either way. */
.toast-stack {
  position: fixed;
  right: 16px;
  bottom: 16px;
  z-index: 500;
  display: flex;
  flex-direction: column;
  gap: var(--space-2);
  max-width: 320px;
}

.toast {
  padding: 10px 14px;
  border-radius: var(--radius);
  background: var(--ink);
  color: var(--paper-raised);
  font-size: 13px;
  box-shadow: 0 2px 10px rgba(0, 0, 0, 0.2);
  animation: toast-in 0.15s ease-out;
}

.toast.is-error { border-left: 3px solid var(--down); }
.toast.is-success { border-left: 3px solid var(--up); }

@keyframes toast-in {
  from { opacity: 0; transform: translateY(6px); }
  to   { opacity: 1; transform: translateY(0); }
}

/* ------------------------------------------------------- visitor map */

/* The pagination strip under the visitor map. A page of dots is a page of a
   list, so this says which page and moves between them. */
.pager {
  display: flex;
  align-items: center;
  gap: 10px;
  flex-wrap: wrap;
  padding-bottom: var(--space-3);
  margin-bottom: var(--space-3);
  border-bottom: 1px solid var(--rule);
}

.pager .spacer { margin-left: auto; }
.pager .btn[disabled] { opacity: 0.45; cursor: default; }
.pager .btn[disabled]:hover { border-color: var(--rule); }

/* 200 dots is a page; 200 rows is a wall — left unconfined this ran the page
   to fifteen thousand pixels. StatCounter's visitor map has no table under it
   at all; this one is kept because clicking a row frames that visit, but it is
   a companion to the map, not the view, and it is sized like one. */
[data-visit-list] {
  max-height: 420px;
  overflow-y: auto;
}

/* ------------------------------------------------------------ goal form */

.goal-form {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(150px, 1fr));
  gap: var(--space-3);
  align-items: end;
  margin-top: var(--space-5);
  padding-top: 18px;
  border-top: 1px solid var(--rule);
}

.goal-form .field { margin-bottom: 0; }

.goal-form input,
.goal-form select,
.goal-form textarea {
  width: 100%;
  min-height: 44px;
  padding: var(--space-2) var(--space-3);
  font-family: var(--sans);
  font-size: var(--text-ui);
  color: var(--ink);
  background: var(--paper);
  border: 1px solid var(--rule);
  border-radius: var(--radius);
}

/* IMP-0090. A domains/exclusions list is one entry per line — plain text is
   the simplest editable form of a list this codebase has no chip/tag widget
   for, and it is trivially copy-pasteable in and out. */
.goal-form textarea {
  min-height: 78px;
  resize: vertical;
  font-family: var(--display);
}

.goal-form input:focus,
.goal-form select:focus,
.goal-form textarea:focus {
  border-color: var(--series);
  outline: 3px solid var(--series-soft);
  outline-offset: 0;
}

/* Errors span the whole grid rather than squeezing into one column. */
.goal-form [data-goal-error] {
  grid-column: 1 / -1;
  margin: 0;
  color: var(--down);
}

/* IMP-0125. Each funnel step is a nested goal-form-shaped mini-grid of its
   own, bordered so an ordered list of 2-5 of them reads as steps rather than
   as one long flat field list — order is the entire point of a funnel, and
   nothing above this visually said so. */
.funnel-step {
  grid-column: 1 / -1;
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(150px, 1fr));
  gap: var(--space-3);
  align-items: end;
  padding: var(--space-3);
  border: 1px solid var(--rule);
  border-radius: var(--radius);
}

.funnel-step[hidden] { display: none; }

.funnel-step-head {
  grid-column: 1 / -1;
  display: flex;
  align-items: center;
  justify-content: space-between;
}

.funnel-step-remove {
  background: none;
  border: none;
  padding: 0;
  font: inherit;
  font-size: 12px;
  color: var(--ink-soft);
  cursor: pointer;
  text-decoration: underline;
}

.funnel-step-remove:hover,
.funnel-step-remove:focus-visible { color: var(--down); }

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

/* IMP-0138. Bare .flash (no category — e.g. a caller that skips
   with_categories) keeps the --down accent as the safest default: an
   unlabelled message reads as a warning rather than a false confirmation.
   Templates that flash a category get the matching accent below. */
.flash {
  padding: 9px var(--space-3);
  border-left: 2px solid var(--down);
  background: var(--paper-raised);
  font-size: 13px;
  margin-bottom: 14px;
}
.flash.success { border-left-color: var(--up); }
.flash.error { border-left-color: var(--down); }

.note {
  font-size: 12px;
  color: var(--ink-faint);
  margin-top: var(--space-2);
}

code, .mono { font-family: var(--display); font-size: 12px; }

.snippet {
  background: var(--paper-sunk);
  border: 1px solid var(--rule);
  border-radius: var(--radius);
  padding: 11px 13px;
  font-family: var(--display);
  font-size: 12px;
  overflow-x: auto;
  white-space: pre;
  color: var(--ink);
}

/* ------------------------------------------------------------------ auth */

.auth,
.auth-shell {
  --paper:        var(--surface-brand);
  --paper-raised: var(--surface-brand-raised);
  --paper-sunk:   var(--surface-brand-sunk);
  --ink:          var(--on-brand);
  --ink-soft:     #aec1d0;
  --ink-faint:    #7f98aa;
  --rule:         rgba(151, 184, 207, 0.22);
  --series:       #58d9ca;
  --series-soft:  rgba(88, 217, 202, 0.13);
  --canada:       #df4351;
  min-height: 100vh;
  display: grid;
  place-items: center;
  padding: clamp(var(--space-6), 5vw, 72px);
  color: var(--ink);
  background:
    radial-gradient(circle at 10% 12%, rgba(88, 217, 202, 0.1), transparent 26rem),
    radial-gradient(circle at 90% 86%, rgba(47, 105, 142, 0.16), transparent 32rem),
    linear-gradient(90deg, transparent 49.9%, rgba(151, 184, 207, 0.055) 50%, transparent 50.1%),
    var(--paper);
}

.auth-shell-split {
  grid-template-columns: minmax(300px, 0.85fr) minmax(420px, 560px);
  gap: clamp(54px, 9vw, 150px);
  align-items: center;
}

.auth-context { width: min(100%, 610px); }

.auth-mark {
  display: inline-flex;
  flex-direction: column;
  min-height: 40px;
  padding: 1px 0 0 52px;
  position: relative;
  font-family: var(--display);
  line-height: 1;
  text-transform: uppercase;
}

.auth-mark:hover { text-decoration: none; }
.auth-home-link {
  display: inline-flex; margin: var(--space-3) 0 var(--space-6); font-size: 13px;
  font-weight: 750; text-decoration: none;
}
.auth-home-link:hover { text-decoration: underline; }

.auth-mark::before {
  content: "CA";
  position: absolute;
  inset: 0 auto auto 0;
  display: grid;
  place-items: center;
  width: 38px;
  height: 38px;
  border: 1px solid rgba(88, 217, 202, 0.48);
  border-radius: var(--radius-brand-control);
  color: var(--cta-ink);
  background: linear-gradient(135deg, var(--cta-from), var(--cta-to));
  box-shadow: 0 0 28px rgba(88, 217, 202, 0.18);
  font-size: var(--text-note);
  font-weight: 900;
  letter-spacing: -0.04em;
}

.auth-mark::after {
  content: "";
  position: absolute;
  left: 33px;
  top: -3px;
  width: 9px;
  height: 9px;
  border: 2px solid var(--paper);
  border-radius: 50%;
  background: var(--canada);
}

.auth-mark span {
  font-size: 15px;
  font-weight: 800;
  letter-spacing: 0.16em;
}

.auth-mark small {
  margin-top: 7px;
  color: var(--ink-faint);
  font-family: var(--sans);
  font-size: var(--text-brand-micro);
  font-weight: 700;
  letter-spacing: 0.13em;
}

.auth-kicker,
.auth-eyebrow {
  color: var(--series);
  font-family: var(--display);
  font-size: 10px;
  font-weight: 800;
  letter-spacing: 0.14em;
  text-transform: uppercase;
}

.auth-kicker { margin: clamp(58px, 10vh, 105px) 0 15px; }
.auth-eyebrow { margin: 0 0 var(--space-3); }

.auth-context h1 {
  max-width: 590px;
  margin: 0;
  font-size: clamp(42px, 5vw, 68px);
  line-height: 1.02;
  letter-spacing: -0.055em;
  text-wrap: balance;
}

.auth-context-copy {
  max-width: 570px;
  margin: 27px 0 0;
  color: var(--ink-soft);
  font-size: 17px;
  line-height: 1.7;
}

.auth-proof,
.auth-steps {
  display: grid;
  gap: 11px;
  margin: 30px 0 0;
  padding: 0;
  list-style: none;
  color: var(--ink-soft);
  font-size: 13px;
}

.auth-proof li,
.auth-steps li {
  position: relative;
  padding-left: 25px;
}

.auth-proof li::before {
  content: "✓";
  position: absolute;
  left: 0;
  color: var(--series);
  font-weight: 900;
}

.auth-steps span {
  position: absolute;
  left: 0;
  color: var(--series);
  font-family: var(--display);
  font-size: 10px;
  font-weight: 800;
}

.auth-canada {
  margin: 34px 0 0;
  color: var(--ink-faint);
  font-size: var(--text-note);
  letter-spacing: 0.04em;
}

.auth-canada::before {
  content: "";
  display: inline-block;
  width: 14px;
  height: 2px;
  margin: 0 9px 3px 0;
  background: var(--canada);
}

.auth-panel { width: 100%; }

.auth-card {
  width: 100%;
  max-width: 520px;
  margin-inline: auto;
  padding: clamp(30px, 5vw, 50px);
  background:
    linear-gradient(145deg, rgba(88, 217, 202, 0.045), transparent 42%),
    var(--paper-raised);
  border: 1px solid var(--rule);
  border-radius: var(--radius-brand-card);
  box-shadow: 0 34px 90px rgba(0, 0, 0, 0.34);
}

.auth-mark-card { margin-bottom: 42px; }
.auth-card-mfa { max-width: 620px; }

.auth-card h1,
.auth-card h2 {
  margin: 0;
  font-size: clamp(28px, 4vw, 39px);
  line-height: 1.08;
  letter-spacing: -0.04em;
}

.auth-intro {
  margin: var(--space-3) 0 var(--space-7);
  color: var(--ink-soft);
  line-height: 1.6;
}

.auth-subheading {
  margin: var(--space-8) 0 var(--space-4);
  font-size: 16px !important;
  letter-spacing: -0.02em !important;
}

.auth-card form {
  display: grid;
  gap: 18px;
}

.auth-field,
.auth-card form > label:not(.sr-only):not(.auth-check) {
  display: grid;
  gap: 7px;
}

.auth-field label,
.auth-card form > label {
  color: var(--ink);
  font-size: 12px;
  font-weight: 750;
}

.auth-label-row {
  display: flex;
  justify-content: space-between;
  gap: 18px;
  align-items: baseline;
}

.auth-label-row a,
.auth-card a {
  color: #77ddd1;
}

.auth-label-row a {
  font-size: var(--text-note);
  font-weight: 650;
}

.auth-field input,
.auth-card form > label input:not([type="checkbox"]),
.auth-card input[type="text"],
.auth-card input[type="password"],
.auth-card input[type="email"] {
  width: 100%;
  min-height: 49px;
  padding: 11px 13px;
  font-family: var(--sans);
  font-size: 15px;
  color: var(--ink);
  background: var(--paper-sunk);
  border: 1px solid var(--rule);
  border-radius: var(--radius-brand-sm);
  box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.015);
}

.auth-field input:focus,
.auth-card input:focus {
  border-color: var(--series);
  outline: 3px solid var(--series-soft);
  outline-offset: 0;
}

.auth-field small {
  color: var(--ink-faint);
  font-size: var(--text-note);
}

.auth-card .btn {
  width: 100%;
  min-height: 50px;
  padding: 10px 18px;
  border-radius: var(--radius-brand-sm);
  font-size: var(--text-base);
  font-weight: 800;
}

.auth-card .btn-primary {
  color: var(--cta-ink);
  background: linear-gradient(135deg, var(--cta-from), var(--cta-to));
  border-color: transparent;
  box-shadow: 0 12px 34px var(--cta-shadow);
}

.auth-card .btn-primary:hover {
  filter: brightness(1.04);
  transform: translateY(-1px);
}

.auth-card .btn-secondary {
  color: var(--ink-soft);
  background: transparent;
  border: 1px solid var(--rule);
  box-shadow: none;
}

.auth-card form + form { margin-top: var(--space-3); }

.auth-switch {
  margin: var(--space-6) 0 0;
  color: var(--ink-faint);
  font-size: 12px;
  text-align: center;
}

.auth-policies {
  display: grid;
  gap: 10px;
  margin: 2px 0 0;
  padding: var(--space-4);
  border: 1px solid var(--rule);
  border-radius: var(--radius-brand-control);
}

.auth-policies legend {
  padding: 0 7px;
  color: var(--ink-faint);
  font-family: var(--display);
  font-size: var(--text-brand-micro);
  font-weight: 800;
  letter-spacing: 0.12em;
  text-transform: uppercase;
}

.auth-check {
  display: grid;
  grid-template-columns: 18px 1fr;
  gap: 9px;
  align-items: start;
  color: var(--ink-soft);
  font-size: var(--text-note);
  font-weight: 500;
  line-height: 1.45;
}

.auth-check input {
  width: 16px;
  height: 16px;
  margin: 1px 0 0;
  accent-color: var(--series);
}

.auth-check small { color: var(--ink-faint); }

.auth-card .flash {
  margin: 0 0 var(--space-5);
  border-left-color: var(--canada);
  background: rgba(223, 67, 81, 0.09);
}
.auth-card .flash.success {
  border-left-color: var(--up);
  background: color-mix(in srgb, var(--up) 12%, transparent);
}

.auth-card img {
  max-width: 100%;
  height: auto;
  padding: var(--space-2);
  border-radius: var(--radius-brand-control);
  background: white;
}

.auth-qr {
  display: grid;
  grid-template-columns: 220px 1fr;
  gap: var(--space-6);
  align-items: center;
  margin: 26px 0;
}

.auth-qr > div {
  display: grid;
  gap: 9px;
  min-width: 0;
}

.auth-qr span {
  color: var(--ink-faint);
  font-family: var(--display);
  font-size: var(--text-brand-micro);
  font-weight: 800;
  letter-spacing: 0.12em;
  text-transform: uppercase;
}

.auth-qr code {
  overflow-wrap: anywhere;
  color: var(--ink);
  font-size: var(--text-note);
}

.auth-qr a { font-size: 12px; }

.auth-recovery-list {
  display: grid;
  grid-template-columns: repeat(2, minmax(0, 1fr));
  gap: var(--space-2);
  margin: var(--space-6) 0;
  padding: 0;
  list-style: none;
}

.auth-recovery-list li {
  padding: 9px 11px;
  background: var(--paper-sunk);
  border: 1px solid var(--rule);
  border-radius: 7px;
  text-align: center;
}

.auth-textarea-label {
  display: grid;
  gap: 7px;
  color: var(--ink);
  font-size: 12px;
  font-weight: 750;
}

.auth-textarea-label textarea {
  width: 100%;
  padding: var(--space-3);
  resize: vertical;
  color: var(--ink);
  background: #081522;
  border: 1px solid var(--rule);
  border-radius: var(--radius-brand-sm);
  font-family: var(--display);
  font-size: 12px;
  line-height: 1.6;
}

.auth-download-row {
  margin: 14px 0 22px;
  font-size: 12px;
}

/* --------------------------------------------------------------- landing */

.landing {
  --paper:        var(--surface-brand);
  --paper-raised: var(--surface-brand-raised);
  --paper-sunk:   var(--surface-brand-sunk);
  --ink:          var(--on-brand);
  --ink-soft:     #afc2d1;
  --ink-faint:    #7892a6;
  --rule:         rgba(151, 184, 207, 0.2);
  --grid:         rgba(151, 184, 207, 0.1);
  --series:       #55dbc9;
  --series-soft:  rgba(85, 219, 201, 0.16);
  --signal:       #ffab3d;
  --up:           #62d6a2;
  /* Audit #99 fixes BUG-0098. One number for the page's side margin, read by
     both the .landing-nav/.landing-hero/… width rule and the chapter rule
     below. They used to carry the figure separately, and because every
     section is also a [data-landing-chapter], the narrow-viewport override
     (0,1,0) lost to the chapter rule (0,3,0) on everything except the nav —
     which is not a chapter. The result was a 15px nav gutter above a 24px
     content gutter on every phone. */
  --landing-gutter: 24px;
  min-height: 100vh;
  color: var(--ink);
  /* IMP-0242: the third layer here was a 0.2%-wide vertical line at 50%. It
     was meant as a fold marker; on a deck of full-viewport slides it draws a
     seam straight down the middle of every composition and reads as a
     rendering artifact, most obviously through the FAQ grid. */
  background:
    radial-gradient(circle at 78% 8%, rgba(52, 119, 150, 0.2), transparent 28rem),
    radial-gradient(circle at 18% 34%, rgba(85, 219, 201, 0.07), transparent 24rem),
    var(--paper);
}

/* IMP-0241. JavaScript opts the landing page into a true viewport deck. The
   nav stays put and main becomes the only vertical scroller. Without the
   opt-in classes this remains normal flow. */
html.landing-scroll-story,
html.landing-scroll-story body {
  height: 100%;
  overflow: hidden;
}

.landing.has-scroll-story {
  --landing-nav-height: 78px;
  height: 100svh;
  overflow: hidden;
}

.landing.has-scroll-story [data-landing-deck] {
  position: relative;
  height: calc(100svh - var(--landing-nav-height));
  overflow-x: hidden;
  overflow-y: auto;
  overscroll-behavior-y: contain;
  /* Audit #96, part of BUG-0095. `y mandatory` is what made a chapter taller
     than the deck unreachable: there is no resting position inside a snap
     area under mandatory snap, so the browser pulls you back off any content
     that does not fit on the first screen. Proximity snaps when you come to
     rest near a boundary and otherwise leaves the scroll alone, which is the
     behaviour a deck actually wants — landing.js does the deliberate
     one-slide advance, snap only tidies up after a free scroll.
     scroll-behavior: smooth is gone from here for a related reason: it
     applied to landing.js's own scrollTo as well as to native anchor jumps,
     so the controller could not choose an instant scroll and the two
     animations fought during a gesture. Smoothness now lives in the one
     scrollTo call that wants it. */
  scroll-snap-type: y proximity;
  scrollbar-width: none;
}

.landing.has-scroll-story [data-landing-deck]::-webkit-scrollbar { display: none; }

/* Audit #96 fixes BUG-0095. A chapter is AT LEAST one viewport, never exactly
   one. The three-way height clamp this replaces (height + min-height +
   max-height, all calc(100svh - nav), with overflow: hidden) treated the
   height of a slide's content as something the design controls. It is not: it
   moves with the viewport's own height, with the locale — French runs longer
   than English on nearly every string here — with the user's zoom level, and
   with any state that reveals text. Everything past the fold was destroyed
   rather than scrolled to, because html and body carry overflow: hidden too.
   Measured on staging before this change, at EVERY viewport from 320x568 to
   1920x1080 the closing chapter cut off part of the footer's legal links,
   which stayed keyboard-focusable while invisible; at 320x568 the hero's two
   calls to action were outside the slide entirely.

   min-height alone keeps the deck feel — a short chapter still fills the
   screen — while a chapter that needs more room simply takes it and the deck
   scrolls through it. landing.js pages within any chapter taller than the
   deck before advancing, so nothing is skipped. */
.landing.has-scroll-story [data-landing-chapter] {
  position: relative;
  isolation: isolate;
  width: min(1180px, calc(100% - var(--landing-gutter) * 2));
  min-height: calc(100svh - var(--landing-nav-height));
  margin-inline: auto;
  scroll-snap-align: start;
}

.landing.has-scroll-story [data-landing-chapter]::before {
  content: "";
  position: absolute;
  z-index: -1;
  inset: clamp(18px, 4vw, 56px) clamp(-48px, -3vw, -18px);
  border-radius: var(--radius-brand-halo);
  pointer-events: none;
  opacity: 0;
  background:
    radial-gradient(circle at 18% 28%, rgba(85, 219, 201, 0.1), transparent 32rem),
    radial-gradient(circle at 84% 72%, rgba(98, 197, 230, 0.08), transparent 26rem);
  transform: scale(0.96);
  transition:
    opacity 760ms cubic-bezier(0.22, 1, 0.36, 1),
    transform 900ms cubic-bezier(0.22, 1, 0.36, 1);
}

/* IMP-0242: 640/820ms outlasted the ~450ms it takes the deck to travel one
   slide, so the incoming chapter was still resolving after it had stopped
   moving — half the reason the deck did not read as sliding. The other half
   was landing.js flipping these classes before the scroll started; it now
   flips them from the scroll position. */
.landing.has-scroll-story [data-landing-chapter] > * {
  transition:
    opacity 420ms cubic-bezier(0.22, 1, 0.36, 1),
    transform 500ms cubic-bezier(0.22, 1, 0.36, 1),
    filter 420ms ease;
  will-change: opacity, transform;
}

.landing.has-scroll-story [data-landing-chapter].is-next > * {
  opacity: 0.18;
  filter: blur(2px) saturate(0.62);
  transform: translate3d(0, 64px, 0) scale(0.955);
}

.landing.has-scroll-story [data-landing-chapter].is-past > * {
  opacity: 0.2;
  filter: blur(2px) saturate(0.65);
  transform: translate3d(0, -52px, 0) scale(0.96);
}

.landing.has-scroll-story [data-landing-chapter].is-active::before { opacity: 1; transform: scale(1); }
.landing.has-scroll-story [data-landing-chapter].is-active > * { opacity: 1; filter: none; transform: none; }

.landing.has-scroll-story :is(
  .landing-steps li,
  .landing-feature-grid article,
  .landing-privacy-grid > div,
  .landing-faq-grid > div
) {
  transition:
    opacity 380ms cubic-bezier(0.22, 1, 0.36, 1),
    transform 460ms cubic-bezier(0.22, 1, 0.36, 1),
    background var(--motion-base) ease;
}

.landing.has-scroll-story .is-next :is(
  .landing-steps li,
  .landing-feature-grid article,
  .landing-privacy-grid > div,
  .landing-faq-grid > div
) {
  opacity: 0.4;
  transform: translateY(18px);
}

.landing.has-scroll-story .is-active :is(
  .landing-steps li,
  .landing-feature-grid article,
  .landing-privacy-grid > div,
  .landing-faq-grid > div
) {
  opacity: 1;
  transform: none;
}

.landing.has-scroll-story .is-active :is(
  .landing-steps li,
  .landing-feature-grid article,
  .landing-privacy-grid > div,
  .landing-faq-grid > div
):nth-child(2) { transition-delay: 55ms; }

.landing.has-scroll-story .is-active :is(
  .landing-steps li,
  .landing-feature-grid article,
  .landing-privacy-grid > div,
  .landing-faq-grid > div
):nth-child(3) { transition-delay: 110ms; }

.landing.has-scroll-story .is-active :is(
  .landing-feature-grid article,
  .landing-privacy-grid > div,
  .landing-faq-grid > div
):nth-child(n+4) { transition-delay: 165ms; }

/* Audit #98 fixes BUG-0097. The rail is only ever drawn where the page has a
   gutter wide enough to hold it. It used to appear from 901px up with a
   `right: max(14px, …)` floor, and between roughly 901 and 1230px that floor
   parked it on top of the centred content column — measured at 1180px its
   left edge was 1143 against a content right edge of 1156, at 1024px it was
   987 against 1000. pointer-events: none meant it never blocked a click,
   which is why it read as a stray mark rather than a bug. From 1281px the
   max() floor no longer binds and the rail sits in real margin. */
.landing-chapter-rail {
  position: fixed;
  z-index: 4;
  top: 50%;
  right: max(14px, calc((100vw - 1280px) / 2));
  display: none;
  gap: 9px;
  padding: var(--space-3) 9px;
  border: 1px solid var(--rule);
  border-radius: 999px;
  background: color-mix(in srgb, var(--paper-raised) 76%, transparent);
  box-shadow: 0 14px 46px rgba(0, 0, 0, 0.24);
  backdrop-filter: blur(14px);
  pointer-events: none;
  transform: translateY(-50%);
}

.landing-chapter-rail i {
  display: block;
  width: 3px;
  height: 7px;
  border-radius: 999px;
  background: var(--ink-faint);
  opacity: 0.55;
  transition:
    height 420ms cubic-bezier(0.22, 1, 0.36, 1),
    opacity 320ms ease,
    background 320ms ease,
    box-shadow 320ms ease;
}

.landing-chapter-rail i.is-active {
  height: 26px;
  opacity: 1;
  background: var(--series);
  box-shadow: 0 0 14px rgba(85, 219, 201, 0.62);
}

@media (min-width: 1281px) {
  .landing-chapter-rail { display: grid; }
}

.landing-nav,
.landing-hero,
.landing-strip,
.landing-section,
.landing-final,
.landing-footer {
  width: min(1180px, calc(100% - var(--landing-gutter) * 2));
  margin-inline: auto;
}

.landing-nav {
  position: relative;
  z-index: 5;
  height: 78px;
  min-height: 78px;
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 18px;
  border-bottom: 1px solid var(--rule);
  background: color-mix(in srgb, var(--paper) 88%, transparent);
  backdrop-filter: blur(18px);
}

.landing-nav .public-nav {
  width: 100%;
  padding-bottom: 0;
  border-bottom: 0;
}

/* Audit #97, part of BUG-0096: the mark is the one thing in the nav row that
   is allowed to give ground. A flex item's default min-width is auto, so
   without this it refuses to go below its text and pushes the links — and
   with them the Create-account button — off the right edge instead. */
.landing-mark {
  display: inline-flex;
  flex-direction: column;
  justify-content: center;
  min-width: 0;
  min-height: 34px;
  padding-left: 46px;
  position: relative;
  overflow: hidden;
  font-family: var(--display);
  text-transform: uppercase;
  line-height: 1;
}

.landing-mark span,
.landing-mark small {
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

.landing-mark:hover { text-decoration: none; }

.landing-mark span {
  font-size: 15px;
  font-weight: 750;
  letter-spacing: 0.2em;
}

.landing-mark small {
  margin-top: 6px;
  color: var(--ink-faint);
  font-family: var(--sans);
  font-size: var(--text-brand-micro);
  font-weight: 650;
  letter-spacing: 0.13em;
}

.landing-nav nav,
.landing-footer nav {
  display: flex;
  align-items: center;
  gap: var(--space-6);
  font-size: 13px;
  font-weight: 600;
}

.landing-footer nav {
  flex-wrap: wrap;
  justify-content: flex-end;
}

.landing-nav nav { flex: 0 0 auto; }
.landing-nav nav a { white-space: nowrap; }

.landing-login { margin-left: var(--space-3); }

.landing .landing-cta,
.landing-secondary {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  min-height: var(--control-height);
  padding: 0 var(--space-5);
  border: 1px solid var(--ink);
  border-radius: var(--radius);
  font-weight: 700;
}

.landing .landing-cta {
  color: var(--cta-ink);
  background: linear-gradient(135deg, var(--cta-from), var(--cta-to));
  border-color: transparent;
  box-shadow: 0 10px 30px var(--cta-shadow);
}

.landing .landing-cta:hover,
.landing-secondary:hover {
  text-decoration: none;
  transform: translateY(-1px);
}

/* Audit #90 fixes BUG-0087. This used to drop min-height to 36px, which put
   the landing nav's Create-account button — the primary conversion action on
   the front door, and the variant a phone renders — under the 44px floor
   --control-height names as "the WCAG 2.5.5/2.5.8 touch-target floor every
   interactive control was already built to". Compactness here is horizontal,
   not vertical: the tighter padding-inline and smaller type still distinguish
   it from the hero CTA, and .landing-nav is 78px tall so the inherited 44px
   sits comfortably inside it. */
.landing-cta-small {
  padding-inline: 15px;
  font-size: 12px;
}

.landing-label-compact { display: none; }

.landing-secondary { background: var(--paper-raised); }

.landing-hero {
  position: relative;
  display: grid;
  grid-template-rows: minmax(0, 1fr) auto;
  gap: 0;
  align-items: stretch;
  padding-block: clamp(22px, 4vh, 52px) 0;
}

/* Audit #96: the right column used to be minmax(480px, 1.1fr), a hard floor
   that the grid honours by overflowing its container rather than by shrinking.
   Both tracks may now collapse; the instrument narrows instead of pushing the
   copy off the slide. */
.landing-hero-stage {
  display: grid;
  grid-template-columns: minmax(0, 0.92fr) minmax(0, 1.08fr);
  gap: clamp(var(--space-8), 6vw, 94px);
  align-items: center;
  min-height: 0;
}

.landing-kicker {
  margin: 0 0 var(--space-4);
  color: var(--series);
  font-family: var(--display);
  font-size: var(--text-note);
  font-weight: 750;
  letter-spacing: 0.12em;
  text-transform: uppercase;
}

/* Audit #96: every vertical figure in the hero is now a function of the
   viewport's HEIGHT as well as its width. A headline sized purely in vw is
   the same 62px on a 1440x1080 screen and a 1440x600 one, and the second is
   where the copy used to run past the bottom of the slide and take the two
   calls to action with it. min(vw, vh) lets whichever dimension is scarcer
   set the size. */
.landing-hero h1 {
  max-width: 650px;
  margin: 0;
  font-size: clamp(34px, min(4.35vw, 6.6vh), 62px);
  line-height: 1.01;
  letter-spacing: -0.055em;
  text-wrap: balance;
}

.landing-lede {
  max-width: 610px;
  margin: clamp(14px, 3vh, var(--space-7)) 0 0;
  color: var(--ink-soft);
  font-size: clamp(15px, min(1.3vw, 2.2vh), 18px);
  line-height: 1.6;
}

.landing-actions {
  display: flex;
  align-items: center;
  gap: var(--space-3);
  flex-wrap: wrap;
  margin-top: clamp(var(--space-4), 3.4vh, var(--space-8));
}

.landing-assurance {
  margin: clamp(var(--space-2), 1.5vh, 14px) 0 0;
  color: var(--ink-faint);
  font-size: 12px;
}

.landing-instrument {
  overflow: hidden;
  background: var(--paper-raised);
  border: 1px solid var(--rule);
  border-radius: var(--radius-brand-panel);
  box-shadow:
    0 34px 90px rgba(0, 0, 0, 0.38),
    0 0 0 1px rgba(85, 219, 201, 0.05),
    0 0 70px rgba(69, 189, 211, 0.07);
  transform: perspective(1100px) rotateY(-2deg) rotateX(1deg);
}

.landing-instrument-head {
  display: flex;
  justify-content: space-between;
  padding: 14px 18px;
  border-bottom: 1px solid var(--rule);
  font-family: var(--display);
  font-size: 12px;
  font-weight: 700;
  background: linear-gradient(90deg, rgba(85, 219, 201, 0.06), transparent);
}

.landing-live {
  display: inline-flex;
  align-items: center;
  gap: 7px;
  color: var(--signal);
  text-transform: uppercase;
  letter-spacing: 0.08em;
  font-size: 10px;
}

.landing-live i {
  width: 7px;
  height: 7px;
  background: currentColor;
  border-radius: 50%;
}

.landing-instrument-grid {
  display: grid;
  grid-template-columns: repeat(3, 1fr);
  border-bottom: 1px solid var(--rule);
}

.landing-instrument-grid div {
  padding: var(--space-5) 18px 17px;
  border-right: 1px solid var(--rule);
}

.landing-instrument-grid div:last-child { border-right: 0; }

.landing-instrument-grid strong {
  display: block;
  font-family: var(--display);
  font-size: clamp(20px, 2.3vw, 30px);
  font-variant-numeric: tabular-nums;
  line-height: 1;
}

.landing-instrument-grid span,
.landing-routes > span {
  display: block;
  margin-top: var(--space-2);
  color: var(--ink-faint);
  font-size: var(--text-brand-micro);
  font-weight: 700;
  letter-spacing: 0.1em;
  text-transform: uppercase;
}

.landing-chart {
  padding: var(--space-6) 18px var(--space-1);
  background:
    radial-gradient(circle at 72% 42%, rgba(85, 219, 201, 0.08), transparent 13rem),
    linear-gradient(var(--grid) 1px, transparent 1px);
  background-size: auto, 100% 47px;
}

.landing-chart svg { display: block; width: 100%; }
.landing-gridline { fill: none; stroke: var(--grid); stroke-width: 1; }
.landing-area { fill: var(--series-soft); }
.landing-line {
  fill: none;
  stroke: var(--series);
  stroke-width: 2.5;
  filter: drop-shadow(0 0 5px rgba(85, 219, 201, 0.45));
}

.landing-routes {
  padding: var(--space-3) 18px var(--space-4);
  border-top: 1px solid var(--rule);
}

.landing-routes ol { list-style: none; margin: var(--space-2) 0 0; padding: 0; }

.landing-routes li {
  display: flex;
  gap: 14px;
  padding: 6px 0;
  border-bottom: 1px solid var(--grid);
  font-family: var(--display);
  font-size: var(--text-note);
}

.landing-routes li:last-child { border-bottom: 0; }
.landing-routes b { overflow: hidden; text-overflow: ellipsis; font-weight: 550; }
.landing-routes em { margin-left: auto; color: var(--ink-faint); font-style: normal; }

.landing-strip {
  display: grid;
  grid-template-columns: repeat(4, 1fr);
  border-block: 1px solid var(--rule);
  background: rgba(13, 27, 44, 0.62);
  backdrop-filter: blur(12px);
}

.landing-hero .landing-strip {
  width: 100%;
  min-height: clamp(70px, 10.5vh, 96px);
  margin-inline: 0;
}

.landing-strip div {
  padding: clamp(11px, 1.8vh, 18px) 22px;
  border-right: 1px solid var(--rule);
}

.landing-strip div:last-child { border-right: 0; }
.landing-strip strong { display: block; font-family: var(--display); font-size: 13px; }
.landing-strip span { display: block; margin-top: var(--space-2); color: var(--ink-faint); font-size: var(--space-3); }

/* IMP-0242. Every content slide is a head row and a body row, and the body
   row takes all the height the head does not.

   What this replaces centred a content block of its own intrinsic height
   inside a full-viewport frame, which is why the slides read as mostly empty:
   at 1440x900 the ink filled 45% of the questions slide, 52% of the pricing
   slide and 62% of this one, all of it as a band floating in the middle with
   dead space above and below. Stretching the body instead means the cards are
   the slide — tall on a tall screen, compact on a short one — and no card
   needs a min-height figure to be told how big to be. That in turn is why the
   @media (max-height: …) table of card heights this file used to carry is
   gone: the sizing is intrinsic now, so there is nothing to restate per
   breakpoint. */
.landing-section {
  position: relative;
  display: grid;
  grid-template-rows: auto minmax(0, 1fr);
  gap: clamp(var(--space-5), 4vh, 50px);
  padding-block: clamp(var(--space-6), 5vh, 60px);
  border-bottom: 1px solid var(--rule);
}

.landing-section-intro { max-width: 760px; }

.landing-section h2,
.landing-final h2 {
  margin: 0;
  font-size: clamp(27px, min(4vw, 6vh), 52px);
  line-height: 1.08;
  letter-spacing: -0.045em;
}

.landing-section-intro > p:not(.landing-kicker) {
  max-width: 640px;
  margin: clamp(10px, 2vh, var(--space-5)) 0 0;
  color: var(--ink-soft);
  font-size: clamp(14px, min(1.15vw, 1.9vh), 16px);
  line-height: 1.65;
}

/* The one body on this page that is a single row of cards, and so the one
   that would otherwise take the whole of a tall slide for three short
   paragraphs — at 1440x900 stretching left 350px between the step number and
   its heading, and at 834x1112 far worse. The other three bodies get their
   proportions from having two rows to fill; this one has to be told. It is
   the only height figure left in the landing CSS, it is stated once, and it
   is relative, so it does not need a breakpoint table behind it. The leftover
   becomes breathing room around the row rather than a hole inside each card. */
.landing-steps {
  display: grid;
  grid-template-columns: repeat(3, 1fr);
  grid-auto-rows: auto;
  gap: 1px;
  margin: 0;
  padding: 1px;
  align-self: center;
  background: var(--rule);
  list-style: none;
  border-radius: var(--radius-brand-grid);
  overflow: hidden;
}

.landing-steps li { min-height: clamp(165px, 25vh, 255px); }

.landing-steps li,
.landing-feature-grid article,
.landing-privacy-grid > div,
.landing-faq-grid > div {
  display: flex;
  flex-direction: column;
  padding: clamp(17px, 2.6vh, var(--space-7));
  background: var(--paper-raised);
  transition: background var(--motion-base) ease;
}

.landing-steps li:hover { background: #102238; }

.landing-card-index {
  color: var(--series);
  font-family: var(--display);
  font-size: var(--text-note);
  font-weight: 750;
}

.landing-steps h3,
.landing-feature-grid h3,
.landing-privacy-grid h3,
.landing-faq-grid h3 {
  margin: 0;
  font-size: clamp(14px, min(1.15vw, 2vh), 16px);
}

/* IMP-0242: a card with an index number puts the number at the top and its
   heading and copy at the foot, and the auto margin is the whole mechanism.
   The rule this replaces was `margin-top: 54px`, a fixed gap that on a tall
   card left the text stranded near the top under a third of a card of blank
   — the same emptiness at card scale that the slides had at screen scale —
   and on a short one was 54px the card could not spare. An auto margin takes
   exactly the space that is going spare, whatever that is, so the card reads
   as composed at 1920x1080 and merely tight at 320x568 without a breakpoint
   between the two. `grid-auto-rows: 1fr` on the grids above makes every card
   in a row the same height, so the feet line up. */
.landing-steps h3,
.landing-feature-grid h3 { margin-top: auto; }

.landing-steps p,
.landing-feature-grid p,
.landing-privacy-grid p,
.landing-faq-grid p {
  margin: clamp(6px, 1.2vh, 11px) 0 0;
  color: var(--ink-soft);
  font-size: clamp(12px, min(0.95vw, 1.6vh), 13px);
  line-height: 1.6;
}

.landing-feature-grid {
  display: grid;
  grid-template-columns: repeat(3, 1fr);
  grid-auto-rows: 1fr;
  border-top: 1px solid var(--rule);
  border-left: 1px solid var(--rule);
  border-radius: var(--radius-brand-grid);
  overflow: hidden;
}

.landing-feature-grid article {
  border-right: 1px solid var(--rule);
  border-bottom: 1px solid var(--rule);
}

.landing-feature-grid article:hover {
  background:
    linear-gradient(145deg, rgba(85, 219, 201, 0.08), transparent 55%),
    #102238;
}

/* The privacy slide used to be a two-column grid of its own — heading left,
   cards right — with a minmax(480px, …) floor on the card column. On a 1440px
   screen that squeezed a 52px headline into a 380px column and ran it to six
   lines while the cards beside it sat half empty; below 1080px it overflowed.
   It is an ordinary head-over-body slide now, like the other three. */
.landing-privacy-grid {
  display: grid;
  grid-template-columns: repeat(2, 1fr);
  grid-auto-rows: 1fr;
  gap: 0;
  border-top: 1px solid var(--rule);
}

.landing-privacy-grid > div {
  padding-inline: 0 var(--space-6);
  background: none;
  border-bottom: 1px solid var(--rule);
}

.landing-privacy-grid > div:nth-child(odd) {
  padding-right: var(--space-7);
  border-right: 1px solid var(--rule);
}

.landing-privacy-grid > div:nth-child(even) { padding-inline: var(--space-7) 0; }
.landing-policy-link { margin: clamp(9px, 1.8vh, var(--space-4)) 0 0; font-size: var(--space-3); color: var(--ink-faint); }
.landing-policy-link a { color: var(--ink); text-decoration: underline; text-underline-offset: 2px; }

/* IMP-0242: the free slide — what the service includes, beside the questions
   people ask before installing it. */
.landing-free-body {
  display: grid;
  grid-template-columns: minmax(0, 0.82fr) minmax(0, 1.18fr);
  gap: clamp(var(--space-5), 2.6vw, var(--space-12));
  align-items: stretch;
}

.landing-plan-card {
  display: grid;
  grid-template-rows: auto minmax(0, 1fr) auto;
  padding: clamp(var(--space-5), 3vh, var(--space-8));
  background:
    radial-gradient(circle at 100% 0%, rgba(85, 219, 201, 0.13), transparent 17rem),
    var(--paper-raised);
  border: 1px solid rgba(85, 219, 201, 0.32);
  border-radius: var(--radius-brand-panel);
  box-shadow: 0 28px 80px rgba(0, 0, 0, 0.26);
}

.landing-plan-label {
  display: block;
  padding-bottom: clamp(var(--space-3), 2vh, 22px);
  border-bottom: 1px solid var(--rule);
  font-family: var(--display);
  font-weight: 750;
}

.landing-plan-card ul {
  display: grid;
  align-content: start;
  gap: clamp(2px, 1vh, 10px);
  margin: clamp(14px, 2.4vh, var(--space-6)) 0;
  padding: 0;
  list-style: none;
}

.landing-plan-card li {
  padding: var(--space-1) 0 var(--space-1) 22px;
  position: relative;
  font-size: clamp(13px, min(1vw, 1.7vh), 14px);
}

.landing-plan-card li::before { content: "✓"; position: absolute; left: 0; color: var(--up); font-weight: 800; }
.landing-plan-card .landing-cta { width: 100%; }

.landing-faq-grid {
  display: grid;
  grid-template-columns: repeat(2, 1fr);
  grid-auto-rows: 1fr;
  gap: 1px;
  background: var(--rule);
  border: 1px solid var(--rule);
  border-radius: var(--radius-brand-grid);
  overflow: hidden;
}

.landing-faq-grid > div { grid-template-rows: auto minmax(0, 1fr); }

.landing-final {
  display: flex;
  flex-direction: column;
  justify-content: flex-start;
  padding-block: clamp(26px, 4.5vh, 58px) 0;
  text-align: center;
  background:
    radial-gradient(circle at 50% 50%, rgba(85, 219, 201, 0.1), transparent 25rem);
}

.landing-final-content { margin-block: auto; padding-block: clamp(var(--space-4), 4vh, 56px); }
.landing-final .landing-kicker { margin-bottom: 18px; }
.landing-final-content > p:not(.landing-kicker) { margin: 18px 0 0; color: var(--ink-soft); font-size: var(--space-4); }
.landing-final .landing-actions { justify-content: center; }

/* Audit #96, the specific case that made BUG-0095 visible at every single
   viewport: this footer carries twelve links, the last of them Accessibility
   and Support, and it is the bottom of the closing chapter. When the chapter
   was height-clamped the last row was cut off everywhere from 320px to
   1920px — and stayed keyboard-focusable while invisible, so tabbing walked
   into links nobody could see. The chapter grows now, so the wrap simply
   costs height instead of content. */
.landing-footer {
  display: grid;
  grid-template-columns: auto 1fr;
  gap: var(--space-5) 34px;
  align-items: center;
  width: 100%;
  min-height: clamp(84px, 12vh, 104px);
  padding-block: 14px;
  border-top: 1px solid var(--rule);
}

.landing-mark::before {
  content: "CA";
  position: absolute;
  inset: 0 auto auto 0;
  display: grid;
  place-items: center;
  width: 32px;
  height: 32px;
  border: 1px solid rgba(85, 219, 201, 0.5);
  border-radius: var(--radius-brand-mark);
  color: var(--cta-ink);
  background: linear-gradient(135deg, var(--cta-from), var(--cta-to));
  box-shadow: 0 0 24px rgba(85, 219, 201, 0.22);
  font-size: 10px;
  font-weight: 900;
  letter-spacing: -0.04em;
}

.landing-mark::after {
  content: "";
  position: absolute;
  left: 27px;
  top: -3px;
  width: 9px;
  height: 9px;
  border: 2px solid var(--paper);
  border-radius: 50%;
  background: #df4351;
}

.landing-gradient-text {
  color: #73dfd2;
  background: linear-gradient(90deg, #78e7d7 0%, #62c5e6 55%, #a6b9ff 100%);
  -webkit-background-clip: text;
  background-clip: text;
  -webkit-text-fill-color: transparent;
}

.landing-canada-note {
  margin: clamp(var(--space-2), 1.7vh, var(--space-4)) 0 0;
  color: var(--ink-faint);
  font-size: var(--text-note);
  letter-spacing: 0.03em;
}

.landing-canada-note::before {
  content: "";
  display: inline-block;
  width: 14px;
  height: 2px;
  margin: 0 9px 3px 0;
  background: #df4351;
}

/* ----------------------------------------------------------- responsive */

@media (max-width: 1080px) {
  .hero { grid-template-columns: minmax(0, 1fr); }
  .arrivals, .reach { border-top: 1px solid var(--rule); }
  .feed { max-height: 220px; }
  .reach-list { max-height: 220px; }

  /* Stacked, there is no map beside this column to take a height from, so the
     `flex-basis: 0` that keeps it from outgrowing the map upstairs would
     collapse it to its headings. Back to sizing themselves. */
  .reach { height: auto; min-height: 0; overflow: visible; }
  .shared, .shared-list { flex: 0 0 auto; }
  .landing-hero-stage { gap: 30px; }
  .landing-hero-copy { max-width: 760px; }
  .landing-instrument { width: 100%; }
  /* Six features across three columns needs 1180px to breathe; at the wide
     breakpoint it becomes 3x2 instead of 2x3. */
  .landing-feature-grid { grid-template-columns: repeat(2, 1fr); }
}

@media (max-width: 900px) {
  .posture-grid { grid-template-columns: repeat(2, minmax(0, 1fr)); }
  .auth-shell-split {
    grid-template-columns: minmax(0, 680px);
    gap: 38px;
    align-content: start;
  }
  .auth-context { width: 100%; }
  .auth-kicker { margin-top: 42px; }
  .auth-context h1 {
    max-width: 620px;
    font-size: clamp(34px, 7vw, 52px);
  }
  .auth-context-copy,
  .auth-proof,
  .auth-steps { display: none; }
  .auth-canada { margin-top: var(--space-5); }
  .auth-card { max-width: 680px; }
  .landing { --landing-gutter: 18px; }
  .landing-hero-stage { grid-template-columns: 1fr; }
  .landing-instrument { display: none; }
  .landing-hero-copy { max-width: 720px; }
  .landing-free-body { grid-template-columns: 1fr; }

  /* Audit #97 fixes BUG-0096. The nav compaction used to start at the narrow
     breakpoint (760px). Measured before this change, French overran the
     viewport by 133px at 900px wide and by 9px at 1024px — the row is nowrap
     with no min-width: 0 anywhere, so it does not wrap or shrink, it simply
     hangs off the right edge, and html carries overflow: hidden on this page
     so there is no scrolling to it. English overran by 57px at 320px. The
     compact treatment belongs at the breakpoint where the longer of the two
     locales stops fitting, not the shorter. */
  .landing.has-scroll-story { --landing-nav-height: 68px; }
  .landing-nav { height: 68px; min-height: 68px; }
  .landing-nav nav > a:not(.landing-login):not(.landing-cta) { display: none; }
  .landing-nav nav { gap: 10px; }
  .landing-nav .landing-mark span { font-size: 13px; letter-spacing: 0.1em; }
  .landing-nav .landing-mark small { display: none; }
  .landing-login { margin-left: 0; }
  .landing-cta-small { padding-inline: var(--space-3); }
  .landing-label-wide { display: none; }
  .landing-label-compact { display: inline; }
}

@media (max-width: 760px) {
  :root { --gutter: 16px; }
  .landing.has-scroll-story { --landing-nav-height: 68px; }
  .landing.has-scroll-story [data-landing-chapter].is-next > * {
    transform: translate3d(0, 30px, 0) scale(0.975);
  }
  .landing.has-scroll-story [data-landing-chapter].is-past > * {
    transform: translate3d(0, -24px, 0) scale(0.98);
  }
  .usage-grid { grid-template-columns: 1fr; }
  /* IMP-0218. The footer's mark and tagline stack rather than sitting side by
     side; the link nav already wraps on its own. */
  .public-footer { grid-template-columns: 1fr; }
  .demo-callout { align-items: stretch; flex-direction: column; }
  .help-grid { grid-template-columns: 1fr; }
  .panel-body table { min-width: 560px; }
  .panel-body table th:first-child,
  .panel-body table td:first-child {
    position: sticky;
    left: 0;
    z-index: 1;
    background: var(--paper-raised);
  }
  .public-nav { align-items: flex-start; flex-direction: column; }
  .public-nav-links { justify-content: flex-start; width: 100%; }
  .shell { grid-template-columns: minmax(0, 1fr); }
  .rail {
    flex-direction: row;
    overflow-x: auto;
    padding: 10px 0;
    border-right: 0;
    border-bottom: 1px solid var(--rule);
    gap: 0;
  }
  .rail-brand, .rail-heading { display: none; }
  .site-link {
    border-left: 0;
    border-bottom: 2px solid transparent;
    white-space: nowrap;
  }
  .site-link[aria-current="page"] {
    border-left: 0;
    /* BUG-0043: the narrow-viewport half of the same marker. */
    border-bottom-color: var(--series);
  }
  .site-link .count { display: none; }
  .utility-nav {
    left: 0; right: 0; bottom: 0; padding: var(--space-2);
    justify-content: flex-start; border-width: 1px 0 0;
    /* IMP-0150. flex-wrap: wrap (the base rule) turned this into a two-row
       block on a narrow phone — an admin or operator session has enough
       items (Network, Reports & access / Accounts + Security, Help, Sign
       out) that they never fit one line under 400px — and at 320x568 that
       second row alone covers close to a fifth of the viewport, permanently.
       A single scrollable row, the same overflow-x pattern .rail already
       uses above for the site tabs, keeps the dock's height constant
       regardless of how many items a given session has. */
    flex-wrap: nowrap; overflow-x: auto; -webkit-overflow-scrolling: touch;
  }
  .utility-nav .btn { min-height: 44px; padding-inline: 10px; flex: none; }
  .metric-value { font-size: 21px; }
  .topbar h1 { font-size: 17px; }
  /* Keep the controls together on their own line instead of splitting the
     pill and the select across the wrap. */
  .topbar-right { margin-left: 0; width: 100%; }
  .range-control { width: 100%; }
  .custom-range {
    justify-content: flex-start;
    align-items: flex-end;
  }
  .custom-range label { flex-direction: column; align-items: flex-start; }
  .range-error { text-align: left; }
  .auth,
  .auth-shell {
    padding: 18px 15px 30px;
    background:
      radial-gradient(circle at 15% 4%, rgba(88, 217, 202, 0.09), transparent 18rem),
      var(--paper);
  }
  .auth-shell-split { gap: var(--space-7); }
  .auth-mark { padding-left: 49px; }
  .auth-kicker { margin: 34px 0 11px; }
  .auth-context h1 {
    font-size: clamp(31px, 10vw, 43px);
    line-height: 1.04;
  }
  .auth-canada { font-size: 10px; }
  .auth-card {
    padding: var(--space-7) var(--space-5);
    border-radius: var(--radius-brand-grid);
  }
  .auth-mark-card { margin-bottom: 34px; }
  .auth-card h1,
  .auth-card h2 { font-size: 29px; }
  .auth-label-row { align-items: center; }
  .auth-qr { grid-template-columns: 1fr; }
  .auth-qr img { justify-self: center; }
  .auth-recovery-list { grid-template-columns: 1fr; }
  .landing { --landing-gutter: 15px; }
  .landing-hero h1 { font-size: clamp(31px, 10.5vw, 46px); }

  /* Audit #100 fixes BUG-0093, and carries IMP-0242's half of the same
     decision. These five regions used to become horizontal scroll-snap
     carousels below this breakpoint — `overflow-x: auto` with `flex: 0 0 74%`
     items and no tabindex — which axe reported as scrollable-region-focusable
     on every mobile cell of the browser matrix: a keyboard user could not
     reach any item past the first. Stacking is the honest layout for a phone
     and needs no keyboard affordance at all, because there is no scroll
     container left to reach into. It also costs height, which is only
     affordable now that a chapter may be taller than the deck (BUG-0095). */
  .landing-steps,
  .landing-feature-grid,
  .landing-privacy-grid,
  .landing-faq-grid {
    grid-template-columns: 1fr;
    grid-auto-rows: auto;
  }
  /* Stacked one per row, the step list sizes itself like every other body on
     the page and the single-row hint above has nothing left to do. */
  .landing-steps { align-self: stretch; }
  .landing-steps li { min-height: 0; }
  .landing-steps h3,
  .landing-feature-grid h3 { margin-top: clamp(10px, 3vh, 26px); }
  .landing-privacy-grid > div,
  .landing-privacy-grid > div:nth-child(odd),
  .landing-privacy-grid > div:nth-child(even) {
    padding-inline: 0;
    border-right: 0;
  }
  .landing-feature-grid article:last-child,
  .landing-privacy-grid > div:last-child { border-bottom: 0; }
  .landing-hero .landing-strip { grid-template-columns: repeat(2, 1fr); }
  .landing-strip div:nth-child(even) { border-right: 0; }
  .landing-strip div:nth-child(-n+2) { border-bottom: 1px solid var(--rule); }

  .landing-final .landing-footer {
    width: 100%;
    grid-template-columns: 1fr;
    gap: var(--space-3);
    padding-block: 14px;
  }
  /* The footer link row was the fifth horizontal scroller. It wraps now; the
     chapter has room for the extra line. */
  .landing-footer nav {
    width: 100%;
    justify-content: flex-start;
    flex-wrap: wrap;
    gap: 10px 14px;
    font-size: var(--text-note);
  }
}

/* The network dashboard is already fully navigable from its site rail. Its
   account/product links are secondary navigation, so on this long analytical
   page they belong after the report instead of floating over whichever chart
   or table happens to pass behind the viewport edge. Other dashboard pages
   retain the fixed utility dock and its existing reserved clearance. */
.network-shell .content { padding-bottom: var(--gutter); }
.network-shell + .utility-nav {
  position: static;
  justify-content: flex-end;
  margin: 0;
  padding: 14px var(--gutter);
  background: var(--paper-sunk);
  border-width: 1px 0 0;
  box-shadow: none;
}

/* The phone breakpoint. It sits after the 760px query rather than before it
   because both touch .landing-strip and these have to win; the design system
   allows one block per breakpoint, so there is one. */
@media (max-width: 480px) {
  /* A phone map is only ~150px tall. The 44px expand control and the three
     44px zoom/reset controls cannot share its right edge without one covering
     most of Zoom in, so fullscreen moves to the otherwise-empty top-left. */
  .map-expand { left: 10px; right: auto; }

  .network-chart-head { align-items: stretch; flex-wrap: wrap; }
  .network-chart-title { flex: 1 1 100%; }
  .network-chart-controls {
    flex: 1 1 100%;
    justify-content: space-between;
    margin-left: 0;
  }
  .network-chart-controls select { flex: 1 1 160px; min-width: 0; }

  /* Five metrics in a two-column phone grid otherwise leave a conspicuous
     empty rule-coloured cell. The final network-wide count is the natural
     summary row, so let it use the full width. */
  .network-shell .readout { grid-template-columns: repeat(2, minmax(0, 1fr)); }
  .network-shell .readout .metric:last-child:nth-child(odd) { grid-column: 1 / -1; }

  .network-shell + .utility-nav {
    margin: 0;
    padding: var(--space-3) var(--gutter);
  }

  .posture-grid { grid-template-columns: 1fr; }
  .landing-hero .landing-strip { grid-template-columns: 1fr; }
  .landing-strip div { border-right: 0; border-bottom: 1px solid var(--rule); }
  .landing-strip div:last-child { border-bottom: 0; }
}

/* Reduced motion: the cooling colour still communicates recency, but nothing
   moves, pulses or slides. */
@media (prefers-reduced-motion: reduce) {
  .landing.has-scroll-story [data-landing-deck] { scroll-behavior: auto; }
  .landing-chapter-rail { display: none; }
  .landing.has-scroll-story [data-landing-chapter]::before,
  .landing.has-scroll-story [data-landing-chapter] > *,
  .landing.has-scroll-story [data-landing-chapter] :is(
    .landing-steps li,
    .landing-feature-grid article,
    .landing-privacy-grid > div,
    .landing-faq-grid > div
  ) {
    opacity: 1;
    filter: none;
    transform: none;
    transition: none;
  }
  *, *::before, *::after {
    animation-duration: 0.01ms !important;
    animation-iteration-count: 1 !important;
    transition-duration: 0.01ms !important;
  }
  .skeleton-row { animation: none; background: var(--paper-raised); }
  .toast { animation: none; }
  /* LEGACY-CNT-0044. This fallback used to target `.feed li.is-fresh`, a
     class nothing in the feed ever sets — only map dots use `is-fresh`. So the
     comment above was aspirational: with the animation flattened to 0.01ms the
     cool keyframes' `forwards` fill snapped straight to the resting colour and
     reduced-motion users got no recency cue at all.

     `animation: none` is required, not merely tidy: an animation's fill state
     outranks normal declarations in the cascade, so without it the colour
     below would still lose to the flattened keyframe. `:first-child` bounds
     the amber to the single newest arrival — the feed prepends, and every
     row keeps `is-entering` for the life of the page, so without it the whole
     list would end up amber and mean nothing. */
  .feed li { color: var(--ink-soft); }
  .feed li.is-entering { animation: none; }
  .feed li.is-entering:first-child { color: var(--signal); }
}
