
  /* ============================================================
     1. Tokens — canonical v3 set (design-system-v3.md §1, verbatim)
     ============================================================ */
  :root{
    color-scheme:light;
    --page:#F8FAFC; --surface:#FFFFFF; --tint:#F1F5F9; --hair:#E2E8F0; --strong:#CBD5E1;
    --deco:#94A3B8; --muted:#64748B; --secondary:#475569; --ink2:#334155; --ink:#0F172A;
    --accent:#4F46E5; --accent-h:#4338CA;
    --link:#4F46E5; --accent-soft:#EEF2FF; --focus:#6366F1;
    --ok-dot:#059669; --ok-ink:#047857; --ok-soft:#ECFDF5;
    --warn-dot:#D97706; --warn-ink:#B45309; --warn-soft:#FFFBEB;
    --crit-dot:#DC2626; --crit-ink:#B91C1C; --crit-soft:#FEF2F2;
    --info-dot:#2563EB; --info-ink:#1D4ED8; --info-soft:#EFF6FF;
    /* unknown is VIOLET in both themes — a state, not an absence. ink/soft 6.48:1. */
    --unk-dot:#7C3AED; --unk-ink:#6D28D9; --unk-soft:#F5F3FF;
    /* neutral is TEAL — fresh/in-sync, deliberately not green (green is scarce: flowing + verified only). ink/soft 5.47:1. */
    --neu-dot:#0D9488; --neu-ink:#0F766E; --neu-soft:#F0FDFA;
    --sh1:0 1px 2px rgba(15,23,42,.05);
    --sh2:0 1px 2px rgba(15,23,42,.05), 0 8px 20px -12px rgba(15,23,42,.12);
    --sans:-apple-system,BlinkMacSystemFont,system-ui,"Segoe UI",Roboto,sans-serif;
    --mono:ui-monospace,"SF Mono",Menlo,monospace;

    /* --- legacy v2 aliases (compat only; delete as pages are ported) --- */
    --bg:var(--page); --sunken:var(--tint); --raise:var(--tint);
    --ink-2:var(--secondary); --ink-3:var(--muted); --hair-2:var(--strong);
    --disp:var(--sans);
    --accent-2:var(--link); --accent-ink:var(--link);
    --accent-btn:var(--accent); --accent-btn-h:var(--accent-h);
    --good:var(--ok-dot); --good-ink:var(--ok-ink); --good-soft:var(--ok-soft);
    --warn:var(--warn-dot); --crit:var(--crit-dot); --info:var(--info-dot);
  }
  /* dark — re-picked SOLID inks/tints (no rgba overlays). Duplicated for the
     media query AND the data-theme attribute so the explicit toggle always
     wins over the OS preference, in both directions (design-system-v3.md §5). */
  @media (prefers-color-scheme:dark){:root{
    color-scheme:dark;
    --page:#0A0E17; --surface:#131926; --tint:#1B2333; --hair:#232B3D; --strong:#33405A;
    --deco:#4A5A73; --muted:#8B97AC; --secondary:#A7B2C5; --ink2:#C7CFDD; --ink:#EDF1F7;
    --accent:#4F46E5; --accent-h:#4338CA; --link:#7C83F7; --accent-soft:#222A4E; --focus:#7C83F7;
    --ok-dot:#059669; --ok-ink:#34D399; --ok-soft:#122B22;
    --warn-dot:#D97706; --warn-ink:#FBBF24; --warn-soft:#2A2112;
    --crit-dot:#DC2626; --crit-ink:#F87171; --crit-soft:#301818;
    --info-dot:#2563EB; --info-ink:#60A5FA; --info-soft:#16233C;
    /* unknown is VIOLET, not gray: gray reads as absent, violet reads as a state.
       Unknown is a severity that outranks ok. ink/soft 5.48:1. */
    --unk-dot:#8B6FD4; --unk-ink:#AA8AE0; --unk-soft:#2A1E3E;
    --neu-dot:#2DD4BF; --neu-ink:#5EEAD4; --neu-soft:#0F2422;
    --sh1:0 1px 2px rgba(0,0,0,.4);
    --sh2:0 1px 2px rgba(0,0,0,.4), 0 10px 24px -14px rgba(0,0,0,.6);

    --bg:var(--page); --sunken:var(--tint); --raise:var(--tint);
    --ink-2:var(--secondary); --ink-3:var(--muted); --hair-2:var(--strong);
    --disp:var(--sans);
    --accent-2:var(--link); --accent-ink:var(--link);
    --accent-btn:var(--accent); --accent-btn-h:var(--accent-h);
    --good:var(--ok-dot); --good-ink:var(--ok-ink); --good-soft:var(--ok-soft);
    --warn:var(--warn-dot); --crit:var(--crit-dot); --info:var(--info-dot);
  }}
  :root[data-theme="dark"]{
    color-scheme:dark;
    --page:#0A0E17; --surface:#131926; --tint:#1B2333; --hair:#232B3D; --strong:#33405A;
    --deco:#4A5A73; --muted:#8B97AC; --secondary:#A7B2C5; --ink2:#C7CFDD; --ink:#EDF1F7;
    --accent:#4F46E5; --accent-h:#4338CA; --link:#7C83F7; --accent-soft:#222A4E; --focus:#7C83F7;
    --ok-dot:#059669; --ok-ink:#34D399; --ok-soft:#122B22;
    --warn-dot:#D97706; --warn-ink:#FBBF24; --warn-soft:#2A2112;
    --crit-dot:#DC2626; --crit-ink:#F87171; --crit-soft:#301818;
    --info-dot:#2563EB; --info-ink:#60A5FA; --info-soft:#16233C;
    /* unknown is VIOLET, not gray: gray reads as absent, violet reads as a state.
       Unknown is a severity that outranks ok. ink/soft 5.48:1. */
    --unk-dot:#8B6FD4; --unk-ink:#AA8AE0; --unk-soft:#2A1E3E;
    --neu-dot:#2DD4BF; --neu-ink:#5EEAD4; --neu-soft:#0F2422;
    --sh1:0 1px 2px rgba(0,0,0,.4);
    --sh2:0 1px 2px rgba(0,0,0,.4), 0 10px 24px -14px rgba(0,0,0,.6);

    --bg:var(--page); --sunken:var(--tint); --raise:var(--tint);
    --ink-2:var(--secondary); --ink-3:var(--muted); --hair-2:var(--strong);
    --disp:var(--sans);
    --accent-2:var(--link); --accent-ink:var(--link);
    --accent-btn:var(--accent); --accent-btn-h:var(--accent-h);
    --good:var(--ok-dot); --good-ink:var(--ok-ink); --good-soft:var(--ok-soft);
    --warn:var(--warn-dot); --crit:var(--crit-dot); --info:var(--info-dot);
  }
  :root[data-theme="light"]{
    color-scheme:light;
    --page:#F8FAFC; --surface:#FFFFFF; --tint:#F1F5F9; --hair:#E2E8F0; --strong:#CBD5E1;
    --deco:#94A3B8; --muted:#64748B; --secondary:#475569; --ink2:#334155; --ink:#0F172A;
    --accent:#4F46E5; --accent-h:#4338CA;
    --link:#4F46E5; --accent-soft:#EEF2FF; --focus:#6366F1;
    --ok-dot:#059669; --ok-ink:#047857; --ok-soft:#ECFDF5;
    --warn-dot:#D97706; --warn-ink:#B45309; --warn-soft:#FFFBEB;
    --crit-dot:#DC2626; --crit-ink:#B91C1C; --crit-soft:#FEF2F2;
    --info-dot:#2563EB; --info-ink:#1D4ED8; --info-soft:#EFF6FF;
    /* unknown is VIOLET in both themes — a state, not an absence. ink/soft 6.48:1. */
    --unk-dot:#7C3AED; --unk-ink:#6D28D9; --unk-soft:#F5F3FF;
    /* neutral is TEAL — fresh/in-sync, deliberately not green (green is scarce: flowing + verified only). ink/soft 5.47:1. */
    --neu-dot:#0D9488; --neu-ink:#0F766E; --neu-soft:#F0FDFA;
    --sh1:0 1px 2px rgba(15,23,42,.05);
    --sh2:0 1px 2px rgba(15,23,42,.05), 0 8px 20px -12px rgba(15,23,42,.12);

    --bg:var(--page); --sunken:var(--tint); --raise:var(--tint);
    --ink-2:var(--secondary); --ink-3:var(--muted); --hair-2:var(--strong);
    --disp:var(--sans);
    --accent-2:var(--link); --accent-ink:var(--link);
    --accent-btn:var(--accent); --accent-btn-h:var(--accent-h);
    --good:var(--ok-dot); --good-ink:var(--ok-ink); --good-soft:var(--ok-soft);
    --warn:var(--warn-dot); --crit:var(--crit-dot); --info:var(--info-dot);
  }

  /* ============================================================
     2. Base
     ============================================================ */
  *{box-sizing:border-box;} body{margin:0; background:var(--page);}
  /* The viewport must never pan sideways. A min-width table inside a clipped
     card still inflates the initial containing block, so clipping the shell is
     not enough — the clip has to be on the root. Verified with a real wheel
     gesture, not with scrollWidth, which reports overflow on pages that cannot
     pan and containment on pages that can.
     clip (not hidden) is deliberate: it creates no scroll container, so the
     sticky rail and top bar keep working — confirmed pinned at y=0 after a
     900px vertical scroll. Wide tables keep their own .scroll containers, so
     nothing becomes unreachable; only the dead space disappears. */
  html{overflow-x:clip;}
  /* DT-115. [hidden] is an attribute selector -- specificity (0,1,0) -- so any
     class rule that sets display ties it and wins on source order, and any
     compound (.live-ctl .btn, .fmsg.show) outranks it outright. The user
     agent's own [hidden]{display:none} sits in a lower-priority origin still,
     so a single author declaration is enough to beat it. That is not a theory:
     .btn{display:inline-flex} rendered a Pause control the script had hidden
     (DT-041 patched that one selector), and .kcap{display:flex} /
     .note{display:flex} do the same to the install-link status and error
     regions on the very next screen.

     !important here is deliberate -- a page's extraCss is injected AFTER this
     file (layout.ts), so specificity and order alone cannot be made to hold.
     The invariant it depends on is that no OTHER important display in these
     stylesheets has a value other than none: an important display:none can
     only agree with this rule, anything else could beat it. Today the only
     other one is .rail{display:none!important} at max-width:1180px.
     tests/cabinet-hidden-attribute.test.ts enforces that, and resolves the real
     cascade for every element the real renderers ship with a hidden attribute.

     Scope note: this styles the HTML hidden ATTRIBUTE only. .vh (19.0),
     aria-hidden and overflow:hidden are untouched -- a visually-hidden skip
     link is still in the tab order, exactly as section 2 requires. */
  [hidden]{display:none!important;}
  .cab{font-family:var(--sans); color:var(--ink); background:var(--page); -webkit-font-smoothing:antialiased;
    display:grid; grid-template-columns:264px 1fr; min-height:100vh; font-size:14px; line-height:1.5;
    max-width:100%;}
  .cab *::selection{background:var(--accent-soft);}
  a{color:inherit; text-decoration:none;} button{font-family:inherit; color:inherit;}
  :focus-visible{outline:2px solid var(--focus); outline-offset:2px; border-radius:6px;}
  .mono{font-family:var(--mono); font-variant-numeric:tabular-nums;}
  /* Skip link — the first tab stop on every cabinet page, targeting the shell's
     one <main>. Without it the first eleven tab stops are the rail, on every
     route (WCAG 2.4.1). Off-screen until focused; never display:none, which
     would take it out of the tab order entirely. */
  .skip{position:absolute; left:-9999px; top:8px; z-index:100; padding:10px 14px; border-radius:10px;
    background:var(--accent); color:#fff; font-size:13.5px; font-weight:600;}
  .skip:focus{left:8px;}
  .main > main{min-width:0; flex:1 1 auto;}
  .main > main:focus{outline:none;}

  /* ============================================================
     3. Chrome: rail
     ============================================================ */
  .rail{background:var(--surface); border-right:1px solid var(--hair); display:flex; flex-direction:column; padding:16px 12px; position:sticky; top:0; height:100vh;}
  .switch{display:flex; align-items:center; gap:10px; width:100%; text-align:left; padding:8px 10px; border:1px solid var(--strong); border-radius:12px; cursor:pointer; background:var(--surface); box-shadow:var(--sh1); transition:border-color .15s;}
  .switch:hover{border-color:var(--link);}
  .switch .mk{width:32px; height:32px; border-radius:9px; flex:0 0 auto; background:var(--accent); color:#fff; display:grid; place-items:center; font-weight:700; font-size:13.5px;}
  .switch .who{display:flex; flex-direction:column; min-width:0; line-height:1.3;}
  .switch .who b{font-size:14px; font-weight:600;}
  .switch .who span{font-size:12px; color:var(--muted);}
  .switch .cv{margin-left:auto; color:var(--muted);}
  .nav{margin-top:16px; display:flex; flex-direction:column; gap:2px;}
  .nav .g{padding:12px 10px 4px; font-size:12px; font-weight:600; color:var(--muted);}
  .nav a{display:flex; align-items:center; gap:10px; padding:8px 10px; border-radius:9px; color:var(--secondary); font-weight:500; font-size:14px; transition:background .14s,color .14s;}
  .nav a svg{width:18px; height:18px; flex:0 0 auto; color:var(--muted);}
  .nav a .ct{margin-left:auto; font-family:var(--mono); font-size:11px; color:var(--muted); font-variant-numeric:tabular-nums;}
  .nav a .ct.info{color:var(--info-ink);}
  .nav a .ct.crit{color:var(--crit-ink); background:var(--crit-soft); padding:1px 6px; border-radius:999px; font-weight:600;}
  .nav a:hover{background:var(--tint); color:var(--ink);} .nav a:hover svg{color:var(--secondary);}
  .nav a.on{background:var(--accent-soft); color:var(--link); font-weight:600;}
  .nav a.on svg{color:var(--link);} .nav a.on .ct:not(.crit){color:var(--link);}
  .rail .ft{margin-top:auto; display:flex; flex-direction:column; gap:2px; padding-top:12px; border-top:1px solid var(--hair);}
  .rail .ft a{display:flex; align-items:center; gap:10px; padding:8px 10px; border-radius:9px; color:var(--secondary); font-size:14px; font-weight:500;}
  .rail .ft a:hover{background:var(--tint); color:var(--ink);}
  .rail .ft a svg{width:18px; height:18px; color:var(--muted);}
  .rail .ft .signout{margin:0;}
  .rail .ft .signout button{display:flex; width:100%; align-items:center; gap:10px; padding:8px 10px; border:0; border-radius:9px; background:none; cursor:pointer; color:var(--muted); font:500 13px var(--sans); text-align:left;}
  .rail .ft .signout button:hover{background:var(--tint); color:var(--ink);}
  .rail .ft a.on svg{color:var(--link);}
  /* Native compact navigation is present for narrow layouts and remains out of
     the accessibility tree on desktop. It deliberately uses <details>/<summary>
     rather than a menu/dialog pattern, so open/close works without JavaScript. */
  .compact-nav{display:none;}
  .compact-nav summary{list-style:none; cursor:pointer; user-select:none;}
  .compact-nav summary::-webkit-details-marker{display:none;}
  .compact-nav-body{background:var(--surface);}
  .compact-nav-links{margin:0;}
  .compact-nav-ft{display:flex; flex-wrap:wrap; gap:4px; padding-top:10px; margin-top:10px; border-top:1px solid var(--hair);}
  .compact-nav-ft a{display:flex; min-width:0; align-items:center; gap:10px; padding:8px 10px; border-radius:9px; color:var(--secondary); font-size:14px; font-weight:500; overflow-wrap:anywhere;}
  .compact-nav-ft a:hover{background:var(--tint); color:var(--ink);}
  .compact-nav-ft a svg{width:18px; height:18px; color:var(--muted);}
  .compact-nav-ft .signout{margin:0;}
  .compact-nav-ft .signout button{display:flex; width:100%; min-width:0; align-items:center; gap:10px; padding:8px 10px; border:0; border-radius:9px; background:none; cursor:pointer; color:var(--muted); font:500 13px var(--sans); text-align:left; white-space:normal;}
  .compact-nav-ft .signout button:hover{background:var(--tint); color:var(--ink);}

  /* ============================================================
     4. Chrome: topbar
     ============================================================ */
  .main{min-width:0; display:flex; flex-direction:column;}
  .top{height:60px; border-bottom:1px solid var(--hair); background:color-mix(in srgb,var(--page) 84%, transparent); backdrop-filter:blur(10px); position:sticky; top:0; z-index:20; display:flex; align-items:center; gap:14px; padding:0 24px;}
  .crumb{font-size:12.5px; color:var(--muted); display:flex; align-items:center; gap:8px; white-space:nowrap; min-width:0;} .crumb b{color:var(--ink); font-weight:600;} .crumb .s{opacity:.45;}
  .crumb b.mono{font-family:var(--mono); font-size:12.5px; overflow:hidden; text-overflow:ellipsis;}
  .search{margin-left:6px; flex:1; max-width:320px; height:36px; border:1px solid var(--strong); border-radius:10px; background:var(--surface); display:flex; align-items:center; gap:9px; padding:0 12px; color:var(--muted); box-shadow:var(--sh1);}
  .search:focus-within{border-color:var(--focus);}
  .search svg{flex:0 0 auto;}
  .search input{min-width:0; width:100%; border:0; outline:0; background:transparent; color:var(--ink); font:13.5px var(--sans);}
  .search input::placeholder{color:var(--muted); opacity:1;}
  .search kbd, .search .k{margin-left:auto; font-family:var(--mono); font-size:11px; color:var(--muted); border:1px solid var(--hair); border-radius:5px; padding:1px 5px;}
  .top .r{margin-left:auto; display:flex; align-items:center; gap:10px;}
  .pill{display:inline-flex; align-items:center; gap:7px; height:36px; padding:0 13px; border-radius:999px; border:1px solid var(--strong); background:var(--surface); font-size:12.5px; color:var(--secondary); font-weight:500; box-shadow:var(--sh1); transition:border-color .15s; cursor:pointer;}
  .pill:hover{border-color:var(--link);}
  .ib{width:36px; height:36px; border:1px solid var(--strong); border-radius:10px; background:var(--surface); display:grid; place-items:center; color:var(--secondary); cursor:pointer; box-shadow:var(--sh1); transition:border-color .15s,color .15s;}
  .ib:hover{color:var(--ink); border-color:var(--link);}
  .av{width:36px; height:36px; border-radius:999px; background:var(--tint); border:1px solid var(--strong); color:var(--secondary); display:grid; place-items:center; font-size:12.5px; font-weight:600;}
  /* The plain 7px bullet, and ONLY the plain bullet. It still has three honest
     users -- apps.ts, app-card.ts and connect-approve.ts each emit a bare
     <span class="dot"> that inherits the badge's own colour and means nothing on
     its own.

     The severity modifiers that used to live here (.dot.ok/.warn/.crit/.info/
     .unknown/.gray/.neutral/.revoked) are GONE ON PURPOSE. They painted the same
     circle in eight hues, which made crit (357deg) and ok (152deg) one mark under
     deuteranopia -- exactly what DESIGN.md 9.1 forbids. Every severity mark in the
     cabinet is now severityGlyph() (views/labels.ts, sizing at 19.2 below).
     Deleting the rules rather than leaving them unused is the point: a future
     class="dot crit" now renders a colourless bullet and gets noticed, instead
     of quietly working and reintroducing the collision. */
  .dot{width:7px; height:7px; border-radius:999px; flex:0 0 auto;}
  .pls{position:relative;} .pls::after{content:""; position:absolute; inset:-4px; border-radius:999px; border:1px solid currentColor; opacity:.5; animation:pl 1.8s ease-out infinite;}
  @keyframes pl{from{transform:scale(.6);opacity:.55;} to{transform:scale(1.5);opacity:0;}}

  /* ============================================================
     5. DegradedBanner (C8) — keyed off the sweep timestamp
     ============================================================ */
  .banner{display:flex; align-items:center; gap:12px; padding:10px 24px; background:var(--warn-soft); border-bottom:1px solid var(--hair); font-size:13.5px; color:var(--ink2);}
  .banner svg{flex:0 0 auto; color:var(--warn-ink);}
  .banner b{color:var(--warn-ink);}
  .banner form{margin-left:auto;}
  .banner.crit{background:var(--crit-soft);}
  .banner.crit svg{color:var(--crit-ink);}
  .banner.crit b{color:var(--crit-ink);}

  /* ============================================================
     6. Page scaffold — NO entrance animation (design-system-v3.md §4:
     "the .wrap rise-on-navigation animation is DELETED")
     ============================================================ */
  .wrap{padding:24px 28px 48px; max-width:1400px; width:100%; margin-inline:auto;}
  .back{display:inline-flex; align-items:center; gap:6px; font-size:12.5px; color:var(--muted); font-weight:600; margin-bottom:14px; cursor:pointer;}
  .back svg{width:15px; height:15px;} .back:hover{color:var(--ink);}
  .head{display:flex; align-items:flex-end; justify-content:space-between; gap:20px; margin-bottom:20px;}
  .head h1{font-size:29px; font-weight:700; letter-spacing:-.03em; margin:0 0 5px; line-height:1.06;}
  .head p{margin:0; color:var(--secondary); font-size:14px;}
  .head p b{color:var(--ink); font-weight:600;}
  .head .sweep, .sweep{font-family:var(--mono); font-size:11px; color:var(--muted); margin-top:5px; font-variant-numeric:tabular-nums;}
  .sweep .s-stale{color:var(--warn-ink);}
  .title-line{display:flex; align-items:center; gap:12px; margin-bottom:7px; flex-wrap:wrap;}
  .title-line h1{margin:0; font-family:var(--mono); font-size:22px; font-weight:600; letter-spacing:-.03em;}
  .store-meta{color:var(--secondary); font-size:12.5px; display:flex; align-items:center; gap:9px; flex-wrap:wrap;}
  .head-actions{display:flex; align-items:center; gap:9px; flex:0 0 auto;}

  /* ============================================================
     7. Buttons (C7) — FLAT, one primary per view region
     ============================================================ */
  .btn{height:36px; padding:0 14px; border-radius:10px; border:1px solid transparent; cursor:pointer; font-size:13.5px; font-weight:600; display:inline-flex; align-items:center; gap:8px; white-space:nowrap; background:none; transition:background .15s,border-color .15s;}
  .btn.pr{background:var(--accent); color:#fff;}
  .btn.pr:hover{background:var(--accent-h);}
  .btn.sec{background:var(--surface); border-color:var(--strong); color:var(--ink2); box-shadow:var(--sh1);}
  .btn.sec:hover{background:var(--tint);}
  .btn.gh{color:var(--secondary);}
  .btn.gh:hover{background:var(--tint); color:var(--ink);}
  .btn.dng{background:var(--surface); border:1px solid color-mix(in srgb, var(--crit-dot) 30%, var(--strong)); color:var(--crit-ink); box-shadow:var(--sh1);}
  .btn.dng:hover{background:var(--crit-soft); border-color:color-mix(in srgb, var(--crit-dot) 45%, var(--strong));}
  .btn.sm{height:30px; padding:0 10px; font-size:12.5px; border-radius:8px;}
  .btn svg{width:16px; height:16px;} .btn.sm svg{width:14px; height:14px;}

  .card{background:var(--surface); border:1px solid var(--hair); border-radius:16px; box-shadow:var(--sh2);}
  .ph{display:flex; align-items:center; justify-content:space-between; gap:12px; padding:14px 20px; border-bottom:1px solid var(--hair);}
  .ph h2{margin:0; font-size:16px; font-weight:650; letter-spacing:-.02em;}
  .ph .cnt{font-size:12.5px; color:var(--muted); font-weight:500;}
  .ph a{font-size:12.5px; color:var(--link); font-weight:600; cursor:pointer;} .ph a:hover{text-decoration:underline;}
  .split, .detail-grid{display:grid; grid-template-columns:minmax(0,1fr) 372px; gap:16px; align-items:start;}
  .stack{display:flex; flex-direction:column; gap:16px; min-width:0;}

  /* ============================================================
     8. Shared vocabulary — Badge (C1), ReasonChip (C2)
     ============================================================ */
  .badge{display:inline-flex; align-items:center; gap:7px; height:26px; padding:0 10px; border-radius:8px; font-size:12.5px; font-weight:600; white-space:nowrap;}
  .badge .dot{width:6px; height:6px;}
  .badge.ok, .badge.good{color:var(--ok-ink); background:var(--ok-soft);}
  .badge.warn{color:var(--warn-ink); background:var(--warn-soft);}
  .badge.crit{color:var(--crit-ink); background:var(--crit-soft);}
  .badge.info{color:var(--info-ink); background:var(--info-soft);}
  .badge.unknown, .badge.gray{color:var(--unk-ink); background:var(--unk-soft);}
  .badge.neutral{color:var(--neu-ink); background:var(--neu-soft);}
  /* revoked is an intended end state, not a failure: muted and struck, never red (DESIGN.md sect 9). */
  .badge.revoked{color:var(--ink-3); background:transparent; text-decoration:line-through;}
  .chip{font-family:var(--mono); font-size:11px; color:var(--secondary); background:var(--tint); border:1px solid var(--hair); padding:3px 7px; border-radius:7px; white-space:nowrap; font-variant-numeric:tabular-nums;}
  /* ReasonChip — explicit variants chosen by the renderer; NO descendant
     overrides (e.g. "tr.sev-crit .code{…}") are permitted anywhere. */
  .reason{font-family:var(--mono); font-size:11px; padding:3px 7px; border-radius:7px; white-space:nowrap;}
  .reason--neutral{color:var(--secondary); background:var(--tint); border:1px solid var(--hair);}
  .reason--crit{color:var(--crit-ink); background:var(--crit-soft);}
  .reason--warn{color:var(--warn-ink); background:var(--warn-soft);}
  .reason--unknown{color:var(--unk-ink); background:var(--unk-soft);}
  .dom{font-family:var(--mono); font-size:12.5px; font-weight:500; white-space:nowrap;}
  .step{color:var(--ink); font-size:13.5px; white-space:nowrap;}
  .tm{font-family:var(--mono); font-size:11px; color:var(--muted); white-space:nowrap; font-variant-numeric:tabular-nums;}
  .tm.warn{color:var(--warn-ink);} .tm.crit{color:var(--crit-ink);}

  /* ============================================================
     9. StatTile v2 (C4) — one band API, zero-value rule
     ============================================================ */
  .stats{display:grid; grid-template-columns:repeat(4,1fr); gap:16px; margin-bottom:16px;}
  .stat{padding:16px 18px 14px; display:flex; flex-direction:column; gap:8px; position:relative; transition:border-color .15s;}
  a.stat:hover{border-color:var(--strong);}
  .stat .l{display:flex; align-items:center; justify-content:space-between;}
  .stat .lbl{font-size:12.5px; color:var(--muted); font-weight:600;}
  .stat .go{color:var(--muted); display:inline-flex;}
  .stat .v{font-size:33px; font-weight:700; letter-spacing:-.04em; line-height:1; font-variant-numeric:tabular-nums; display:flex; align-items:baseline; gap:7px;}
  .stat .v .u{font-size:14px; color:var(--muted); font-weight:600; letter-spacing:normal;}
  .stat .v.crit{color:var(--crit-ink);} .stat .v.warn{color:var(--warn-ink);} .stat .v.info{color:var(--info-ink);}
  .stat .cap{font-size:12px; color:var(--muted);}
  .stat .cap b{color:var(--secondary); font-weight:600;}
  .stat.sev-crit{border-color:color-mix(in srgb, var(--crit-dot) 32%, var(--hair));}
  .stat.sev-crit::before{content:""; position:absolute; left:-1px; top:14px; bottom:14px; width:3px; border-radius:0 3px 3px 0; background:var(--crit-dot);}
  /* Unknown carries a stripe too — it is a severity that outranks ok, not an
     absence, so a fleet with no signal must not look like a calm one. */
  .stat.sev-unknown{border-color:color-mix(in srgb, var(--unk-dot) 32%, var(--hair));}
  .stat.sev-unknown::before{content:""; position:absolute; left:-1px; top:14px; bottom:14px; width:3px; border-radius:0 3px 3px 0; background:var(--unk-dot);}

  /* DistBar — the one proportional band, reused by StatTile and RolloutModule.
     Sums to the stated total; ok 45% opacity, warn 90%, crit 100%, unknown = --deco 80%. */
  .band{display:flex; gap:2px; height:6px; border-radius:3px; overflow:hidden; margin-top:2px;}
  .band i{border-radius:2px; min-width:3px;}
  .band .b-ok{background:var(--ok-dot); opacity:.45;}
  .band .b-warn{background:var(--warn-dot); opacity:.9;}
  .band .b-crit{background:var(--crit-dot);}
  .band .b-info{background:var(--info-dot); opacity:.85;}
  .band .b-unknown{background:var(--unk-dot);}

  /* ============================================================
     10. SeverityLadder (C3) — the needs-attention triage list variant
     ============================================================ */
  .attn-item{display:flex; align-items:center; gap:14px; padding:13px 20px; border-bottom:1px solid var(--hair); position:relative;}
  .attn-item:last-child{border-bottom:0;}
  .attn-item::before{content:""; position:absolute; left:0; top:10px; bottom:10px; width:3px; border-radius:0 3px 3px 0;}
  .attn-item.sev-crit::before{background:var(--crit-dot);}
  .attn-item.sev-warn::before{background:var(--warn-dot);}
  .attn-item.sev-unknown::before{background:var(--unk-dot);}
  .attn-item .tx{min-width:0; display:flex; flex-direction:column; gap:4px;}
  .attn-item .t1{font-size:13.5px; font-weight:600; display:flex; align-items:center; gap:8px; flex-wrap:wrap;}
  .attn-item .t2{font-size:12.5px; color:var(--secondary); display:flex; align-items:center; gap:8px; flex-wrap:wrap;}
  .attn-item .ac{margin-left:auto; display:flex; align-items:center; gap:6px; flex:0 0 auto;}
  .attn-item .open{font-size:12.5px; color:var(--link); font-weight:600; padding:6px 8px; border-radius:8px;}
  .attn-item .open:hover{background:var(--tint);}

  /* ============================================================
     11. Table v2 (C6) — group headers, per-row action column, aging
     ============================================================ */
  table{width:100%; border-collapse:collapse;}
  /* Wide tables scroll inside their card rather than cropping — but with no
     affordance an operator read a clipped column as missing data rather than as
     scrollable, which is how /health, /setups and /stores appeared to "lose"
     their Status and Action columns below 1180px.
     The shadows below are pure CSS and self-arming: the two local-attachment
     covers scroll away with the content, revealing a scroll-attachment shadow
     only on the side where content actually continues. Zero JS, and it costs
     nothing when the table already fits. */
  .scroll{
    overflow-x:auto;
    background:
      linear-gradient(to right, var(--surface) 30%, transparent) left center,
      linear-gradient(to left,  var(--surface) 30%, transparent) right center,
      radial-gradient(farthest-side at 0 50%,    rgba(2,6,23,.16), transparent) left center,
      radial-gradient(farthest-side at 100% 50%, rgba(2,6,23,.16), transparent) right center;
    background-repeat:no-repeat;
    background-size:40px 100%, 40px 100%, 14px 100%, 14px 100%;
    background-attachment:local, local, scroll, scroll;
  }
  .scroll:focus-visible{outline:2px solid var(--focus); outline-offset:-2px; border-radius:6px;}
  thead th{text-align:left; padding:10px 16px; border-bottom:1px solid var(--hair); font-size:12px; color:var(--secondary); font-weight:600; white-space:nowrap; background:var(--tint);}
  thead th:first-child{padding-left:20px;} thead th:last-child{padding-right:20px;}
  thead th.r, td.r{text-align:right;}
  tbody td{padding:11px 16px; border-bottom:1px solid var(--hair); vertical-align:middle; font-size:13.5px;}
  tbody td:first-child{padding-left:20px;} tbody td:last-child{padding-right:20px;}
  tbody tr:last-child td{border-bottom:0;}
  /* SeverityLadder (C3) — table-row carrier: 3px stripe, crit adds a 4% wash.
     One visual spec, two mechanical carriers (this + the .stripe/attn-item
     block-element carrier above). */
  tr.sev-crit td{background:color-mix(in srgb, var(--crit-dot) 4%, transparent);}
  tr.sev-crit td:first-child{border-left:3px solid var(--crit-dot); padding-left:17px;}
  tr.sev-warn td:first-child{border-left:3px solid var(--warn-dot); padding-left:17px;}
  tr.sev-unknown td:first-child{border-left:3px solid var(--unk-dot); padding-left:17px;}
  tr.grp td{padding:8px 16px 8px 20px; background:var(--tint); border-bottom:1px solid var(--hair); font-size:12px; font-weight:600; color:var(--secondary);}
  tr.grp .g-in{display:flex; align-items:center; gap:8px;}
  tr.grp .g-cap{font-weight:400; color:var(--muted); font-size:11.5px; min-width:0; overflow:hidden; text-overflow:ellipsis; white-space:nowrap;}
  tr.grp .g-ct{font-family:var(--mono); font-size:11px; color:var(--muted); font-weight:500; font-variant-numeric:tabular-nums;}
  tr.grp form{margin-left:auto;}
  tr.grp .btn.sm{height:26px; font-size:12px;}
  tr.more td{padding:0; border-bottom:1px solid var(--hair);}
  tr.more a{display:block; width:100%; color:var(--link); font-size:12.5px; font-weight:600; padding:10px; text-align:center;}
  tr.more a:hover{background:var(--tint);}
  .diag{display:flex; flex-direction:column; align-items:flex-start; gap:4px; min-width:0;}
  .diag .d1{font-size:13.5px; color:var(--ink); white-space:nowrap;}
  .diag .d1.quiet{color:var(--secondary);}
  .arrow-cell{width:28px; color:var(--muted);}
  td.act-cell{white-space:nowrap;}
  /* --deco is 2.80:1 and is never text (DESIGN.md §3.3). */
  td.act-cell .none{color:var(--muted);}

  .seg{display:flex; gap:2px; background:var(--tint); border:1px solid var(--hair); border-radius:9px; padding:2px;}
  .seg a{height:26px; display:inline-flex; align-items:center; padding:0 11px; color:var(--muted); font-size:12px; font-weight:600; border-radius:7px;}
  .seg a[aria-current="true"]{background:var(--surface); color:var(--ink); box-shadow:var(--sh1);}

  /* ============================================================
     12. Cockpit severity tiles (fleet-health summary strip)
     ============================================================ */
  .cockpit{padding:20px; margin-bottom:16px;}
  .ctiles{display:grid; grid-template-columns:repeat(4,1fr); gap:12px; margin-bottom:20px;}
  .ctile{position:relative; border:1px solid var(--hair); border-radius:12px; background:var(--surface); padding:14px 16px 12px; display:flex; flex-direction:column; gap:6px; transition:border-color .15s;}
  .ctile:hover{border-color:var(--strong);}
  .ctile .lbl{display:flex; align-items:center; gap:7px; font-size:12.5px; color:var(--muted); font-weight:600;}
  .ctile .num{font-size:33px; font-weight:700; letter-spacing:-.04em; line-height:1; font-variant-numeric:tabular-nums;}
  .ctile .cap{font-size:12px; color:var(--muted);}
  .ctile.crit{border-color:color-mix(in srgb, var(--crit-dot) 32%, var(--hair));}
  .ctile.crit::before{content:""; position:absolute; left:-1px; top:12px; bottom:12px; width:3px; border-radius:0 3px 3px 0; background:var(--crit-dot);}
  .ctile.crit .num{color:var(--crit-ink);}
  .ctile.warn .num{color:var(--warn-ink);}
  .ctile.unknown .num{color:var(--unk-ink);}
  .ctile.ok .num{color:var(--ok-ink);}

  /* ============================================================
     13. FleetPulse (C5) — staleness seismograph; SSR: a server loop of <i>
     ============================================================ */
  .pulse-h{display:flex; align-items:baseline; justify-content:space-between; gap:12px; margin-bottom:10px;}
  .pulse-h h3{margin:0; font-size:16px; font-weight:650; letter-spacing:-.02em;}
  .pulse-h .note{font-family:var(--mono); font-size:11px; color:var(--muted); font-variant-numeric:tabular-nums;}
  /* scoped to <div class="pulse"> (the mockups' actual element) so this never
     collides with connect-shopify.ts's unrelated <span class="gly gray pulse">
     waiting-indicator, which predates this component. */
  div.pulse{display:flex; align-items:flex-end; gap:2px; height:36px;}
  .pulse i{flex:1 1 0; min-width:2px; max-width:7px; border-radius:2px 2px 1px 1px; height:var(--h,6px);}
  .pulse i.crit{background:var(--crit-dot);}
  .pulse i.warn{background:var(--warn-dot); opacity:.9;}
  /* unknown was painted with --deco, the DECORATIVE token — so a fleet that has
     sent no signal rendered as gray furniture rather than as a state, which is
     the exact failure the violet unknown exists to prevent. It is a severity
     that outranks ok; it gets its own hue at full strength. */
  .pulse i.unknown{background:var(--unk-dot);}
  /* ok was at .45 opacity, which made a fully-healthy fleet the least legible
     screen in the product. Flowing is the outcome we sell — it reads clearly. */
  .pulse i.ok{background:var(--ok-dot); opacity:.75;}
  .pulse-legend{display:flex; align-items:center; gap:16px; margin-top:12px; font-size:12.5px; color:var(--secondary); flex-wrap:wrap;}
  .pulse-legend .li{display:inline-flex; align-items:center; gap:6px;}
  .pulse-legend .exp{margin-left:auto; font-family:var(--mono); font-size:11px; color:var(--muted);}

  /* ============================================================
     14. RolloutModule (C11) — config drift / fleet push
     ============================================================ */
  .roll{padding:16px 20px;}
  .roll .master{display:flex; align-items:center; gap:8px; font-size:12.5px; color:var(--secondary); margin-bottom:10px;}
  .roll .vcap{font-family:var(--mono); font-size:11px; color:var(--muted); margin-top:8px; font-variant-numeric:tabular-nums;}
  .roll .rac{display:flex; align-items:center; gap:8px; margin-top:12px;}
  .roll .rac a{font-size:12.5px; color:var(--link); font-weight:600;}
  .roll .note{font-size:12px; color:var(--muted); margin-top:10px; line-height:1.5; max-width:72ch;}

  /* ============================================================
     15. Filter bar
     ============================================================ */
  .filters{display:flex; align-items:center; gap:10px; margin:16px 0 14px; flex-wrap:wrap;}
  .fsearch{height:36px; width:280px; border:1px solid var(--strong); border-radius:10px; background:var(--surface); color:var(--ink); display:flex; align-items:center; gap:9px; padding:0 12px; box-shadow:var(--sh1);}
  /* Both class names, because a screen still ships the older .filter-search
     markup — the rule used to name only .fsearch and so matched nothing on
     /stores and /setups: measured border change on focus was 1.00. A 2px ring
     replaces the removed native outline rather than only tinting the border. */
  .fsearch:focus-within, .filter-search:focus-within{border-color:var(--focus); box-shadow:0 0 0 2px var(--focus);}
  .fsearch svg{color:var(--muted); flex:0 0 auto;}
  .fsearch input{min-width:0; width:100%; border:0; outline:0; background:transparent; color:var(--ink); font:13.5px var(--sans);}
  .fsearch input::placeholder{color:var(--muted); opacity:1;}
  .facet{height:32px; padding:0 12px; border:1px solid var(--strong); border-radius:999px; background:var(--surface); color:var(--secondary); display:inline-flex; align-items:center; gap:7px; font-size:12.5px; font-weight:500; box-shadow:var(--sh1); transition:border-color .15s; cursor:pointer;}
  .facet:hover{border-color:var(--link);}
  .facet[aria-current="true"]{background:var(--accent-soft); border-color:transparent; color:var(--link); font-weight:600;}
  .facet .fn{font-family:var(--mono); font-size:11px;}
  .facet .fc{font-family:var(--mono); font-size:11px; color:var(--muted); font-variant-numeric:tabular-nums;}
  .facet[aria-current="true"] .fc{color:var(--link);}
  .facet.off{border-style:dashed; box-shadow:none; color:var(--muted);}
  .fright{margin-left:auto; font-size:12.5px; color:var(--muted); display:flex; align-items:center; gap:6px;}

  /* ============================================================
     16. Truth-state primitives (C12) — loading / empty / error
     ============================================================ */
  .sk{background:var(--tint); border-radius:8px; animation:skp 1.2s ease-in-out infinite;}
  @keyframes skp{0%,100%{opacity:1;} 50%{opacity:.6;}}
  .sk-tiles{display:grid; grid-template-columns:repeat(4,1fr); gap:12px; margin-bottom:20px;}
  .sk-tile{height:92px; border-radius:12px;}
  .sk-lane{height:36px; border-radius:8px;}
  .sk-row{height:20px; margin:14px 20px;}
  .empty-box{padding:48px 24px; text-align:center;}
  .empty-box .ring{width:64px; height:64px; border-radius:999px; background:var(--tint); display:grid; place-items:center; margin:0 auto 16px; color:var(--muted);}
  .empty-box h3{margin:0 0 6px; font-size:16px; font-weight:650; letter-spacing:-.02em;}
  .empty-box p{margin:0 0 16px; color:var(--secondary); font-size:14px;}
  .err-box{display:flex; align-items:center; gap:12px; padding:20px; font-size:13.5px; color:var(--ink2);}
  .err-box svg{color:var(--crit-dot); flex:0 0 auto;}
  .err-box form{margin-left:auto;}

  /* ============================================================
     17. Activity / lifecycle feed rows, partner/app summary rows
     ============================================================ */
  .prow{display:flex; align-items:center; gap:13px; padding:16px 20px;}
  .prow .pm{width:36px; height:36px; border-radius:10px; background:var(--accent); color:#fff; display:grid; place-items:center; font-weight:700; font-size:14px; flex:0 0 auto;}
  .prow .pn{font-weight:600; font-size:14px;}
  .prow .pmeta{font-family:var(--mono); font-size:11px; color:var(--muted); margin-top:3px;}
  .prow .pr{margin-left:auto; text-align:right; display:flex; flex-direction:column; align-items:flex-end; gap:6px;}
  .act{padding:4px 20px 6px;} .act ul{margin:0; padding:0;}
  .act li{list-style:none; display:flex; gap:12px; padding:12px 0; border-bottom:1px solid var(--hair);} .act li:last-child{border-bottom:0;}
  .act .ic{width:28px; height:28px; border-radius:8px; background:var(--tint); border:1px solid var(--hair); display:grid; place-items:center; color:var(--muted); flex:0 0 auto; margin-top:1px;}
  .act .ic svg{width:14px; height:14px;}
  .act .ic.ok, .act .ic.good{color:var(--ok-ink);} .act .ic.warn{color:var(--warn-ink);} .act .ic.info{color:var(--info-ink);} .act .ic.crit{color:var(--crit-ink);}
  .act .tx{flex:1; font-size:13.5px; color:var(--ink); line-height:1.5;}
  .act .tx .t{font-family:var(--mono); font-size:11px; color:var(--muted); display:block; margin-top:3px;}
  .act .va{display:block; text-align:center; padding:12px; font-size:12.5px; color:var(--link); font-weight:600; border-top:1px solid var(--hair); cursor:pointer;} .act .va:hover{background:var(--tint);}

  /* ============================================================
     18. Motion, theme-swap, responsive
     ============================================================ */
  /* Applied by the theme toggle for exactly one frame so every surface swaps
     in a single frame instead of staggered per-property transitions (§5). */
  .no-anim *, .no-anim *::before, .no-anim *::after{transition:none!important; animation:none!important;}
  @media (prefers-reduced-motion:reduce){*{animation:none!important; transition:none!important;}}
  /* Below 1180px the desktop rail is replaced by a native disclosure. The rail
     is display:none (and therefore not exposed as duplicate navigation); the
     compact details remains keyboard- and no-JS-operable. */
  @media (max-width:1180px){
    .cab{grid-template-columns:1fr;}
    .rail{display:none !important;}
    .compact-nav{display:block; position:sticky; top:0; z-index:30; border-bottom:1px solid var(--hair); background:var(--surface);}
    .compact-nav summary{display:flex; align-items:center; gap:10px; min-height:52px; padding:8px 14px; color:var(--ink);}
    .compact-nav summary::after{content:''; width:9px; height:9px; margin-left:auto; border-right:2px solid var(--muted); border-bottom:2px solid var(--muted); transform:rotate(45deg) translateY(-2px);}
    .compact-nav[open] summary::after{transform:rotate(225deg) translateY(-2px);}
    .compact-nav-label{font-size:14px; font-weight:700;}
    .compact-nav-current{min-width:0; overflow:hidden; text-overflow:ellipsis; white-space:nowrap; color:var(--muted); font-size:13px;}
    /* The disclosure remains pinned, while its open body owns the vertical
       overflow. This matters at 400% zoom where a 320px device becomes an
       80x225 CSS viewport: focus must scroll the navigation body rather than
       leave later destinations below the viewport or pan the page. */
    .compact-nav-body{max-height:calc(100vh - 52px); max-height:calc(100dvh - 52px); overflow-y:auto; overscroll-behavior-y:contain; scroll-padding-block:8px; padding:0 12px 12px; border-top:1px solid var(--hair); box-shadow:var(--sh2);}
    .compact-nav-links .g{padding-top:10px;}
    .compact-nav-links a{padding:9px 10px;}
    .compact-nav-ft{display:grid; grid-template-columns:repeat(2,minmax(0,1fr));}
    .compact-nav-ft a,.compact-nav-ft .signout button{min-height:38px;}
    .stats, .sk-tiles, .ctiles{grid-template-columns:repeat(2,1fr);}
    .split, .detail-grid{grid-template-columns:1fr;}
  }
  @media (max-width:900px){
    /* The account address is the first thing to go — the destinations stay. */
    .rail .ft a[href^="mailto:"]{display:none;}
  }
  @media (max-width:640px){
    .stats, .sk-tiles, .ctiles{grid-template-columns:1fr;}
    .fsearch{width:100%;}
    /* The store domain is nowrap mono and the action was flex:0 0 auto, so the
       two collided on every row at 390px. The action drops to its own line and
       the domain is allowed to truncate rather than push. */
    .attn-item{flex-wrap:wrap; gap:6px 14px;}
    .attn-item .tx{flex:1 1 100%; min-width:0;}
    .attn-item .t2{min-width:0;}
    .attn-item .t2 .dom{overflow:hidden; text-overflow:ellipsis; min-width:0;}
    .attn-item .ac{margin-left:0; width:100%; justify-content:flex-start;}
    .attn-item .open{padding-left:0;}
  }
  @media (max-width:200px){
    /* 320/390/768px devices at 400% become 80/97/192 CSS px. Two footer
       columns cannot contain even the native Sign out button at those widths;
       one bounded column keeps every target inside the disclosure scroller. */
    .compact-nav-ft{grid-template-columns:1fr;}
    .compact-nav-body{padding-inline:6px;}
    .compact-nav-links a{min-width:0; gap:4px; padding-inline:6px; overflow-wrap:anywhere;}
    .compact-nav summary{gap:4px; padding-inline:6px;}
    .compact-nav summary::after{flex:0 0 auto;}
    .compact-nav-ft a,.compact-nav-ft .signout button{width:100%; min-width:0; flex-direction:column; align-items:flex-start; gap:4px; padding:6px; overflow-wrap:anywhere;}
  }
  @media (max-width:760px){
    .head{align-items:flex-start; flex-direction:column;}
    .facet-row{grid-template-columns:120px minmax(0,1fr) auto;}
    /* The top bar had no collapse order, so breadcrumb, search and the status
       pill rendered on top of each other on every mobile cabinet screen. Order
       of yielding: search box -> connector label -> breadcrumb truncates. */
    .top{gap:8px; padding:0 14px;}
    /* The workspace name and separator are redundant here — the rail's switcher
       already names the workspace — so only the current page survives. */
    .crumb{flex:0 1 auto; min-width:0; overflow:hidden;}
    .crumb span{display:none;}
    .crumb b{overflow:hidden; text-overflow:ellipsis; white-space:nowrap; font-size:14px;}
    .search{display:none;}
    .top .r{gap:6px; flex:0 0 auto;}
  }

  /* ============================================================
     19. Phase-8 primitives — specified in the design system, never built.
     Appended as one block rather than woven into the sections above so the
     diff is reviewable and so nothing already shipped is restyled by accident.
     Everything here is additive: new classes, or new severity variants of a
     mechanism that already exists.
     ============================================================ */

  /* --- 19.0 visually-hidden utility ----------------------------------------
     A real <caption> / label that a screen reader reads and a sighted operator
     does not see. Replaces the inline clip:rect() one-offs the pages grew. */
  .vh{position:absolute; width:1px; height:1px; margin:-1px; padding:0; overflow:hidden; clip:rect(0 0 0 0); clip-path:inset(50%); white-space:nowrap; border:0;}

  /* --- 19.1 the unobservable hatch (DESIGN.md 9.3) -------------------------
     "We could not look" and "we looked and found nothing" are different facts
     and must not share a mark. unknown is violet and filled; unobservable is a
     DRAWN GAP -- a repeating 135deg rule at 10% alpha (the spec ceiling)
     behind the cell, with the word still present on top.

     The hatch paints the CELL, never the badge sitting in it. That is a
     contrast decision, not a stylistic one: a badge is opaque, so no text ever
     lands on a stripe and every ink keeps its full ratio. Computed, worst case,
     over a stripe rather than the plain surface --
       light: unk-ink #6D28D9 over striped --surface  5.80:1  (plain 7.10)
       dark : unk-ink #AA8AE0 over striped --surface  5.14:1  (plain 6.57)
     and with the opaque badge in place the ink keeps its own pairing --
       light: 6.48:1 on --unk-soft   dark: 5.48:1 on --unk-soft.
     Hatching the badge itself would have dropped dark to 4.14:1, under the
     floor -- which is why the texture stops at the cell edge.

     Both themes get their own ink so the gap is drawn with the surface's own
     contrast direction: dark-on-light, light-on-dark. Declared here rather than
     in the token block so the whole primitive reads in one place; the
     data-theme selectors outrank the media query in both directions, as
     everywhere else in this file. */
  :root{--hatch-ink:rgba(15,23,42,.10);}
  @media (prefers-color-scheme:dark){:root{--hatch-ink:rgba(240,242,245,.10);}}
  :root[data-theme="dark"]{--hatch-ink:rgba(240,242,245,.10);}
  :root[data-theme="light"]{--hatch-ink:rgba(15,23,42,.10);}
  /* .cab-scoped so it outranks "tr.sev-crit td{background:...}" -- that
     shorthand resets background-image, which would erase the gap on exactly
     the rows most likely to carry one. */
  .cab .hatch{background-image:repeating-linear-gradient(135deg, var(--hatch-ink) 0 1px, transparent 1px 6px);}

  /* --- 19.2 shape-encoded severity glyphs (DESIGN.md 5 + 9.1) --------------
     Shapes and rationale live with the markup, in views/labels.ts
     (severityGlyph). This is only the box the SVG sits in and the ink it
     inherits. The glyph replaces the dot; the word stays.

     The ink tokens are used rather than the dot tokens: the dot values are
     tuned as >=3:1 non-text marks, while the inks clear 4.5:1 on every surface
     the glyph appears on, and inside a badge the ink is already the text
     colour, so mark and word cannot drift. Computed on --surface / --page /
     --tint respectively:
       crit  6.47 / 6.18 / 5.91 (light)   6.72 / 6.98 / 6.35 (dark)
       warn  5.02 / 4.80 / 4.58           11.13 / 11.56 / 10.52
       info  6.70 / 6.41 / 6.12            7.31 /  7.59 /  6.90
       ok    5.48 / 5.24 / 5.01            9.67 / 10.04 /  9.13
       unk   7.10 / 6.79 / 6.49            6.57 /  6.82 /  6.20
       neu   5.47 / 5.23 / 5.00           12.56 / 13.05 / 11.87
       muted 4.76 / 4.55 / 4.34            4.70 /  4.88 /  4.44  (revoked) */
  .gly{display:inline-flex; align-items:center; justify-content:center; flex:0 0 auto; width:16px; height:16px; line-height:0; color:var(--muted);}
  .gly svg{display:block; width:100%; height:100%;}
  .gly.crit{color:var(--crit-ink);}
  .gly.warn{color:var(--warn-ink);}
  .gly.info{color:var(--info-ink);}
  .gly.ok, .gly.good{color:var(--ok-ink);}
  .gly.unknown, .gly.gray{color:var(--unk-ink);}
  .gly.neutral{color:var(--neu-ink);}
  .gly.revoked{color:var(--muted);}
  /* In a badge the glyph rides at the cap height of a 12.5px label. */
  .badge .gly{width:13px; height:13px; margin-left:-1px;}
  /* Group headers and legends set their own ink on the row; the glyph follows
     the severity, not the row. */
  tr.grp .gly, .pulse-legend .gly, .ctile .lbl .gly{width:14px; height:14px;}
  /* The top-bar connector pill is the one status mark on EVERY screen, so it
     is the one that most needed a shape rather than a hue. */
  .pill .gly{width:12px; height:12px;}
  /* The remaining contexts, sized to the label they sit beside rather than to a
     single global number -- a 16px mark next to 11.5px preflight copy reads as an
     icon, not as that line's status. All of them are declared HERE rather than in
     the page stylesheets so the glyph's sizing stays readable in one place.
       .pre-pill        11.5px  (set-up-a-store preflight)
       .filter-toggle   12.5px  (setups filter bar)
       .il .v           13px    (setup-progress + app-detail info lists)
       .status-panel    13px    (connect-shopify wizard)
       .status-line     14px    (app-detail header)
     The .il .v and .status-line rules also replace a hard-coded
     style="width:6px;height:6px" that used to shrink the dot inline: a 6px box
     would have crushed the SVG into an unreadable smudge, which is precisely the
     failure mode a shape-encoded mark cannot afford. */
  .pre-pill .gly{width:12px; height:12px;}
  .filter-toggle .gly{width:13px; height:13px;}
  .il .v .gly, .status-panel .gly{width:13px; height:13px;}
  .status-line .gly{width:14px; height:14px;}

  /* --- 19.3 the 3px left severity stripe (DESIGN.md 9.4) -------------------
     Two mechanical carriers only, both already present above: a ::before for
     block elements, border-left for table rows. This completes the severity
     set on both -- crit/warn/unknown existed piecemeal, info did not exist at
     all, and the stat tile and cockpit tile each carried only part of it.

     ok deliberately gets NO stripe, on either carrier. The stripe is the
     item-level URGENCY carrier; putting one on every flowing store would make
     the mark mean "row" instead of "act on this", which is how a severity
     signal dies. The one exception is store-detail's single .health-card,
     whose whole subject IS the verdict -- see store-detail-css.ts. */
  .stat.sev-warn{border-color:color-mix(in srgb, var(--warn-dot) 32%, var(--hair));}
  .stat.sev-warn::before{content:""; position:absolute; left:-1px; top:14px; bottom:14px; width:3px; border-radius:0 3px 3px 0; background:var(--warn-dot);}
  .stat.sev-info{border-color:color-mix(in srgb, var(--info-dot) 32%, var(--hair));}
  .stat.sev-info::before{content:""; position:absolute; left:-1px; top:14px; bottom:14px; width:3px; border-radius:0 3px 3px 0; background:var(--info-dot);}
  .attn-item.sev-info::before{background:var(--info-dot);}
  tr.sev-info td:first-child{border-left:3px solid var(--info-dot); padding-left:17px;}
  /* The cockpit tiles had a stripe on crit only, so a fleet whose worst state
     was "unknown" or "stale" rendered as a calm grid of equal tiles. */
  .ctile.warn{border-color:color-mix(in srgb, var(--warn-dot) 32%, var(--hair));}
  .ctile.warn::before{content:""; position:absolute; left:-1px; top:12px; bottom:12px; width:3px; border-radius:0 3px 3px 0; background:var(--warn-dot);}
  .ctile.unknown{border-color:color-mix(in srgb, var(--unk-dot) 32%, var(--hair));}
  .ctile.unknown::before{content:""; position:absolute; left:-1px; top:12px; bottom:12px; width:3px; border-radius:0 3px 3px 0; background:var(--unk-dot);}
  .ctile.info{border-color:color-mix(in srgb, var(--info-dot) 32%, var(--hair));}
  .ctile.info::before{content:""; position:absolute; left:-1px; top:12px; bottom:12px; width:3px; border-radius:0 3px 3px 0; background:var(--info-dot);}

  /* --- 19.4 the credential ladder column (DESIGN.md 8, 9, 12) --------------
     Six states -- fresh / aging / expiring / expired / revoked / never shown --
     resolved by domain/connector-ports.ts's credentialLadderState, so the fleet
     tables and the Connections screen can never assert different truths about
     one credential. revoked is muted and struck by .badge.revoked (section 8),
     never red, and never the same style as expired.

     Read the TODO on CREDENTIAL_NOT_OBSERVED in views/labels.ts before wiring
     real values in: no per-store credential state reaches these read models
     today, so every cell is hatched and reports "never shown". */
  .cred-col{width:168px;}
  td.cred-col{white-space:nowrap;}
  @media (max-width:640px){.cred-col{width:auto;}}
