/* ===========================================================================
   Answermend — app.answermend.com — the authenticated dashboard.

   ⚠️ THIS DIRECTORY IS NOT PUBLISHED BY tools/publish-wp.mjs, EVER.
   08-website.md §8.2: the app lives on a subdomain — separate cookie scope,
   separate deploy target, no marketing-site cache in front of it — and its
   pages are not trying to rank. Every page here carries noindex,nofollow and
   canonicalises to https://app.answermend.com/. publish-wp takes an explicit
   file argument, so nothing here is picked up by accident; the danger is
   somebody typing one of these paths at it. Don't.

   WHY THIS IS A SHARED STYLESHEET AND THE MARKETING PAGES INLINE THEIRS.
   build/*.html inline their <style> because WordPress DISCARDS the built
   <head> at publish, so a <link> there would vanish (see the header comment in
   build/pricing.html). That constraint does not exist on a static subdomain
   served as files. Six pages sharing 900 lines of duplicated CSS would drift
   within a week, so the app takes the one file. The tokens below are COPIED
   from build/pricing.html — the production shell, Direction B,
   design/directions/B_BUILD_SPEC.md, owner-approved 2026-08-31 — including the
   contrast corrections that file records. This is not a second design system:
   if a token changes there, it changes here, and the two are meant to be
   diff-able. Nothing below invents a colour, a size or a family.

   THE ONE RULE THIS FILE EXISTS TO ENFORCE VISUALLY
   ------------------------------------------------------------------
   "Not measured" must never be able to be mistaken for zero, for a loss, or
   for a low score. So the four states are carried by THREE signals each —
   colour, border style, and the presence or absence of a numeral:

     cited        solid  green tint   real numerals   "2 of 3 runs"
     mentioned    solid  grey  tint   real numerals   "3 of 3 runs"
     missing      solid  red   tint   real numerals   "0 of 3 runs"   <- a real zero
     not measured DASHED, NO FILL, grey, and an EM DASH where the numeral goes,
                  plus the reason it did not run.

   A measured zero is a solid red chip carrying the digit 0. An unmeasured cell
   is a dashed outline carrying no digit at all. Screenshot the two side by side
   and they are different objects, not different shades of one.
   =========================================================================== */

/* ===========================================================================
   THE THREE BRAND FACES, SELF-HOSTED. Added 2026-09-03.

   Until today this directory loaded no webfont at all and every page carried a
   comment saying so. That comment named the exact repair — "the woff2 files
   for Space Grotesk 500/700, Inter 400/500/600 and IBM Plex Mono 400/500 need
   to sit under this directory, with an @font-face block in app.css pointing at
   them" — and asserted the files "are not in this repo". That last half was
   wrong: all four have been in design/assets/fonts/ since 2026-08-31. They are
   COPIED IN here, byte-identical (sha256 verified against the source copies),
   exactly the way favicon-512.png was copied rather than re-linked — this
   subdomain is a separate deploy target and design/ is not part of it.

   NOTHING WAS DRAWN, GENERATED OR SUBSTITUTED. These are the same four files
   the marketing site serves from wp-content/uploads/2026/08/, declared by
   build/wp-snippets/seo-meta-bridge.php §1 and by build/brand-kit.html. Their
   provenance, byte counts and subsetting decisions are recorded in
   design/assets/SOURCES.md and rebuildable with design/assets/fonts/subset.py.
   All three families are SIL OFL.

   CSP: services/deploy/app/nginx-app.conf serves this host under
   `font-src 'self'`, and these are same-origin relative URLs, so the policy
   already permits them. NOTHING IN THE POLICY IS WEAKENED OR NEEDS TO BE — the
   thing it forbids is a cross-origin face, which is why a fonts.gstatic.com
   link was never the answer here.

   WEIGHT RANGES, READ OFF THE FILES RATHER THAN ASSUMED (fvar wght axis):
     spacegrotesk-var-latin  variable, 500–700
     inter-var-latin         variable, 400–600 (opsz pinned at 14 upstream)
     ibmplexmono-400-latin   static 400
     ibmplexmono-500-latin   static 500
   Every weight this stylesheet asks for is inside those ranges. So the two
   variable faces are declared with a RANGE rather than as separate discrete
   weights — which is where this file deliberately differs from
   seo-meta-bridge.php, and the difference is a fix, not a drift: .btn-secondary
   and `table.t tbody th` both ask for font-weight:550, and a set of discrete
   400/500/600 declarations has no face at 550 and snaps it to one of them. The
   range instances the axis at the weight actually requested.

   NO unicode-range, ON PURPOSE. It exists to stop a browser fetching a face
   for text that cannot use it, and it only pays when a family is split across
   several files. Each family here is ONE file covering the whole latin subset,
   so a range would buy nothing and can only be wrong: get it one codepoint too
   narrow and a glyph the file actually contains — → ↗ ≠ ≥ ✓, every one of
   which these pages render — silently falls back to a system face with nothing
   on screen saying so. The production shell declares none either.

   font-display:swap, not optional. These pages are behind a sign-in and the
   first paint is text a customer is waiting on; `block` would hold that text
   invisible for up to three seconds, and `optional` would mean a slow first
   load never gets the brand face at all. Swapping is visible and honest.
   =========================================================================== */
@font-face{
  font-family:"Space Grotesk";
  font-style:normal;
  font-weight:500 700;
  font-display:swap;
  src:url("spacegrotesk-var-latin.82752f8ab4.woff2") format("woff2");
}
@font-face{
  font-family:"Inter";
  font-style:normal;
  font-weight:400 600;
  font-display:swap;
  src:url("inter-var-latin.5d0cc9826c.woff2") format("woff2");
}
@font-face{
  font-family:"IBM Plex Mono";
  font-style:normal;
  font-weight:400;
  font-display:swap;
  src:url("ibmplexmono-400-latin.d377dc3b23.woff2") format("woff2");
}
@font-face{
  font-family:"IBM Plex Mono";
  font-style:normal;
  font-weight:500;
  font-display:swap;
  src:url("ibmplexmono-500-latin.20cb13e819.woff2") format("woff2");
}

:root{
  /* ground */
  --bg:#FBFBFC;
  --pap:#F7F7F9;
  --card:#FFFFFF;
  --ink:#0A0E12;

  /* text */
  --tx2:#59646E;
  --tx3:#646E78;       /* muted / mono labels — #8A95A0 failed at 2.79–3.05:1 */
  --on-ink:#9BA6AF;

  /* line */
  --line:#E7E9EC;
  --line2:#F0F1F3;

  /* accent */
  --acc:#F07800;
  --accD:#AD5500;
  --accS:#FFF3E4;
  --accB:#FADFBE;
  --accTx:#8A5200;

  /* the four states — never invent a fifth */
  --grn:#12784A; --grnS:#E9F6EF; --grnB:#CDE7D6;   /* cited */
  --red:#C42B1C; --redS:#FDEDEB; --redB:#F3D3CF;   /* missing */
  /* mentioned — deliberately NOT a success colour. The two values below were
     literals in .pm until 2026-09-04; they became tokens so the dark scheme
     can move them WITHOUT a component override (the values are unchanged). */
  --pmS:#F3F4F6; --pmB:#E5E7EA;                    /* mentioned, --tx2 text */
  --dash:#848D96;      /* the not-measured carrier. Load-bearing and corrected
                          twice upstream; clears --bg 3.26 · --pap 3.15 ·
                          --card 3.37 · --accS 3.08. Do not lighten it. */
  --dash-fill:#FCFCFD;

  /* ── SURFACE-ROLE TOKENS. Added 2026-09-04, for the dark scheme. ─────────
     --ink was doing two jobs: "the text colour" and "the dark thing text sits
     on" (.btn-primary's text, .btn-dark's ground, .skip). One token cannot
     flip both ways in a dark scheme, so the second job gets its own name:
       --onAcc  text on the accent — §1b corrected it to the ink hex because
                #FFFFFF on #F07800 measures 2.84:1. It stays DARK in both
                schemes, because the accent itself does not change.
       --dis    the disabled-control ground (was a literal #EEEFF1).
     Values in this scheme are identical to what the literals rendered. */
  --onAcc:#0A0E12;
  --dis:#EEEFF1;

  --inkCard:#131A20;
  --inkLine:#5C6A75;
  --inkRing:#FFD79A;

  --f-disp:"Space Grotesk",Inter,sans-serif;
  --f-sans:"Inter","Helvetica Neue",Arial,sans-serif;
  --f-mono:"IBM Plex Mono",monospace;

  /* ── THE APP TYPE SCALE. Added 2026-09-03. ────────────────────────────────
     The owner's report was "in dashboard everything shows properly but it's
     like a basic landing page, not an app", and the measurement behind it was
     this file: h1 was 40px in the DISPLAY face, body was 16px, and .sec put
     40px of air above and below every band. Those are build/pricing.html's
     numbers, and they are right there — a marketing page has one message and
     the whole viewport to give it.

     A dashboard's viewport belongs to the data. Linear, Vercel, Stripe and
     Plausible all set a page title at 20-24px IN THE BODY FACE and run body
     copy at 13-14px, for the same reason: a 40px title in a display face is
     ~90px of chrome, and 90px is a row of tiles.

     THE DISPLAY FACE IS NOW THE WORDMARK'S AND NOTHING ELSE'S on this host.
     It stays loaded (.wm), and it stays the marketing site's headline face —
     this is a scale for app.answermend.com, not a change to Direction B. The
     KPI numeral moved with it, and gained tabular figures, which Space Grotesk
     does not offer: a column of numbers that changes width as it counts is a
     column that jitters on every refresh.

     Sizes are TOKENS so that a screen cannot set its own. Every per-page
     font-size in this file is one of these six or a deliberate exception with
     a comment beside it. */
  --t-page:22px;     /* the one h1 on a screen */
  --t-card:15px;     /* a card's h2 */
  --t-sub:13px;      /* an h3 inside a card */
  --t-body:14px;     /* running text, table cells, list rows */
  --t-small:12.5px;  /* captions, secondary lines */
  --t-fine:11.5px;   /* mono fine print */
  --t-label:10.5px;  /* the uppercase mono eyebrow */

  /* ── DENSITY. One set of steps, used everywhere. ──────────────────────────
     Measured against what the two dashboards I could actually load put on
     screen (DASHBOARD_SPEC §1: Plausible and Fathom, real screens, 09-03):
     both run ~14-16px of padding inside a card and ~10-12px between cards, on
     a shell bar of ~48px. This file was running 20px, 14px and 59px. */
  --sp-1:6px;
  --sp-2:10px;       /* between cards, and between grid cells */
  --sp-3:14px;       /* inside a card, vertical */
  --sp-4:16px;       /* inside a card, horizontal */
  --shell-h:46px;    /* the persistent bar, on every screen, unchanging */
}

/* ===========================================================================
   THE DARK SCHEME. Added 2026-09-04. ONE BLOCK, TOKENS ONLY.

   No component below this file's :root knows which scheme it is in — every
   rule reaches for a token, so the whole scheme is this redefinition and
   nothing else. If a dark-mode bug ever needs a component override, the token
   is wrong; fix the token.

   THE FOUR STATES DO NOT LEAN ON HUE HERE ANY MORE THAN THEY DO IN LIGHT.
   Each chip still carries its word ("cited" / "mentioned" / "missing" /
   "not measured"), its border style (solid vs dashed), and its numeral or em
   dash — the three signals that survive a red/green-blind reading. The hues
   below only have to keep TEXT legible on their own fills, and each pair was
   measured (script in the 2026-09-04 session, WCAG 2.x): every state's text
   clears 4.5:1 on its own fill in this scheme, and --dash clears 3:1 on all
   four grounds, same bar as light.

   --onAcc IS NOT REDEFINED: the accent doesn't change, so the text on it
   cannot either. --acc itself is unchanged — #F07800 measures 6.0:1 against
   this --bg, which is better than it does against the light one.            */
/* ---------- dark ----------------------------------------------------------
   THREE-WAY, added 2026-09-04. The tokens below appear TWICE on purpose and the
   two copies must stay identical:

     @media (prefers-color-scheme: dark) + :root:not([data-theme="light"])
        the OS asks for dark and the reader has not overridden it. The :not()
        is what lets an explicit LIGHT choice win on a dark desktop — without
        it, "Light" would be ignored by exactly the people who chose it.

     :root[data-theme="dark"]
        the reader asked for dark, whatever the OS says.

   Plain CSS cannot share one declaration block between a media query and a
   bare selector, and this project has no build step, so they are written out
   twice. tools/app-guard-check.mjs asserts they are token-identical (design-drift
   reads the marketing site, not this file) — two copies of one palette drift, and
   a half-dark theme is worse than either. */
