/* Vendored locally (fonts/), not loaded from Google Fonts at runtime —
   only the two weight ranges actually used ('Bricolage Grotesque' 400/500, Manrope
   300-800), Latin subset only. Re-fetch from Google Fonts' CSS API and
   re-run this same pair of curl commands if new weights/scripts are
   ever needed; see RELEASE.md. */
@font-face{font-family:'Bricolage Grotesque';font-style:normal;font-weight:200 800;font-display:swap;src:url('fonts/bricolage.a97804dc9f.woff2') format('woff2-variations')}
@font-face{font-family:'Manrope';font-style:normal;font-weight:300 800;font-display:swap;src:url('fonts/manrope.a30ddcd349.woff2') format('woff2')}
:root{
  /* Ground, card, raised — measured as CIE L* steps, not WCAG ratios.
     Contrast ratio is the wrong instrument down here: its +0.05 floor
     squashes every near-black pair into "about 1.1:1" whether they are
     visibly different planes or not. L* is what the eye is actually
     doing, and two adjacent large fields need roughly 5 L* before they
     read as separate planes. The old stack was #0B0B0C / #141416 /
     #1B1B1D — L* 3.0 / 6.4 / 9.8, so 3.3 and 3.4 apart. On a good
     monitor in a dark room that reads; on a laptop under office light
     the cards vanished and the 12%-white hairline was left doing all
     the work of saying "this is a card". Now 2.3 / 7.5 / 12.3 — steps
     of 5.3 and 4.7.
     Every text/background pair is re-measured against these three with
     lib.js's own contrastRatio() whenever they move.

     The card tier is now as light as this palette can make it. --gold
     (#BE4A1E) is the darkest token the app ever uses AS TEXT (card
     titles, the CHECKPOINT rail label, the brand mark's "365").

     STALE NUMBERS, now corrected — and the earlier correction here was
     itself unfair. This paragraph asserted 4.77:1 on the card and
     4.60:1 on the raised surface and built "a measured ceiling, not a
     guess" on them; a previous pass said those numbers "do not
     reproduce", which reads as an accusation that they were never
     measured. They reproduce exactly — for the colour this token USED
     to hold. #A9812E, the gold this app shipped until 1.64.0, measures
     4.77:1 on the card and 4.60:1 on the raised surface to the
     hundredth, verified with lib.js's own contrastRatio(). "Replace
     the gold accent with orange #BE4A1E" then moved the token, and the
     measurements written around it were never re-derived.

     Against the value --gold actually holds now the same function
     gives 3.41:1 on the card, 3.28:1 on raised and 4.03:1 on the page
     ground — about a point lower, and under the 4.5:1 AA minimum for
     normal-size text. So the ceiling those numbers described was real
     when it was written and is not real now: nothing pins the surface
     tiers to a gold-on-card contrast limit today, and anything
     reasoning from it (how light the card may go, why the raised tier
     sits only 1.6 L* above it) should be re-derived rather than
     inherited.

     The lesson is the one this file keeps paying for: a measured
     number is only true of the value it was measured against, and a
     token swap silently invalidates every figure written near it.
     Re-measure when a colour moves — and before calling a predecessor's
     number wrong, check what it was measured against.

     --gold as TEXT is no longer used anywhere: the accent ramp below
     resolves that role to --accent-ink (#F0A97F on dark, 8.69:1;
     #A63A12 on light, 6.07:1) and leaves --gold to fills, where white
     measures 5.01:1 on it.

     The card tier is still the lightest of the three deliberately,
     because the card is what a practitioner looks at all day. The
     raised tier sits close to it and separates by --elev-3 rather than
     by lightness, which is what actually reads as "floating free of
     the page" for something that overlaps other content anyway. */
  --ink:#06060A; --ink-2:#1C1B22; --charcoal:#201E26;
  /* Bare "R,G,B" triplets alongside the solid hex above — lets any
     rule that needs a translucent wash of paper/ink write
     rgba(var(--paper-rgb),X) instead of re-hardcoding the literal
     triplet (this file used to have 20+ standalone rgba(250,247,241,…)
     occurrences that silently stayed dark-theme-only; every one below
     now resolves through these two, so the light-theme override block
     flips them all at once). */
  --paper-rgb:250,247,241; --ink-rgb:6,6,10;
  /* --paper-faint was rgba(...,.38) — 3.37:1 against --ink, below the
     4.5:1 WCAG 2.1 AA minimum for body text. .5 measures 5.06:1 (verified
     via the standard relative-luminance formula against every dark
     background token this app uses); --paper-dim (.62, 7.3:1) was
     already comfortably passing and is unchanged. */
  --paper:#FAF7F1; --paper-dim:rgba(var(--paper-rgb),.62); --paper-faint:rgba(var(--paper-rgb),.5);
  --gold:#BE4A1E; --gold-light:#F0A97F; --gold-rgb:190,74,30; --gold-light-rgb:240,169,127;
  /* Forced-dark scope: same dark-ground end of the ramp as :root. */
  --accent-ink:#F0A97F; --accent-fill:var(--gold); --accent-fill-hover:#A63A12; --on-accent:#FFFFFF;
  /* ===== The accent ramp, ported from the website's tokens.css =====
     Three steps, not one colour, and which step you use depends on
     whether the accent is a FILL or a GLYPH:

       #A63A12  dark  — hover fills, and the accent as text on LIGHT
       #BE4A1E  mid   — the brand fill
       #F0A97F  pale  — the accent as text on DARK

     A saturated brand colour is a fine fill (a rule, a chip, a button
     face). It is usually not a text colour. This app had been using
     --gold (#BE4A1E) for both, and as text it measures 3.41:1 on the
     card and 3.28:1 on the raised surface — under the 4.5:1 AA floor,
     on every card heading, section eyebrow, .check-area-name, .tagline
     and .settings-subhead in the product. (The comment at the top of
     this file used to claim 4.77:1 and 4.60:1; those do not reproduce.)

     --accent-ink is ground-aware: it means "the accent, as text,
     legible against whatever this sits on", and each theme block below
     resolves it to the right end of the ramp. So a rule written once
     stays correct when the surface under it flips.

     --on-accent is text sitting ON the accent fill, and it INVERTS the
     old answer: buttons and active pills used --ink (near-black), which
     measures 4.03:1 on #BE4A1E. White measures 5.01:1. Same value in
     both themes, because the fill is the same in both.

     --accent-fill-hover replaces a hover that went PALER. .btn:hover
     moved to --gold-light (#F0A97F), which is fine under near-black
     text and unreadable under white (about 1.75:1) — so the hover has
     to travel the other way along the ramp. White on #A63A12 is
     9.53:1, and a darker press state is what the website does too.
     Measured with lib.js's own contrastRatio(), same as every other
     number in this file. */
  --accent-ink:#F0A97F;
  --accent-fill:var(--gold);
  --accent-fill-hover:#A63A12;
  --on-accent:#FFFFFF;
  /* ===== The brand mark's dot is NOT this accent, on purpose =====
     The gold circle closing the "C" in the Compliance 365 mark
     (index.html's three lockups, onboarding.html, report.js's report
     masthead, and the canvas favicon app.js draws) is #A9812E and
     stays that way. It is the one place the old gold survives, and it
     survives deliberately: the identity is two colours — a gold mark
     beside an orange interface — not one colour that half-migrated.

     Written down because it looks exactly like an oversight and is
     not. "Replace the gold accent with orange #BE4A1E" rewrote those
     very SVG lines — it changed their stroke and left the dot alone —
     so keeping it was a decision then, and has been re-confirmed
     since. Do not "finish the migration": the dot appears 23 times
     across this app, the owner console, the marketing page and the
     public website (its header, footer, homepage, the homepage's 3D
     scene, favicon.svg and logo.svg). They move together or not at
     all, and the answer is not at all.

     No contrast floor applies to it: a dot is a fill, never text. */
  --line:rgba(255,255,255,.12);
  /* Status/severity hues — pass/warn/fail plus a fourth, --critical,
     distinct from --fail (High vs. Critical residual risk read as two
     different reds everywhere else in this app; this token used to be
     a bare hex-only-in-one-place value, never reusable). All four are
     verified ≥4.5:1 against --ink below (dark theme unchanged from
     before this token pass — these were already the shipped values);
     the light-theme block darkens each one to clear 4.5:1 against
     --ink's new near-white value, since a color tuned to sit on near-
     black fails badly on near-white (see that block's own comment). */
  --pass:#8fbf9a; --warn:#F0A97F; --fail:#c97a7a; --critical:#e0908f;

  /* Canonical easing — every transition in this file uses this curve
     (previously hand-typed 3 separate times, drifting-risk waiting to
     happen). A gentle deceleration, not a bounce. */
  --ease:cubic-bezier(.22,1,.36,1);

  /* Elevation — three flat layers, darkest to lightest, used instead of
     shadows (this UI has almost none) to read as "closer to the user":
     surface = the default card/panel background, surface-raised = a
     layer ABOVE that (inputs, popovers, toasts — things that sit on a
     card), surface-hover = a translucent wash applied OVER whichever
     surface a hovered/active row or nav item already has, so it works
     regardless of what's underneath. Aliases onto the existing --ink-2/
     --charcoal tokens rather than replacing them, so nothing already
     referencing those two breaks. */
  --surface:var(--ink-2);
  --surface-raised:var(--charcoal);
  --surface-hover:rgba(var(--paper-rgb),.05);

  /* ===== Elevation, as light rather than as a drop shadow =====
     A black drop shadow on a near-black ground has almost nothing left
     to darken, which is why "just add a box-shadow" does nothing here
     and why this file went without one for so long. What actually
     reads as an object lying on the page is three things at once, so
     they ship as one token:
       1. the surface being measurably lighter than the ground (above),
       2. --edge: a 1px light-catch along the top and a 1px shade along
          the bottom — the bevel of a physical panel picking up the
          same overhead light everything else on the page is lit by,
       3. a wide, soft ambient shadow that darkens the ground
          immediately around the card, which deepens (2) by contrast.
     --elev-1 resting card · --elev-2 lifted (hover, hero, the
     Automation Centre) · --elev-3 floating free of the page (drawer,
     modal, command palette, toast). --edge is exported on its own for
     surfaces that want the bevel without the shadow.
     The light theme redefines all of these: there the ground is the
     LIGHTEST thing on screen, so the shadow carries the depth and the
     bevel is nearly nothing. Never hard-code a shadow below — a
     literal rgba(0,0,0,…) is either invisible in one theme or crude in
     the other, which is the exact bug these tokens exist to prevent.
     Each level is three shadows, not one: a tight contact shadow that
     pins the edge to the page, a mid shadow that gives it thickness,
     and a wide ambient one. A single blurred shadow reads as a sticker;
     the three together read as an object with a height. */
  --edge:inset 0 1px 0 rgba(255,255,255,.09),inset 0 -1px 0 rgba(0,0,0,.45);
  --elev-1:var(--edge),0 1px 2px rgba(0,0,0,.55),0 4px 10px rgba(0,0,0,.42),0 14px 34px rgba(0,0,0,.42);
  --elev-2:var(--edge),0 2px 4px rgba(0,0,0,.6),0 8px 18px rgba(0,0,0,.5),0 26px 60px rgba(0,0,0,.52);
  --elev-3:var(--edge),0 4px 10px rgba(0,0,0,.6),0 14px 30px rgba(0,0,0,.55),0 40px 90px rgba(0,0,0,.65);
  /* Ambient accent — one soft gold wash from the top-left corner, as
     if the panel were lit by the same lamp as the brand mark. Used on
     the two surfaces that lead a view (the Dashboard's posture hero
     and the Automation Centre) and nowhere else: it stops meaning
     "this is the one to read first" the moment a third thing has it. */
  --glow:radial-gradient(ellipse 80% 130% at 0% 0%,rgba(var(--gold-rgb),.2),transparent 60%);

  /* Corner radius, as three tiers rather than the six ad hoc values
     this file had grown (3/4/6/8px, chosen per-rule). The step between
     tiers is what makes nesting legible — a 6px control inside a 10px
     card inside a 14px panel — and 6px on a full-width card, the old
     default, is the single most dating detail in a dark UI.
     sm = controls (buttons, inputs, chips), md = cards and tiles,
     lg = panels that float above the page. */
  --r-sm:8px; --r-md:14px; --r-lg:20px;
  /* Height of the sticky topbar, as a token because two other sticky
     things now have to sit below it and a hard-coded 71px in three
     places is three chances to drift. It is 16px padding top and
     bottom + a 1px bottom border + the 38px tall control row inside —
     measured, and asserted in test/styles-tokens.test.mjs so a change
     to .top's padding that silently slides every sticky header under it
     fails a test rather than shipping. */
  --topbar-h:71px;
  /* A sticky <thead> sits directly under the topbar; the Statement of
     Applicability's family headers then sit under BOTH. Kept as a
     derived token so the family offset can never disagree with the
     header height it is supposed to clear.
     40px, MEASURED in the browser (th is padding:12px top and bottom
     around an 11px/--fs-1 line box, which lays out at 16px), not the
     38px this started as — inherited from the offset of the sticky
     thead that never actually stuck, so nothing had ever rendered it.
     Two pixels is enough: at 38px the family header sat under the
     column names it is supposed to clear, which reads as a rendering
     glitch rather than an off-by-two. */
  --thead-h:40px;
  /* Height of a bulk-action bar sticking above a table, published by
     renderSoaBulkBar() onto the view element when one is showing so the
     sticky <thead> and family headers can offset by it. Declared here
     with the no-bar default rather than relying on a var() fallback:
     an inline-only custom property is exactly what
     styles-tokens.test.mjs exists to catch, and 0px is also the right
     answer for every table that never has a bar. */
  --bulk-h:0px;

  /* Strict type scale — six steps, everything in the stylesheet below
     now resolves to one of these (nearest-neighbour, consolidating what
     used to be 24+ near-duplicate ad hoc sizes like 12.5/13.5/11.5px).
     Two documented, deliberate exceptions sit OUTSIDE this scale rather
     than being forced into it, because collapsing them would visibly
     change the app's identity, not just tidy it up:
       --fs-eyebrow (9.5px) — the small-caps tracked-out label tier
       (chips, table headers, section eyebrows) is already its own
       consistent micro-type system; folding it up to 11px would make
       every badge/label noticeably larger.
       --fs-hero (42px) — the "big scannable number" tier (the
       posture-scan gauge, the Board view's hero tiles, and the
       Dashboard's own hero score) was 42px/46px;
       unified to one shared hero size rather than force-fit to the
       scale's 34px ceiling, which would shrink Board view's whole
       reason for being (a number you can read from across the room). */
  --fs-1:11px; --fs-2:13px; --fs-3:15px; --fs-4:20px; --fs-5:26px; --fs-6:34px;
  --fs-eyebrow:9.5px; --fs-hero:42px;
}
/* Light "paper" theme — a Settings toggle and a "Toggle light theme"
   command palette entry both flip this attribute on <html> (see
   App.toggleLightTheme in app.js; persisted via Store.setSetting the
   same way every other per-tenant setting is, demo mode included —
   there's no separate "in-memory only" code path because demo mode's
   own Store.setSetting already only ever writes to this browser, never
   a real Settings list). Same gold accent, same structure, just the
   ink/paper relationship inverted.

   --pass/--warn/--fail/--critical/--gold/--gold-light are NOT simply
   reused at their dark-theme values here: each one was chosen to sit
   on near-black ink, and measured against #FAF7F1 they land between
   1.75:1 and 3.35:1 — all fail the 4.5:1 WCAG AA minimum for text
   (verified with this file's own contrastRatio() math in lib.js, same
   formula the paper-faint comment above already used). Every value
   below is a darkened/desaturated variant of the SAME hue, re-tuned
   to clear 4.5:1 against #FAF7F1 (each verified ≥4.53:1) — recognizably
   the same green/gold/red family, just legible as light-theme text
   instead of only as a dark-theme accent. */