@media (prefers-color-scheme: dark){
  :root:not([data-theme="light"]){
    /* ground */
    --bg:#0B1016;
    --pap:#111820;
    --card:#151D26;
    --ink:#E7EBEF;

    /* text */
    --tx2:#ACB6BF;
    --tx3:#9AA5AF;

    /* line */
    --line:#28323C;
    --line2:#1D2630;

    /* accent */
    --accD:#F07800;
    --accS:#2A1C09;
    --accB:#553A16;
    --accTx:#F2B368;

    /* the four states */
    --grn:#63CA95; --grnS:#0F241A; --grnB:#215233;
    --red:#F4907F; --redS:#2C1512; --redB:#5E2C24;
    --pmS:#1B232C; --pmB:#303B46;
    --dash:#8B959E;
    --dash-fill:#10161D;

    --dis:#1B232C;
  }
}

:root[data-theme="dark"]{
    /* ground */
    --bg:#0B1016;
    --pap:#111820;
    --card:#151D26;
    --ink:#E7EBEF;

    /* text */
    --tx2:#ACB6BF;
    --tx3:#9AA5AF;

    /* line */
    --line:#28323C;
    --line2:#1D2630;

    /* accent */
    --accD:#F07800;
    --accS:#2A1C09;
    --accB:#553A16;
    --accTx:#F2B368;

    /* the four states */
    --grn:#63CA95; --grnS:#0F241A; --grnB:#215233;
    --red:#F4907F; --redS:#2C1512; --redB:#5E2C24;
    --pmS:#1B232C; --pmB:#303B46;
    --dash:#8B959E;
    --dash-fill:#10161D;

    --dis:#1B232C;
}

*,*::before,*::after{box-sizing:border-box;}
html{-webkit-text-size-adjust:100%;}
body{
  margin:0;background:var(--bg);color:var(--ink);
  font-family:var(--f-sans);font-size:var(--t-body);line-height:1.55;
  -webkit-font-smoothing:antialiased;
}
p{margin:0;line-height:1.6;}
h1,h2,h3,h4{margin:0;}
ul,ol{margin:0;padding:0;list-style:none;}
a{color:inherit;}

:focus-visible{outline:2px solid var(--accD);outline-offset:2px;border-radius:4px;}

.skip{
  position:absolute;left:0;top:-64px;z-index:99;
  background:var(--ink);color:var(--card);padding:12px 18px;
  font-family:var(--f-mono);font-size:12px;letter-spacing:.1em;text-transform:uppercase;
  text-decoration:none;
}
.skip:focus{top:0;}
.sr-only{
  position:absolute;width:1px;height:1px;padding:0;margin:-1px;
  overflow:hidden;clip:rect(0 0 0 0);white-space:nowrap;border:0;
}

/* ---------- layout ---------- */
.wrap{max-width:1280px;margin:0 auto;padding:0 24px;position:relative;}
.sec{padding:16px 0 32px;}
.sec-pap{background:var(--pap);}

/* ---------- type ---------- */
.kick{
  font-family:var(--f-mono);font-weight:500;font-size:var(--t-label);
  letter-spacing:.18em;text-transform:uppercase;color:var(--tx3);display:block;
}
/* THE BODY FACE, NOT THE DISPLAY FACE — see the scale block in :root. An h1
   here names the screen; it is not a headline selling it. */
h1{font-family:var(--f-sans);font-weight:650;font-size:var(--t-page);line-height:1.2;letter-spacing:-.015em;margin:4px 0 0;}
h2{font-family:var(--f-sans);font-weight:600;font-size:var(--t-card);line-height:1.25;letter-spacing:-.008em;}
h3{font-family:var(--f-sans);font-weight:600;font-size:var(--t-sub);letter-spacing:0;line-height:1.35;}
h4{font-family:var(--f-mono);font-weight:500;font-size:var(--t-label);letter-spacing:.16em;text-transform:uppercase;color:var(--tx3);}
/* .lbl is the same eyebrow, as a <p>. Most of the small uppercase labels in
   this app are captions on a card, not sections of the document — marking
   them up as <h4> under an <h2> skipped a heading level on every page and
   handed a screen-reader user an outline the page does not have. Same
   pixels, no heading. */
.lbl{font-family:var(--f-mono);font-weight:500;font-size:var(--t-label);letter-spacing:.16em;text-transform:uppercase;color:var(--tx3);}
.lede{font-size:var(--t-small);color:var(--tx2);max-width:78ch;margin-top:5px;}
.mono{font-family:var(--f-mono);}
/* max-width: the measure cap, 2026-09-04. On a wide card a .fine or .p-body
   paragraph ran 130–160 characters per line, which is unreadable prose. 70ch
   is measured in each class's OWN font (ch follows font-size), so the cap
   lands around the same optical width on both. It binds only where the
   element lays out as a block: .fine is usually an inline <span> (a cardhead
   stamp, a table sublabel via .d-block) and an inline box ignores max-width,
   so nothing right-aligned with .ml-auto moves. Tables, KPI tiles and chart
   captions use .kn/.kfig/.dom/caption and are untouched. */
.fine{font-family:var(--f-mono);font-size:var(--t-fine);color:var(--tx3);letter-spacing:.02em;max-width:70ch;}
.muted{color:var(--tx3);}
.dom{font-family:var(--f-mono);font-size:var(--t-fine);color:var(--tx3);}

/* ---------- buttons ---------- */
.btn{
  display:inline-block;font-family:var(--f-sans);font-weight:600;font-size:13.5px;
  line-height:1.2;text-decoration:none;border-radius:8px;border:1px solid transparent;
  padding:9px 16px;cursor:pointer;
}
.btn-primary{background:var(--acc);color:var(--onAcc);}
.btn-primary:focus-visible{outline-color:var(--ink);}
.btn-secondary{border-color:var(--line);background:var(--card);color:var(--ink);font-weight:550;padding:8px 14px;}
.btn-dark{background:var(--ink);color:var(--card);font-weight:600;font-size:var(--t-small);padding:8px 13px;border-radius:7px;}
.btn-sm{font-size:var(--t-small);padding:7px 12px;}
.btn-block{display:block;width:100%;text-align:center;}

/* ── ONE PRIMARY PER SCREEN. Added 2026-09-04. ─────────────────────────────
   brand.html showed two accent buttons at once (the next-action's "What to
   fix" and the run card's "Run a scan"), and brands.html did the same
   (next-action + "Add a brand"). Two primaries is zero primaries.

   THE NEXT ACTION WINS ON BOTH SCREENS, and the reason is the product's
   promise, not taste: the dashboard's job is "where am I missing, and what do
   I do about it" — the .na card is the server's answer to exactly that,
   derived from real state. Running another scan re-measures what is already
   measured, and adding a brand widens the account; both are real, neither is
   the reason the customer opened the page.

   DONE HERE RATHER THAN IN THE PAGE SCRIPTS because the scripts belong to
   other sessions this run: the buttons keep their .btn-primary class and this
   file re-dresses the demoted one in the secondary coat. When brand.js /
   brands.js can be edited, the honest change is the class itself and these
   two rules come out.

   The demotion is CONDITIONAL, in the selector:
   · #bh-run's button demotes only while a .na card is actually on the screen
     (body:has). On a brand with no scans there is no next-action card and
     "Run the first scan" IS the primary — it stays accent there. A browser
     without :has() keeps today's two-primary state, which renders fine.
   · #br-add's button demotes except when brands.js adopts the whole card into
     the .es empty state on a zero-brand account — there the form is the hero
     and its button is the one primary on the screen. */
body:has(.na .btn-primary) #bh-run .btn-primary,
#br-add .btn-primary{
  background:var(--card);color:var(--ink);border-color:var(--line);font-weight:550;
}
.es #br-add .btn-primary{
  background:var(--acc);color:var(--onAcc);border-color:transparent;font-weight:600;
}
.btn[aria-disabled="true"],.btn:disabled{
  background:var(--dis);color:var(--tx3);border-color:var(--line);box-shadow:none;cursor:not-allowed;
}

/* ---------- chips (the four states, verbatim from the shell) ---------- */
.p{
  font-family:var(--f-mono);font-weight:500;font-size:var(--t-label);
  padding:2px 7px;border-radius:5px;display:inline-block;line-height:1.4;
  border:1px solid transparent;white-space:nowrap;
}
.pc{color:var(--grn);background:var(--grnS);border-color:var(--grnB);}
.pm{color:var(--tx2);background:var(--pmS);border-color:var(--pmB);}
.pr{color:var(--red);background:var(--redS);border-color:var(--redB);}
.pu{color:var(--tx3);background:transparent;border:1px dashed var(--dash);}
.pa{color:var(--accTx);background:var(--accS);border-color:var(--accB);}

/* ---------- the theme control ---------------------------------------------
   Text, not a glyph. Feather is MIT and CLAUDE.md rule 5 permits it, but the
   set vendored in tools/app-nav.mjs carries six glyphs and none of them is a
   sun, a moon or a monitor — drawing them from memory is inventing an asset,
   which is the failure that rule exists for. A word also says which of the
   three states is active without the reader decoding a picture. */
.thm{
  font-family:var(--f-mono);font-size:var(--t-label);color:var(--tx2);
  background:none;border:1px solid var(--line);border-radius:99px;
  padding:3px 10px;cursor:pointer;line-height:1.4;white-space:nowrap;
}
.thm:hover{color:var(--ink);border-color:var(--tx3);}
.thm:focus-visible{outline:2px solid var(--accD);outline-offset:2px;}

/* ===========================================================================
   .rp — A FACT ABOUT THE ACCOUNT, NOT A MEASUREMENT. Added 2026-09-04.

   THE DEFECT THIS FIXES: the four state chips are the product's one visual
   argument — cited vs mentioned vs missing vs not measured — and they were
   being spent on ROLES. On data.html, red (.pr) meant both "missing" and
   "owner only", and green (.pc) meant both "cited" and "admin" on team.html.
   The one grammar this design must keep unambiguous was saying two things.

   The fix is NOT a fifth colour in the state set (that dilutes it the same
   way) — it is a separate register that shares nothing with it:

     measurement chip (.p.pc/.pm/.pr/.pu)  5px rounded rect, a state hue or
                                           the dash, a numeral or an em dash
     account-fact chip (.rp)               a 99px PILL, monochrome ink scale,
                                           never a state hue, never a numeral

   Roles are ranked by AUTHORITY, so the scale is darkness, which survives
   every colour-vision reading:
     owner   filled ink            the whole account answers to this person
     admin   paper, strong border  can act on the account
     member  hairline outline      can read it
   The word in the chip is still the primary carrier, as it is on the states.

   JS PARITY IS PENDING AND NAMED: app.js roleChip(), team.js:193 and
   brands.js:1980 still emit .p.pa/.pc/.pm for roles; those files belong to
   other sessions this run and are flagged in the report. When they move, the
   one change is `p pa` → `rp rp-owner` (etc.) — the classes below are the
   contract. */
.rp{
  font-family:var(--f-mono);font-weight:500;font-size:var(--t-label);
  padding:2px 9px;border-radius:99px;display:inline-block;line-height:1.4;
  border:1px solid var(--line);color:var(--tx2);background:transparent;white-space:nowrap;
}
.rp-owner{background:var(--ink);border-color:var(--ink);color:var(--card);}
.rp-admin{background:var(--pap);border-color:var(--tx3);color:var(--ink);}
.rp-member{border-color:var(--line);color:var(--tx2);}

/* The k/n line that sits under a chip. It is NEVER omitted on a measured cell
   and NEVER present on an unmeasured one — see .kn-none below. */
.kn{font-family:var(--f-mono);font-size:var(--t-label);color:var(--tx3);display:block;margin-top:4px;letter-spacing:.02em;}
/* The em dash that stands where a numeral would be. Same size and same slot as
   .kn so the layout does not shift, but it is not a number and cannot be read
   as one. */
.kn-none{font-family:var(--f-mono);font-size:var(--t-label);color:var(--dash);display:block;margin-top:4px;}

/* ---------- the EXAMPLE DATA chip (same object as build/pricing.html .ex) ---------- */
.ex{
  font-family:var(--f-mono);font-weight:500;font-size:11px;letter-spacing:.12em;
  color:var(--accTx);background:var(--accS);border:1px solid var(--accB);
  padding:3px 9px;border-radius:5px;display:inline-block;
}