:root[data-theme="light"]{
  /* --charcoal (--surface-raised) was #F1EDE3 — DARKER than the card
     it is supposed to float above, which is backwards, and dark enough
     that every status hue measured 4.14-4.18:1 on it, under the 4.5:1
     AA floor. (The light-theme hues below were tuned against --ink,
     the page ground, and never re-checked against this surface.) It
     cannot go lighter than the card, since the card is already white,
     so it matches the card and separates by --elev-3 alone — the same
     rule the dark theme's raised tier follows, for the same reason:
     a surface that overlaps other content is read as floating by its
     shadow, not by its lightness. Worst pair on it is now 4.84:1. */
  --ink:#FAF7F1; --ink-2:#FFFFFF; --charcoal:#FFFFFF;
  --paper-rgb:11,11,12; --ink-rgb:250,247,241;
  /* .52 (this token's dark-theme alpha) measures only 3.88:1 against
     #FAF7F1 — .6 clears 4.5:1 (verified: 5.09:1). --paper-dim's .68
     alpha was already comfortably passing (6.8:1) and is unchanged. */
  --paper:#0B0B0C; --paper-dim:rgba(var(--paper-rgb),.68); --paper-faint:rgba(var(--paper-rgb),.6);
  --gold:#A63A12; --gold-light:#8A2F0E; --gold-rgb:166,58,18; --gold-light-rgb:138,47,14;
  /* Light ground takes the DARK end of the ramp as text — #A63A12 is
     6.07:1 on cream, 6.49:1 on white, 5.91:1 on the panel ground (the
     website's own measured figures, same token). The fill and its
     on-colour do not change with the theme: #BE4A1E under white is
     5.01:1 on either ground. */
  --accent-ink:#A63A12;
  --accent-fill:#BE4A1E;
  --accent-fill-hover:#8A2F0E;
  --on-accent:#FFFFFF;
  --pass:#477d54; --warn:#8c6d28; --fail:#b85050; --critical:#ca413f;
  --line:rgba(11,11,12,.14);
  --surface-hover:rgba(var(--paper-rgb),.045);
  /* Inverted for a light ground: the bevel highlight is near-invisible
     on white so it drops to a hairline, and the ambient shadow — which
     had almost nothing to bite on in the dark theme — becomes the thing
     carrying the depth. The alphas are far lower than the dark theme's:
     the same black that reads as a soft halo on #06060A reads as
     dirt on #FAF7F1. */
  --edge:inset 0 1px 0 rgba(255,255,255,1),inset 0 -1px 0 rgba(11,11,12,.07);
  --elev-1:var(--edge),0 1px 2px rgba(11,11,12,.07),0 4px 10px rgba(11,11,12,.06),0 12px 28px rgba(11,11,12,.07);
  --elev-2:var(--edge),0 2px 4px rgba(11,11,12,.08),0 10px 22px rgba(11,11,12,.1),0 24px 50px rgba(11,11,12,.1);
  --elev-3:var(--edge),0 6px 14px rgba(11,11,12,.1),0 20px 44px rgba(11,11,12,.14),0 44px 90px rgba(11,11,12,.18);
  --glow:radial-gradient(ellipse 80% 130% at 0% 0%,rgba(var(--gold-rgb),.13),transparent 60%);
  /* The light theme's GROUND cannot move the way the dark theme's card
     did, so all of its extra depth is in the shadows above. Measured
     against #FAF7F1, --gold/--pass/--fail land at 4.56/4.54/4.57:1 —
     within 0.07 of the AA floor. Darkening the paper even to #F5F1E7
     drops all three to ~4.3:1 and fails. */
}
*{margin:0;padding:0;box-sizing:border-box}
body{background:var(--ink);color:var(--paper);font-family:Manrope,sans-serif;-webkit-font-smoothing:antialiased;font-size:var(--fs-2)}
button{font-family:Manrope;cursor:pointer}
/* Tabular figures wherever a number needs to hold still and stay
   comparable to the one above/below it — KPI values, table cells
   (scores/dates/counts), chip badges, trend deltas, key-value pairs in
   drawers. Never applied to prose. */
.kpi b,.kpi .trend,.board-tile b,.gauge-num,.score-box b,td,.chip,.trend,.spark-caption .gold-t,.wiz-gap-row,.d-kv b,.roadmap-phase b{font-variant-numeric:tabular-nums}

/* Themed scrollbars — thin, charcoal track, gold-tinted thumb, same
   idea in both engines. Global rather than per-scrollable-element:
   every scroll surface in this app (nav, drawer, table overflow,
   feed, gsearch results) should read as part of the same system. */
*{scrollbar-width:thin;scrollbar-color:rgba(190, 74, 30,.5) var(--ink-2)}
::-webkit-scrollbar{width:10px;height:10px}
::-webkit-scrollbar-track{background:var(--ink-2)}
::-webkit-scrollbar-thumb{background:rgba(190, 74, 30,.45);border-radius:99px;border:2px solid var(--ink-2)}
::-webkit-scrollbar-thumb:hover{background:rgba(190, 74, 30,.7)}

/* Skeleton shimmer — async list placeholders (Documents,
   posture-scan checks) swap their text "Loading…" state
   for this instead. A moving gradient band, not a pulse — reads as
   "content is arriving" rather than "something is wrong". */
@keyframes shimmer{0%{background-position:-135% 0}100%{background-position:135% 0}}
.skeleton{background:linear-gradient(90deg,rgba(var(--paper-rgb),.04) 25%,rgba(var(--paper-rgb),.1) 37%,rgba(var(--paper-rgb),.04) 63%);background-size:400% 100%;animation:shimmer 1.6s ease-in-out infinite;border-radius:4px;color:transparent!important;pointer-events:none}
.skeleton-row td{padding-top:15px;padding-bottom:15px}

/* Staggered row reveal — JS adds .row-reveal on render, then .show one
   frame later with a per-row transition-delay (see revealRows() in
   app.js); a fresh page has nothing to stagger FROM, so this is a
   subtle entrance, not a layout shift. */
.row-reveal{opacity:0;transform:translateY(4px);transition:opacity .3s var(--ease),transform .3s var(--ease)}
.row-reveal.show{opacity:1;transform:translateY(0)}

/* Kills every animation/transition app-wide for users who've asked for
   less motion — count-up (app.js checks this directly, since a rAF
   loop isn't something CSS alone can stop), row stagger, skeleton
   shimmer, drawer/toast/modal slides, the busy-spinner, all of it.
   Deliberately a blanket rule at the very top of the cascade rather
   than threading a guard through every individual rule above. */
@media (prefers-reduced-motion: reduce){
  *,*::before,*::after{animation-duration:.001ms!important;animation-iteration-count:1!important;transition-duration:.001ms!important;scroll-behavior:auto!important}
}
/* One consistent, visible keyboard-focus treatment for every native
   interactive element in the app (buttons, links, inputs, selects,
   textareas) — the browser default outline is inconsistent against
   this dark, mostly border-less UI (many buttons/toggles/pills have
   border:none). :focus-visible only shows for keyboard/programmatic
   focus, never a mouse click, so this never adds a ring a mouse user
   didn't ask for. Components that already define their own visible
   focus treatment (the global search input's gold border, the version
   tag/control-id link's underline) are left as they are — this is a
   fallback for everything else, not a replacement for a better
   component-specific treatment. */
a:focus-visible,button:focus-visible,input:focus-visible,select:focus-visible,textarea:focus-visible,[tabindex]:focus-visible{
  outline:2px solid var(--gold-light);outline-offset:2px;border-radius:2px;
}
/* The drawer/modal/wizard-step containers themselves get programmatic
   focus() on open/step-change purely so a screen reader announces the
   new content and Tab starts from a sensible place — they already have
   their own strong visual chrome (borders, the .open transition), so a
   ring around the entire panel would just be visual noise, not a
   useful cue. Every focusable control inside them still gets the
   normal ring above. */
.drawer:focus-visible,.modal-box:focus-visible,.wizard-step:focus-visible{outline:none}
/* Visible only while focused — the standard treatment, and the reason
   it can be the first thing in the tab order without costing a
   sighted mouse user anything. Positioned over the topbar rather than
   pushing layout, so nothing reflows when it appears.
   .main takes tabindex="-1" so the jump actually moves focus there;
   without it the browser scrolls but focus stays on the link, and the
   next Tab goes straight back into the nav the user just skipped. */
.skip-link{position:fixed;top:8px;left:8px;z-index:100;transform:translateY(-150%);background:var(--accent-fill);color:var(--on-accent);font-weight:700;font-size:var(--fs-1);letter-spacing:.06em;text-transform:uppercase;padding:10px 18px;border-radius:var(--r-sm);text-decoration:none;box-shadow:var(--elev-3);transition:transform .18s var(--ease)}
.skip-link:focus-visible,.skip-link:focus{transform:translateY(0)}
/* The landmark itself must not draw a focus ring when jumped to — it is
   a scroll target, not a control, and a ring around the whole content
   column is noise. The skip link's own ring already showed the user
   where they were. */
.main:focus,.main:focus-visible{outline:none}
.app{display:none;grid-template-columns:236px 1fr;min-height:100vh}

/* ===== sign-in gate ===== */
.gate{display:none;min-height:100vh;flex-direction:column;align-items:center;justify-content:center;text-align:center;padding:32px}
.gate .lockup{display:flex;align-items:center;gap:14px;margin-bottom:10px}
.gate h1{font-family:'Bricolage Grotesque', 'Helvetica Neue', Arial, sans-serif;font-weight:500;font-size:var(--fs-6);margin-top:22px}
.gate p{color:var(--paper-dim);max-width:52ch;line-height:1.7;margin-top:12px;font-size:var(--fs-2)}
.gate .btns{display:flex;gap:12px;margin-top:30px;flex-wrap:wrap;justify-content:center}
.gate .note{margin-top:26px;font-size:var(--fs-1);color:var(--paper-faint);max-width:56ch;line-height:1.7}
.gate .resid{margin-top:34px;padding-top:20px;border-top:1px solid var(--line);font-size:var(--fs-1);color:var(--paper-faint);letter-spacing:.04em;max-width:46ch;line-height:1.7}
.gate .resid b{color:var(--paper-dim);display:block;letter-spacing:.14em;text-transform:uppercase;font-size:var(--fs-eyebrow);margin-bottom:4px}

/* ===== busy overlay ===== */
.busy{display:none;position:fixed;inset:0;background:rgba(var(--ink-rgb),.82);z-index:80;align-items:center;justify-content:center;flex-direction:column;gap:18px}
.busy .ring{width:52px;height:52px;border-radius:50%;border:3px solid rgba(var(--paper-rgb),.15);border-top-color:var(--gold);animation:spin 1s linear infinite}
@keyframes spin{to{transform:rotate(360deg)}}
.busy span{font-size:var(--fs-1);letter-spacing:.14em;text-transform:uppercase;color:var(--paper-dim)}

/* ===== onboarding wizard ===== */
.wizard{display:none;min-height:100vh;align-items:flex-start;justify-content:center;padding:56px 24px 80px}
.wizard-inner{max-width:640px;width:100%}
.wizard-step{display:none}
.wizard-step.on{display:block}
.wizard-progress{display:flex;gap:8px;justify-content:center;margin-bottom:36px}
.wizard-dot{width:7px;height:7px;border-radius:50%;background:var(--line);transition:background .2s var(--ease),width .2s var(--ease)}
.wizard-dot.on{background:var(--gold);width:20px;border-radius:4px}
.wizard-dot.done{background:var(--gold-light)}
.wizard-hero{text-align:center}
.wizard-hero h1{font-family:'Bricolage Grotesque', 'Helvetica Neue', Arial, sans-serif;font-weight:500;font-size:var(--fs-5);margin-top:18px}
.wizard-hero p{color:var(--paper-dim);max-width:52ch;margin:14px auto 0;line-height:1.7;font-size:var(--fs-2)}
.wiz-perm-row{display:flex;gap:16px;padding:12px 0;border-bottom:1px solid var(--line);align-items:baseline}
.wiz-perm-row:last-child{border-bottom:none}
.wiz-perm-row .wiz-perm-name{font-weight:700;font-size:var(--fs-1);color:var(--gold-light);white-space:nowrap;letter-spacing:.02em}
.wiz-perm-row .wiz-perm-why{color:var(--paper-dim);font-size:var(--fs-2);line-height:1.55}
.wiz-cap-row{display:flex;align-items:center;justify-content:space-between;gap:10px;padding:11px 0;border-bottom:1px solid var(--line);font-size:var(--fs-2)}
.wiz-cap-row:last-child{border-bottom:none}
.wiz-cap-row .wiz-cap-label{color:var(--paper)}
.wiz-cap-row .wiz-cap-note{color:var(--paper-faint);font-size:var(--fs-1);margin-top:2px}
.wiz-fw-row{display:flex;align-items:center;justify-content:space-between;gap:16px;padding:19px 22px}
.wiz-fw-row b{display:block;font-size:var(--fs-2)}
.wiz-fw-row p{color:var(--paper-dim);font-size:var(--fs-2);margin-top:4px;line-height:1.5}
.wiz-gap-row{display:flex;justify-content:space-between;gap:12px;padding:9px 0;border-bottom:1px solid var(--line);font-size:var(--fs-2)}
.wiz-gap-row:last-child{border-bottom:none}
.wiz-gap-row .wiz-gap-title{color:var(--paper)}

/* ===== sidebar ===== */
.side{background:var(--ink-2);border-right:1px solid var(--line);padding:22px 0;display:flex;flex-direction:column;position:sticky;top:0;height:100vh}
.side .lockup{display:flex;align-items:center;gap:10px;padding:0 22px 20px;border-bottom:1px solid var(--line)}
/* Scrolls independently so a tall nav-item list never pushes .foot/
   .version-tag below the viewport with no way to reach them —
   min-height:0 overrides flex's default min-height:auto, which is
   what let this overflow past .side's fixed 100vh instead of scrolling. */
.nav-scroll{flex:1;min-height:0;overflow-y:auto}
.word{display:flex;align-items:baseline;line-height:1;font-size:var(--fs-2)}
.word .c1{font-weight:300;letter-spacing:.13em;color:var(--paper)}
.word .c2{font-weight:800;color:var(--accent-ink);margin-left:.05em}
.prod{padding:14px 22px 6px;font-size:var(--fs-1);letter-spacing:.24em;text-transform:uppercase;color:var(--accent-ink);font-weight:700}
.nav-item{display:flex;align-items:center;justify-content:space-between;width:100%;background:none;border:none;color:var(--paper-dim);font-size:var(--fs-2);padding:11px 22px;text-align:left;transition:color .15s var(--ease),background .15s var(--ease);border-left:3px solid transparent;cursor:pointer}
.nav-item:hover{color:var(--paper);background:var(--surface-hover)}
.nav-item:active{background:rgba(var(--paper-rgb),.08)}
.nav-item:disabled,.nav-item[disabled]{opacity:.4;cursor:not-allowed;pointer-events:none}
.nav-item.on{color:var(--paper);background:var(--surface-hover);border-left-color:var(--gold)}
.nav-item .n{background:rgba(190, 74, 30,.18);color:var(--gold-light);font-size:var(--fs-1);font-weight:800;border-radius:99px;padding:2px 8px}
.nav-group{margin-top:16px;padding-top:14px;border-top:1px solid var(--line)}
.nav-group:first-child{margin-top:2px;padding-top:0;border-top:none}
.nav-label{padding:0 22px 6px;font-size:var(--fs-eyebrow);letter-spacing:.16em;text-transform:uppercase;color:var(--paper-faint);font-weight:700}
summary.nav-label{cursor:pointer;list-style:none;user-select:none}
summary.nav-label::-webkit-details-marker{display:none}
summary.nav-label::before{content:'▸';display:inline-block;width:9px;margin-right:5px;color:var(--paper-faint);transition:transform .15s var(--ease)}
details[open]>summary.nav-label::before{transform:rotate(90deg)}
summary.nav-label:hover{color:var(--paper-dim)}
/* A collapsed group with something in it worth seeing (an overdue
   badge on one of its hidden nav-items) gets a small dot on its own
   summary — not the aggregated count itself (badge semantics differ
   too much across items to sum meaningfully), just "expand me". Only
   shown while collapsed; once open, the real badges are visible and
   this would be redundant. */
details.nav-group:not([open])>summary.nav-label.has-alert::after{content:'';display:inline-block;width:6px;height:6px;border-radius:50%;background:var(--gold-light);margin-left:6px;vertical-align:middle}
.side .foot{margin-top:auto;padding:16px 22px 0;border-top:1px solid var(--line);font-size:var(--fs-1);color:var(--paper-faint);line-height:1.7}
.side .foot b{color:var(--paper-dim);display:block;letter-spacing:.1em;text-transform:uppercase;font-size:var(--fs-eyebrow)}
.version-tag{margin:10px 22px 16px;background:none;border:none;padding:0;text-align:left;font-family:Manrope,sans-serif;font-size:var(--fs-1);letter-spacing:.06em;color:var(--paper-faint);cursor:pointer;text-decoration:underline;text-decoration-color:transparent;transition:color .15s var(--ease),text-decoration-color .15s var(--ease)}
.version-tag:hover,.version-tag:focus-visible{color:var(--gold-light);text-decoration-color:currentColor}
.version-tag:active{color:var(--accent-ink)}