/* ---------- the shell: a 240px rail ---------- */
/* ── A RAIL, AND IT IS A DELIBERATE REVERSAL. 2026-09-04. ─────────────────
   This block argued for a BAR yesterday, and the argument was sound: the three
   products in DASHBOARD_SPEC §1 I could load as real screens — Plausible,
   Fathom, Search Console — all run top-bar-only, and §3.4 concluded "a sidebar
   signals breadth we do not have".

   THE OWNER HAS LOOKED AT BOTH AND WANTS THE RAIL. That is the whole reason,
   it is his product, and it is written here so nobody reads §3.4 next month and
   "corrects" it back. Reversing it again is an owner's call, not a session's.

   ONE MARKUP, TWO LAYOUTS, NO JAVASCRIPT IN EITHER.
     ≥1041px  a fixed 240px rail; <body> is inset by the same 240px.
     ≤1040px  the same <header> flattens to a slim top strip (wordmark, Help,
              sign out) and the four destinations move to .tabbar at the bottom
              of the screen, in the thumb zone.

   THE HAMBURGER IS GONE. It hid four items behind a tap on the screens with the
   least room to find anything, and it needed a script to open. The tab bar
   needs none, shows all four at once, and marks the current one.

   THE GLYPHS ARE FEATHER (MIT) PATH DATA, INLINED BY tools/app-nav.mjs — see
   the licence note at ICONS there. CLAUDE.md rule 5 was amended by the owner on
   2026-09-04 to draw the distinction this block used to enforce the wrong side
   of: adopting a licensed set is not inventing an asset. NOTHING IS DRAWN HERE.
   The wordmark stays TEXT, because a logo identifies the business and is not
   interchangeable with a licensed glyph — that half of rule 5 is unchanged. */
.appbar{
  background:var(--card);border-bottom:1px solid var(--line);
  position:sticky;top:0;z-index:20;
}
.appbar-inner{display:flex;align-items:stretch;gap:18px;min-height:var(--shell-h);}
.wm{
  font-family:var(--f-disp);font-weight:700;font-size:15px;letter-spacing:-.02em;
  text-decoration:none;color:var(--ink);display:flex;align-items:center;flex:none;
}
.wm span{border-bottom:2px dashed var(--acc);padding-bottom:1px;}
.appnav{display:flex;gap:2px;align-items:stretch;font-size:13px;color:var(--tx2);}
.appnav a{
  text-decoration:none;color:var(--tx2);padding:0 10px;
  display:flex;align-items:center;gap:9px;border-radius:0;
}
.appnav a:hover{color:var(--ink);background:var(--pap);}
.appnav a[aria-current="page"]{color:var(--ink);font-weight:600;box-shadow:inset 0 -2px 0 var(--acc);}
/* "You are INSIDE this section" — a scan, a share-of-voice view or a brand
   history is under Brands but is not Brands, so the parent gets ARIA's
   `true` (current item within a set) and never `page`, which claims to BE
   the page being read. Visually it is the ink colour and the weight without
   the accent rule, so the two states cannot be confused at a glance either. */
.appnav a[aria-current="true"]{color:var(--ink);font-weight:600;box-shadow:inset 0 -2px 0 var(--line);}
/* The glyph inherits the link's colour, so the current item's icon goes to ink
   with its label and no second rule is needed to keep them in step. */
.ic{flex:none;}
.navsep{display:block;width:1px;height:16px;background:var(--line);flex:none;align-self:center;margin:0 6px;}
/* THE "Pro" BADGE ON A LOCKED SLOT. Every proLock in tools/app-nav.mjs is false
   today and the block beside PRIMARY says why for each feature — a locked slot
   is a claim that something exists AND can be bought, and nothing here is both
   yet. The rule is defined because the renderer emits the badge the moment one
   flips, and a badge with no rule ships as bare text inside a nav item.
   IT CARRIES NO PRICE, EVER: 04-pricing.md owns those. */
.navlock{
  margin-left:auto;font-family:var(--f-mono);font-size:9.5px;letter-spacing:.1em;
  text-transform:uppercase;color:var(--accTx);background:var(--accS);
  border:1px solid var(--accB);border-radius:99px;padding:2px 6px;
}
.appbar-right{margin-left:auto;display:flex;align-items:center;gap:12px;flex:none;}
.appbar-right a{
  text-decoration:none;display:flex;align-items:center;gap:7px;color:var(--tx3);
}
.appbar-right a:hover{color:var(--ink);}
.orgpill{
  font-family:var(--f-mono);font-size:var(--t-label);color:var(--tx2);
  border:1px solid var(--line);border-radius:99px;padding:3px 9px;background:var(--pap);
  white-space:nowrap;
}
/* EMPTY MEANS ABSENT, NOT AN EMPTY PILL. Four of the eight screens never read
   who is signed in; a bordered pill with nothing in it is a slot that looks
   like it failed to load. */
.orgpill:empty{display:none;}

/* ---------- the bottom tab bar (narrow screens only) ----------
   Hidden by default and shown at ≤1040px. It is the WHOLE of the navigation
   there: the rail's link list is hidden at the same breakpoint, so if this rule
   is ever deleted a phone has no navigation at all. app-nav-check's `tabbar`
   rule asserts the markup; this is the half that makes it visible. */
.tabbar{display:none;}
.tabbar a{
  flex:1;display:flex;flex-direction:column;align-items:center;gap:3px;
  padding:7px 4px calc(6px + env(safe-area-inset-bottom,0px));
  text-decoration:none;color:var(--tx3);font-size:var(--t-label);letter-spacing:.01em;
}
.tabbar a[aria-current="page"]{color:var(--ink);font-weight:600;box-shadow:inset 0 2px 0 var(--acc);}
.tabbar a[aria-current="true"]{color:var(--ink);font-weight:600;box-shadow:inset 0 2px 0 var(--line);}

/* THE SHELL PAGES, AT EVERY WIDTH. This class is what separates the eight
   signed-in screens and the 404 from index.html and verify.html, which carry a
   wordmark bar and no navigation at all — the rail and the tab bar are both
   scoped to it. The base declaration is the one thing true at every width: a
   page beside a full-height rail must be at least as tall as the rail, or a
   short screen ends with the rail running past the bottom of the content.
   tools/app-nav-check.mjs's `shell-body` rule keeps the class where it belongs. */
body.app{min-height:100vh;}

/* ── THE RAIL PROPER, ≥1041px ────────────────────────────────────────────── */
@media (min-width:1041px){
  /* ⚠️ SCOPED TO `body.app`, AND THAT IS NOT TIDINESS. index.html (the sign-in
     form) and verify.html carry a <header class="appbar"> too — a wordmark and
     a link back to the marketing site, and no navigation at all. An unscoped
     rail rule turned the SIGN-IN PAGE into a 240px empty rail with the form
     pushed 240px right; measured headless on 2026-09-04 before the scope was
     added. tools/app-nav-check.mjs's `shell-body` rule asserts the class is on
     every shell page and on neither pre-session one. */
  body.app{padding-left:240px;}
  body.app .appbar{
    position:fixed;top:0;left:0;bottom:0;width:240px;
    border-bottom:0;border-right:1px solid var(--line);overflow-y:auto;
  }
  body.app .appbar-inner{
    flex-direction:column;align-items:stretch;gap:18px;
    max-width:none;padding:16px 14px;height:100%;
  }
  body.app .appnav{flex-direction:column;gap:2px;font-size:13.5px;}
  body.app .appnav a{padding:8px 10px;border-radius:7px;}
  /* ON THE RAIL THE ACCENT MOVES TO THE LEADING EDGE. On a horizontal bar a
     bottom rule reads as a tab; in a column it would read as a divider between
     two items, and the column already has one (.navsep). */
  body.app .appnav a[aria-current="page"]{background:var(--pap);box-shadow:inset 2px 0 0 var(--acc);}
  body.app .appnav a[aria-current="true"]{background:var(--pap);box-shadow:inset 2px 0 0 var(--line);}
  body.app .navsep{width:100%;height:1px;margin:8px 0;align-self:stretch;}
  /* margin-top:auto pins the account block to the foot of the rail — identity
     and the way out, below the destinations, on every screen. */
  body.app .appbar-right{margin-left:0;margin-top:auto;flex-direction:column;align-items:stretch;gap:6px;}
  body.app .appbar-right a{padding:6px 10px;border-radius:7px;font-size:var(--t-small);}
  body.app .appbar-right a:hover{background:var(--pap);}
  body.app .orgpill{align-self:flex-start;max-width:100%;overflow:hidden;text-overflow:ellipsis;}
  /* The rail is outside .wrap, so the content column keeps its own gutters and
     simply starts 240px further in. */
  body.app .wrap{max-width:1120px;}
  /* WITH THE BAR GONE FROM THE TOP, THE PAGE HEAD STARTS AT THE VIEWPORT EDGE.
     14px of crumb padding was right under a 47px bar and is too tight against
     nothing. 22px is the smallest inset that still reads as deliberate, and it
     is added rather than taken from anywhere else — this app is already denser
     than the products it is judged against (14px body, 14/16px card padding),
     so space is never bought by shrinking. */
  body.app .phead{padding-top:22px;}
}

/* ---------- the scan-context bar ----------
   scan.html, share-of-voice.html and fixes.html are three views of ONE scan,
   not three destinations. They are useless without a scan id, so they are not
   in the primary nav — a bare "Share of voice" item lands on "the address did
   not name a scan", which reads as a bug. Instead they get this strip, on the
   pages where a scan is in hand, and app.js carries the id across.

   Nothing new: --pap ground, --line rule, the .lbl eyebrow already used on
   every card, and the current tab is the same card-white the rest of the app
   uses for a raised surface. */
.subbar{background:var(--pap);border-bottom:1px solid var(--line);}
.subbar-inner{display:flex;align-items:center;gap:8px 14px;padding-block:6px;flex-wrap:wrap;}
.subnav{display:flex;gap:4px;flex-wrap:wrap;align-items:center;}
.subnav a{
  text-decoration:none;color:var(--tx2);font-size:var(--t-small);
  padding:4px 9px;border-radius:6px;border:1px solid transparent;
}
.subnav a:hover{color:var(--ink);background:var(--card);}
.subnav a[aria-current="page"]{
  color:var(--ink);font-weight:600;background:var(--card);border-color:var(--line);
}

/* ---------- page head ---------- */
.phead{padding:14px 0 0;}
.crumb{font-family:var(--f-mono);font-size:var(--t-label);color:var(--tx3);letter-spacing:.06em;}
.crumb a{text-decoration:none;color:var(--tx3);}
.crumb a:hover{color:var(--ink);}
.head-row{display:flex;align-items:baseline;gap:12px;flex-wrap:wrap;margin-top:2px;}
.head-row .ex{margin-bottom:0;}

/* ---------- the simulated-scan banner ---------- */
/* ⚠️ CORRECTED 2026-09-03. This said "every page in this directory renders
   fabricated data", which was true when the screens were mockups and stopped
   being true when they were wired to the API. The class did not become dead —
   it changed job, and the new one matters more.

   It now marks ONE SCAN as simulated: `model.simulated === true`, meaning no
   answer engine was asked anything, so every count and every competitor domain
   under it is evidence about nothing. share-of-voice.js and fixes.js are the
   two callers, and both say so in their own words above the mount.

   Still a banner and not a footnote, for the original reason: a screenshot of
   this page will end up in a deck, and the caveat has to travel with it.
   DEFINITION_OF_DONE Type B7. */
.exbanner{
  display:flex;gap:12px;align-items:flex-start;
  background:var(--accS);border:1px solid var(--accB);border-radius:9px;
  padding:10px 14px;margin-top:12px;font-size:var(--t-small);color:var(--accTx);
}
.exbanner b{color:var(--ink);}

/* ---------- cards / grid ---------- */
.card{background:var(--card);border:1px solid var(--line);border-radius:10px;padding:var(--sp-3) var(--sp-4);}
.card + .card{margin-top:var(--sp-2);}
.card > h2,.card > h3{margin-bottom:4px;}
.grid{display:grid;gap:var(--sp-2);}
/* A grid item's default is min-width:auto, which means it REFUSES to get
   narrower than its own min-content — and the min-content of a card holding
   the prompt matrix is that table's 660px min-width. The result was the card
   sitting 704px wide inside a 458px track and the whole PAGE scrolling
   sideways on a phone, while .tbl-wrap's overflow-x:auto sat there doing
   nothing because the box it was meant to clip had already been widened.
   min-width:0 hands the constraint back to the track, and the table then
   scrolls inside its own container the way it was always meant to. */
.grid > *,.stack > *{min-width:0;}
.g2{grid-template-columns:1fr 1fr;align-items:start;}
.g3{grid-template-columns:repeat(3,1fr);}
.g4{grid-template-columns:repeat(4,1fr);}
.g-side{grid-template-columns:minmax(0,1fr) 320px;align-items:start;}
.stack{display:grid;gap:var(--sp-2);}

/* ---------- KPI tiles ---------- */
/* Used sparingly and never as the top of a brand page — see the Admodifier
   note carried in brand.html: a stale tile is worse than no tile. Every tile
   carries the date it was measured, in the tile. */
.k{border:1px solid var(--line);border-radius:9px;padding:12px 13px;background:var(--card);}
.k.dash{border-style:dashed;border-color:var(--dash);background:var(--dash-fill);}
/* THE BODY FACE, AND TABULAR FIGURES. Space Grotesk has proportional numerals:
   a tile reading "3 of 8" and one reading "11 of 18" set to different widths,
   so a strip of four re-flowed every time a scan landed. Inter's tnum keeps
   the column still. */
.kfig{font-family:var(--f-sans);font-weight:650;font-size:24px;letter-spacing:-.02em;line-height:1.05;font-variant-numeric:tabular-nums;}
.kfig .of{font-size:13px;color:var(--tx3);font-weight:500;letter-spacing:0;}
.kfig.g{color:var(--grn);}
.kfig.u{color:var(--tx3);}
/* The unmeasured tile's figure is a COUNT OF CHECKS THAT DID NOT RUN. It is
   deliberately typeset smaller and in mono so it does not read as a score in
   the same units as the tiles beside it. */
.kfig.count{font-family:var(--f-mono);font-size:19px;font-weight:500;letter-spacing:0;}
.klabel{font-size:var(--t-small);color:var(--tx2);margin-top:5px;}
.kdesc{font-family:var(--f-mono);font-size:9.5px;color:var(--tx3);margin-top:2px;line-height:1.45;}
.kstamp{font-family:var(--f-mono);font-size:9.5px;color:var(--tx3);margin-top:7px;padding-top:6px;border-top:1px solid var(--line2);}

/* ===========================================================================
   THE KPI STRIP. Added 2026-09-03 — DASHBOARD_SPEC_2026-09-03.md §3.1 B2.

   The .k tile already existed and was already right; what it lacked was
   SCALE and a place at the top of the screen. These rules give the headline
   tile display size, give a tile room for a certified change chip beside its
   figure, and give the dashed not-measured tile room for the reason that must
   travel with it.

   ⚠️ NO SPARKLINE PER TILE, AND THE RULES FOR ONE WERE DELETED FROM THIS FILE
   THE DAY THEY WERE WRITTEN. Spec §2.2 pattern 3: a sparkline per tile is the
   common bad habit at this density — it appears in good products only where
   there are dozens of points, and we have a handful of scans. There is ONE
   chart on the screen, below the strip, shared. A per-tile line would also
   have been four more surfaces on which a scan that measured nothing could be
   drawn as a dip.                                                            */
.krow{display:flex;align-items:flex-start;gap:8px;flex-wrap:wrap;}
.kfig-lg{font-size:30px;}
/* The reason the not-measured tile carries. Same mono/grey as .kdesc, spaced
   off it, because a count of gaps with no reason beside it is the defect this
   whole state exists to prevent. */
.kwhy{font-family:var(--f-mono);font-size:9.5px;color:var(--tx3);margin-top:5px;line-height:1.45;}
/* The caption under the whole strip — the overlap note, and the server's own
   sentence when a coverage change suppressed every chip. */
.kfoot{font-size:var(--t-small);color:var(--tx2);margin-top:10px;}
.kfoot + .kfoot{margin-top:8px;}

/* ===========================================================================
   .kdelta — A CERTIFIED CHANGE, AND ONLY A CERTIFIED ONE.

   ⚠️ THIS CHIP IS RENDERED ONLY WHERE services/api/lib/history/delta.mjs
   RETURNED THE VERDICT `changed`: measured on both sides, comparable across
   the model stamp, and larger than the run-to-run spread the repeated runs
   themselves showed inside each scan. Every other verdict — unmeasured, across
   a model change, within variance, variance never observed, no difference —
   gets NO CHIP AT ALL, and there is deliberately no rule below for a "no
   change" or "0%" chip, because absence of a certified change is not a
   measured flatline (DASHBOARD_SPEC §3.3).

   COLOUR IS MEANING, NOT DIRECTION. On `missing`, up is the bad direction; on
   cited and mentioned it is the good one. brand.js picks the class from what
   the movement means for that metric, so green never appears on a rise in
   missing. No new colour: the same --grn/--red families the four states use.  */
.kdelta{
  font-family:var(--f-mono);font-size:11px;letter-spacing:.02em;
  border-radius:5px;padding:3px 8px;display:inline-block;margin-top:4px;border:1px solid transparent;
}
.kdelta.up{color:var(--grn);background:var(--grnS);border-color:var(--grnB);}
.kdelta.down{color:var(--red);background:var(--redS);border-color:var(--redB);}

/* The legend swatch when the chart drew POINTS AND NO LINE — below three
   measured scans. A line swatch over a chart of dots is the page contradicting
   itself, so the swatch becomes a dot. */
.trend-legend i.dotonly{
  width:7px;height:7px;border-top:0;border-radius:99px;background:var(--grn);
}
.trend-legend i.men.dotonly{background:var(--tx3);}

/* ===========================================================================
   .fixlist — B5, the ranked items from the most recent scan.

   Every sentence in a row is written by services/api/lib/export/fixes.mjs and
   travels through the API verbatim; nothing here styles a claim into existence.
   The chip families match lib/export/html.mjs so one item is recognisable in
   the downloadable report, on this screen and on fixes.html.                  */
.fixlist{display:grid;gap:var(--sp-2);counter-reset:fix;}
.fixrow{padding-top:10px;border-top:1px solid var(--line2);}
.fixrow:first-child{padding-top:0;border-top:none;}
.fixhead{display:flex;align-items:baseline;gap:var(--sp-2);flex-wrap:wrap;}
.fixtitle{font-family:var(--f-sans);font-weight:600;font-size:var(--t-body);letter-spacing:-.005em;line-height:1.35;color:var(--ink);}
/* The figure the item was derived from, printed beside it and never in a
   footnote: an item pasted into an email with its numbers stripped off is the
   failure mode fixes.mjs was written against. */
.fixfinding{font-size:13px;color:var(--ink);margin-top:5px;}
.fixnext{font-size:var(--t-small);color:var(--tx2);margin-top:4px;}

/* ===========================================================================
   .cov — COVERAGE. How much of a scan actually ran.

   ⚠️ THIS IS NOT A SCORE AND THE MARKUP THAT USES IT SAYS SO EVERY TIME.
   It counts prompt × engine CHECKS that returned, out of the checks the scans
   planned — model.checks.returned of model.checks.planned, recomputed from the
   rows by lib/export/model.mjs. "18 of 20" here means eighteen checks came
   back; it does not mean the brand was named in eighteen of anything.

   THERE IS NO "PROFILE STRENGTH", NO "READINESS" AND NO PERCENTAGE-OF-DONE ON
   THIS ELEMENT. The readiness score is a public-tool endpoint (POST
   /v1/readiness) computed by fetching a site; DASHBOARD_SPEC §3.1 records that
   it is not stored per brand or per scan and marks a dashboard block for it as
   phase 2. No signed-in screen may print a figure it does not hold. Coverage
   is the only completeness fraction the dashboard actually has, and it is
   printed with both halves or not at all.

   A real <progress>, so a screen reader announces it as one and so the value
   and the maximum are in the accessibility tree rather than in a width. */
.cov{display:block;width:100%;height:8px;border-radius:99px;-webkit-appearance:none;appearance:none;border:0;background:var(--line2);}
.cov::-webkit-progress-bar{background:var(--line2);border-radius:99px;}
.cov::-webkit-progress-value{background:var(--ink);border-radius:99px;}
.cov::-moz-progress-bar{background:var(--ink);border-radius:99px;}
.covfig{font-family:var(--f-mono);font-size:var(--t-small);color:var(--ink);letter-spacing:.02em;}
.covnote{font-size:var(--t-small);color:var(--tx2);margin-top:7px;}

/* ===========================================================================
   .na — THE NEXT ACTION. One card, one button, chosen from real state.

   Its picture is deliberately none of the four already in this file. It is a
   normal card ground with an --ink left rule: --acc solid is WITHHELD
   (.gated), dashed grey is NOT MEASURED (.nm), paper-and-solid is NOTHING YET
   (.es), and an ordinary card is data. Ink is the token the primary dark
   control already wears (.btn-dark) and no state register uses it as a rail,
   so a reader who has learned the four states does not have to unlearn one.

   IT IS RENDERED ONLY WHEN THE STATE THAT CHOOSES IT HAS BEEN READ. A failed
   read renders AM.renderError and no next action at all: "add a brand" printed
   over a 503 is advice derived from a list nobody managed to fetch. */
.na{border-left:3px solid var(--ink);}
.na .lbl{color:var(--tx3);}
.na h2,.na h3{margin-top:5px;}
.na .btnrow{margin-top:12px;}
.na .na-why{font-size:var(--t-small);color:var(--tx2);margin-top:7px;}
.na .na-state{
  font-family:var(--f-mono);font-size:var(--t-label);color:var(--tx3);
  margin-top:10px;padding-top:9px;border-top:1px solid var(--line2);display:block;line-height:1.55;
}

/* ---------- tables ---------- */
.tbl-wrap{overflow-x:auto;border:1px solid var(--line);border-radius:10px;background:var(--card);}
table.t{width:100%;border-collapse:collapse;min-width:720px;}
table.t caption{
  caption-side:top;text-align:left;padding:12px 14px 0;
  font-family:var(--f-mono);font-size:var(--t-label);letter-spacing:.14em;text-transform:uppercase;color:var(--tx3);
}
table.t thead th{
  font-family:var(--f-mono);font-weight:500;font-size:10px;letter-spacing:.1em;text-transform:uppercase;
  color:var(--tx3);text-align:left;padding:9px 13px;border-bottom:1px solid var(--line);vertical-align:bottom;
}
table.t tbody th{
  text-align:left;font-weight:600;font-size:13px;color:var(--ink);
  padding:9px 13px;border-bottom:1px solid var(--line2);vertical-align:top;
}
table.t td{font-size:var(--t-small);color:var(--tx2);padding:9px 13px;border-bottom:1px solid var(--line2);vertical-align:top;}
table.t tbody tr:last-child th,table.t tbody tr:last-child td{border-bottom:none;}
table.t td.num{font-family:var(--f-mono);font-size:12px;color:var(--ink);font-variant-numeric:tabular-nums;}
/* A whole ROW that could not be measured gets the dashed treatment too, so the
   state survives being skimmed rather than read cell by cell. */
tr.row-unmeasured td,tr.row-unmeasured th{background:var(--dash-fill);}
tr.row-unmeasured th{box-shadow:inset 3px 0 0 var(--dash);}

/* ---------- the not-measured block ---------- */
/* The first-class UI state. Dashed, unfilled, grey, and it always says WHY —
   lib/store/schema.mjs refuses to store an unmeasured cell without a reason,
   so there is always one to print, and printing it is the whole point. */
.nm{
  border:1px dashed var(--dash);border-radius:9px;background:var(--dash-fill);
  padding:11px 13px;color:var(--tx2);font-size:13px;
}
.nm h4,.nm .lbl{margin-bottom:5px;}
.nm .why{font-family:var(--f-mono);font-size:var(--t-fine);color:var(--tx3);display:block;margin-top:5px;}
/* THE MACHINE STAMP ON A STATE THAT DID HAPPEN. `.why` is deliberately styled
   only inside a register block (.nm, .gated), so one emitted anywhere else
   renders as unstyled body text glued to the sentence above it — caught in the
   browser on 2026-09-04 on the usage card. brand.js renderSchedule()'s
   `registered:true` branch is a MEASURED, positive state, so it must not wear
   the dashed .nm shell; it still ends with the same stamp every other state
   ends with, and this is where that stamp is styled. */
.schedok .why{font-family:var(--f-mono);font-size:var(--t-fine);color:var(--tx3);display:block;margin-top:5px;}
.nm-inline{
  font-family:var(--f-mono);font-size:11px;color:var(--tx3);
  border:1px dashed var(--dash);border-radius:5px;padding:2px 7px;display:inline-block;
}

/* ===========================================================================
   .gated — WITHHELD BY PLAN. A FOURTH THING, and it had no picture until
   2026-09-03, which is exactly how it came to be drawn as a fifth thing it is
   not.

   services/api/lib/export/model.mjs deletes `competitors_named` from every row
   on the free tier — deliberately, because per-prompt competitor attribution
   is the Starter row in 04-pricing.md §4.2a — and it says so in the payload it
   sends, in `competitor_detail: { available:false, reason:'paid_tier' }`.
   Neither share-of-voice.js nor scan.js read that field. They read the deleted
   array, found nothing in it, and printed "no other business was named ...
   Measured, and the answer is none" over a payload whose ungated
   `competitors.tally` held eighteen domains.

   That is 01-strategy.md §1.4 rule 2 inverted: NOT SHOWN IS NOT ZERO, in
   exactly the way not measured is not zero. So withheld gets its own picture,
   and it is neither of the two it was being confused with:

     measured        SOLID, neutral ground, a numeral        we know, here it is
     not measured    DASHED, no fill, grey, an em dash       we do not know
     WITHHELD        SOLID, ACCENT ground and border         we know, and this
                     no numeral in the gated slot            plan does not show it

   Accent and solid, because a plan boundary is not a fault: gate.mjs's own
   words are "a gap the reader can see is an upgrade prompt; a gap they cannot
   is a bug". Dashed would file it under "we could not check", which is a
   different and equally wrong sentence.
   =========================================================================== */