/* ===== topbar / main ===== */
.main{min-width:0}
/* Sticky, so content scrolls THROUGH it — at .96 opacity that read as
   a solid slab that content vanished behind. Where the browser can
   blur what's behind it, the bar goes properly translucent and the
   page stays visible underneath; the @supports guard keeps the opaque
   fallback for engines that can't, where .72 alpha alone would leave
   text scrolling legibly through the toolbar. */
.top{display:flex;align-items:center;justify-content:space-between;gap:16px;padding:16px 30px;border-bottom:1px solid var(--line);position:sticky;top:0;background:rgba(var(--ink-rgb),.96);z-index:20}
@supports ((backdrop-filter:blur(1px)) or (-webkit-backdrop-filter:blur(1px))){
  .top{background:rgba(var(--ink-rgb),.72);backdrop-filter:blur(16px) saturate(150%);-webkit-backdrop-filter:blur(16px) saturate(150%)}
}
.client{display:flex;align-items:center;gap:10px;font-size:var(--fs-2);color:var(--paper-dim);min-width:0}
.client .cname{color:var(--paper);font-weight:700;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
.top .right{display:flex;gap:10px;align-items:center}
.gsearch{position:relative;flex:1;max-width:340px;margin:0 18px}
.gsearch input{width:100%;background:rgba(var(--paper-rgb),.05);border:1px solid var(--line);color:var(--paper);font-size:var(--fs-2);padding:9px 12px;border-radius:var(--r-sm);font-family:Manrope;transition:border-color .15s var(--ease),background .15s var(--ease);cursor:pointer}
.gsearch input:hover{border-color:rgba(190, 74, 30,.5)}
.gsearch input:focus{outline:none;border-color:var(--gold);background:rgba(var(--paper-rgb),.07)}
/* Row styling shared by the command palette below — one visual
   language for "a searchable/selectable row", whether it's a record
   hit or a command. */
.gsearch-row{display:flex;align-items:center;gap:10px;padding:10px 14px;cursor:pointer;border-bottom:1px solid var(--line);transition:background .15s var(--ease)}
.gsearch-row:last-child{border-bottom:none}
.gsearch-row:hover,.gsearch-row.hi,.gsearch-row:focus-visible{background:var(--surface-hover)}
.gsearch-row .gs-type{font-size:var(--fs-eyebrow);letter-spacing:.1em;text-transform:uppercase;color:var(--gold-light);font-weight:700;white-space:nowrap}
.gsearch-row .gs-label{color:var(--paper-dim);font-size:var(--fs-2);overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
.gsearch-row .gs-label mark{background:none;color:var(--gold-light);font-weight:800;padding:0}
.gsearch-empty{padding:14px;font-size:var(--fs-2);color:var(--paper-faint)}

/* ===== command palette ===== */
.cmdk-overlay{display:none;position:fixed;inset:0;background:rgba(11,11,12,.75);z-index:90}
.cmdk-overlay.open{display:block}
.cmdk{display:none;position:fixed;top:14vh;left:50%;transform:translateX(-50%);width:min(600px,calc(100% - 40px));max-height:70vh;background:var(--surface);border:1px solid var(--line);border-radius:var(--r-lg);z-index:91;box-shadow:var(--elev-3);flex-direction:column;overflow:hidden}
.cmdk.open{display:flex}
.cmdk-inputwrap{border-bottom:1px solid var(--line);flex:none}
.cmdk-input{width:100%;background:none;border:none;color:var(--paper);font-family:Manrope;font-size:var(--fs-3);padding:18px 20px}
.cmdk-input:focus{outline:none}
.cmdk-results{overflow-y:auto;flex:1}
.cmdk-group-label{padding:12px 14px 6px;font-size:var(--fs-eyebrow);letter-spacing:.16em;text-transform:uppercase;color:var(--paper-faint);font-weight:700}
.cmdk-results .gsearch-row .gs-key{margin-left:auto;font-size:var(--fs-eyebrow);color:var(--paper-faint);white-space:nowrap}
.cmdk-hint{display:flex;gap:16px;padding:10px 16px;border-top:1px solid var(--line);font-size:var(--fs-eyebrow);color:var(--paper-faint);flex:none}
.cmdk-hint span{display:flex;align-items:center;gap:5px}
.cmdk-hint kbd{background:rgba(var(--paper-rgb),.08);border:1px solid var(--line);border-radius:3px;padding:1px 5px;font-family:Manrope;font-size:var(--fs-eyebrow)}

/* ===== Boardroom Mode — a full-screen, auto-cycling slide deck built
   from the Board view's own data, for presenting at a live QBR.
   Sidebar/topbar hidden; real Fullscreen API requested on entry, with
   this fixed-position, viewport-covering overlay as the "maximised
   window" fallback when fullscreen is denied/unsupported — either
   way the deck itself looks identical. The only way out is the exit
   button or Escape (wired in app.js), never a nav click since the
   nav itself is hidden. One visual per slide, pure ink background,
   zero chrome: no cards, no borders, nothing but the number/chart and
   its label.

   The deck re-declares every color token it uses at its own dark,
   fixed values — deliberately NOT inheriting whichever theme is
   currently active. A QBR presentation is meant to be legible on a
   dimmed meeting-room screen regardless of what the presenter happens
   to have their own console set to; light theme's darkened
   --pass/--warn/--fail/--gold (tuned for 4.5:1 on a near-white page —
   see :root[data-theme="light"]'s own comment) would be low-contrast
   and muddy against a dark backdrop, and the chart SVGs embedded in
   every slide (fingerprint/journey/riskLandscape/etc., all rendered
   with {palette:'app'}) read these SAME token names via var() — so
   redeclaring them here, scoped to .boardroom-deck, is what keeps
   Boardroom Mode's own palette self-consistent no matter which theme
   the rest of the app is in. */
.boardroom-mode .side,.boardroom-mode .top,.boardroom-mode .trial-banner{display:none!important}
.boardroom-exit{display:none;position:fixed;top:20px;right:20px;z-index:96}
.boardroom-mode .boardroom-exit{display:inline-flex}
.boardroom-deck{
  display:none;position:fixed;inset:0;z-index:90;
  --ink:#06060A; --paper:#FAF7F1; --paper-dim:rgba(250,247,241,.62); --paper-faint:rgba(250,247,241,.5);
  --paper-rgb:250,247,241; --ink-rgb:6,6,10;
  --gold:#BE4A1E; --gold-light:#F0A97F; --gold-rgb:190,74,30; --gold-light-rgb:240,169,127;
  --pass:#8fbf9a; --warn:#F0A97F; --fail:#c97a7a; --critical:#e0908f;
  --line:rgba(255,255,255,.12);
  background:var(--ink);color:var(--paper);
}
.boardroom-mode .boardroom-deck{display:block}
.boardroom-deck.bd-idle{cursor:none}
.bd-progress{position:fixed;top:0;left:0;right:0;height:3px;background:rgba(var(--paper-rgb),.08);z-index:2}
.bd-progress i{display:block;height:100%;width:0;background:var(--gold)}
.boardroom-deck.bd-run .bd-progress i{animation:bdProgress 12s linear forwards}
.boardroom-deck.bd-paused .bd-progress i{animation-play-state:paused}
@keyframes bdProgress{from{width:0%}to{width:100%}}
@media(prefers-reduced-motion:reduce){.bd-progress{display:none}}
.bd-slide{position:absolute;inset:0;display:flex;flex-direction:column;align-items:center;justify-content:center;text-align:center;gap:28px;padding:80px;box-sizing:border-box}
.bd-slide h2{font-family:'Bricolage Grotesque', 'Helvetica Neue', Arial, sans-serif;font-weight:500;font-size:30px;color:var(--paper-dim);letter-spacing:.01em;margin:0}
.bd-num{font-family:'Bricolage Grotesque', 'Helvetica Neue', Arial, sans-serif;font-weight:500;font-size:11vw;line-height:1;color:var(--paper);font-variant-numeric:tabular-nums}
.bd-num small{font-size:.32em;color:var(--paper-faint);font-weight:400;margin-left:10px}
.bd-sub{font-size:19px;color:var(--paper-dim);max-width:64ch;line-height:1.7}
.bd-sub .d-kv{font-size:19px;padding:10px 0}
.bd-sub .d-kv span{color:var(--paper-faint)}
.bd-sub .d-kv b{color:var(--paper)}
.bd-chart{width:100%;display:flex;justify-content:center}
.bd-chart svg{width:100%;height:auto}
.bd-nav{position:fixed;inset:0;display:flex;justify-content:space-between;align-items:center;pointer-events:none;padding:0 28px;z-index:2}
.bd-arrow{pointer-events:auto;background:none;border:none;color:rgba(var(--paper-rgb),.28);font-family:'Bricolage Grotesque', 'Helvetica Neue', Arial, sans-serif;font-size:52px;line-height:1;cursor:pointer;padding:24px;transition:color .15s var(--ease)}
.bd-arrow:hover,.bd-arrow:focus-visible{color:var(--gold-light)}
.bd-dots{position:fixed;bottom:30px;left:50%;transform:translateX(-50%);display:flex;gap:11px;z-index:2}
.bd-dot{width:8px;height:8px;border-radius:50%;background:rgba(var(--paper-rgb),.25);border:none;cursor:pointer;padding:0;transition:background .15s var(--ease),transform .15s var(--ease)}
.bd-dot:hover{background:rgba(var(--paper-rgb),.5)}
.bd-dot.on{background:var(--gold);transform:scale(1.3)}
@media(max-width:860px){.bd-num{font-size:64px}.bd-slide{padding:32px}}
@keyframes flashRow{0%{background:rgba(240, 169, 127,.35)}100%{background:transparent}}
tr.flash{animation:flashRow 1.8s ease}
.btn{background:var(--accent-fill);color:var(--on-accent);font-weight:700;font-size:var(--fs-1);letter-spacing:.06em;text-transform:uppercase;padding:10px 18px;border-radius:var(--r-sm);border:none;transition:background .2s var(--ease),transform .1s var(--ease),opacity .15s var(--ease)}
.btn:hover{background:var(--accent-fill-hover)}
.btn:active{transform:translateY(1px)}
.btn:disabled,.btn[disabled]{opacity:.4;cursor:not-allowed;pointer-events:none;transform:none}
.btn.ghost{background:none;border:1px solid var(--line);color:var(--paper-dim)}
.btn.ghost:hover{border-color:var(--gold);color:var(--gold-light);background:var(--surface-hover)}
.btn.ghost:active{background:rgba(var(--paper-rgb),.08)}
.btn.sm{padding:6px 12px;font-size:var(--fs-1)}
/* No border, no fill, low-contrast text until hover/focus — for an
   affordance that needs to stay on every row of a long list (the
   posture scan's "Not via Microsoft?") without visually competing with
   the pass/fail/review result next to it. Unlike .btn.ghost, this reads
   as part of the row's quiet metadata rather than a call to action;
   :focus-visible keeps it discoverable via keyboard even though hover
   is the only cue for a mouse user scanning the list. */
.btn.quiet{background:none;border:1px solid transparent;color:var(--paper-faint);padding:6px 8px}
.btn.quiet:hover,.btn.quiet:focus-visible{color:var(--gold-light);background:var(--surface-hover)}
.btn.quiet:active{background:rgba(var(--paper-rgb),.08)}
/* Square icon button (topbar Settings) — same ghost chrome as its
   neighbours, sized so the glyph is optically centred rather than
   sitting in a text-shaped box. */
.btn.icon-only{padding:6px 9px;display:inline-flex;align-items:center;justify-content:center;line-height:0}
.view{display:none;padding:36px;max-width:1280px}
.view.on{display:block}
.vhead{margin-bottom:31px}
.vhead h1{font-family:'Bricolage Grotesque', 'Helvetica Neue', Arial, sans-serif;font-weight:500;font-size:var(--fs-5)}
.vhead p{color:var(--paper-dim);margin-top:6px;font-size:var(--fs-2);max-width:70ch;line-height:1.6}
.rule{width:26px;height:1px;background:var(--gold);margin-bottom:14px}
.modenote{font-size:var(--fs-1);letter-spacing:.08em;color:var(--paper-faint);white-space:nowrap;overflow:hidden;text-overflow:ellipsis}

/* Persistent trial banner — a demo/trial licence (see currentEntitlementType()
   in app.js) while it's still valid. Deliberately unobtrusive: a slim
   strip, no icon/animation, no dismiss control (it's meant to stay
   visible for the life of the trial rather than be dismissed once and
   forgotten) — sits between the topbar and whichever view is active,
   so it's present everywhere without competing with page content. */
.trial-banner{display:flex;align-items:center;justify-content:center;gap:8px;padding:7px 16px;background:rgba(190, 74, 30,.12);border-bottom:1px solid rgba(190, 74, 30,.3);color:var(--gold-light);font-size:var(--fs-1);font-weight:700;letter-spacing:.04em}

/* ===== cards / tables ===== */
/* overflow-x:auto unconditionally — harmless for the many .card uses
   that never overflow (plain text/flex content), and means every
   register's table (most cards wrap one) scrolls within its own card
   on a narrow viewport instead of blowing out the whole page's width.
   A handful of views already set this same rule inline redundantly
   (from before this was global) — harmless, not worth unwinding. */
.card{background:var(--surface);border:1px solid var(--line);border-radius:var(--r-md);padding:26px;overflow-x:auto;box-shadow:var(--elev-1)}
.grid{display:grid;gap:19px}
table{width:100%;border-collapse:collapse}
/* ===== Two label tiers, deliberately =====
   Tracked-out small-caps is this app's signature, and it was applied to
   every label in it — nav groups, card titles, table headers, KPI
   captions, form labels, chart legends, timestamps. When every label is
   a LABEL, none of them rank, and the treatment stops meaning anything.
   So it now marks exactly one tier:
     SMALL-CAPS  a boundary — nav group, card title, section eyebrow,
                 settings subhead, button. "A new part of the page."
     SENTENCE    a label ON data — table header, KPI caption, form field,
                 legend, timestamp. These name a value; they are read,
                 not scanned, and lowercase is simply faster to read.
   Sentence-case labels also step up from 9.5px to 11px: lowercase has
   less presence than caps at the same size, and the tracking that made
   caps legible is what makes lowercase look loose, so it goes too. */
th{font-size:var(--fs-1);letter-spacing:.01em;color:var(--paper-faint);font-weight:700;text-align:left;padding:12px 14px;border-bottom:1px solid var(--line)}
td{padding:14px;border-bottom:1px solid var(--line);font-size:var(--fs-2);color:var(--paper-dim);vertical-align:middle}
/* Evidence drawer's per-check data table (App.viewEvidence/
   renderEvidenceData in app.js) — the drawer is much narrower than a
   full-width register view, so this tightens the base table/th/td
   rules above rather than fighting them with a wider layout. */
.evidence-table{font-size:11.5px}
.evidence-table th{padding:6px 8px;font-size:10px;letter-spacing:.08em}
.evidence-table td{padding:6px 8px;color:var(--paper-dim);white-space:nowrap;overflow:hidden;text-overflow:ellipsis;max-width:160px}
/* Actions register: 11 columns plus a per-row button cluster (Complete
   / Corrective action / Edit / Delete, all white-space:nowrap so the
   buttons themselves never wrap) is more than table{width:100%}'s
   default auto-layout can fit — with nothing to stop it, the browser
   crushes the one column that CAN wrap (the action title) down to a
   handful of characters per line rather than letting the table exceed
   its container, so a normal title wraps across 4-5 lines and the
   button cluster on a CAPA row visually overflows past the table edge.
   A min-width on the title column is the fix: it gives the table a
   legitimate reason to grow past 100%, and .card's existing
   overflow-x:auto (unused until now because the table never actually
   needed to overflow) takes it from there with a normal horizontal
   scrollbar — the same pattern the Statement of Applicability's much
   wider table already relies on. */
.act-title{min-width:240px}
tr:last-child td{border-bottom:none}
tbody tr{transition:background .15s var(--ease)}
tbody tr:hover{background:var(--surface-hover);cursor:pointer}
tbody tr:focus-visible{outline:2px solid var(--gold-light);outline-offset:-2px;background:var(--surface-hover)}
.chip{display:inline-block;font-size:var(--fs-eyebrow);letter-spacing:.1em;text-transform:uppercase;font-weight:800;padding:4px 10px;border-radius:3px;border:1px solid}
.sev-Critical{color:var(--critical);border-color:rgba(224,144,143,.45)}
.sev-High{color:var(--fail);border-color:rgba(201,122,122,.4)}
.sev-Medium{color:var(--warn);border-color:rgba(240, 169, 127,.4)}
.sev-Low{color:var(--pass);border-color:rgba(143,191,154,.4)}
.sev-Prohibited{color:var(--critical);border-color:rgba(224,144,143,.45)}
.sev-Limited{color:var(--warn);border-color:rgba(240, 169, 127,.4)}
.sev-Minimal{color:var(--pass);border-color:rgba(143,191,154,.4)}
.st-Open{color:var(--fail);border-color:rgba(201,122,122,.4)}
.st-Inprogress,.st-Intreatment{color:var(--warn);border-color:rgba(240, 169, 127,.4)}
.st-Done,.st-Closed,.st-Implemented,.st-Monitored,.st-Reviewed,.st-Completed{color:var(--pass);border-color:rgba(143,191,154,.4)}
.st-Proposed,.st-Notstarted{color:var(--paper-faint);border-color:var(--line)}
.src{font-size:var(--fs-1);color:var(--paper-faint);letter-spacing:.06em}
.gold-t{color:var(--gold-light);font-weight:700}
.id-t{font-size:var(--fs-1);color:var(--paper-faint);font-weight:700;letter-spacing:.04em;white-space:nowrap}
/* Unscoped from .id-t (its only home until now) so the SoA's title
   cell can reuse the exact same link-button look — see renderSoaRow()
   in app.js. Every existing .lnk usage (Risks/Actions/Vendors/AI
   Systems id cells) already sits inside a .id-t td, so this widens
   where the rule can be reused without changing how any of those
   still look. */
.lnk{font:inherit;color:inherit;background:none;border:0;padding:0;text-align:left;cursor:pointer;text-decoration:underline;text-decoration-color:transparent;transition:text-decoration-color .15s var(--ease),color .15s var(--ease)}
.lnk:hover,.lnk:focus-visible{color:var(--gold-light);text-decoration-color:currentColor}
.lnk:active{color:var(--accent-ink)}

/* ===== dashboard ===== */
.appetite-banner{background:rgba(201,122,122,.1);border:1px solid rgba(201,122,122,.4);border-radius:6px;padding:14px 18px;margin-bottom:16px;font-size:var(--fs-2);color:var(--paper);line-height:1.6}
.appetite-banner a{color:var(--gold-light)}
.spark-caption{display:flex;justify-content:space-between;align-items:baseline;font-size:var(--fs-1);color:var(--paper-faint);margin-bottom:8px;gap:10px}
.spark-caption b{color:var(--paper-dim);font-weight:800}
.spark-caption .gold-t{font-size:var(--fs-3)}
.spark-legend{display:flex;gap:18px;margin-top:10px;font-size:var(--fs-1);letter-spacing:.01em;color:var(--paper-faint)}
.spark-legend span{display:flex;align-items:center;gap:6px}
.spark-legend i{width:14px;height:2px;display:inline-block;border-radius:2px}
.roadmap-label{font-size:var(--fs-1);letter-spacing:.14em;text-transform:uppercase;color:var(--gold-light);font-weight:700;margin-bottom:14px}
.roadmap-track{display:grid;grid-template-columns:repeat(4,1fr);gap:10px}
.roadmap-phase{position:relative;background:rgba(var(--paper-rgb),.06);border-radius:4px;padding:10px 12px;overflow:hidden;transition:background .2s var(--ease)}
.roadmap-phase span{position:relative;font-size:var(--fs-1);color:var(--paper-dim);display:block}
.roadmap-phase b{position:relative;font-size:var(--fs-4);font-weight:800;color:var(--paper-faint);display:block;margin-top:4px}
.roadmap-fill{position:absolute;inset:0;background:rgba(190, 74, 30,.18);width:0;transition:width .8s var(--ease)}
.roadmap-phase.active .roadmap-fill{background:rgba(190, 74, 30,.28)}
.roadmap-phase.active b{color:var(--gold-light)}
.roadmap-phase.done .roadmap-fill{background:rgba(143,191,154,.22)}
.roadmap-phase.done b{color:var(--pass)}
.gs-stepper{display:flex;align-items:flex-start;gap:0;overflow-x:auto;margin-bottom:20px;padding-bottom:2px}
.gs-node{display:flex;flex-direction:column;align-items:center;flex:0 0 auto;width:88px;text-align:center}
.gs-node-ic{width:28px;height:28px;border-radius:50%;display:flex;align-items:center;justify-content:center;border:1.5px solid var(--line);color:var(--paper-faint);font-size:11px;font-weight:700;margin-bottom:8px;flex-shrink:0;transition:border-color .3s var(--ease),color .3s var(--ease),background .3s var(--ease),box-shadow .3s var(--ease)}
.gs-node.done .gs-node-ic{background:var(--accent-fill);border-color:var(--accent-fill);color:var(--on-accent)}
.gs-node.current .gs-node-ic{border-color:var(--gold);color:var(--gold-light);box-shadow:0 0 0 4px rgba(240, 169, 127,.15)}
.gs-node-label{font-size:10.5px;line-height:1.3;color:var(--paper-faint)}
.gs-node.current .gs-node-label{color:var(--paper);font-weight:700}
.gs-node.done .gs-node-label{color:var(--paper-dim)}
.gs-conn{flex:1 1 14px;min-width:14px;height:1.5px;background:var(--line);align-self:flex-start;margin:13px -1px 0;position:relative}
.gs-conn.done{background:var(--gold)}
.gs-conn i{position:absolute;top:50%;left:0;width:5px;height:5px;border-radius:50%;background:var(--gold);transform:translateY(-50%);box-shadow:0 0 6px rgba(190, 74, 30,.7);opacity:0}
.gs-conn.active i{animation:gsFlow 1.8s linear infinite}
@keyframes gsFlow{0%{left:0;opacity:0}15%{opacity:1}85%{opacity:1}100%{left:100%;opacity:0}}
@media (prefers-reduced-motion: reduce){.gs-conn.active i{animation:none;opacity:1;left:50%}}
.gs-row{display:flex;align-items:flex-start;gap:12px;padding:10px 0;border-bottom:1px solid var(--line)}
.gs-row:last-child{border-bottom:none}
.gs-check{width:20px;height:20px;border-radius:50%;border:1.5px solid var(--line);flex-shrink:0;display:flex;align-items:center;justify-content:center;color:var(--ink);margin-top:1px}
.gs-row.done .gs-check{background:var(--pass);border-color:var(--pass)}
.gs-text{flex:1;display:flex;flex-direction:column;gap:2px}
.gs-text b{font-size:var(--fs-2);color:var(--paper);font-weight:700}
.gs-row.done .gs-text b{color:var(--paper-dim)}
.gs-text span{font-size:var(--fs-1);color:var(--paper-faint)}
.verify-ok{color:var(--pass);font-size:var(--fs-1)}
.verify-stale{color:var(--warn);font-size:var(--fs-1)}
.evidence-link{color:var(--gold-light);font-size:var(--fs-1);text-decoration:none;border-bottom:1px solid rgba(240, 169, 127,.4);transition:color .15s var(--ease),border-color .15s var(--ease)}
.evidence-link:hover{color:var(--paper);border-color:var(--gold-light)}
/* 175px, not 230px. Shared by every KPI strip in the app (Statement of
   Applicability, Actions, Vendors, AI systems, Financial risk analysis,
   Documents, Training); the Dashboard additionally quietens its own
   strip via .kpis-compact below.

   Measured, because an earlier comment here asserted a value the rule
   did not carry and nobody caught it. The content column tops out at
   1208px and the widest track that still yields N columns is:
       columns   1280px   1440px   1600px+
         6        151px    177px    190px
         5        184px    216px    231px
   At 230px a six-tile strip landed FOUR across, orphaning two on a
   half-empty second row — the single ugliest thing in the app. 175px
   fits all six from 1440px up, which is the common desktop case.
   Below ~1400px six tiles still wrap to 5+1: no single track width
   avoids that without shrinking tiles past legibility at 1280px, so
   this is a deliberate trade, not an oversight. The real fix for the
   worst offender is fewer tiles — the Statement of Applicability's
   first four are one distribution (implemented/in progress/not
   started/excluded) already drawn as a chart directly beneath them. */
.kpis{grid-template-columns:repeat(auto-fit,minmax(175px,1fr));gap:13px}
/* Content cards, not numbers. The course catalogue used .kpis, whose
   175px track is sized for a big figure and a caption — a course card
   carries a title, a meta line, an audience sentence and a button, and
   at 175px that wraps into a column of fragments. */
.card-grid{grid-template-columns:repeat(auto-fit,minmax(300px,1fr));gap:13px}
.kpi .kpi-num{display:flex;align-items:baseline;gap:7px;flex-wrap:wrap}
.kpi b{display:block;font-weight:800;font-size:var(--fs-6);color:var(--gold-light);line-height:1.1}
.kpi b small{font-size:var(--fs-3);color:var(--paper-dim);font-weight:500}
.kpi .trend{font-size:var(--fs-2);font-weight:800;white-space:nowrap}
.kpi span{display:block;margin-top:7px;font-size:var(--fs-1);letter-spacing:.01em;color:var(--paper-faint);font-weight:700}
.kpi .sub{margin-top:8px;font-size:var(--fs-1);color:var(--paper-dim);line-height:1.4}
/* ===== Proportion meter — the tile's own dead space, put to work =====
   Every summary tile in this app is a figure, a caption and an optional
   sub-line stacked at the TOP of a fixed-height card, which leaves the
   bottom third of each one empty. On a strip where the figures are
   proportions that is not just wasted space, it is wasted meaning: the
   Dashboard's ten framework-readiness tiles rendered "0%" ten times in
   identical type, and a reader had to compare digits to see that one
   framework was at 21% and the rest had not started. A 3px bar in the
   space that was already empty makes that comparison pre-attentive —
   you see the one long bar in the strip before you read a single label.

   Only ever attached where the number IS a part-of-whole (readiness,
   implemented-of-applicable, completion). A count with no denominator
   ("8 open actions") gets no meter, because a bar with nothing to fill
   against is decoration, and decoration on a compliance figure is how
   a reader learns to distrust the ones that do mean something.

   Inherits its colour from the tile via currentColor on the fill's own
   inline tone, so a meter never needs a second copy of the severity
   logic that already coloured the figure above it. The track is the
   same 12%-white hairline wash the rest of the app separates planes
   with, so it disappears into the card until it has something to say.

   margin-top:auto pins it to the bottom of the tile REGARDLESS of how
   many lines the caption above wrapped to, which is what keeps a strip
   of six tiles reading as one aligned row rather than six ragged ones
   — the tiles are already stretch-aligned by the grid, they just had
   nothing at the bottom to align. */
.kpi{display:flex;flex-direction:column}
.kpi-meter{margin-top:auto;padding-top:14px;display:block}
.kpi-meter i{display:block;height:3px;border-radius:3px;background:rgba(var(--paper-rgb),.12);overflow:hidden;position:relative;--kpi-meter-pct:0%}
/* --kpi-meter-pct is declared here rather than only passed inline by
   kpiMeter(): the fill is set per tile via a style attribute, so the
   token has no :root value, and an inline-only custom property is
   exactly the kind of thing styles-tokens.test.mjs exists to catch. A
   0% default is also the right failure mode — a meter whose percentage
   never arrives reads as empty rather than inheriting a stale width
   from whatever rendered before it. */
.kpi-meter i::after{content:'';position:absolute;inset:0 auto 0 0;width:var(--kpi-meter-pct);background:currentColor;border-radius:inherit;transition:width .7s var(--ease)}
/* The fill colour tracks the figure it belongs to. Default is the same
   gold-light every unremarkable figure uses; kpiTile() overrides it to
   the severity tone when (and only when) it coloured the figure too. */
.kpi-meter{color:var(--gold-light)}
.kpi-empty .kpi-meter{color:var(--paper-faint)}
@media (prefers-reduced-motion: reduce){.kpi-meter i::after{transition:none}}
.dot{display:inline-block;width:6px;height:6px;border-radius:50%}
.kpi .dot{margin-right:4px}
/* ===== Dashboard hierarchy: hero, demoted strip, section tiers =====
   The Dashboard used to open with six equal-weight KPI tiles — the
   posture score, the number this whole app is built around, rendered at
   exactly the same size as "exclusions missing justification" — and then
   ran three screens of identically-styled cards with nothing marking one
   as more important than the next. These three pieces give it an
   entry point and a shape:
     .dash-hero   the one row a practitioner opens this view for — the
                  posture score, and what to do about it, side by side.
     .kpis-compact the remaining KPIs, deliberately smaller and tighter
                  than the shared .kpis above so they read as supporting
                  detail. The narrower track also keeps 5-6 tiles on one
                  row instead of orphaning the last ones onto a second.
     .section-head a labelled rule dividing "Position" (where we stand)
                  from "Operations" (what's moving and what's overdue). */
.dash-hero{grid-template-columns:minmax(0,4fr) minmax(0,8fr);align-items:stretch;margin-bottom:16px}
/* Next 3 actions hides itself when there's nothing to rank; the score
   tile then takes the full width rather than sitting beside a gap. */
.dash-hero.solo{grid-template-columns:minmax(0,1fr)}
/* The slot is a plain wrapper JS re-renders into; making it flex is what
   lets the card inside actually fill the row's height rather than
   floating at its natural height beside a much taller Next 3 actions. */
.hero-slot{display:flex}
/* The posture score is the number this app exists to produce, so it
   gets the ambient accent wash and sits one elevation step above the
   strip beneath it. background-image (not the background shorthand)
   so .card's own surface colour stays underneath the glow. */
.kpi-hero{flex:1;display:flex;flex-direction:row;align-items:center;gap:26px;padding:30px 26px;background-image:var(--glow);box-shadow:var(--elev-2)}
/* Ring beside the figure, not above it: the hero sits in a 4fr column
   next to "Next 3 actions" in an 8fr one, and that column is wider than
   it is tall at every desktop width — a stacked ring would push the
   figure below the fold of the card while leaving the space either side
   of it empty, which is the problem this ring was added to solve.
   Below the dash-hero's own stacking breakpoint the row still works:
   the card goes full-width and the ring simply has more room. */
.hero-ring{width:118px;flex:none}
.hero-body{min-width:0}
/* The hero's caption and sub-line no longer need to reserve the bottom
   of the card, since the ring sets the card's height — they sit with
   the figure as one block. */
.kpi-hero .hero-body{display:flex;flex-direction:column;justify-content:center}
@media(max-width:560px){.kpi-hero{gap:18px}.hero-ring{width:92px}}
.kpi-hero b{font-size:var(--fs-hero)}
.kpi-hero b small{font-size:var(--fs-4)}
.kpi-hero span{font-size:var(--fs-1);letter-spacing:.18em;margin-top:11px}
.kpi-hero .sub{font-size:var(--fs-2);margin-top:11px}
/* Track width now comes from .kpis itself; this only quietens the type,
   so the Dashboard's strip reads as support for the hero above it. */
.kpis-compact .kpi{padding:19px}
.kpis-compact .kpi b{font-size:var(--fs-5)}
/* ===== Strip label — the caption a whole row of tiles shares =====
   Lighter than .section-head (which draws a rule and opens a new part
   of the view); this only names the strip directly beneath it, so it
   reuses the app's existing tracked-out small-caps label tier at
   --paper-faint rather than introducing a third heading weight.
   Exists so a tile caption never has to repeat its strip's subject —
   see the Dashboard's two strips in index.html. */
.strip-label{font-size:var(--fs-eyebrow);letter-spacing:.16em;text-transform:uppercase;color:var(--paper-faint);font-weight:700;margin:22px 0 10px}
/* The first strip follows the hero row, which already carries its own
   bottom margin — no need to double it. */
.dash-hero + .strip-label{margin-top:4px}
/* Quiet status, for a column where the same few values repeat down every
   row. A bordered .chip per row turns a long table into a stack of
   high-contrast boxes competing for attention with the row content — the
   dot carries the same colour coding at a fraction of the visual weight,
   and sentence case stops it shouting. Chips stay where a status is the
   exception rather than the rule. */
/* ===== The SoA's summary tiles, as filters =====
   .kpi-on marks the tile currently filtering the table. It reuses the
   gold edge every selected control in this app uses rather than
   inventing a "selected card" treatment, and adds a filled left bar so
   the state survives a screenshot, a monochrome display, and the ~8% of
   men who would not reliably see a gold border against this surface.
   .soa-slice is the same affordance at the weight of the quiet
   distribution line it sits in — a link, not a button, because turning
   those three counts into buttons would rebuild the six-tile strip
   1.57.0 deliberately collapsed. */
/* A tile with nothing behind it cannot be a filter — opening an empty
   table costs a click and a moment of doubt. It used to be silently
   identical to one that could be clicked: same card, same weight, just
   no pointer and no hover, so "inert" was something you discovered by
   trying. It now reads as an empty state, with the figure and its
   caption receding to the tone the rest of the app uses for "nothing
   here". kpiTile() omits the severity colour at zero rather than
   fighting it with !important, since that colour is set inline. */
.kpi-empty b,.kpi-empty span,.kpi-empty .sub{color:var(--paper-faint)}
/* ===== "You can click this" — visible without hovering =====
   A hover-only cue answers "is this clickable?" only once you have
   already guessed that it might be, which is exactly the confusion
   three near-identical strips with three different behaviours caused:
   the tiles that filter and the tiles that are pure context looked the
   same until you moved a mouse across them. Interactive summary tiles
   now carry a small chevron at rest; a total or an empty count simply
   does not have one, so the difference is legible at a glance and in a
   screenshot.
   Deliberately scoped to the summary-tile component rather than every
   .card[data-action]: the larger content cards that navigate have their
   own visible link, and several already use their top-right corner. */
.kpi[data-action],button.kpi,.board-tile[data-action],button.board-tile{position:relative}
.kpi[data-action]::after,button.kpi::after,.board-tile[data-action]::after,button.board-tile::after{content:'';position:absolute;top:15px;right:15px;width:6px;height:6px;border-right:1.5px solid var(--paper-faint);border-bottom:1.5px solid var(--paper-faint);transform:rotate(-45deg);transition:border-color .15s var(--ease)}
.kpi[data-action]:hover::after,button.kpi:hover::after,.board-tile[data-action]:hover::after,button.board-tile:hover::after,.kpi-on::after{border-color:var(--gold-light)}
.kpi-on{border-color:var(--gold-light);background:var(--surface-hover);box-shadow:var(--elev-2),inset 3px 0 0 var(--gold)}
.soa-mix{margin:0 0 16px;font-size:var(--fs-1);color:var(--paper-faint);display:flex;gap:6px;align-items:baseline;flex-wrap:wrap}
.soa-slice{font:inherit;color:inherit;padding:0;border:none;background:none;text-decoration:underline;text-decoration-color:transparent;text-underline-offset:3px;transition:color .15s var(--ease),text-decoration-color .15s var(--ease)}
.soa-slice:hover,.soa-slice:focus-visible{color:var(--gold-light);text-decoration-color:currentColor}
/* "0 unsupported" — the one slice that renders at zero (see renderSoa).
   Not a button, because there is nothing to filter to; coloured as the
   good result it is, rather than sitting in the same grey as counts
   that are merely absent. */
.soa-slice-ok{color:var(--pass)}
.soa-slice.on{color:var(--gold-light);font-weight:700;text-decoration-color:currentColor}
/* Sits directly above the table it describes, so "showing 2 of 93" is
   read at the moment the short table would otherwise look like missing
   data rather than a filter. Shared by every register whose summary
   tiles filter — was .soa-focus-bar when only one did. */
/* ===== Bulk action bar =====
   Deliberately the SAME shape as .focus-bar directly above it (both sit
   between a register's summary and its rows and both describe a subset)
   but filled rather than tinted, because this one is a mode: while it is
   showing, a control here writes to every ticked row at once. Sticky, so
   scrolling 92 rows to tick the last one never leaves the actions the
   selection is for off-screen. Sits below the sticky <thead> for the
   same reason the family headers do. */
.bulk-bar{position:sticky;top:var(--topbar-h);z-index:5;display:flex;align-items:center;gap:12px;flex-wrap:wrap;margin:0 0 12px;padding:11px 16px;border:1px solid var(--accent-fill);border-radius:var(--r-sm);background:var(--surface-raised);box-shadow:var(--elev-2);font-size:var(--fs-2);color:var(--paper-dim)}
.bulk-bar .bulk-count{color:var(--paper)}
.bulk-bar .bulk-count b{font-variant-numeric:tabular-nums;font-size:var(--fs-3);font-weight:800;color:var(--accent-ink);margin-right:4px}
.bulk-bar .bulk-sep{width:1px;align-self:stretch;background:var(--line)}
.bulk-bar .bulk-field{display:inline-flex;align-items:center;gap:8px}
.bulk-bar .bulk-field > span{font-size:var(--fs-1);letter-spacing:.06em;text-transform:uppercase;color:var(--paper-faint);font-weight:700}
/* Pushes the two quiet text actions to the far end, away from the
   destructive-adjacent buttons, so "Clear selection" is never the thing
   a hand lands on by accident after "Mark not applicable". */
.bulk-bar .bulk-spacer{flex:1 1 auto}
/* The row checkbox and its selected-row wash. The checkbox is inside the
   Control cell (see _soaSel in app.js), so it needs its own spacing
   rather than a column's. accent-color paints the native control in the
   brand fill in both themes without rebuilding it. */
.soa-sel,.act-sel,.vendor-sel,.doc-sel{accent-color:var(--accent-fill);width:15px;height:15px;margin-right:9px;vertical-align:-2px;cursor:pointer}
/* A ticked row has to be obvious while scanning a long table — the
   checkbox alone is 15px of signal in a full-width row. Left bar plus a
   wash, the same pairing .kpi-on uses, so the state survives a
   screenshot and a monochrome display. */
/* .row-sel is the register-agnostic name; .soa-row-sel predates it and
   is kept so the Statement of Applicability's rows keep their class. */
.soa-row-sel > td,.row-sel > td{background:rgba(var(--gold-rgb),.08);box-shadow:inset 3px 0 0 var(--accent-fill)}
.soa-row-sel > td:not(:first-child),.row-sel > td:not(:first-child){box-shadow:none}
/* The Actions and Vendor rows are themselves clickable (they open a
   drawer), and already have a hover wash. A selected row must still
   read as selected while hovered, so the selection wash wins. */
.row-sel:hover > td{background:rgba(var(--gold-rgb),.12)}
@media(max-width:860px){.bulk-bar{position:static}}
.focus-bar{display:flex;align-items:center;justify-content:space-between;gap:16px;flex-wrap:wrap;margin:0 0 12px;padding:11px 16px;border:1px solid rgba(var(--gold-light-rgb),.35);border-left:3px solid var(--gold);border-radius:var(--r-sm);background:rgba(var(--gold-rgb),.07);font-size:var(--fs-2);color:var(--paper-dim)}
.focus-bar b{color:var(--paper);font-variant-numeric:tabular-nums}
.status-quiet{display:inline-flex;align-items:center;gap:7px;color:var(--paper-dim);white-space:nowrap}
.status-quiet i{width:6px;height:6px;border-radius:50%;background:currentColor;flex:none}
.status-quiet.sq-pass{color:var(--pass)}
.status-quiet.sq-warn{color:var(--warn)}
.status-quiet.sq-none{color:var(--paper-faint)}
/* ===== One section heading for the whole app =====
   Was .dash-section, used only on the Dashboard. Meanwhile Training,
   Documents and Attestations divided themselves with bare <h2> tags —
   and NOTHING in this file matched a bare h2 (every h2 rule here is
   scoped: .bd-slide h2, .drawer h2, .automation-centre h2, this one).
   So those six headings rendered at the browser's default: Manrope
   19.5px/700, against the Dashboard's 'Bricolage Grotesque' 20px/500. Same size,
   different family, different weight — near enough to read as a
   mistake rather than as a decision, which is exactly how it looked.
   Renamed rather than aliased so there is one name for one thing; the
   Dashboard's two usages moved with it. */
.section-head{margin:36px 0 18px;padding-top:24px;border-top:1px solid var(--line);display:flex;align-items:baseline;gap:16px;flex-wrap:wrap}
.section-head h2{font-family:'Bricolage Grotesque', 'Helvetica Neue', Arial, sans-serif;font-weight:500;font-size:var(--fs-4);color:var(--paper);margin:0}
.section-head p{margin:0;color:var(--paper-faint);font-size:var(--fs-1);line-height:1.5;max-width:80ch}
/* The first section in a view sits right under the .vhead intro, which
   already separates it — a second rule and 36px of air there reads as a
   gap rather than as structure. */
.vhead + .section-head{margin-top:22px;padding-top:0;border-top:none}
/* The section header already supplies the separation whatever follows it
   would otherwise double up on. */
.section-head + .grid,.section-head + .card,.section-head + .automation-centre,.section-head + .filters{margin-top:0}
@media(max-width:1000px){.dash-hero{grid-template-columns:1fr}}

/* Automation Centre: the Dashboard's operational answer to "what is
   automatic, what is proven, and what still needs a person?" The deep
   surface and restrained gold signal make it a control-room panel,
   without reusing red/green posture colours as a misleading compliance
   verdict. The three cards are buttons because each leads directly to
   the relevant working surface; the keyboard focus state is as clear as
   the hover state. */
.automation-centre{margin:18px 0 16px;padding:26px;border:1px solid rgba(var(--gold-light-rgb),.26);border-radius:var(--r-lg);background:var(--glow),linear-gradient(150deg,var(--surface),var(--ink));box-shadow:var(--elev-2)}
.automation-centre-head{display:flex;justify-content:space-between;gap:18px;align-items:flex-start}
.automation-eyebrow{display:block;color:var(--gold-light);font-size:var(--fs-eyebrow);font-weight:800;letter-spacing:.2em;text-transform:uppercase;margin-bottom:8px}
.automation-centre h2{font-family:'Bricolage Grotesque', 'Helvetica Neue', Arial, sans-serif;font-size:clamp(1.45rem,2.5vw,2rem);font-weight:500;line-height:1.15;color:var(--paper);margin:0}
.automation-intro{max-width:76ch;color:var(--paper-dim);font-size:var(--fs-2);line-height:1.6;margin:12px 0 22px}
.automation-monitor{display:inline-flex;align-items:center;gap:8px;flex:0 0 auto;color:var(--paper-faint);font-size:var(--fs-eyebrow);font-weight:800;letter-spacing:.1em;text-transform:uppercase;padding:7px 9px;border:1px solid var(--line);border-radius:99px;white-space:nowrap}
.automation-monitor i{width:7px;height:7px;border-radius:50%;background:var(--paper-faint)}
.automation-monitor.is-active{color:var(--pass);border-color:rgba(143,191,154,.42);background:rgba(143,191,154,.07)}
.automation-monitor.is-active i{background:var(--pass);box-shadow:0 0 0 4px rgba(143,191,154,.12)}
/* --pass is tuned to clear 4.5:1 on the plain card, but this pill lays
   a 7% green wash under it, which erodes the pair to 4.35:1 in the
   light theme — the last sub-AA text left in the app after the accent
   ramp landed. Uses the website's own measured "ok" ink (tokens.css
   --ok-ink, 6.18:1 on cream) rather than darkening the shared --pass,
   which would drag every other pass state with it for one pill's sake. */
:root[data-theme="light"] .automation-monitor.is-active{color:#046A4E}
.automation-grid{display:grid;grid-template-columns:repeat(3,minmax(0,1fr));gap:10px}
.automation-card{appearance:none;text-align:left;color:inherit;font:inherit;cursor:pointer;background:rgba(var(--paper-rgb),.035);border:1px solid rgba(var(--paper-rgb),.1);border-radius:var(--r-sm);padding:18px;transition:transform .18s var(--ease),border-color .18s var(--ease),background .18s var(--ease),box-shadow .18s var(--ease)}
.automation-card:hover,.automation-card:focus-visible{transform:translateY(-2px);background:rgba(var(--gold-light-rgb),.09);border-color:rgba(var(--gold-light-rgb),.55);box-shadow:var(--elev-2);outline:none}
.automation-card:focus-visible{box-shadow:0 0 0 3px rgba(var(--gold-light-rgb),.28),var(--elev-2)}
.automation-card-kicker{display:block;font-size:var(--fs-eyebrow);font-weight:800;letter-spacing:.14em;text-transform:uppercase;color:var(--accent-ink);margin-bottom:16px}
.automation-card>b{display:block;font-family:'Bricolage Grotesque', 'Helvetica Neue', Arial, sans-serif;font-size:clamp(2rem,3.3vw,2.7rem);font-weight:500;line-height:1;color:var(--paper)}
.automation-card>b small{font-family:Manrope,sans-serif;font-size:var(--fs-2);font-weight:700;color:var(--gold-light)}
.automation-card h3{font-size:var(--fs-2);color:var(--paper);margin:11px 0 5px}
.automation-card p{font-size:var(--fs-1);line-height:1.55;color:var(--paper-dim);margin:0;min-height:3.2em}
.automation-link{display:block;margin-top:14px;color:var(--gold-light);font-size:var(--fs-1);font-weight:800}
.automation-footer{display:flex;justify-content:space-between;gap:16px;border-top:1px solid rgba(var(--paper-rgb),.1);margin-top:18px;padding-top:13px;color:var(--paper-faint);font-size:var(--fs-1);line-height:1.5}
.automation-footer>span{max-width:56ch}.automation-lock{color:var(--gold-light);font-size:10px;margin-right:5px}
@media (max-width:780px){.automation-centre{padding:20px}.automation-centre-head{flex-direction:column}.automation-grid{grid-template-columns:1fr}.automation-card p{min-height:0}.automation-footer{flex-direction:column;gap:6px}}
/* Assurance basis — deliberately NOT a red/green pass-fail scale.
   These rank how much is known about a control, and an "asserted"
   control is not failing, it is simply resting on a weaker basis than a
   demonstrated one. Colour follows that gradient (gold = strongest
   evidence, dimming toward unsupported) so the eye reads strength, not
   alarm; the genuine alarm case (exceptions) is flagged separately in
   the existing verify-stale red. */
.assur{display:inline-block;font-size:var(--fs-1);font-weight:700;letter-spacing:.06em;text-transform:uppercase;padding:2px 7px;border-radius:3px;border:1px solid;white-space:nowrap}
.assur-demonstrated{color:var(--pass);border-color:color-mix(in srgb,var(--pass) 45%,transparent)}
.assur-evidenced{color:var(--gold-light);border-color:color-mix(in srgb,var(--gold-light) 45%,transparent)}
.assur-asserted{color:var(--paper-dim);border-color:var(--line)}
.assur-unsupported{color:var(--fail);border-color:color-mix(in srgb,var(--fail) 45%,transparent)}
.grid-note{margin-top:11px;font-size:var(--fs-1);line-height:1.5;color:var(--paper-faint)}
/* ===== One treatment for a card you can act on =====
   A tile you can click says so by rising off the page on hover: with a
   real elevation system in place the lift is the cheapest available
   affordance, and it costs one compositor property. transform is listed
   in the transition so the blanket prefers-reduced-motion rule at the
   top of this file kills it.

   This used to be TWO rules — .kpi[data-action] for tiles a view built
   as divs, .kpi-btn for tiles a view built as buttons — which is
   exactly how identical-looking tiles ended up hovering in one module
   and sitting dead in another. It is now keyed on what actually makes a
   card actionable (it is a button, or it carries data-action) rather
   than on which class the view that built it happened to reach for, so
   any clickable card in any module picks this up with nothing to
   remember. Every .card[data-action] in the app is a filter or a
   navigation tile — checked — so none of them is a card that merely
   happens to contain something clickable. */
.card[data-action],.board-tile[data-action],button.card,button.board-tile{cursor:pointer;transition:border-color .15s var(--ease),background .15s var(--ease),box-shadow .18s var(--ease),transform .18s var(--ease)}
button.card,button.board-tile{appearance:none;-webkit-appearance:none;font:inherit;color:inherit;text-align:left;width:100%}
.card[data-action]:hover,.board-tile[data-action]:hover,button.card:hover,button.board-tile:hover{border-color:var(--gold-light);background:var(--surface-hover);box-shadow:var(--elev-2);transform:translateY(-3px)}
.d-kv.clickable{cursor:pointer;margin:0 -8px;padding:6px 8px;border-radius:4px;transition:background .15s var(--ease)}
.d-kv.clickable:hover{background:var(--surface-hover)}
.board-hero{grid-template-columns:repeat(auto-fit,minmax(210px,1fr))}
.board-tile{text-align:center;padding:34px 20px}
.board-tile b{font-size:var(--fs-hero);font-weight:800;color:var(--gold-light);line-height:1;display:block}
.board-tile b small{font-size:var(--fs-4);color:var(--paper-dim);font-weight:500}
.board-tile b .trend{font-size:var(--fs-3);margin-left:4px}
.board-tile span{display:block;margin-top:12px;font-size:var(--fs-2);letter-spacing:.01em;color:var(--paper-faint);font-weight:700}
.dash-2{grid-template-columns:minmax(0,7fr) minmax(0,5fr);margin-top:16px;align-items:start}
@media(max-width:1000px){.dash-2{grid-template-columns:1fr}}
.card h3{font-size:var(--fs-1);letter-spacing:.2em;text-transform:uppercase;color:var(--accent-ink);font-weight:700;margin-bottom:16px}
/* ===== Risk register head: heatmap + summary, one row =====
   The heatmap column is sized to the heatmap (its cells must stay
   square), and the tiles take everything left over rather than the
   page taking it. The tiles wrap to two-up inside their own column at
   this width, which is what keeps four of them legible beside a 560px
   neighbour instead of squeezing four 150px slivers.
   Below 1100px the two stack: at that point the remaining column is
   narrower than the .kpis track minimum anyway, so side-by-side would
   just orphan tiles onto ragged rows. */
.risk-head{grid-template-columns:minmax(0,560px) minmax(0,1fr);align-items:start;margin-bottom:16px}
.risk-head > .kpis{grid-template-columns:repeat(2,minmax(0,1fr));align-content:start}
@media(max-width:1100px){
  .risk-head{grid-template-columns:1fr}
  .risk-head > .card{max-width:560px}
  .risk-head > .kpis{grid-template-columns:repeat(auto-fit,minmax(175px,1fr))}
}
.heat{display:grid;grid-template-columns:auto repeat(5,1fr);gap:4px;max-width:430px}
.heat .lab{font-size:var(--fs-eyebrow);color:var(--paper-faint);display:flex;align-items:center;justify-content:center;letter-spacing:.08em}
.cell{aspect-ratio:1.6;border-radius:3px;display:flex;align-items:center;justify-content:center;font-weight:800;font-size:var(--fs-2);font-variant-numeric:tabular-nums;transition:transform .15s var(--ease),filter .15s var(--ease);border:none;padding:0;width:100%;font-family:inherit;cursor:pointer}
.cell:hover{transform:scale(1.06);filter:brightness(1.15)}
.cell.on{outline:2px solid var(--gold-light);outline-offset:1px}
.heat-legend{display:flex;gap:16px;margin-top:14px;flex-wrap:wrap}
.heat-legend span{display:flex;align-items:center;gap:7px;font-size:var(--fs-1);letter-spacing:.01em;color:var(--paper-faint);font-weight:700}
.heat-legend i{width:11px;height:11px;border-radius:3px;display:inline-block}
.spark{width:100%;height:64px;margin-top:4px}
.feed{list-style:none;max-height:340px;overflow:auto}
.feed li{padding:11px 0;border-bottom:1px solid var(--line);font-size:var(--fs-2);color:var(--paper-dim);line-height:1.55}
.feed li:last-child{border-bottom:none}
.feed time{display:block;font-size:var(--fs-1);letter-spacing:.01em;color:var(--paper-faint);margin-bottom:3px}
.feed b{color:var(--gold-light)}

/* ===== scan ===== */
.scan-grid{grid-template-columns:minmax(0,5fr) minmax(0,7fr);align-items:start}
/* Coverage — what the tenant's licensing lets Checkpoint check at all —
   reads as a footnote to the gauge, not a separate section, and stacking
   it there fills the column the much taller checks list would otherwise
   leave empty. */
.scan-side{display:flex;flex-direction:column;gap:19px;min-width:0}
@media(max-width:1000px){.scan-grid{grid-template-columns:1fr}}
.gauge{display:flex;align-items:center;gap:24px}
.gauge svg{width:110px;flex:none}
.gauge-num{font-weight:800;font-size:var(--fs-hero);color:var(--gold-light);line-height:1}
.gauge-num small{font-size:var(--fs-3);color:var(--paper-dim);font-weight:500}
/* Posture scan's per-category dashboard header — see renderScanChecks()
   in app.js. A plain full-width button rather than a <summary>/<details>
   pair: this app's existing drawer/toast/modal machinery already all
   run on data-action + a JS render pass, and a native <details> would
   be a second, inconsistent way of holding UI state alongside
   _scanCatOpen for no real benefit. */
.check-area-group{margin:16px 0 4px}
.check-area-group:first-child{margin-top:0}
.check-area-toggle{display:flex;align-items:center;gap:10px;width:100%;background:none;border:none;padding:9px 6px;margin:0 -6px;cursor:pointer;text-align:left;border-radius:6px;font:inherit}
.check-area-toggle:hover,.check-area-toggle:focus-visible{background:var(--surface-hover)}
.rag-dot{width:8px;height:8px;border-radius:50%;flex:none}
.rag-dot.rag-red{background:var(--fail)}
.rag-dot.rag-amber{background:var(--warn)}
.rag-dot.rag-green{background:var(--pass)}
.check-area-name{font-size:var(--fs-eyebrow);letter-spacing:.18em;color:var(--accent-ink);text-transform:uppercase;font-weight:700}
.check-area-summary{flex:1;color:var(--paper-faint);font-size:var(--fs-2);text-align:right}
.check-area-chevron{color:var(--paper-faint);flex:none;transition:transform .2s var(--ease)}
.check-area-body{padding-top:2px}
/* Status pills lead, expand/collapse follows. The toolbar used to hold
   only the two expand/collapse buttons and sat flush right; with a
   filter group in it the two jobs separate — triage on the left, where
   reading starts, and the accordion controls kept where they were. They
   wrap onto two lines rather than shrinking below a tappable size. */
.scan-cat-toolbar{display:flex;gap:12px;align-items:center;justify-content:space-between;flex-wrap:wrap;margin-bottom:12px}
.scan-cat-expand{display:flex;gap:8px;margin-left:auto}
/* .filters' own 16px bottom margin is the gap between a filter row and
   the table under it; inside this toolbar the toolbar's own margin
   already does that job. */
.scan-status-filters{margin-bottom:0}
/* The count carries the weight — the label says which bucket, the
   number says how much is in it, and on the active pill both invert
   against the gold fill. */
.scan-status-filters .f-pill b{font-weight:800;font-variant-numeric:tabular-nums;color:var(--paper)}
.scan-status-filters .f-pill.on b{color:var(--on-accent)}
.scan-status-filters .f-pill:disabled b{color:inherit}
.scan-empty{color:var(--paper-dim);font-size:var(--fs-2);margin:18px 2px;display:flex;align-items:center;gap:10px;flex-wrap:wrap}
.check-row-group{border-bottom:1px solid var(--line)}
.check-row-group:last-child{border-bottom:none}
.check-row{display:flex;justify-content:space-between;align-items:center;gap:10px;padding:11px 2px;font-size:var(--fs-2);opacity:.25;transition:opacity .4s var(--ease)}
.check-row.show{opacity:1}
.check-row .lbl{flex:1;color:var(--paper-dim)}
/* Posture scan, one line per check that opens in place. */
#v-scan > .card,#v-scan > div > .card{margin-bottom:19px}
.scan-how{font-size:var(--fs-2);line-height:1.65;color:var(--paper-dim)}
.scan-how p{margin:0 0 8px}
.scan-how b{color:var(--gold-light)}
.scan-summary{display:flex;align-items:center;gap:28px;flex-wrap:wrap}
.scan-summary .gauge svg{width:92px}
.scan-cap{font-size:10px;letter-spacing:.14em;text-transform:uppercase;color:var(--paper-faint);margin-top:6px}
#scanTrend{display:flex;align-items:center;gap:8px;margin-top:6px}
.scan-tiles{display:grid;grid-template-columns:repeat(4,minmax(96px,1fr));gap:10px;flex:1 1 420px}
.scan-tile{display:flex;flex-direction:column;align-items:flex-start;gap:2px;padding:12px 14px;border:1px solid var(--line);border-radius:var(--r-sm,6px);background:none;color:var(--paper-dim);font:inherit;cursor:pointer;text-align:left}
.scan-tile b{font-size:var(--fs-4,24px);font-weight:800;font-variant-numeric:tabular-nums;line-height:1.1}
.scan-tile span{font-size:var(--fs-1);letter-spacing:.04em}
.scan-tile:hover:not(:disabled),.scan-tile:focus-visible{border-color:var(--accent-fill,var(--gold))}
.scan-tile.on{border-color:var(--gold);background:var(--surface-hover)}
.scan-tile:disabled{opacity:.55;cursor:default}
.scan-run{display:flex;flex-direction:column;gap:8px;align-items:flex-start}
@media(max-width:700px){.scan-tiles{grid-template-columns:repeat(2,1fr)}}
.chk-main{width:100%;background:none;border:none;font:inherit;text-align:left;cursor:pointer;justify-content:flex-start}
.chk-main:hover,.chk-main:focus-visible{background:var(--surface-hover)}
.chk-main .chip{flex:none;min-width:64px;text-align:center}
.chk-main .lbl{display:flex;flex-direction:column;gap:2px;min-width:0;color:var(--paper)}
.chk-head{color:var(--paper-faint);font-size:var(--fs-1);white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
.chk-detail{padding:2px 6px 14px 84px;font-size:var(--fs-2);color:var(--paper-dim);line-height:1.6}
.chk-note{margin:0 0 6px;overflow-wrap:anywhere}
.chk-btns{display:flex;gap:8px;flex-wrap:wrap;margin-top:10px}
@media(max-width:700px){.chk-detail{padding-left:6px}}
.fix-list{display:flex;flex-direction:column}
.fix-row{display:flex;align-items:center;gap:12px;width:100%;padding:10px 4px;border:none;border-bottom:1px solid var(--line);background:none;font:inherit;text-align:left;cursor:pointer;color:var(--paper)}
.fix-row:last-child{border-bottom:none}
.fix-row:hover,.fix-row:focus-visible{background:var(--surface-hover)}
.fix-row .lbl{flex:1;display:flex;flex-direction:column;gap:2px;min-width:0}
.fix-n{flex:none;width:24px;height:24px;border-radius:50%;display:inline-flex;align-items:center;justify-content:center;font-weight:800;font-size:var(--fs-1);background:rgba(190,74,30,.18);color:var(--gold-light)}
/* Proposed risks: a compact queue, one row per business risk. */
.prop-list{display:flex;flex-direction:column}
.prop-row{display:grid;grid-template-columns:minmax(0,1fr) auto;align-items:center;gap:6px 12px;border-bottom:1px solid var(--line);padding:4px 0}
.prop-row:last-child{border-bottom:none}
.prop-main{display:flex;align-items:center;gap:12px;background:none;border:none;font:inherit;color:var(--paper);text-align:left;cursor:pointer;padding:10px 4px;min-width:0}
.prop-main:hover,.prop-main:focus-visible{background:var(--surface-hover)}
.prop-main .chip{flex:none;min-width:64px;text-align:center}
.prop-title{flex:1;display:flex;flex-direction:column;gap:3px;min-width:0}
.prop-title .src{font-size:var(--fs-1)}
.prop-btns{display:flex;gap:6px}
.prop-detail{grid-column:1/-1;padding:0 6px 12px 88px;font-size:var(--fs-2);color:var(--paper-dim)}
.prop-finding{display:flex;align-items:center;gap:8px;justify-content:space-between;padding:4px 0;border-bottom:1px dashed var(--line)}
.prop-actions{margin:2px 0 6px 18px;line-height:1.6}
@media(max-width:700px){.prop-row{grid-template-columns:1fr}.prop-detail{padding-left:6px}}
.proposed-card{border:1px solid rgba(240, 169, 127,.35);border-radius:6px;padding:22px 24px;margin-top:14px;background:rgba(190, 74, 30,.05)}
.proposed-card h4{font-family:'Bricolage Grotesque', 'Helvetica Neue', Arial, sans-serif;font-weight:500;font-size:var(--fs-3)}
.proposed-card .meta{margin:8px 0 12px;font-size:var(--fs-1);color:var(--paper-dim);line-height:1.7}
.proposed-card .meta b{color:var(--gold-light)}
/* A suggestion that moves a control BACKWARDS reads as a warning, not as
   progress: confirming it removes an implementation status the
   practitioner recorded. Gold (the app's "this is good / do this")
   treatment is deliberately withheld here. */
.proposed-card.sugg-down{border-color:rgba(224,138,110,.45);background:rgba(224,138,110,.06)}
.sugg-down-flag{display:flex;align-items:flex-start;gap:7px;margin:10px 0 2px;font-size:var(--fs-1);line-height:1.6;color:var(--fail)}
.sugg-down-flag svg{flex:none;width:14px;height:14px;margin-top:2px}
.sugg-down-t{color:var(--fail)}
/* Bulk decision bar above a queue of proposal/suggestion cards. */
.bulk-bar{display:flex;align-items:center;flex-wrap:wrap;gap:8px;margin-top:14px;padding:10px 14px;border:1px dashed var(--line);border-radius:6px}
.bulk-bar .bulk-count{flex:1 1 auto;min-width:150px;font-size:var(--fs-1);color:var(--paper-dim);letter-spacing:.02em}
/* Pending-suggestion count on a framework tab — same visual language as
   the sidebar's nav-item badges. */
.f-pill .pill-n{margin-left:7px;padding:1px 6px;border-radius:99px;font-size:var(--fs-1);font-weight:800;background:rgba(190, 74, 30,.22);color:var(--gold-light)}
.f-pill.on .pill-n{background:rgba(0,0,0,.22);color:var(--on-accent)}
.soa-group-row td{background:rgba(190, 74, 30,.07);border-top:2px solid rgba(190, 74, 30,.25);padding-top:10px}

/* ===== soa ===== */
.soa-bar{height:7px;background:rgba(var(--paper-rgb),.08);border-radius:99px;overflow:hidden;margin:8px 0 20px}
.soa-bar i{display:block;height:100%;background:linear-gradient(90deg,var(--gold),var(--gold-light));transition:width .6s var(--ease)}
.fw-chips{display:flex;gap:5px;flex-wrap:wrap}
.fw-chips span{font-size:var(--fs-eyebrow);letter-spacing:.08em;color:var(--paper-faint);border:1px solid var(--line);border-radius:3px;padding:2px 7px}
select.mini,input.mini,textarea.mini{background:var(--surface-raised);color:var(--paper);border:1px solid var(--line);border-radius:3px;padding:6px 8px;font-family:Manrope;font-size:var(--fs-2);transition:border-color .15s var(--ease)}
select.mini:hover,input.mini:hover,textarea.mini:hover{border-color:rgba(190, 74, 30,.6)}
/* :focus-visible, and the ring is NOT suppressed. This was
   `select.mini:focus{outline:none;border-color:var(--gold)}`, which is
   more specific than the global a/button/input/select:focus-visible
   ring above and therefore removed it from every .mini control in the
   app — the SoA's 92 status dropdowns, the framework target-level
   selects, the Settings inputs. That left a gold BORDER as the only
   keyboard-focus cue on controls that already carry a status-coloured
   border (st-Open is red-bordered, st-Implemented green), so on those
   the focused state was very nearly the unfocused one. The global rule
   documents itself as "a fallback for everything else, not a
   replacement for a better component-specific treatment" — a border
   tint that collides with the control's own status colour is not a
   better treatment. Keeping the border tint AND the ring: the ring
   does the accessibility work, the border keeps the original intent. */
select.mini:focus-visible,input.mini:focus-visible,textarea.mini:focus-visible{border-color:var(--gold)}
/* A mouse click still gets the quiet border tint on its own, which is
   what the old :focus rule was really for. */
select.mini:focus,input.mini:focus,textarea.mini:focus{border-color:var(--gold)}
select.mini:disabled,input.mini:disabled,textarea.mini:disabled{opacity:.5;cursor:not-allowed}
/* SoA Status dropdown colour-coding — reuses the same .st-* status
   tokens the chips elsewhere already use, so red/amber/green reads at
   a glance without scanning every row's text. Kept to a faint tint,
   not a full-saturation fill, so the table stays readable. */
select.mini.st-Open{color:var(--fail);border-color:rgba(201,122,122,.5);background:rgba(201,122,122,.1)}
select.mini.st-Inprogress,select.mini.st-Intreatment{color:var(--warn);border-color:rgba(240, 169, 127,.5);background:rgba(240, 169, 127,.1)}
select.mini.st-Implemented{color:var(--pass);border-color:rgba(143,191,154,.5);background:rgba(143,191,154,.1)}
select.mini.st-Notstarted{color:var(--paper-faint);border-color:var(--line);background:var(--surface-raised)}
.toggle{width:34px;height:19px;border-radius:99px;background:rgba(var(--paper-rgb),.15);border:none;position:relative;transition:background .2s var(--ease)}
/* The switch is 34x19 — under the 24px minimum WCAG 2.2 AA asks of a
   pointer target (2.5.8), and there are 93 of them in the ISO SoA
   alone, one per control row, plus the framework, Settings and Trust
   Center switches.
   The hit area is expanded rather than the switch: growing the visual
   control to 24px tall would add 5px to all 93 SoA rows and change the
   density of the table a practitioner reads all day, to fix something
   that is about where a finger or cursor lands, not about how big the
   control looks. An invisible ::before centred on the switch takes the
   target to 34x24 while the painted pill stays exactly as it was.
   ::before, because ::after is already the moving knob.
   The parent must not clip it — the switch sits in table cells with no
   overflow of their own, so nothing here needs a change beyond this. */
.toggle::before{content:"";position:absolute;left:0;right:0;top:50%;height:24px;transform:translateY(-50%)}
.toggle::after{content:"";position:absolute;top:2.5px;left:3px;width:14px;height:14px;border-radius:50%;background:var(--paper);transition:left .2s var(--ease),width .1s var(--ease)}
.toggle:hover{background:rgba(var(--paper-rgb),.24)}
.toggle.on{background:var(--gold)}
.toggle.on:hover{background:var(--gold-light)}
.toggle.on::after{left:17px}
.toggle:active::after{width:17px}
.toggle:disabled,.toggle[disabled]{opacity:.4;cursor:not-allowed;pointer-events:none}

/* ===== read-only (Viewer) sessions — UX only, never enforcement; see
   the READONLY comment in app.js. Every control applyReadOnlyUi() locks
   gets this class alongside the native disabled attribute. */
.ro-locked{opacity:.45;cursor:not-allowed!important}
.role-chip{display:none;font-size:var(--fs-eyebrow);letter-spacing:.1em;text-transform:uppercase;color:var(--gold-light);border:1px solid var(--gold);border-radius:3px;padding:3px 8px;margin-left:8px}

/* ===== reports ===== */
.rep-grid{grid-template-columns:repeat(auto-fit,minmax(260px,1fr))}
.rep-card h4{font-family:'Bricolage Grotesque', 'Helvetica Neue', Arial, sans-serif;font-weight:500;font-size:var(--fs-3);margin-bottom:8px}
.rep-card p{font-size:var(--fs-2);color:var(--paper-dim);line-height:1.7;margin-bottom:16px}
.rep-card .tagline{font-size:var(--fs-eyebrow);letter-spacing:.16em;text-transform:uppercase;color:var(--accent-ink);font-weight:700;margin-bottom:10px}

/* ===== frameworks admin ===== */
.fw-admin-row{display:flex;align-items:center;justify-content:space-between;gap:24px}
.fw-admin-row b{font-family:'Bricolage Grotesque', 'Helvetica Neue', Arial, sans-serif;font-weight:500;font-size:var(--fs-3)}
.fw-admin-tag{margin-left:10px;font-size:var(--fs-eyebrow);letter-spacing:.12em;color:var(--accent-ink);border:1px solid rgba(190, 74, 30,.5);border-radius:3px;padding:2px 6px;font-weight:700;vertical-align:middle}
.fw-admin-row p{margin-top:6px;font-size:var(--fs-2);color:var(--paper-dim);line-height:1.65;max-width:56ch}
.fw-admin-row .toggle{flex:none}
.settings-subhead{font-size:var(--fs-1);letter-spacing:.2em;text-transform:uppercase;color:var(--accent-ink);font-weight:700;margin:34px 0 6px}
.settings-subdesc{font-size:var(--fs-2);color:var(--paper-dim);max-width:56ch;line-height:1.65;margin-bottom:16px}

/* ===== drawer ===== */
/* Closed by position (off-screen) AND visibility — the same pairing,
   for the same reason, that the mobile off-canvas .side already uses
   below: an element parked off-screen is still in the tab order, so
   its controls stay keyboard-reachable while invisible (WCAG 2.1 SC
   2.4.3).

   This drawer had only the position half, and the consequence was not
   theoretical: once any record's drawer had been opened and closed,
   the first five Tab stops from the top of the page landed inside it,
   at x = 1495-1710px on a 1440px viewport — Edit, Add treatment
   action, Accept residual, Close, and Delete. Enter on that last one
   opened "Permanently remove this risk?" for a record the user could
   not see.

   visibility is transitioned at 0s but DELAYED by the slide's own
   duration when closing, so the panel stays visible for the 350ms it
   takes to slide out and only then leaves the tab order; opening
   applies it immediately so the slide-in is visible and the
   programmatic focus() on open still lands. */
.drawer{position:fixed;top:0;right:-480px;width:460px;max-width:94vw;height:100vh;background:var(--surface);border-left:1px solid var(--line);box-shadow:var(--elev-3);z-index:50;visibility:hidden;transition:right .35s var(--ease),visibility 0s linear .35s;overflow-y:auto;padding:34px}
.drawer.open{right:0;visibility:visible;transition:right .35s var(--ease),visibility 0s}
.drawer .x,.side .x{position:absolute;top:18px;right:18px;background:none;border:1px solid var(--line);color:var(--paper-dim);border-radius:3px;width:30px;height:30px;cursor:pointer;transition:border-color .15s var(--ease),color .15s var(--ease),background .15s var(--ease)}
.drawer .x:hover,.side .x:hover{border-color:var(--gold);color:var(--gold-light);background:var(--surface-hover)}
.drawer .x:active,.side .x:active{background:rgba(var(--paper-rgb),.08)}
.side .nav-close{display:none}
.drawer h2{font-family:'Bricolage Grotesque', 'Helvetica Neue', Arial, sans-serif;font-weight:500;font-size:var(--fs-4);margin:6px 40px 4px 0}
.d-sec{margin-top:22px;padding-top:18px;border-top:1px solid var(--line)}
.d-sec h4{font-size:var(--fs-eyebrow);letter-spacing:.2em;text-transform:uppercase;color:var(--accent-ink);font-weight:700;margin-bottom:12px}
.d-kv{display:flex;justify-content:space-between;font-size:var(--fs-2);padding:6px 0;color:var(--paper-dim);gap:14px}
.d-kv b{color:var(--paper);text-align:right}
.score-pair{display:flex;gap:14px}
.score-box{flex:1;border:1px solid var(--line);border-radius:6px;padding:14px;text-align:center}
.score-box b{font-size:var(--fs-5);font-weight:800;display:block}
.score-box span{font-size:var(--fs-1);letter-spacing:.01em;color:var(--paper-faint)}
.overlay,.nav-overlay{position:fixed;inset:0;background:rgba(0,0,0,.5);z-index:40;opacity:0;pointer-events:none;transition:opacity .3s var(--ease)}
.overlay.open,.nav-overlay.open{opacity:1;pointer-events:auto}

/* Mobile nav toggle — a plain hamburger button, hidden entirely on
   desktop (§ sidebar media query below shows it and turns .side into
   a slide-in drawer instead of a permanent grid column). A direct
   child of #appShell, NOT of .top — .top is itself a positioned
   (sticky) stacking context, and a z-index on a descendant can never
   escape its own ancestor's stacking context no matter how high it's
   set, so nesting the toggle inside .top would trap it below .side/
   .nav-overlay regardless of its own z-index. Fixed-positioned here
   instead, it competes directly with them and safely wins. */
.nav-toggle{display:none;background:none;border:1px solid var(--line);color:var(--paper-dim);border-radius:4px;width:36px;height:36px;align-items:center;justify-content:center;transition:border-color .15s var(--ease),color .15s var(--ease)}
.nav-toggle:hover{border-color:var(--gold);color:var(--gold-light)}
.nav-toggle:active{background:var(--surface-hover)}

/* ===== toast ===== */
.toast{position:fixed;bottom:26px;left:50%;transform:translateX(-50%) translateY(20px);background:var(--surface-raised);border:1px solid rgba(240, 169, 127,.4);color:var(--paper);border-radius:var(--r-sm);padding:13px 22px;font-size:var(--fs-2);opacity:0;transition:opacity .35s var(--ease),transform .35s var(--ease);z-index:60;pointer-events:none;box-shadow:var(--elev-3)}
.toast.show{opacity:1;transform:translateX(-50%) translateY(0)}
/* A failure must not look like a success. The default toast reads as
   confirmation — gold accent, quiet border — which is exactly wrong for
   "that didn't save". The error variant borders and accents in the fail
   colour and is announced assertively (see toast() in app.js). */
.toast.err{border-color:rgba(212,106,106,.65);box-shadow:var(--elev-3),inset 3px 0 0 var(--fail)}
.toast.err b{color:var(--fail)}

/* ===== modal (replaces prompt()/confirm()) ===== */
.modal-overlay{display:none;position:fixed;inset:0;background:rgba(11,11,12,.75);z-index:90}
.modal-overlay.open{display:block}
.modal-box{display:none;position:fixed;top:50%;left:50%;transform:translate(-50%,-50%);background:var(--surface);border:1px solid var(--line);border-radius:var(--r-lg);padding:31px;max-width:460px;width:calc(100% - 40px);z-index:91;box-shadow:var(--elev-3);max-height:calc(100vh - 40px);overflow-y:auto}
.modal-box.open{display:block}
.modal-box h3{font-family:'Bricolage Grotesque', 'Helvetica Neue', Arial, sans-serif;font-weight:500;font-size:var(--fs-4);margin-bottom:10px;color:var(--paper)}
.modal-box .m-msg{color:var(--paper-dim);font-size:var(--fs-2);line-height:1.6;margin-bottom:14px;white-space:pre-line}
.modal-box .m-field{margin-bottom:12px}
.modal-box .m-field label{display:block;font-size:var(--fs-1);letter-spacing:.01em;color:var(--paper-faint);margin-bottom:6px}
.modal-box .m-field input,.modal-box .m-field textarea,.modal-box .m-field select{width:100%;background:var(--surface-raised);color:var(--paper);border:1px solid var(--line);border-radius:3px;padding:8px 10px;font-family:Manrope;font-size:var(--fs-2);transition:border-color .15s var(--ease)}
.modal-box .m-field.m-check{display:flex;gap:10px;align-items:flex-start;margin-bottom:8px}
.modal-box .m-field.m-check input{width:auto;flex:0 0 auto;margin-top:3px;accent-color:var(--gold)}
.modal-box .m-field.m-check label{margin:0;color:var(--paper);line-height:1.5;font-size:var(--fs-2)}
.modal-box .m-field input:hover,.modal-box .m-field textarea:hover,.modal-box .m-field select:hover{border-color:rgba(190, 74, 30,.6)}
.modal-box .m-field input:focus,.modal-box .m-field textarea:focus,.modal-box .m-field select:focus{outline:none;border-color:var(--gold)}
.modal-box .m-field textarea{min-height:70px;resize:vertical}
.modal-box .m-error{color:var(--fail);font-size:var(--fs-1);margin-top:4px;display:none}
.modal-box .m-error.show{display:block}
.modal-box .m-btns{display:flex;gap:10px;margin-top:18px;justify-content:flex-end}
.toast b{color:var(--gold-light)}
.filters{display:flex;gap:8px;margin-bottom:16px;flex-wrap:wrap}
.f-pill{background:none;border:1px solid var(--line);color:var(--paper-dim);border-radius:3px;padding:7px 14px;font-size:var(--fs-1);letter-spacing:.06em;transition:border-color .15s var(--ease),background .15s var(--ease),color .15s var(--ease),transform .1s var(--ease)}
.f-pill:hover{border-color:var(--gold);color:var(--gold-light)}
.f-pill:active{transform:translateY(1px)}
.f-pill:disabled,.f-pill[disabled]{opacity:.4;cursor:not-allowed;pointer-events:none}
.f-pill.on{background:var(--accent-fill);border-color:var(--accent-fill);color:var(--on-accent);font-weight:700}

/* ===== Cross-framework mapping (formerly "Control Constellation") =====
   Was a radial SVG network graph — these classes were its node/edge/
   legend styling, applied per-render and toggled on hover/selection.
   Replaced with a plain table (app.js's renderConstellation()), which
   needs none of that: status reuses the same st-* chip classes every
   other control table in the app already has, and "also satisfies"
   reuses .fw-chips (below), the same pill style the Statement of
   Applicability already uses for a control's raw cross-framework map
   list. Only the toolbar/count layout survives from the old view. */
.cx-toolbar{display:flex;justify-content:space-between;align-items:center;gap:16px;flex-wrap:wrap;margin-bottom:4px}
.cx-toolbar .filters{margin-bottom:0}
/* When nothing can cross-map, the view explains why and offers the way
   forward instead of drawing a table of empty cells. Sized like prose,
   not like a data panel, because that is what it is. */
.cx-empty{max-width:70ch}
.cx-empty h3{margin-bottom:10px}
.cx-empty p{color:var(--paper-dim);font-size:var(--fs-2);line-height:1.65;margin:0 0 12px}
.cx-empty p:last-child{margin-bottom:0}
.cx-empty a{color:var(--gold-light)}
.cx-empty a.btn{color:var(--on-accent)}
/* The controls with no overlap are still reachable, just not in the way
   of the ones that matter — collapsed by default. */
/* Statement of Applicability family headers. Sticky, because the whole
   point is knowing which family you are in 60 rows down; the offset
   clears the table's own sticky <thead>. Sits ON the table surface
   rather than in a card of its own so it reads as part of the list. */
/* ===== Sticky column headers — why this is behind a media query =====
   These offsets are relative to the WINDOW, which is what scrolls: the
   sidebar is sticky/100vh and .main does not scroll on its own.

   .card carries overflow-x:auto so a wide register scrolls inside its
   own card on a narrow viewport. CSS resolves an overflow of `visible`
   on one axis to `auto` when the other axis is not visible, so that one
   declaration silently made every .card a scroll container on BOTH
   axes — and position:sticky resolves against the nearest scrollport,
   not the window. Those cards are as tall as their content and
   therefore never scroll vertically, so a sticky header inside one was
   pinned to a scrollport that cannot move: inert, in every register, at
   every width. The family headers below have been sticky (and have
   never once stuck) since they were written; the 38px offset in that
   rule was reserving room for a sticky <thead> that was never declared
   at all.

   Restoring the window as the sticky container means the card must stop
   being a scroll container, which it can only afford where the table
   already fits: measured across every register, nothing overflows at
   1400px and up, the Actions register (11 columns) overflows by ~134px
   at 1280 and ~390px at 1024. So the trade is made on width — above
   1400px the headers stick and no table needs to scroll sideways; below
   it the card keeps its horizontal scroll and the headers behave as
   they always have. Scoped with :has(table) so only the cards this is
   about lose their overflow, never the ones wrapping a chart.

   z-index: the topbar is 20 and must stay above both; the <thead> sits
   above the family rows, which sit above ordinary cells. */
@media (min-width:1400px){
  .card:has(table){overflow:visible}
  /* --bulk-h is 0 unless a bulk bar is showing above this table, in
     which case renderSoaBulkBar() sets it to the bar's measured height
     on the view. Three sticky things then stack in DOM order rather
     than landing on each other: topbar, bulk bar, <thead>, family
     header. Measured rather than a constant because the bar wraps to
     two lines on a narrow desktop, and a hard-coded height would let
     the column names slide under it exactly when the bar is tallest. */
  .card:has(table) thead th{position:sticky;top:calc(var(--topbar-h) + var(--bulk-h));z-index:3;background:var(--surface)}
  .card:has(table) .soa-family th{top:calc(var(--topbar-h) + var(--bulk-h) + var(--thead-h))}
}
/* ===== The same headers, below 1400px, by a different mechanism =====
   Above 1400 the window is the scrollport and the headers offset down
   from the topbar. That only works because the card can afford to stop
   clipping: every register's table fits at that width. Below it they do
   not — the Actions register's 11 columns overflow by ~134px at 1280 and
   ~363px at 1024 — so the card has to clip something.

   Measured, because the obvious fixes do not work. position:sticky
   resolves against the NEAREST scrollport, so any clipping ancestor
   captures it:
     overflow-x:auto alone     — y computes to auto, the card becomes a
                                 scrollport that never scrolls
                                 vertically, and the header is pinned to
                                 something that cannot move: inert. This
                                 is what shipped, and why the headers
                                 did nothing below 1400.
     overflow-x:auto + y:clip  — clip computes to hidden; still a
                                 scrollport; still inert (verified).
     overflow:visible          — the header DOES stick, and the page
                                 gains 363px of horizontal scroll at
                                 1024, which is worse than the problem.

   So below 1400 the card becomes the scrollport deliberately — a
   bounded pane, at most the viewport minus the topbar — and the header
   sticks to the TOP OF THE PANE rather than to the topbar. The table
   scrolls both ways inside it, the page never scrolls sideways, and the
   column names stay put. max-height only ever constrains, so a short
   table (a three-row summary, a report card) never becomes a pane at
   all.

   The bulk bar goes static here. It sticks to the WINDOW, and the pane
   scrolls under it — so a sticky bar would sit over the very column
   names this block exists to keep visible. Of the two, the header is
   needed continuously; the bar is used once per selection. */
@media (max-width:1399px){
  .card:has(table){overflow:auto;max-height:calc(100vh - var(--topbar-h) - 28px)}
  .card:has(table) thead th{position:sticky;top:0;z-index:3;background:var(--surface)}
  /* Relative to the pane, not the page: the thead above it is the only
     thing this has to clear. */
  .card:has(table) .soa-family th{top:var(--thead-h)}
  .bulk-bar{position:static}
}
.soa-family th{position:sticky;top:var(--thead-h);z-index:2;background:var(--surface-raised);border-top:1px solid var(--line);border-bottom:1px solid var(--line);padding:11px 14px;text-transform:none;letter-spacing:0}
.soa-family span{color:var(--accent-ink);font-size:var(--fs-2);font-weight:700;letter-spacing:.02em}
.soa-family b{float:right;color:var(--paper-faint);font-size:var(--fs-1);font-weight:600}
.cx-rest{margin-top:16px}
.cx-rest>summary{cursor:pointer;color:var(--paper-faint);font-size:var(--fs-2);letter-spacing:.01em;font-weight:700;padding:10px 0}
.cx-rest>summary:hover{color:var(--gold-light)}
.cx-rest[open]>summary{margin-bottom:10px}
.cx-count{color:var(--paper-faint);font-size:11.5px;margin:6px 0 14px}
.cx-thumb-card{overflow:hidden}
.cx-thumb-link{display:block;color:inherit;text-decoration:none}

/* ===== Compliance Fingerprint + Certification Journey =====
   Both cards render window.ReportEngine.charts.fingerprint()/journey()
   output straight into the DOM (see app.js's renderComplianceFingerprint/
   renderCertificationJourney) — the SVG markup itself carries the dark
   'app' palette already baked in as literal fill/stroke colors (report.js
   knows nothing about this file's CSS custom properties), so the CSS
   here only ever targets structural classes (.rpt-fp-num for the
   count-up hook, .rpt-journey-pulse for the "you are here" marker),
   never re-colors chart geometry. */
.fp-head{display:flex;justify-content:space-between;align-items:flex-start;flex-wrap:wrap;gap:10px;margin-bottom:10px}
.fp-head .filters{margin-bottom:0}
.fp-body{max-width:260px;margin:0 auto}
.fp-body svg{display:block}
.fp-body [data-tip]{cursor:pointer}
.fp-caption{text-align:center;color:var(--paper-faint);font-size:11.5px;margin-top:8px}
.journey-body svg{display:block;width:100%;height:auto}
.journey-body [data-tip]{cursor:pointer}
.journey-note{color:var(--paper-dim);font-size:12.5px;margin-top:10px;line-height:1.6}
.journey-note b{color:var(--paper)}
@keyframes rptJourneyPulse{0%{r:6;opacity:1}50%{r:10;opacity:.55}100%{r:6;opacity:1}}
.rpt-journey-pulse{animation:rptJourneyPulse 2.4s ease-in-out infinite}
@media(max-width:1000px){.fp-card,.journey-card{grid-column:1}}

/* Single shared floating tooltip (app.js's initSvgTooltip/showTip) —
   one DOM node reused by every SVG viz on the page rather than a
   per-chart tooltip element. Position is set inline (left/top) by JS;
   this only owns appearance + the fade. */
.svg-tip{position:fixed;z-index:80;background:var(--surface-raised);border:1px solid var(--line);border-radius:var(--r-sm);padding:6px 10px;font-size:11.5px;color:var(--paper);box-shadow:var(--elev-2);pointer-events:none;opacity:0;transition:opacity .12s var(--ease);max-width:240px}
.svg-tip.on{opacity:1}
.pc-fp-glyph{width:60px;height:60px}
.pc-fp-glyph svg{display:block;width:100%;height:100%}

/* ===== Assurance Pulse + Risk Landscape =====
   Both render window.ReportEngine.charts.activityGrid()/riskLandscape()
   straight into the DOM, same "SVG carries its own literal colors,
   this CSS only ever targets structural classes" split as the
   Fingerprint/Journey cards above. */
.ap-body{overflow-x:auto}
.ap-body svg{display:block;min-width:520px}
.ap-body [data-tip]{cursor:pointer}
@keyframes rptAgFadeIn{from{opacity:0;transform:scale(.6)}to{opacity:1;transform:scale(1)}}
.rpt-ag-cell{opacity:0;transform-origin:center;transform-box:fill-box;animation:rptAgFadeIn .35s var(--ease) forwards}
.rl-head{display:flex;justify-content:space-between;align-items:center;flex-wrap:wrap;gap:8px;margin-bottom:10px}
.rl-head .filters{margin-bottom:0}
#rlLandscapeWrap svg{display:block;max-width:340px;margin:0 auto}
.rpt-rl-bubble{cursor:pointer;transition:opacity .15s var(--ease)}
.rpt-rl-bubble:hover,.rpt-rl-bubble:focus-visible{opacity:1;outline:none}
.rpt-rl-trail{opacity:.5}
.feed-filter-chip{display:inline-flex;align-items:center;gap:6px;background:rgba(190, 74, 30,.14);border:1px solid rgba(190, 74, 30,.4);color:var(--gold-light);border-radius:99px;padding:3px 10px 3px 12px;font-size:10.5px;letter-spacing:.04em}
.feed-filter-chip button{background:none;border:none;color:inherit;cursor:pointer;font-size:13px;line-height:1;padding:0 0 0 4px}

/* ===== mobile (<=860px): sidebar becomes an off-canvas drawer =====
   Below this width there isn't room for a permanent 236px column next
   to real content, so .side stops being a grid column at all and
   becomes a fixed-position slide-in panel — the exact same internal
   layout (.nav-scroll flex:1 + overflow-y:auto, vertical .nav-group
   stacking) it already uses on desktop, just positioned off-screen by
   default and toggled via App.toggleNav()/the hamburger button in
   .top. Closed by transform (off-screen) AND visibility (so its
   buttons drop out of tab order while hidden, not just out of view —
   WCAG 2.1 SC 2.4.3), opened by both switching back. */
@media(max-width:860px){
  .app{grid-template-columns:1fr}
  .nav-toggle{display:flex;position:fixed;top:12px;left:14px;z-index:56}
  .side .nav-close{display:block}
  .side{position:fixed;top:0;left:0;height:100vh;width:min(84vw,300px);z-index:55;transform:translateX(-100%);visibility:hidden;transition:transform .3s var(--ease);box-shadow:0 0 0 transparent}
  .side.open{transform:translateX(0);visibility:visible;box-shadow:var(--elev-3)}
  .top{flex-wrap:wrap;padding:14px 16px 14px 60px;gap:10px}
  .top .client{min-width:0}
  .top .modenote{display:none}
  .gsearch{order:3;flex-basis:100%;max-width:none;margin:0}
  .view{padding:18px}
  .vhead h1{font-size:var(--fs-4)}
  /* every "+ Add X" panel's 2-column form (risks/actions/audits/
     reviews/calendar/vendors/AI systems all share this exact inline
     style) — one column is more usable than two ~150px-wide inputs
     side by side. */
  [style="grid-template-columns:1fr 1fr;gap:12px"]{grid-template-columns:1fr!important}
}

/* ===== Scan drift ("Changed since the previous scan") =====
   Grouped by direction rather than interleaved, because the question
   a practitioner opens this card with is "did anything get worse",
   and an improvement sitting between two regressions makes that
   harder to answer, not easier. Groups render in the order
   regressed → stopped answering → improved → started answering
   again; see renderScanDrift(). */
.drift-caption{color:var(--paper-dim);font-size:var(--fs-1);letter-spacing:.04em;margin-bottom:14px}
.drift-group + .drift-group{margin-top:18px}
.drift-head{font-size:var(--fs-eyebrow);letter-spacing:.14em;text-transform:uppercase;color:var(--paper-dim);margin-bottom:8px}
.drift-head-bad{color:var(--fail)}
.drift-head-good{color:var(--pass)}
.drift-note{color:var(--paper-dim);font-size:var(--fs-1);line-height:1.55;margin:-4px 0 8px;max-width:64ch}
.drift-row{display:flex;align-items:baseline;justify-content:space-between;gap:16px;padding:8px 0;border-top:1px solid var(--line)}
.drift-row .drift-label{font-size:var(--fs-2);line-height:1.5}
.drift-row .drift-move{font-size:var(--fs-1);white-space:nowrap;color:var(--paper-dim);letter-spacing:.04em}
.drift-row .drift-move i{font-style:normal}
.drift-row .drift-move b{font-weight:600}
.drift-none{color:var(--paper-dim);font-size:var(--fs-2);line-height:1.6;max-width:64ch}
@media (max-width:560px){
  /* the label wraps at this width, and a right-aligned status pair
     on its own baseline reads as belonging to the wrong row. */
  .drift-row{flex-direction:column;gap:2px}
  .drift-row .drift-move{white-space:normal}
}

/* ===== Registers: table first ===== */
.reg-strip{margin-bottom:12px;grid-template-columns:repeat(auto-fit,minmax(150px,1fr));gap:10px}
.reg-strip .kpi{padding:12px 14px}
.reg-strip .kpi b{font-size:var(--fs-4,24px)}
.reg-strip .kpi span{margin-top:3px}
.reg-strip .kpi .sub{margin-top:3px}
.reg-strip .kpi-meter{padding-top:8px}
.reg-tidy{display:flex;flex-wrap:wrap;align-items:center;gap:6px 8px;margin:0 0 14px;font-size:var(--fs-1)}
.reg-tidy-h{font-size:var(--fs-eyebrow);letter-spacing:.14em;text-transform:uppercase;color:var(--warn);font-weight:700;margin-right:4px}
.tidy-item{font:inherit;font-size:var(--fs-1);padding:4px 10px;border-radius:99px;border:1px solid rgba(224,138,110,.45);background:none;color:var(--paper-dim);cursor:pointer}
.tidy-item:hover,.tidy-item:focus-visible{color:var(--paper);border-color:var(--warn)}
.tidy-item.on{background:var(--warn);color:var(--on-accent,#111);border-color:var(--warn)}
.reg-charts{margin-top:18px}
.reg-charts > summary{cursor:pointer;list-style:none;display:flex;flex-direction:column;gap:2px}
.reg-charts > summary::-webkit-details-marker{display:none}
.reg-charts > summary h3{margin:0}
.reg-charts > summary h3::after{content:' \25BE';color:var(--paper-faint)}
.reg-charts[open] > summary{margin-bottom:14px}
.treat-cell{display:flex;flex-direction:column;gap:3px;min-width:110px;font-size:var(--fs-1)}
.treat-bar{display:block;height:4px;border-radius:3px;background:rgba(var(--paper-rgb),.12);overflow:hidden}
.treat-bar b{display:block;height:100%;background:var(--pass)}
.inline-edit{width:100%;min-width:0;max-width:130px;font-size:var(--fs-1);padding:4px 5px}
select.inline-edit{max-width:112px}
.grp-row td{background:rgba(var(--paper-rgb),.04);padding-top:12px;font-size:var(--fs-2)}
.vtier{display:flex;flex-direction:column;gap:6px;font-size:12.5px}
.vtier label{display:flex;gap:8px;align-items:center}

/* Simple menu: specialist views hidden (applyNavMode()). */
body.nav-simple .nav-full{display:none!important}
.nav-mode{display:block;width:100%;background:none;border:none;text-align:left;padding:8px 22px;font:inherit;font-size:var(--fs-1);color:var(--paper-faint);cursor:pointer;text-decoration:underline;text-decoration-color:transparent}
.nav-mode:hover,.nav-mode:focus-visible{color:var(--gold-light);text-decoration-color:currentColor}

/* SoA: one line per control. */
.soa-title{color:var(--paper);max-width:420px}
.soa-title .soa-just{max-width:420px;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
#soaRows td{padding-top:7px;padding-bottom:7px}
#soaRows .fw-chips span{font-size:10px;padding:1px 5px}

/* Settings in sections. */
.settings-bar{display:flex;gap:12px;align-items:center;flex-wrap:wrap;margin:0 0 16px}
.settings-bar .filters{margin-bottom:0}
#settingsSearch{min-width:220px;margin-left:auto}
.set-sec[hidden]{display:none}

/* Dashboard: secondary sections folded. */
.dash-more{margin-top:22px;border-top:1px solid var(--line);padding-top:14px}
.dash-more > summary{cursor:pointer;list-style:none}
.dash-more > summary::-webkit-details-marker{display:none}
.dash-more > summary h2{display:inline;margin:0}
.dash-more > summary h2::after{content:' \25BE';color:var(--paper-faint);font-size:.7em}
.dash-more[open] > summary h2::after{content:' \25B4'}
.dash-more > summary p{color:var(--paper-dim);font-size:var(--fs-2);margin:4px 0 0}
.dash-more[open] > summary{margin-bottom:14px}

.soa-maps{flex-wrap:nowrap;white-space:nowrap}
#soaRows .soa-row td:nth-child(6){white-space:nowrap}
.soa-fam-toggle{display:flex;justify-content:space-between;align-items:center;width:100%;background:none;border:none;font:inherit;color:inherit;cursor:pointer;padding:0;text-align:left}
.soa-fam-toggle b{display:inline-flex;align-items:center;gap:6px}
.soa-fam-flag{font-style:normal;color:var(--warn)}
.soa-tools td{padding:6px 10px !important}

/* Evidence check and mock audit. */
.ev-check-head{display:flex;justify-content:space-between;align-items:center;gap:12px;flex-wrap:wrap}
.ev-check-head > span{display:flex;flex-direction:column;gap:3px;flex:1;min-width:240px}
.ev-check-list{margin-top:8px}
.ev-check-list > summary{cursor:pointer}
.mock-score{display:flex;gap:16px;align-items:center;margin:14px 0}
.mock-score > b{font-size:var(--fs-hero,44px);font-weight:800;line-height:1}
.mock-score > span{display:flex;flex-direction:column;gap:4px;color:var(--paper)}

.doc-tools{margin-top:18px}
.doc-tools > .reg-charts{margin-top:12px}

/* Retired assets, record history, leavers, register review (1.126.0) */
tr.asset-retired td { opacity: .85; }
.hist-row { align-items: flex-start; padding: 6px 0; border-bottom: 1px solid var(--line); }
.hist-row > span { min-width: 96px; }
.hist-row > b { font-weight: 400; text-align: left; color: var(--paper); }
.d-sec.leaver h4 { display: flex; gap: 8px; align-items: center; flex-wrap: wrap; }
.rr-card h3 { font-size: 16px; }
.asset-queue { display: flex; align-items: center; gap: 10px; flex-wrap: wrap; padding: 10px 14px; margin-bottom: 10px; font-size: 13px; }
.asset-queue > span { flex: 1; min-width: 240px; }
.lnk.asset-name { color: inherit; text-align: left; font: inherit; }