.gated{
  border:1px solid var(--accB);background:var(--accS);border-radius:11px;
  padding:var(--sp-3) var(--sp-4);color:var(--accTx);font-size:13px;
}
.gated .lbl{color:var(--accTx);margin-bottom:5px;}
.gated p + p{margin-top:8px;}
/* The server's own sentence for WHY it is withheld, in the same mono slot
   .nm uses for the reason a check did not run. Same job, different colour,
   and never the same colour. */
.gated .why{
  font-family:var(--f-mono);font-size:var(--t-fine);color:var(--accTx);
  display:block;margin-top:6px;
}
/* The table-cell form, beside .nm-inline and deliberately unlike it: solid
   where that one is dashed. A cell that says "not on this plan" must not be
   readable as a cell that says "we could not check". */
.gated-inline{
  font-family:var(--f-mono);font-size:11px;color:var(--accTx);
  border:1px solid var(--accB);background:var(--accS);
  border-radius:5px;padding:2px 7px;display:inline-block;
}

/* ===========================================================================
   .es — THE "NOTHING HERE YET" STATE. Added 2026-09-03.

   THE DEFECT THIS EXISTS TO FIX, and it is the same defect as the one at the
   top of this file, one level up.

   Every screen in this app already distinguishes "we could not check" from
   "we checked and it is none" IN WORDS — the copy in brands.js, team.js,
   account.js and the three scan views is careful about it to the sentence. It
   did not distinguish them TO THE EYE. A failure renders .nm: dashed, grey,
   unfilled. And so did half the empty states — a brand waiting for its first
   scan, an organization with nobody invited yet, an account whose provisioning
   has not run. Two different findings, one picture. Somebody skimming reads
   the picture, and the picture said "something is wrong here" about an account
   that was working perfectly.

   So the same three-signal rule the chips follow, applied to whole regions:

     could not check   DASHED, --dash-fill ground, grey, an HTTP stamp,
     (.nm)             and a "Try again" — a hole in the record
     nothing yet       SOLID border, card-white ground, an ACCENT RAIL down
     (.es)             the left edge, numbered steps, and ONE primary action —
                       a place in a sequence, not a hole

   Border style, ground colour and the presence of a way forward. Screenshot
   the two side by side and they are different objects.

   .es-quiet is the same object with the accent rail turned off and the paper
   ground: for a region that is legitimately "not yet" but is NOT where the
   next action lives — a trend with one scan on it, a comparison with nothing
   to compare. It must still not be dashed, because it is still not a failure.

   WHAT IS DELIBERATELY NOT IN HERE: any illustration, icon or figure. There is
   no icon set in this project (CLAUDE.md rule 5) and an empty state is exactly
   where one gets invented. The step numerals below are CSS counters over the
   text that is already there — typography, not a drawn asset. Nor does any
   .es ever contain a sample brand, a sample domain or an illustrative
   number: this directory was a set of fabricated mockups until 2026-09-02 and
   an empty state is the easiest place to put one back. It says what WILL
   appear. It never draws it.
   =========================================================================== */
.es{
  background:var(--card);
  border:1px solid var(--line);
  border-left:3px solid var(--acc);
  border-radius:10px;
  padding:16px 18px 18px;
}
/* Same block, no rail, paper ground — a "not yet" that is not the next action. */
.es-quiet{
  background:var(--pap);
  border-left:1px solid var(--line);
  padding:13px 15px 15px;
}
.es > .lbl{display:block;}
.es h2,.es h3{
  font-family:var(--f-sans);font-weight:650;font-size:18px;line-height:1.25;
  letter-spacing:-.015em;margin-top:6px;
}
.es-quiet h2,.es-quiet h3{font-size:var(--t-card);font-weight:600;letter-spacing:-.008em;margin-top:5px;}
/* Measured, not decorative: ~62 characters is the line length the rest of this
   app's .lede sets at 720px, expressed in a unit that survives the type scale
   changing. */
.es-lede{font-size:13.5px;color:var(--tx2);margin-top:7px;max-width:66ch;}
.es-quiet .es-lede{font-size:13px;}

/* WHAT THIS PAGE WILL SHOW — prose about a future state, never a drawing of
   one. Same .drow grid the rest of the app uses for a definition.

   ⚠️ THE RULE STAYS; ITS USE ON brands.html DOES NOT. Removed from the
   first-run block 2026-09-03, where it was a three-row definition table — "one
   card per brand" / "what each card carries" / "what it will not carry", each
   with a sub-paragraph — describing cards to somebody who had never seen one.
   Accurate, and unreadable at that moment: an empty state that explains a
   component which does not exist yet is a manual, and the owner named that
   screen a document twice. Nothing replaced it and nothing DREW it — the ban on
   a sample card, a sample domain or an illustrative figure inside an .es is
   unchanged, and is why the compression had to be a cut rather than a picture.
   Those facts now live where they are used: the location rule on the Location
   field's own hint, the k-of-n rule and the four states in brands.html's "How
   to read this page" disclosure, the state descriptions under brands.js's
   tiles.

   ⚠️ AND THE SELECTOR IS NOT DEAD — seven other screens use it (brand.js,
   scan.js, share-of-voice.js, fixes.js, team.js, account.js, data.js), each in
   an empty state of its own. Deleting the rule while trimming one page would
   have silently unspaced all seven. Do not remove it on the evidence of one
   file. */
.es-will{margin-top:14px;}

/* A HOST FOR THE FORM ITSELF, WHICH IS WHAT AN EMPTY STATE SHOULD LEAD WITH.
   On a zero-brand account brands.js moves the whole #br-add card into the .es
   block, so the first thing under the headline is the input. Inside .es the
   card drops its own chrome — a bordered card inside a bordered block reads as
   two objects, and it is one. */
.es > .card{
  border:0;background:transparent;border-radius:0;padding:0;
}
.es > .card + .card{margin-top:0;}

/* The sequence. Numerals are counters over the list that is already in the
   DOM, so the order is in the markup and not only in the paint. */
.es-steps{counter-reset:es-step;display:grid;gap:var(--sp-2);margin-top:14px;}
.es-steps li{
  counter-increment:es-step;position:relative;padding-left:34px;
  font-size:13px;color:var(--tx2);line-height:1.5;min-height:22px;
}
.es-steps li::before{
  content:counter(es-step);
  position:absolute;left:0;top:0;width:22px;height:22px;
  display:flex;align-items:center;justify-content:center;
  border-radius:99px;background:var(--accS);border:1px solid var(--accB);
  color:var(--accTx);font-family:var(--f-mono);font-weight:500;font-size:11px;
  line-height:1;
}
.es-steps li b{color:var(--ink);font-weight:600;}
.es-steps li .sub{display:block;color:var(--tx3);font-size:12px;margin-top:2px;}

/* .tight — THE STEPPER AS A STRIP, not as a section of a document.
   Added 2026-09-03. The three steps used to be three stacked paragraphs with
   two sub-paragraphs under them, above the form; each is now a name and one
   short clause, side by side, BELOW the form. Three columns on a desktop, and
   it falls back to the stacked list on a phone, where three 100px columns
   would be worse than three rows. Nothing is drawn: the numerals are still the
   counters the block above defines.

   auto-fit AND NOT THREE FIXED COLUMNS. Between 1041px and ~1100px the sidebar
   is still beside the block and three forced tracks leave 137px of text per
   step, which wraps a nine-word clause to four lines and stops reading as a
   strip. 190px is the floor measured in the browser at which one clause holds
   two lines; below it the steps wrap to a second row rather than get narrower. */
.es-steps.tight{grid-template-columns:repeat(auto-fit,minmax(190px,1fr));gap:14px;margin-top:14px;}
.es-steps.tight li{font-size:var(--t-small);line-height:1.45;}
/* A step that is NOT this person's to take — it needs a role they do not have,
   or a screen they cannot reach. Hollow numeral, so the sequence still reads
   as a sequence and this one still reads as not theirs. */
.es-steps li.off::before{background:transparent;border-color:var(--line);color:var(--tx3);}

/* ---------- AM.stepList: a step carrying the state that was READ ----------
   Added 2026-09-04. Four states, and the fourth is why this is not a checklist.

     done    solid accent marker, a tick glyph in place of the numeral
     now     the numeral, accent, and the one primary button
     todo    the numeral, hollow — a place in the sequence, nothing claimed
     unread  DASHED marker with an EM DASH where the numeral goes, and the
             reason beside it

   ⚠️ `unread` IS NOT A GREYER `todo`. It is the app's not-measured grammar
   applied to a step: a request that did not come back tells us nothing about
   whether that step is done, and both a tick and a cross would be a claim
   nobody measured (01-strategy.md §1.4 rule 2). Dashed border, no fill, no
   numeral — the same three signals the .pu chip and the .k.dash tile carry, so
   the three read as one object across the app.

   THE GLYPHS ARE TEXT SET BY `content`, not assets. Same device as
   .sortarrow::after and .disc's affordance; CLAUDE.md rule 5 is about drawn
   images and invented brand assets, and a character in a CSS declaration is
   neither. Nothing is fetched and no file is added. */
.es-steps li.st-done::before{content:"\2713";background:var(--accS);border-color:var(--accB);color:var(--accTx);}
.es-steps li.st-todo::before{background:transparent;border-color:var(--line);color:var(--tx3);}
.es-steps li.st-unread::before{
  content:"\2014";background:transparent;border:1px dashed var(--dash);color:var(--dash);
}
/* A finished step is history, not an instruction: it stays legible and stops
   competing with the one step that is actually next. */
.es-steps li.st-done b{color:var(--tx2);font-weight:550;}
.es-steps li.st-now b{color:var(--ink);}
/* The fact the state was read FROM. Mono, so it reads as a record rather than
   as more advice — the same slot .na-state and .why occupy elsewhere. */
.es-steps li .st-ev{
  display:block;font-family:var(--f-mono);font-size:var(--t-label);letter-spacing:.02em;
  color:var(--tx3);margin-top:4px;
}
.es-steps li .btnrow{margin-top:8px;}

.es .btnrow{margin-top:14px;}
/* The closing line: what was actually read. It is the sentence that keeps this
   block from being mistaken for the dashed one above, in words as well as in
   paint, and it is never omitted. */
.es-fine{
  /* display:block IS A GUARD, not tidiness. The margin-top and the border-top
     below do nothing vertical on an inline box, so a caller who reached for a
     <span> got the separator rule drawn straight through the text above it —
     which happened on 2026-09-04, at 375px, on the setup block. Pinned here so
     the class is correct whatever element it lands on. */
  display:block;
  font-family:var(--f-mono);font-size:11px;color:var(--tx3);letter-spacing:.02em;
  line-height:1.6;margin-top:18px;padding-top:14px;border-top:1px solid var(--line2);
}
.es-quiet .es-fine{margin-top:14px;padding-top:11px;}

/* ===========================================================================
   .px-* — THE LABELLED EXAMPLE PLATE. Added 2026-09-04, on the owner's call.

   ⚠️ THIS REVERSES THE PARAGRAPH ABOVE, AND THE REVERSAL IS NARROW.
   The .es header says an empty state "says what WILL appear. It never draws
   it." The owner has decided an app empty state may DRAW it, provided the
   drawing says what it is — and that is now the only exception. Everything
   else in that paragraph stands: outside a plate, no .es may contain a sample
   brand, a sample domain or an illustrative number.

   THE COMPONENT IS NOT NEW AND WAS NOT INVENTED HERE. build/homepage.html has
   shipped it since 2026-09-03 (its "THE PRODUCT SHOWCASE" block): the same
   frame, the same EXAMPLE DATA chip, the same invented brand — Northlight on
   example.com — and the same caption register, "Example data, not a customer.
   4 of 5 questions shown — …". This is that component ported onto the app's own
   tokens. What changed in the port is that the plate here uses THE APP'S REAL
   COMPONENTS inside it — .k, .kfig, .klabel, .p.pc/.pm/.pr/.pu, .kn, .kn-none
   — where the homepage had to mirror them as .px-k/.px-fig, because that page
   does not load this stylesheet. An example built from the real parts teaches
   the real grammar and cannot drift away from it.

   ── WHY AN EXAMPLE IS ALLOWED AT ALL, STATED SO IT STAYS BOUNDED ───────────

   The four states are the product's whole argument and a customer meets them
   on the screen where they have no data — which is the one screen that could
   not show them. "Not measured is dashed, uncoloured, and carries its reason"
   is a sentence in an empty state and an OBJECT in a plate, and only the
   second one teaches it.

   ── THE FIVE CONDITIONS, FOUR OF THEM MACHINE-CHECKED ──────────────────────

   tools/app-nofigures-check.mjs was INVERTED on 2026-09-04 rather than
   exempted: a figure element in app markup is still an ERROR unless it is
   inside a plate that carries, in its own subtree, (1) this class, (2) the
   visible EXAMPLE DATA chip, (3) a "not a customer" caption, and (4) nothing
   but RFC 2606 example domains. Read that file's header before changing any
   plate; deleting the chip is a FAILING build, which is the point.

   The fifth is not checkable there and is the screens' job: A PLATE MAY NEVER
   APPEAR WHERE A REAL RESULT WOULD RENDER. It ships `hidden` in the markup,
   a page script MOVES it into the empty state, and it is never mounted once
   the account has data of its own.

   AND file-in-markup STAYS ABSOLUTE — no plate may hold a specimen robots.txt
   or schema block. A number is read on the page it is on; a file body is
   pasted onto somebody else's domain, where the chip does not travel with it.
   =========================================================================== */
.px-plate{
  background:var(--card);border:1px solid var(--line);border-radius:10px;overflow:hidden;
}
/* The identifying strip. The chip is INSIDE the plate and at the top of it, so
   that a crop, a screenshot or a scroll cannot separate the figures from the
   word that qualifies them. */
.px-bar{
  display:flex;align-items:center;gap:8px 12px;flex-wrap:wrap;
  padding:10px 14px;border-bottom:1px solid var(--line2);background:var(--pap);
}
.px-bar .t{font-family:var(--f-sans);font-weight:600;font-size:var(--t-small);letter-spacing:-.005em;}
.px-bar .ex{margin-left:auto;}
.px-meta{
  font-family:var(--f-mono);font-size:var(--t-fine);line-height:1.7;color:var(--tx3);
  padding:9px 14px;border-bottom:1px solid var(--line2);
}
.px-block{padding:13px 14px;border-bottom:1px solid var(--line2);}
.px-block:last-child{border-bottom:0;}
.px-note{font-size:var(--t-small);line-height:1.6;color:var(--tx2);margin-top:8px;}
.px-note b{color:var(--ink);font-weight:600;}

/* The four tiles, at plate scale. Not .kfig-lg: a 46px numeral inside an
   example would out-shout every real figure on the screens this sits beside. */
.px-tiles{display:grid;grid-template-columns:repeat(4,minmax(0,1fr));gap:8px;margin-top:10px;}
.px-tiles .kfig{font-size:20px;}
.px-tiles .kfig.count{font-size:15px;}
.px-tiles .k{padding:10px 11px;}

/* One row per domain the answers actually named. `.you` marks the example
   brand's own row with an accent rail — "this is you", never a verdict: the
   row looks identical whether the finding is good or bad, and the STATE is
   carried by the chip and the numeral only. */
.px-dl{display:grid;gap:6px;margin-top:10px;}
.px-d{
  display:grid;grid-template-columns:minmax(0,1fr) auto;gap:5px 12px;align-items:center;
  border:1px solid var(--line);border-radius:8px;padding:8px 11px;background:var(--card);
}
.px-d.you{box-shadow:inset 3px 0 0 var(--acc);}
.px-d .who{font-family:var(--f-mono);font-size:var(--t-fine);color:var(--tx3);min-width:0;overflow-wrap:anywhere;}
.px-d.you .who{color:var(--ink);font-weight:500;}
.px-d .st{display:flex;align-items:center;gap:8px;flex-wrap:wrap;justify-content:flex-end;min-width:0;}
/* Inside a row the k/n sits BESIDE its chip rather than under it — the .kn
   default is display:block, which is right in a tile and wrong here. */
.px-d .kn,.px-d .kn-none{display:inline-block;margin-top:0;}
.px-d .kn.zero{color:var(--red);}

/* THE CAPTION, AND IT IS LOAD-BEARING RATHER THAN DECORATIVE. It is one of the
   four things app-nofigures-check requires inside the subtree; a plate whose
   caption is deleted fails the build. It says what the chip cannot: not merely
   that these are examples, but that nobody real is being described. */
.px-cap{
  font-family:var(--f-mono);font-size:var(--t-fine);line-height:1.7;color:var(--tx3);
  padding:10px 14px;background:var(--pap);border-top:1px solid var(--line2);
}
.px-cap b{color:var(--tx2);font-weight:500;}

@media (max-width:1180px){
  .px-tiles{grid-template-columns:repeat(2,minmax(0,1fr));}
}
@media (max-width:767px){
  .px-bar .ex{margin-left:0;order:-1;}
  .px-bar .t{width:100%;}
  .px-d{grid-template-columns:1fr;gap:6px;}
  .px-d .st{justify-content:flex-start;}
}

/* ===========================================================================
   .disc — A CARD THAT IS A DISCLOSURE. Added 2026-09-03.

   The demotion device DASHBOARD_SPEC_2026-09-03.md §3.1 asks for: content that
   is TRUE and WORTH KEEPING but that must not lead. A real <details>, so it
   works with no JavaScript, is found by the browser's own find-in-page, and
   announces its own expanded state to a screen reader — none of which a
   scripted show/hide gives for free.

   ⚠️ WHAT MAY NOT GO IN ONE. A disclosure is for EXPLANATION, never for a
   measured value or the reason a value is missing. The reason a check did not
   run stays printed beside the cell (.nm, AM.notMeasured) — hiding it behind a
   click would turn "not measured, and here is why" back into a silent gap,
   which is the whole defect 01-strategy.md §1.4 rule 2 exists against. Prose
   about notation collapses; evidence does not.

   The marker is the WORDS "show" and "hide", not a glyph. There is no icon set
   in this project (CLAUDE.md rule 5) and a chevron here is exactly where one
   gets invented in CSS; the two words are also the only marker that survives a
   font falling back, which a drawn arrow does not.                            */
.disc{padding:11px 14px;}
.disc > summary{
  cursor:pointer;list-style:none;display:flex;align-items:center;gap:var(--sp-2);
}
.disc > summary::-webkit-details-marker{display:none;}
.disc > summary::after{
  content:"show";margin-left:auto;
  font-family:var(--f-mono);font-size:var(--t-label);letter-spacing:.14em;text-transform:uppercase;color:var(--tx3);
  border:1px solid var(--line);border-radius:99px;padding:3px 10px;
}
.disc[open] > summary::after{content:"hide";}
.disc > summary:hover::after{color:var(--ink);border-color:var(--tx3);}
.disc .disc-title{
  font-family:var(--f-sans);font-weight:600;font-size:13px;letter-spacing:0;color:var(--ink);
}
/* ── .disc-mini — A DISCLOSURE INSIDE A CARD, NOT AS ONE. Added 2026-09-03.
   The demotion the owner asked for on the Brands screen: a paragraph that
   DEFINES A FIELD does not belong above the form, and it does not belong in a
   card of its own either — that is the same paragraph with a border round it.
   It becomes a line the reader can open, sitting directly under the heading
   whose terms it defines. Same <details> mechanics as .disc, so it still
   works with no JavaScript and is still found by find-in-page. */
.disc-mini{margin-top:6px;border:0;background:none;padding:0;}
.disc-mini > summary{
  cursor:pointer;list-style:none;display:inline-flex;align-items:center;gap:7px;
  font-size:var(--t-small);color:var(--tx3);
}
.disc-mini > summary::-webkit-details-marker{display:none;}
.disc-mini > summary::after{
  content:"show";
  font-family:var(--f-mono);font-size:9.5px;letter-spacing:.12em;text-transform:uppercase;color:var(--tx3);
  border:1px solid var(--line);border-radius:99px;padding:1px 7px;
}
.disc-mini[open] > summary::after{content:"hide";}
.disc-mini > summary:hover{color:var(--ink);}
.disc-mini .disc-body{margin-top:8px;padding-top:8px;}

/* ── .hint — THE FIELD EXPLANATION, AT THE FIELD. ─────────────────────────
   Where prose about an input goes. brands.js already prints the Location
   rule this way and the dashboard rewrite put it there on purpose; this is
   the class that makes it a pattern the other screens can use rather than a
   one-off. It is NOT for a measured value or for the reason one is missing —
   those stay printed at full size beside the cell (.nm). */
.hint{font-size:var(--t-small);color:var(--tx3);margin-top:5px;line-height:1.5;}
.disc-body{margin-top:11px;padding-top:11px;border-top:1px solid var(--line2);}
.disc-body > .lbl:first-child{display:block;}

/* ---------- the state legend ---------- */
.legend{display:grid;grid-template-columns:repeat(4,1fr);gap:var(--sp-2);}
.legend .li{border:1px solid var(--line);border-radius:9px;padding:11px 12px;background:var(--card);}
.legend .li.u{border-style:dashed;border-color:var(--dash);background:var(--dash-fill);}
.legend p{font-size:var(--t-small);color:var(--tx2);margin-top:7px;}

/* ---------- append-only change log ---------- */
/* The Admodifier client portal ships a change log instead of metric tiles, on
   purpose: a tile that has not been recomputed since March is worse than no
   tile, because it is confidently wrong and nothing on it says so. A log entry
   is stamped with the moment it was true and stays true forever. Entries are
   appended; nothing here edits or removes one. */
.log{border-left:2px solid var(--line);margin-left:6px;padding-left:0;}
.log li{position:relative;padding:0 0 14px 22px;}
.log li::before{
  content:"";position:absolute;left:-5px;top:6px;width:8px;height:8px;border-radius:99px;
  background:var(--tx3);border:2px solid var(--card);
}
.log li.up::before{background:var(--grn);}
.log li.down::before{background:var(--red);}
/* An unmeasured entry's dot is hollow and dashed — it marks a hole in the
   record, not a movement in it. */
.log li.unm::before{background:var(--dash-fill);border:1.5px dashed var(--dash);width:9px;height:9px;left:-5.5px;}
.log .when{font-family:var(--f-mono);font-size:var(--t-label);color:var(--tx3);letter-spacing:.06em;}
.log .what{font-size:13.5px;color:var(--ink);margin-top:2px;font-weight:600;}
.log .detail{font-size:var(--t-small);color:var(--tx2);margin-top:3px;}
.log li:last-child{padding-bottom:0;}

/* ---------- trend chart ---------- */
.trend{border:1px solid var(--line);border-radius:10px;background:var(--card);padding:14px 14px 8px;}
.trend svg{display:block;width:100%;height:auto;}
.trend-legend{display:flex;gap:16px;flex-wrap:wrap;margin-top:6px;font-size:var(--t-small);color:var(--tx2);}
.trend-legend i{display:inline-block;width:14px;height:0;border-top:2px solid var(--grn);vertical-align:middle;margin-right:7px;}
.trend-legend i.men{border-top-color:var(--tx3);border-top-style:solid;}
.trend-legend i.gap{border-top:2px dashed var(--dash);}
.trend-note{margin-top:9px;font-size:var(--t-small);color:var(--tx2);border-top:1px solid var(--line2);padding-top:9px;}

/* ---------- share of voice bars ---------- */
.sov-row{padding:11px 0;border-top:1px solid var(--line2);}
.sov-row:first-of-type{border-top:none;}
.sov-prompt{font-size:13.5px;color:var(--ink);font-weight:600;}
.sov-meta{font-family:var(--f-mono);font-size:var(--t-label);color:var(--tx3);margin-top:3px;letter-spacing:.04em;}
.sov-bars{margin-top:9px;display:grid;gap:6px;}
.sov-bar{display:grid;grid-template-columns:180px minmax(0,1fr) 74px;gap:12px;align-items:center;}
.sov-name{font-size:var(--t-small);color:var(--tx2);overflow:hidden;text-overflow:ellipsis;white-space:nowrap;}
.sov-name.you{color:var(--ink);font-weight:600;}
.sov-track{background:var(--line2);border-radius:5px;height:14px;position:relative;overflow:hidden;}
.sov-fill{position:absolute;left:0;top:0;bottom:0;background:var(--tx3);border-radius:5px;}
.sov-fill.you{background:var(--acc);}
.sov-kn{font-family:var(--f-mono);font-size:11px;color:var(--tx3);text-align:right;}
/* A MEASURED ZERO still gets a mark. A bar of width 0 is invisible, and an
   invisible bar in a solid track is one glance away from looking like the
   dashed unmeasured track below it. So k=0 draws a red tick at the origin and
   the k/n line still prints the digit: "0 of 6". Something we measured is
   always drawn. */
.sov-fill.zero{width:4px!important;background:var(--red);border-radius:2px;}
.sov-kn.zero{color:var(--red);}

/* A prompt whose runs did not come back has NO bar and NO track fill — an
   empty DASHED track with nothing in it at all, which cannot be read as
   "0% share" because there is no bar, no tick and no numeral. */
.sov-track.unm{background:transparent;border:1px dashed var(--dash);}
.sov-kn.unm{color:var(--dash);}

/* ---------- the "not switched on" block ---------- */
/* Same register as build/tools/*.html's !API_LIVE branch: say what is off,
   name the flag, and never leave a control that looks like it would work. */
.offblock{
  border:1px dashed var(--dash);border-radius:9px;background:var(--dash-fill);
  padding:var(--sp-3) var(--sp-4);
}
.offblock h2,.offblock h3{margin-bottom:5px;font-family:var(--f-sans);font-weight:600;font-size:var(--t-card);letter-spacing:-.008em;line-height:1.3;}
.offblock p{font-size:13px;color:var(--tx2);margin-top:7px;}
.offblock code{font-family:var(--f-mono);font-size:12px;color:var(--ink);}
.offblock ul{margin-top:10px;display:grid;gap:7px;}
.offblock ul li{font-size:var(--t-small);color:var(--tx2);padding-left:15px;position:relative;}
.offblock ul li::before{content:"";position:absolute;left:0;top:8px;width:5px;height:5px;border-radius:99px;background:var(--dash);}

/* ---------- forms ---------- */
.field{
  width:100%;font-family:var(--f-sans);font-size:13.5px;color:var(--ink);
  background:var(--card);border:1px solid var(--line);border-radius:8px;padding:9px 11px;
}
.field::placeholder{color:var(--tx3);}
.field[aria-invalid="true"]{border-color:var(--red);}
.field:disabled{background:var(--pap);color:var(--tx3);cursor:not-allowed;}
.label{display:block;font-size:13px;color:var(--tx2);margin-bottom:5px;}
/* A value handed over once — AM.copyField(). The input and its button sit on
   one line and wrap onto two below ~420px; `min-width:0` is what lets the
   input actually shrink inside the flex row instead of forcing the card wider
   than the viewport, which is how a 400-character credential becomes a
   horizontal scrollbar on a phone. */
.copyblock{margin-top:8px;}
.copyrow{display:flex;gap:8px;align-items:center;flex-wrap:wrap;}
.copyrow .field{flex:1 1 240px;min-width:0;font-size:var(--t-small);}
.copyrow .btn{flex:0 0 auto;}
/* Empty until the button has been pressed; an empty status line must not
   reserve a gap that reads as a missing sentence. */
.copyblock .fine:empty{display:none;}
/* Rendered but empty at parse, never display:none — a role="alert" region that
   is hidden at load is not registered by every screen reader and the first
   message it is given is silently dropped. Copied from build/pricing.html. */
.err{margin-top:8px;font-size:13px;color:var(--red);}
.err:empty{margin-top:0;}
.ok{margin-top:8px;font-size:13px;color:var(--grn);}
.ok:empty{margin-top:0;}

/* ---------- definition rows ---------- */
.drow{display:grid;grid-template-columns:170px minmax(0,1fr);gap:12px;padding:7px 0;border-top:1px solid var(--line2);font-size:13px;}
/* In the 340px sidebar the two-column drow squeezes the value into ~120px and
   every line wraps mid-word. There it stacks. */
.side .drow{grid-template-columns:1fr;gap:2px;}
.side .drow .dk{padding-top:0;}
.drow:first-of-type{border-top:none;}
.drow .dk{font-family:var(--f-mono);font-size:var(--t-fine);color:var(--tx3);letter-spacing:.02em;padding-top:2px;}
.drow .dv{color:var(--ink);}
.drow .dv .sub{display:block;color:var(--tx2);font-size:12px;margin-top:2px;}

/* ---------- footer ---------- */
.appfoot{border-top:1px solid var(--line);margin-top:24px;}
.appfoot .wrap{display:flex;gap:16px;flex-wrap:wrap;padding-block:14px 24px;align-items:center;}
.appfoot p,.appfoot a{font-size:var(--t-fine);color:var(--tx3);}
.appfoot a{text-decoration:none;}
.appfoot a:hover{color:var(--ink);}
.appfoot .right{margin-left:auto;display:flex;gap:16px;}

/* ---------- motion ---------- */
.appnav a,.btn{transition:color .15s ease,background-color .15s ease;}
@media (prefers-reduced-motion:reduce){*{transition:none!important;animation:none!important}}

/* ---------- responsive ---------- */
@media (max-width:1040px){
  .g-side{grid-template-columns:1fr;}
  .g4{grid-template-columns:1fr 1fr;}
  .legend{grid-template-columns:1fr 1fr;}

  /* ── BELOW THE RAIL'S BREAKPOINT ────────────────────────────────────────
     The <header> flattens back to a slim sticky strip carrying the wordmark,
     Help and sign out; the four DESTINATIONS move to the bottom bar, where a
     thumb reaches them.

     Help and sign out are NOT in the bottom bar, and that is not an oversight:
     one leaves the app for another host and one ends the session, and a
     sign-out sitting in the thumb zone beside "Brands" is a sign-out people
     hit by accident. They stay in the strip, which is where they are on the
     rail too. */
  body.app .appnav{display:none;}
  .tabbar{
    display:flex;position:fixed;left:0;right:0;bottom:0;z-index:30;
    background:var(--card);border-top:1px solid var(--line);
  }
  /* The bar covers the foot of the page, so the page gets that height back as
     padding — otherwise the last card on every screen sits under it. */
  body.app{padding-bottom:64px;}
  /* ⚠️ VISUALLY HIDDEN, NOT display:none. Caught in the 375px pass on
     2026-09-04: `display:none` takes a node out of the ACCESSIBILITY TREE as
     well as the layout, so Help and Sign out — which are icon-only on a phone —
     became two links with no accessible name at all. The label is still the
     name; it is only the pixels that go. */
  .appbar-right a .navlabel{
    position:absolute;width:1px;height:1px;padding:0;margin:-1px;
    overflow:hidden;clip:rect(0 0 0 0);white-space:nowrap;border:0;
  }
  .appbar-right{gap:8px;}
  .toasts{bottom:76px;}
}
@media (max-width:860px){
  .g3{grid-template-columns:1fr;}
  .g2{grid-template-columns:1fr;}
}
@media (max-width:767px){
  .wrap{padding:0 14px;}
  h1{font-size:20px;}
  h2{font-size:14.5px;}
  .sec{padding:12px 0 22px;}
  .g4{grid-template-columns:1fr;}
  /* THE KPI STRIP KEEPS TWO COLUMNS ON A PHONE (DASHBOARD_SPEC §3.4). Stacked
     one-up, four tiles push the chart and the fix list below three screens of
     scrolling, and a strip that has to be scrolled is not a strip. The dashed
     not-measured tile keeps its whole identity at half width — border style
     and the absence of a numeral do not depend on how wide it is. */
  .g4.kpi{grid-template-columns:1fr 1fr;gap:var(--sp-2);}
  .g4.kpi .k{padding:12px 12px;}
  /* 34px, NOT THE DESKTOP 46px AND NOT THE OLD 30px. At half of 375px a tile is
     ~165px wide and "11 of 18" at 46px overflows it; at 30px the figure stopped
     leading the tile. Raised with the strip on 2026-09-04 — the point of the
     change is that the number is the first thing read, and that is as true on a
     phone. The selector is `.g4.kpi .kfig-lg` because that is what the rule it
     replaced used, and a bare `.kfig-lg` here would lose to it on specificity
     and silently do nothing. */
  .g4.kpi .kfig-lg{font-size:34px;}
  .g4.kpi .kfig.u.kfig-lg{font-size:34px;}
  .g4.kpi .kfig.count.kfig-lg{font-size:21px;}
  .legend{grid-template-columns:1fr;}
  /* The scan bar keeps its label above the tabs rather than beside them, so
     three tabs get the full 375px minus the wrap padding and never overflow. */
  .subbar-inner{gap:6px;}
  .subbar-inner .lbl{width:100%;}
  .appbar-inner{gap:var(--sp-2);}
  .orgpill{display:none;}
  .drow{grid-template-columns:1fr;gap:2px;}
  /* The empty states are the first screen a new account sees and a phone is
     where a lot of them see it. 24px of side padding inside an 18px .wrap
     leaves 291px of text at 375px; 17px leaves 305px. The step numerals stay
     26px — shrinking them is what makes a counted list stop reading as one —
     so only the text inset comes in with them. */
  .es{padding:14px 14px 16px;}
  .es-quiet{padding:12px 12px 14px;}
  .es h2,.es h3{font-size:16.5px;}
  .es-steps li{padding-left:32px;}
  /* Three columns at 375px would be ~100px each and every step would wrap to
     five lines. The strip becomes the stacked list again — still one clause per
     step, which is the compression that mattered; the columns were only ever
     the shape it took where there was room. */
  .es-steps.tight{grid-template-columns:1fr;gap:11px;margin-top:16px;}
  /* The disclosure keeps the same side inset as .es on a phone. */
  .disc{padding:11px 12px;}
  /* A full-width button beats two half-width ones that each wrap to two lines.
     Only inside .es: the rest of the app's .btnrow rows are secondary controls
     next to content, and stacking those would push content off the fold. */
  .es .btnrow{display:grid;gap:var(--sp-2);}
  .es .btnrow .btn{display:block;width:100%;text-align:center;}
  .sov-bar{grid-template-columns:1fr;gap:4px;}
  .sov-kn{text-align:left;}
  .sov-name{white-space:normal;}
  /* The matrix cannot fit 375px as a grid. It stays a real table and scrolls,
     rather than being reflowed into cards — a prompt x engine cell means
     nothing without its column header, and a reflow drops the header. */
  table.t{min-width:660px;}
}

/* ---------- small utilities ----------
   These exist so that NO HTML file in this directory writes var(--token) in an
   inline style attribute. tools/qa-sweep.mjs reads each .html in isolation and
   cannot see this stylesheet, so an inline var() there is reported as an
   undefined token on every page — and it is right to, for the marketing pages
   it was written for, where the <style> block IS the file. Keeping the tokens
   on this side of the line keeps the sweep meaningful instead of noisy. */
.p-note{font-size:var(--t-small);color:var(--tx2);margin-top:8px;}
.p-body{font-size:13px;color:var(--tx2);margin-top:7px;max-width:70ch;}
/* A stored answer, quoted. pre-wrap keeps the engine's own line breaks
   without the page adopting its line length; the measure cap and
   overflow-wrap keep a 3000-character answer from widening anything. */
.anstext{
  white-space:pre-wrap;overflow-wrap:anywhere;max-width:70ch;
  font-size:13px;line-height:1.55;color:var(--tx2);
  border-left:2px solid var(--line);padding:2px 0 2px 12px;
}
/* A cited URL: full addresses, and a full address must break rather than
   scroll the page sideways at 375px. */
.anscite{overflow-wrap:anywhere;}
.kfig.r{color:var(--red);}
.fine.red{color:var(--red);}
.sov-row.unm-row{background:var(--dash-fill);border-radius:9px;padding:11px 12px;}
.linkbtn{
  margin-left:auto;background:none;border:0;padding:0;cursor:pointer;
  text-decoration:underline;color:var(--tx3);
  font-family:var(--f-mono);font-size:var(--t-fine);letter-spacing:.02em;
}
.linkbtn:hover{color:var(--ink);}

/* ---------- CSP-DRIVEN UTILITIES ----------------------------------------
   services/deploy/app/nginx-app.conf serves this subdomain under
   `default-src 'none'; script-src 'self'; style-src 'self'` — with NO
   'unsafe-inline'. Under CSP Level 2+ that blocks a style="" ATTRIBUTE in
   markup, not merely a <style> block. Every one of the 134 inline styles
   these six pages used to carry would have been dropped by the browser on
   the real host, while a local `python3 -m http.server` — which sends no CSP
   at all — rendered them perfectly. The layout looked correct everywhere
   except production.

   So the spacing and layout below are classes, and the one inline <script>
   moved to signin.js. Nothing here is a new design decision: each rule is
   the exact declaration that used to sit in the attribute it replaces.

   The share-of-voice bar widths could not become classes — they are DATA,
   and real data is not drawn from a fixed set of percentages. They are
   carried as data-w="83.3" and applied by app.js through the CSSOM, which
   CSP permits from an allowed script (style-src governs markup attributes
   and <style>, not element.style set by same-origin JavaScript). A bar whose
   width never arrives is still correct: the k/n numeral beside it is the
   number that matters, and it is in the HTML.
   ------------------------------------------------------------------------ */
.mt-6{margin-top:6px;}
.mt-7{margin-top:7px;}
.mt-8{margin-top:8px;}
.mt-9{margin-top:9px;}
.mt-10{margin-top:10px;}
.mt-12{margin-top:12px;}
.mt-14{margin-top:14px;}
.mt-16{margin-top:16px;}
.mt-18{margin-top:18px;}
.mt-20{margin-top:20px;}
.mt-22{margin-top:22px;}
.mt-26{margin-top:26px;}
.mb-9{margin-bottom:9px;}
.ml-auto{margin-left:auto;}
.ml-10{margin-left:10px;}
.d-block{display:block;}
.ta-left{text-align:left;}
.fw-400{font-weight:400;}
.fs-12{font-size:12px;}
.maxw-640{max-width:640px;}
/* the card header row: title left, a fine-print stamp pushed right */
.cardhead{display:flex;align-items:baseline;gap:12px;flex-wrap:wrap;}
/* a row of secondary buttons under a card */
.btnrow{display:flex;gap:8px;flex-wrap:wrap;margin-top:12px;}
/* the prompt column in the scan matrix */
.col-prompt{width:32%;}
/* a share-of-voice bar with no name column — the sidebar's whole-scan totals */
.sov-solo{grid-template-columns:1fr;gap:4px;}
/* <summary> defaults to the text cursor in some engines; it is a control */
.nm-why > summary{cursor:pointer;}

/* ===========================================================================
   PART 1b's SURFACES. Added 2026-09-04.

   Five affordances, built once in app.js and styled once here: the help mark,
   the toast, the loading skeleton, the sort control and the period selector.
   Nothing below is a new colour — every rule reaches for a token that already
   existed, because a sixth colour on a screen whose whole argument is four
   states is a colour that will be read as a fifth state.
   =========================================================================== */

/* ---------- help ----------
   The circled question mark, beside the thing it explains. It is a LINK: it
   works with the script dead, it can be opened in a new tab deliberately, and
   it is announced as a link rather than as a button that does nothing.

   NO ASSET. CLAUDE.md rule 5 sends icons to a design tool; this is a text glyph
   in a bordered circle, the way the rest of this stylesheet draws .p chips —
   nothing is drawn and no SVG is emitted. */
.helpq{
  display:inline-flex;align-items:center;justify-content:center;
  width:15px;height:15px;border-radius:99px;border:1px solid var(--line);
  font-family:var(--f-mono);font-size:10px;line-height:1;color:var(--tx3);
  text-decoration:none;vertical-align:middle;margin-left:6px;flex:none;
}
.helpq:hover{border-color:var(--tx3);color:var(--ink);background:var(--pap);}
.helpq:focus-visible{outline:2px solid var(--acc);outline-offset:2px;}
.helplink{
  font-family:var(--f-mono);font-size:var(--t-fine);letter-spacing:.02em;color:var(--tx3);
}
.helplink:hover{color:var(--ink);}

/* ---------- action feedback ----------
   Bottom-centre, above everything, and it never covers the control that
   started the action — a confirmation that hides the button is a confirmation
   the reader has to move to read.

   ⚠️ THE `working` TOAST DOES NOT BORROW THE DASHED BORDER. Dashed-and-unfilled
   is the not-measured carrier and it means one thing on this product: a check
   that did not run. Spending it on "we are waiting" would make the one visual
   grammar that carries an honesty rule ambiguous. A working toast is a solid
   neutral rail, the same as an informational one. */
.toasts{
  position:fixed;left:50%;bottom:16px;transform:translateX(-50%);
  z-index:60;display:grid;gap:8px;width:min(420px,calc(100vw - 24px));
  pointer-events:none;
}
.toast{
  pointer-events:auto;position:relative;
  background:var(--card);border:1px solid var(--line);border-left:3px solid var(--tx3);
  border-radius:9px;padding:11px 34px 11px 13px;
  box-shadow:0 6px 20px rgba(10,14,18,.10);
}
.toast-msg{font-size:var(--t-body);color:var(--ink);line-height:1.4;}
.toast-detail{font-family:var(--f-mono);font-size:var(--t-fine);color:var(--tx3);margin-top:4px;line-height:1.45;}
.toast-x{
  position:absolute;top:6px;right:7px;width:22px;height:22px;
  background:none;border:0;border-radius:5px;cursor:pointer;
  color:var(--tx3);font-size:15px;line-height:1;
}
.toast-x:hover{color:var(--ink);background:var(--pap);}
.toast.t-ok{border-left-color:var(--grn);}
.toast.t-failed{border-left-color:var(--red);}
.toast.t-working{border-left-color:var(--tx3);}
.toast.t-info{border-left-color:var(--line);}

/* ---------- loading shape ----------
   Grey blocks and nothing else. There is no rule here that could hold a
   numeral, because AM.skeleton() builds empty spans and takes no content —
   see its header for why that is structural rather than a convention.

   The pulse is the only animation in this stylesheet, and the reduced-motion
   query above already turns every animation off globally. */
.sk{display:grid;gap:var(--sp-2);}
.sk-tiles{grid-template-columns:repeat(4,minmax(0,1fr));}
.sk-cell{
  border:1px solid var(--line);border-radius:9px;padding:12px 13px;background:var(--card);
  display:grid;gap:8px;align-content:start;
}
.sk-rows .sk-cell,.sk-table .sk-cell{padding:11px 12px;}
.sk-b{display:block;background:var(--line);border-radius:4px;animation:sk-pulse 1.4s ease-in-out infinite;}
.sk-fig{height:22px;width:44%;border-radius:5px;}
.sk-line{height:9px;}
.sk-short{width:32%;background:var(--line2);}
@keyframes sk-pulse{0%,100%{opacity:1}50%{opacity:.45}}

/* ---------- sorting ----------
   The whole header cell is the control, so the target is the size people aim
   at. The arrow is a text glyph set by CSS content, matching how .disc's
   show/hide affordance is already drawn — no asset, nothing emitted. */
th .sortbtn{
  display:inline-flex;align-items:center;gap:5px;width:100%;
  background:none;border:0;padding:0;cursor:pointer;text-align:inherit;
  font:inherit;color:inherit;letter-spacing:inherit;text-transform:inherit;
}
th .sortbtn:hover{color:var(--ink);}
th .sortbtn:focus-visible{outline:2px solid var(--acc);outline-offset:2px;}
.sortarrow::after{content:"↕";font-family:var(--f-mono);font-size:10px;color:var(--dash);}
th[aria-sort="ascending"] .sortarrow::after{content:"↑";color:var(--ink);}
th[aria-sort="descending"] .sortarrow::after{content:"↓";color:var(--ink);}

/* ---------- period control ----------
   A segmented control, not a <select>: three options is not a list to search,
   and a pressed state is readable at a glance where a collapsed select is not. */
.seg{display:inline-flex;border:1px solid var(--line);border-radius:7px;overflow:hidden;background:var(--card);}
.segbtn{
  background:none;border:0;border-left:1px solid var(--line);cursor:pointer;
  padding:4px 10px;font-family:var(--f-mono);font-size:var(--t-fine);letter-spacing:.02em;color:var(--tx3);
}
.segbtn:first-child{border-left:0;}
.segbtn:hover{color:var(--ink);background:var(--pap);}
.segbtn[aria-pressed="true"]{color:var(--ink);background:var(--pap);font-weight:500;}
.segbtn:focus-visible{outline:2px solid var(--acc);outline-offset:-2px;}

/* ===========================================================================
   THE KPI STRIP AT SCOREBOARD SCALE. 2026-09-04.

   .kfig-lg was 30px — a well-set figure in a document. The first thing on this
   screen should be a number you can read from across the room, so it goes to
   46px. Nothing else shrinks to pay for it: this app already runs denser than
   the products it is being compared with (14px body against their 16, 14/16px
   card padding against their 22, a 46px shell against a 240px rail), and buying
   space by compressing what is already tight is how a dense screen becomes an
   unreadable one.

   ── THE 3px TOP RULE IS ON THE THREE MEASURED STATES AND NOWHERE ELSE ───────

   cited = green, mentioned = neutral, missing = red. The NOT-MEASURED tile gets
   no rule, keeps its dashed border and keeps its transparent fill.

   THIS IS THE READING, NOT A PREFERENCE. The moment `not measured` takes a
   colour it stops being a different KIND of object and becomes a fourth
   outcome — which is 01-strategy.md §1.4 rule 2 undone by a stylesheet. The
   instinct to give it a rule so the row looks consistent is exactly the
   instinct the dashed border exists to refuse.

   AND IT IS ENFORCED HERE RATHER THAN TRUSTED TO brand.js. The last rule in
   this block strips any colour rule off a dashed tile, so adding `rule-c` to
   the not-measured tile in a page script changes nothing on screen. Mutation
   tested 2026-09-04: `.k.dash.rule-c` renders with the dash border and no
   coloured edge.                                                             */
.kfig-lg{font-size:46px;letter-spacing:-.03em;}
.kfig-lg .of{font-size:15px;}
.k.rule-c{border-top:3px solid var(--grn);}
.k.rule-m{border-top:3px solid var(--dash);}
/* THERE IS NO `.rule-r`, AND THE MISSING TILE IT WOULD HAVE COLOURED IS NOT
   BUILT. `missing` on this product is `answered − named` — the exact
   arithmetic complement of the headline tile — so a strip carrying Named AND
   Missing prints one number twice and spends a slot doing it. The red family is
   already on screen where it belongs: the .p.pr chip in the glossary, the
   `missing` row of the per-engine table, and the .kdelta.down chip when a
   certified movement went the wrong way. A rule defined here and used by
   nothing is a rule somebody applies later to make the row look even. */
/* THE REFUSAL. A dashed tile takes no coloured rule, whatever a script asks
   for. Not measured is not a fourth outcome. */
.k.dash{border-top:1px dashed var(--dash)!important;}

/* The em dash at display size on the not-measured tile, and the reason WITH IT
   rather than behind a hover — a touch device has no hover, so a reason only
   reachable that way is a reason half the readers cannot read. */
.kfig.u.kfig-lg{font-size:46px;color:var(--dash);}
.kfig.count.kfig-lg{font-size:26px;}

/* THE FOURTH SLOT IS THE NEXT ACTION, NOT A FIFTH METRIC. Same tile box, no
   coloured rule (it is not a measurement), the fix title at card-title size and
   the figure it was derived from underneath, verbatim from the server. */
.k.knext{background:var(--pap);}
.knext-kick{font-family:var(--f-mono);font-size:var(--t-label);letter-spacing:.16em;text-transform:uppercase;color:var(--tx3);}
.knext-title{
  font-family:var(--f-sans);font-weight:600;font-size:var(--t-card);line-height:1.3;
  letter-spacing:-.005em;color:var(--ink);margin-top:6px;
}
.knext-why{font-family:var(--f-mono);font-size:9.5px;color:var(--tx3);margin-top:5px;line-height:1.45;}
.knext .btnrow{margin-top:10px;}

@media (max-width:767px){
  .sk-tiles{grid-template-columns:repeat(2,minmax(0,1fr));}
  .toasts{left:12px;right:12px;transform:none;width:auto;}
}

/* ===========================================================================
   PRINT. Added 2026-09-04. A scan result is exactly what gets printed for a
   boss, and until today it printed with the 240px rail, the tab bar and every
   button. Drop the chrome, keep the evidence.

   TOKENS FIRST, RULES SECOND — same discipline as the dark scheme: the page
   re-grounds to paper by redefining tokens, so components print with the same
   grammar they render with.

   THE ONE HONESTY RULE ON PAPER: most printers drop background fills, so a
   chip's tint cannot be trusted to arrive — the WORD in the chip, its border,
   and the numeral / em dash are what survive, and they are exactly the three
   signals the states already carry. `not measured` must never print as blank:
   --dash darkens to #444444 here so the dashed border and the em dash are
   unmissable in ink — a state that prints as white space would be read as
   zero, which is 01-strategy.md §1.4 rule 2 on a page nobody can correct.   */
@media print{
  :root{
    --bg:#FFFFFF; --pap:#FFFFFF; --card:#FFFFFF; --ink:#000000;
    --tx2:#222222; --tx3:#3A3A3A;
    --line:#999999; --line2:#BBBBBB;
    --dash:#444444; --dash-fill:#FFFFFF;
    --pmS:#FFFFFF; --pmB:#8A8A8A;
    --grnS:#FFFFFF; --redS:#FFFFFF; --accS:#FFFFFF;
    --grn:#0A5C36; --red:#9C2114; --accTx:#6B4000;
    /* The brand orange measures 2.84:1 on paper; ink does not do brand
       recognition, it does legibility, so the accent prints as its own
       dark variant (5.15:1) and the .es rail survives a mono printer. */
    --acc:#AD5500;
  }
  /* Chrome off: navigation, controls, toasts, the skip link, the footer's
     link row. The buttons go because a button on paper is a claim the paper
     cannot honour. */
  .skip,.appbar,.tabbar,.subbar,.appfoot .right,
  .btn,.btnrow,.toasts,.helpq,.linkbtn,.seg,
  #bh-run,#br-add{display:none !important;}
  body.app{padding-left:0 !important;padding-bottom:0 !important;}
  body{background:#FFFFFF;color:#000000;}
  /* The evidence keeps its shape. Scrollable tables un-clip: on paper there
     is no scrollbar, so the container must show everything it holds. */
  .tbl-wrap{overflow:visible;border-color:var(--line);}
  table.t{min-width:0 !important;}
  .card,.k,.nm,.es,.gated,.tbl-wrap,.trend{break-inside:avoid;}
  /* An unmeasured ROW's inset box-shadow does not print; a real border does. */
  tr.row-unmeasured th{box-shadow:none;border-left:3px double var(--dash);}
  /* A closed <details> cannot be forced open from CSS (the content sits in a
     hidden UA slot), and nothing measured is ever inside one — the .disc
     header forbids evidence in a disclosure — so collapsed prose prints
     collapsed. Only the interactive "show/hide" affordance comes off, because
     on paper it is a control that cannot be pressed. */
  .disc > summary::after,.disc-mini > summary::after{content:"";border:0;padding:0;}
}
