/* ==========================================================================
   GuideHerd Product Design System (ADR-0019)

   The permanent visual language for every GuideHerd application. It once
   mirrored the guideherd.ai marketing site; as of #261 the PRODUCT has its
   own identity — the "workbench" — deliberately distinct from marketing
   while staying recognisably GuideHerd (see the token block below). Layered:
     - #261 tokens: the cool-neutral, sans, compact, precise visual language.
     - #262 components + application-layout primitives (wide, multi-column).
     - #263 application shell (top bar + nav rail + main working area).
   Calm, dense, operator-focused — an operational workspace, not a document.

   Rules of the house:
   - This file is the single source of the product's look. Applications
     consume tokens and `gh-` components; they do NOT redefine palette,
     type, or spacing. Page stylesheets may only COMPOSE these primitives
     and add page-specific layout (the design-system gate enforces this).
   - Everything themes through custom properties: dark mode is a token
     override away (see the [data-theme="dark"] block), no component
     changes required.
   ========================================================================== */

/* ---------- TOKENS ---------- */
/* ==========================================================================
   PRODUCT DESIGN LANGUAGE — "workbench" (GitLab #261)

   This is the product's OWN visual identity, deliberately distinct from the
   guideherd.ai marketing system while staying recognisably GuideHerd. The
   marketing site is warm paper, Fraunces serif display, and pill buttons —
   editorial, spacious, promotional. The application is a staff tool: a calm
   cool-neutral foundation, one restrained sans voice, compact type, precise
   corners, and colour reserved for state — an advanced workbench, not a
   consumer app.

   Only the token VALUES change here. Every token NAME is stable, so all
   seven product surfaces and every component below inherit the new identity
   as composition, not rework (that is the point of doing this at the token
   layer first — #262 components and #263 shell build on top without
   re-inventing anything). The design-system gate keeps pages from ever
   defining their own colour, font, or token, so this file is the single
   source of the product's look.

   Brand kinship is carried by the accent (GuideHerd teal) and the cool,
   slightly-navy ink — the product and the website read as one family. What
   is dropped is everything that made the app look like a brochure: the warm
   cream surfaces, the serif headlines, the 40px display numbers, the pills.

   Contrast: every token that lands on TEXT holds WCAG 2.2 AA (>=4.5:1); the
   axe-core suite (#143) enforces this across every surface state.
   ========================================================================== */
:root {
  /* Brand ink — a cool, faintly-navy near-black. Kin to the marketing
     navy, cooled and desaturated for long reading on a neutral ground. */
  --ink:        #17212B;
  --ink-soft:   #3A4650;
  /* The ink alphas are washes and hairlines (borders, rails, hovers), not
     text — they are free to fall below the text ratio. */
  --ink-60:     rgba(23, 33, 43, 0.60);
  --ink-40:     rgba(23, 33, 43, 0.40);
  --ink-20:     rgba(23, 33, 43, 0.20);
  --ink-10:     rgba(23, 33, 43, 0.10);
  --ink-05:     rgba(23, 33, 43, 0.05);

  /* Foundation — cool neutral. The page is a soft off-white; raised
     surfaces are clean white; there is no warmth and no decoration. */
  --paper:      #F6F7F9;
  --paper-2:    #EDEFF2;
  --paper-3:    #E1E5EA;
  --cream:      #FFFFFF;

  /* The one primary accent — GuideHerd teal, product-tuned. --accent is a
     fill/indicator only (dots, rails, the focus ring, the spinner) and is
     never text; --accent-ink is the readable teal for text and links. */
  --accent:     #189E9B;
  --accent-deep:#0F807D;
  --accent-ink: #0B6360;   /* 6.2:1 on paper — the text-safe teal */

  /* Semantic bases. Kept as named tokens so the status mappings below read
     as intent, not hex. These are STATE colours; nothing decorative uses
     them. (--warm/--red-flag retain their names for one referrer each.) */
  --warm:       #D09628;   /* warning base (amber) */
  --warm-ink:   #8A5300;   /* warning text — 6.5:1 on paper */
  --red-flag:   #B3261E;   /* error — 7.0:1 on paper */

  /* Semantic status — success | warning | error | info | muted.
     Colour communicates state and nothing else. */
  --status-ok:       var(--accent-ink);
  --status-ok-bg:    rgba(24, 158, 155, 0.12);
  --status-warn:     var(--warm-ink);
  --status-warn-bg:  rgba(208, 150, 40, 0.18);
  --status-bad:      var(--red-flag);
  --status-bad-bg:   rgba(179, 38, 30, 0.10);
  --status-info:     #185EA6;              /* 6.4:1 on paper */
  --status-info-bg:  rgba(24, 94, 166, 0.10);
  --status-mute:     #55606B;              /* neutral state — 5.8:1 on paper */
  --status-mute-bg:  var(--ink-05);

  /* Surfaces (what components actually reference) */
  --surface:        var(--paper);
  --surface-raised: var(--cream);
  --surface-alt:    var(--paper-2);
  --line:           var(--ink-10);
  --line-strong:    var(--ink-20);
  --line-heavy:     var(--ink);
  --text:           var(--ink);
  --text-soft:      var(--ink-soft);
  /* SECONDARY TEXT IS STILL TEXT (#143): section notes, field labels, the
     header surface-name, the signed-in identity. A solid cool grey at
     5.8:1 on paper / 6.2:1 on white — read as a value, not derived from an
     ink alpha, so its contrast never drifts with the ink. */
  --text-dim:         #55606B;
  --text-placeholder: #6A7480;   /* 4.8:1 on white — placeholders are read too */

  /* Type — ONE sans voice. The product retires the marketing serif: the
     display token now resolves to the same highly-readable sans, so every
     heading, stat number, and dialog title that referenced --serif becomes
     workbench sans with no markup change. --serif is kept as an alias so
     nothing breaks; #262 migrates call-sites to --display and adds weight
     for hierarchy. */
  --display: "Inter Tight", "Inter", ui-sans-serif, system-ui, sans-serif;
  --sans:    "Inter Tight", "Inter", ui-sans-serif, system-ui, sans-serif;
  --serif:   var(--display);
  --mono:    "JetBrains Mono", ui-monospace, Menlo, monospace;

  /* Compact, deliberate scale. Body drops to 15px for density; the display
     step drops from 40px to 30px so headings and figures stop consuming
     vertical space. Strong hierarchy comes from weight and spacing in
     #262, not from size. */
  --fs-xs:   11px;   /* metadata, labels, eyebrows */
  --fs-sm:   13px;   /* dense body, tables, controls */
  --fs-base: 15px;   /* body */
  --fs-md:   17px;   /* section body lead */
  --fs-lg:   20px;   /* section heading */
  --fs-xl:   24px;   /* page title */
  --fs-2xl:  30px;   /* the largest figure we allow */

  /* Spacing — 8px base, unchanged so no layout shifts under the reskin;
     density is bought with type and radius here, and padding in #262. */
  --sp-1: 8px;
  --sp-2: 16px;
  --sp-3: 24px;
  --sp-4: 32px;
  --sp-5: 48px;
  --sp-6: 64px;
  --sp-7: 96px;

  /* Shape — precise, not rounded. The pill is retired; buttons take the
     same small control radius as everything else. (--radius-pill keeps its
     name for its three referrers; #262 renames it to --radius-control.) */
  --radius-card:   6px;   /* cards, panels, dialogs */
  --radius-badge:  3px;   /* badges & tags */
  --radius-pill:   6px;   /* controls — buttons, inputs */
  --radius-input:  6px;   /* reserved alias for form controls (#262) */

  /* Elevation — reserved for #262 to lift raised surfaces off the page so
     the app can lean on light shadow instead of a border on every card.
     Nothing renders these yet. */
  --elevation-1:       0 1px 2px rgba(23, 33, 43, 0.06), 0 1px 1px rgba(23, 33, 43, 0.04);
  --elevation-2:       0 6px 16px rgba(23, 33, 43, 0.08), 0 2px 4px rgba(23, 33, 43, 0.05);
  --elevation-popover: 0 12px 32px rgba(23, 33, 43, 0.16);
  --overlay:           rgba(23, 33, 43, 0.45);   /* dialog/scrim backdrop */

  /* LEX — reserved assistive layer (#268). Lex will surface contextual
     suggestions, summaries, recommended actions, and inline assistance; it
     must be visually distinct from both the brand accent and every state
     colour so a suggestion never reads as chrome or as an error. A
     restrained indigo, held apart on the wheel from teal/amber/red/blue.
     These tokens exist so the primitive has a home; #261 renders NONE of
     them — no behaviour, no permanent AI surface, by design. */
  --lex-accent:     #5A5BD6;
  --lex-accent-ink: #4442B0;                     /* 7.5:1 on paper */
  --lex-surface:    rgba(90, 91, 214, 0.06);     /* inline suggestion wash */
  --lex-line:       rgba(90, 91, 214, 0.30);     /* suggestion rule/hairline */

  /* Focus — one visible language everywhere: a ring in the page ground
     then the accent, so it reads on any surface. */
  --focus-ring: 0 0 0 2px var(--surface), 0 0 0 4px var(--accent-deep);
}

/* Dark mode — the full token remap onto a cool slate ground. It is LIVE, in
   three states (the platform-standard pattern):
     1. :root above is the light default.
     2. @media (prefers-color-scheme: dark) below applies dark automatically
        for anyone whose OS is in dark mode — UNLESS they've explicitly chosen
        light (data-theme="light"), which must win over the OS.
     3. [data-theme="dark"] lets an explicit choice force dark even on a light
        OS. A small persisted toggle sets data-theme; with nothing set, the OS
        governs and there is no flash.
   No component below changes for any of this — it is entirely token-driven.
   The two dark blocks carry identical values: KEEP THEM IN SYNC. */
@media (prefers-color-scheme: dark) {
  :root:not([data-theme="light"]) {
    color-scheme: dark;
    --ink:        #E6EBF0;
    --ink-soft:   #C2CAD2;
    --ink-60:     rgba(230, 235, 240, 0.60);
    --ink-40:     rgba(230, 235, 240, 0.40);
    --ink-20:     rgba(230, 235, 240, 0.22);
    --ink-10:     rgba(230, 235, 240, 0.12);
    --ink-05:     rgba(230, 235, 240, 0.06);
    --paper:      #12191F;
    --paper-2:    #0D141A;
    --paper-3:    #1F2933;
    --cream:      #171F27;
    --text-dim:         #9AA6B2;
    --text-placeholder: #7E8A96;
    /* Teal TEXT lifts for the dark ground (7.4:1), like the state colours. */
    --accent-ink:      #3FC1BD;
    --status-ok:       #33C3BF;
    --status-ok-bg:    rgba(24, 158, 155, 0.20);
    --status-warn:     #E0A93B;
    --status-warn-bg:  rgba(208, 150, 40, 0.18);
    --status-bad:      #E7695C;
    --status-bad-bg:   rgba(179, 38, 30, 0.28);
    --status-info:     #6FA8E8;
    --status-info-bg:  rgba(24, 94, 166, 0.24);
    --status-mute:     #9AA6B2;
    --lex-accent-ink:  #ACADF2;
    --lex-surface:     rgba(120, 121, 230, 0.14);
    --lex-line:        rgba(120, 121, 230, 0.40);
    --overlay:         rgba(3, 8, 12, 0.55);
    --elevation-1:       0 1px 2px rgba(0, 0, 0, 0.30);
    --elevation-2:       0 6px 16px rgba(0, 0, 0, 0.40);
    --elevation-popover: 0 12px 32px rgba(0, 0, 0, 0.55);
    --focus-ring: 0 0 0 2px var(--surface), 0 0 0 4px var(--accent);
  }
}
[data-theme="dark"] {
  color-scheme: dark;
  --ink:        #E6EBF0;
  --ink-soft:   #C2CAD2;
  --ink-60:     rgba(230, 235, 240, 0.60);
  --ink-40:     rgba(230, 235, 240, 0.40);
  --ink-20:     rgba(230, 235, 240, 0.22);
  --ink-10:     rgba(230, 235, 240, 0.12);
  --ink-05:     rgba(230, 235, 240, 0.06);
  --paper:      #12191F;
  --paper-2:    #0D141A;
  --paper-3:    #1F2933;
  --cream:      #171F27;
  --text-dim:         #9AA6B2;   /* 5.9:1 on the dark ground */
  --text-placeholder: #7E8A96;
  /* Teal TEXT and the state colours lift for the dark ground; the teal
     FILL accent is unchanged. */
  --accent-ink:      #3FC1BD;
  --status-ok:       #33C3BF;
  --status-ok-bg:    rgba(24, 158, 155, 0.20);
  --status-warn:     #E0A93B;
  --status-warn-bg:  rgba(208, 150, 40, 0.18);
  --status-bad:      #E7695C;
  --status-bad-bg:   rgba(179, 38, 30, 0.28);
  --status-info:     #6FA8E8;
  --status-info-bg:  rgba(24, 94, 166, 0.24);
  --status-mute:     #9AA6B2;
  --lex-accent-ink:  #ACADF2;
  --lex-surface:     rgba(120, 121, 230, 0.14);
  --lex-line:        rgba(120, 121, 230, 0.40);
  --overlay:         rgba(3, 8, 12, 0.55);
  --elevation-1:       0 1px 2px rgba(0, 0, 0, 0.30);
  --elevation-2:       0 6px 16px rgba(0, 0, 0, 0.40);
  --elevation-popover: 0 12px 32px rgba(0, 0, 0, 0.55);
  --focus-ring: 0 0 0 2px var(--surface), 0 0 0 4px var(--accent);
}
/* An explicit light choice also pins the native control scheme to light,
   so it wins over a dark OS completely. */
:root[data-theme="light"] { color-scheme: light; }

/* ---------- RESET (the site's reset, verbatim in spirit) ---------- */
*, *::before, *::after { box-sizing: border-box; }
html, body { margin: 0; padding: 0; }

/* The native `hidden` attribute must win over component display.
   The user agent expresses `hidden` as `[hidden] { display: none }` in its
   own stylesheet, and EVERY author declaration outranks the entire UA
   stylesheet regardless of specificity. So any primitive here that sets
   `display` — cards, rows, headers, buttons, fields, badges — silently
   defeats `hidden` on itself. `!important` is what restores the native
   behavior: it lifts this rule into the author-important layer, above
   both normal author rules and element inline styles.
   Without it, every consuming application has to re-declare
   `.thing[hidden] { display: none }` for each primitive it toggles —
   which is a workaround for a defect that belongs here, in the shared
   layer, once. */
[hidden] { display: none !important; }
html { -webkit-font-smoothing: antialiased; text-rendering: optimizeLegibility; }
body {
  font-family: var(--sans);
  font-size: var(--fs-base);
  line-height: 1.55;
  color: var(--text);
  background: var(--surface);
  font-feature-settings: "ss01", "cv11";
}
img, svg { display: block; max-width: 100%; }
a { color: inherit; text-decoration: none; }
button { font: inherit; cursor: pointer; }
ul { list-style: none; padding: 0; margin: 0; }
h1, h2, h3, h4 { margin: 0; font-weight: 400; letter-spacing: -0.01em; }
p { margin: 0; }

@media (prefers-reduced-motion: reduce) {
  *, *::before, *::after {
    animation-duration: 0.01ms !important;
    animation-iteration-count: 1 !important;
    transition-duration: 0.01ms !important;
  }
}

/* Accessibility: one visible focus language everywhere. */
:focus-visible { outline: none; box-shadow: var(--focus-ring); border-radius: 2px; }
.gh-skip-link {
  position: absolute; left: -9999px; top: 0; z-index: 100;
  background: var(--ink); color: var(--paper);
  padding: 10px 16px; font-size: var(--fs-sm); border-radius: 0 0 4px 0;
}
.gh-skip-link:focus-visible { left: 0; }
.gh-visually-hidden {
  position: absolute !important; width: 1px; height: 1px;
  overflow: hidden; clip: rect(0 0 0 0); white-space: nowrap;
}

/* ---------- LAYOUT ---------- */
/* The application container fills a workstation display. Staff live in this
   product on 27-32" monitors for hours; the old 1360px cap left a third of a
   wide screen as empty margin (#262). The default is now a wide application
   width with slim, responsive gutters; --reading stays narrow for the few
   places that are genuinely prose or a single form column. */
.gh-container { max-width: 1920px; margin: 0 auto; padding: 0 clamp(20px, 2.4vw, 44px); }
.gh-container--reading { max-width: 1100px; }
/* Full-bleed: an operational view (a wide table, a dense board) that should
   use the whole viewport, gutters aside. */
.gh-container--wide { max-width: none; }
@media (max-width: 900px) { .gh-container, .gh-container--reading { padding: 0 var(--sp-3); } }

.gh-section { padding: var(--sp-5) 0 0; }
.gh-section + .gh-section { margin-top: var(--sp-5); border-top: 1px solid var(--line); }

/* Section header: the site's eyebrow/kicker hierarchy, application-scaled. */
.gh-section-head {
  display: flex; align-items: baseline; justify-content: space-between;
  gap: var(--sp-3); margin-bottom: var(--sp-3); flex-wrap: wrap;
}
.gh-eyebrow {
  font-size: var(--fs-xs); letter-spacing: 0.18em; text-transform: uppercase;
  color: var(--text-dim); font-feature-settings: "tnum";
  display: inline-flex; align-items: center; gap: 10px;
}
.gh-eyebrow-dot {
  width: 6px; height: 6px; background: var(--accent);
  border-radius: 50%; display: inline-block; flex: none;
}
.gh-section-title { font-family: var(--serif); font-size: var(--fs-lg); line-height: 1.15; }
.gh-section-note { font-size: var(--fs-sm); color: var(--text-dim); }

/* ---------- APPLICATION HEADER (the site's sticky header) ---------- */
.gh-app-header {
  position: sticky; top: 0; z-index: 50;
  background: color-mix(in srgb, var(--surface) 88%, transparent);
  backdrop-filter: blur(12px); -webkit-backdrop-filter: blur(12px);
  border-bottom: 1px solid var(--line);
}
.gh-app-header-row {
  display: flex; align-items: center; justify-content: space-between;
  gap: var(--sp-4); height: 64px;
}
.gh-wordmark {
  font-family: var(--serif); font-weight: 500; font-size: 20px;
  color: var(--text); letter-spacing: -0.02em;
  display: inline-flex; align-items: flex-start;
}
.gh-wordmark-sup {
  font-family: var(--sans); font-weight: 500; font-size: 9px;
  /* --accent measures 2.71:1 on paper; --accent-ink is the same hue
     family at 5.50:1 (#143). */
  letter-spacing: 0.08em; color: var(--accent-ink);
  margin-left: 2px; margin-top: 3px; line-height: 1;
}
/* The application name sits beside the wordmark as a quiet area label —
   every product reads "GuideHerd AI · <application>". */
.gh-app-name {
  font-size: var(--fs-xs); letter-spacing: 0.18em; text-transform: uppercase;
  color: var(--text-dim); border-left: 1px solid var(--line-strong);
  padding-left: var(--sp-2); margin-left: var(--sp-2);
  align-self: center; padding-top: 2px;
}
.gh-app-header-meta {
  display: inline-flex; align-items: center; gap: var(--sp-2);
  font-size: var(--fs-sm); color: var(--text-dim);
}
.gh-app-header-meta a { color: var(--text); }
.gh-app-header-meta a:hover { color: var(--accent-ink); }
@media (max-width: 640px) {
  .gh-app-header-row { height: auto; min-height: 56px; padding: 10px 0; }
  .gh-app-header-meta { font-size: var(--fs-xs); }
}

/* ---------- BUTTONS (the site's pill hierarchy) ---------- */
.gh-btn {
  display: inline-flex; align-items: center; justify-content: center; gap: 10px;
  padding: 10px 18px; border-radius: var(--radius-pill);
  font-size: var(--fs-sm); font-weight: 500; letter-spacing: 0.005em;
  border: 1px solid transparent; transition: all .2s ease;
  font-family: var(--sans); background: none; color: var(--text);
}
.gh-btn-primary { background: var(--ink); color: var(--paper); }
.gh-btn-primary:hover { background: var(--accent-ink); color: #FBF8F1; }
.gh-btn-outline { border-color: var(--line-heavy); color: var(--text); background: transparent; }
.gh-btn-outline:hover { background: var(--ink); color: var(--paper); }
.gh-btn-ghost { color: var(--text); padding-left: 4px; padding-right: 4px; }
.gh-btn-ghost:hover { color: var(--accent-ink); }
.gh-btn-danger { border-color: var(--status-bad); color: var(--status-bad); background: transparent; }
.gh-btn-danger:hover { background: var(--status-bad); color: var(--cream); }
.gh-btn-full { width: 100%; }
.gh-btn[disabled] { opacity: 0.45; cursor: not-allowed; }

/* ---------- CARDS & STATS ---------- */
/* A card now leans on a hairline plus the lightest elevation instead of a
   heavy border, so a dense grid of them reads as grouped surfaces rather
   than a wall of boxes (#262, "fewer borders"). --flat opts out where a
   card sits inside another surface; --raised lifts an interactive one. */
.gh-card {
  background: var(--surface-raised);
  border: 1px solid var(--line);
  border-radius: var(--radius-card);
  padding: var(--sp-3);
  box-shadow: var(--elevation-1);
}
.gh-card--featured { border-left: 2px solid var(--accent); }
.gh-card--flat { box-shadow: none; }
.gh-card--raised { box-shadow: var(--elevation-2); }
/* A compact card for dense dashboards — less internal padding. */
.gh-card--compact { padding: var(--sp-2); }

/* Stat: the site's aside-label / serif-number pairing. */
.gh-stats { display: grid; grid-template-columns: repeat(auto-fit, minmax(150px, 1fr)); gap: var(--sp-2); }
.gh-stat {
  background: var(--surface-raised); border: 1px solid var(--line);
  border-radius: var(--radius-card); padding: var(--sp-2) var(--sp-3);
}
.gh-stat-n {
  font-family: var(--serif); font-size: var(--fs-2xl); line-height: 1.1;
  font-feature-settings: "tnum"; color: var(--text);
}
.gh-stat-l {
  font-size: var(--fs-xs); letter-spacing: 0.15em; text-transform: uppercase;
  color: var(--text-dim); margin-top: 2px;
}
.gh-stat--bad .gh-stat-n { color: var(--status-bad); }

/* ---------- TABLES (the site's editorial package-table) ---------- */
.gh-table-wrap { overflow-x: auto; }
.gh-table {
  border-collapse: collapse; width: 100%; font-size: var(--fs-sm);
  border-top: 1px solid var(--line-heavy); border-bottom: 1px solid var(--line-heavy);
}
.gh-table th {
  text-align: left; font-weight: 500; font-size: var(--fs-xs);
  letter-spacing: 0.14em; text-transform: uppercase; color: var(--text-dim);
  padding: 10px 14px 10px 0; border-bottom: 1px solid var(--line-heavy);
  white-space: nowrap;
}
.gh-table td {
  text-align: left; padding: 10px 14px 10px 0;
  border-bottom: 1px solid var(--line-strong);
  vertical-align: top; color: var(--text-soft);
}
.gh-table tr:last-child td { border-bottom: 0; }
.gh-table tbody tr { transition: background .15s; }
.gh-table tbody tr:hover { background: var(--surface-raised); }
.gh-table .gh-num { font-feature-settings: "tnum"; }
.gh-table caption { text-align: left; }

/* Identifiers read in mono, quietly. */
.gh-mono { font-family: var(--mono); font-size: 12px; color: var(--text-soft); }

/* ---------- FORMS ---------- */
.gh-field { display: flex; flex-direction: column; gap: 6px; }
.gh-label {
  font-size: var(--fs-xs); letter-spacing: 0.15em; text-transform: uppercase;
  color: var(--text-dim);
}
.gh-input, .gh-select, .gh-textarea {
  font: inherit; font-size: var(--fs-sm); color: var(--text);
  background: var(--surface-raised);
  border: 1px solid var(--line-strong); border-radius: 6px;
  padding: 10px 12px; width: 100%; transition: border-color .2s;
}
/* A placeholder is read, so it is held near the text ratio too (#143):
   --text-placeholder measures 4.8:1 on white. */
.gh-input::placeholder { color: var(--text-placeholder); }
.gh-input:focus-visible, .gh-select:focus-visible, .gh-textarea:focus-visible {
  border-color: var(--accent-deep); box-shadow: var(--focus-ring);
}
/* Chrome hands a date/time input's focus to a field INSIDE its shadow
   DOM (the month/day/year spinners), so the input element itself matches
   neither :focus nor :focus-visible — and a keyboard user tabbing through
   the Operations date windows saw no indication of where they were at all
   (#143, WCAG 2.4.7). `:focus-within` is the selector that actually
   matches; a date field has no mouse-focus case worth suppressing, so it
   is unconditional rather than visible-only. */
.gh-input[type="date"]:focus-within, .gh-input[type="time"]:focus-within,
.gh-input[type="datetime-local"]:focus-within, .gh-input[type="month"]:focus-within,
.gh-input[type="week"]:focus-within {
  outline: none; border-color: var(--accent-deep); box-shadow: var(--focus-ring);
}
.gh-form-error { color: var(--status-bad); font-size: var(--fs-sm); }

/* Checkbox row: a native checkbox inside a readable label, carrying the
   brand accent and the shared focus language. General-purpose — any form
   surface with a boolean control (administration toggles, future portal
   preferences) composes this rather than restyling checkboxes locally. */
.gh-check {
  display: flex; align-items: flex-start; gap: 10px;
  font-size: var(--fs-sm); color: var(--text); cursor: pointer;
}
/* `.gh-checkbox` is the STANDALONE form: a toggle in a table cell, where
   the row supplies the meaning and an aria-label supplies the name, so
   there is no adjacent text to wrap. Same control, same focus language —
   before this existed, those checkboxes rendered with the browser's
   default accent and, worse, no focus ring at all (#169). */
.gh-check input[type="checkbox"], .gh-checkbox {
  width: 16px; height: 16px; margin-top: 2px; flex-shrink: 0;
  accent-color: var(--accent-deep); cursor: pointer;
}
.gh-check input[type="checkbox"]:focus-visible,
.gh-checkbox:focus-visible { outline: none; box-shadow: var(--focus-ring); }
.gh-check .gh-check-note { display: block; color: var(--text-dim); font-size: var(--fs-xs); }

/* Search: input + pill action as one quiet row. */
.gh-search { display: flex; gap: var(--sp-1); align-items: stretch; }
.gh-search .gh-input { flex: 1; }

/* ---------- BADGES (the site's pkg-badge treatment, semantic) ---------- */
.gh-badge {
  display: inline-flex; align-items: center; gap: 6px;
  font-family: var(--sans); font-size: 10px; font-weight: 500;
  letter-spacing: 0.12em; text-transform: uppercase;
  padding: 4px 8px; border-radius: var(--radius-badge);
  color: var(--status-mute); background: var(--status-mute-bg);
  white-space: nowrap;
}
.gh-badge::before {
  content: ""; width: 5px; height: 5px; border-radius: 50%;
  background: currentColor; flex: none;
}
.gh-badge--ok    { color: var(--status-ok);   background: var(--status-ok-bg); }
.gh-badge--warn  { color: var(--status-warn); background: var(--status-warn-bg); }
.gh-badge--bad   { color: var(--status-bad);  background: var(--status-bad-bg); }
.gh-badge--plain::before { display: none; }

/* ---------- ALERTS ---------- */
.gh-alert {
  border: 1px solid var(--line-strong); border-left: 2px solid var(--text-dim);
  border-radius: 6px; background: var(--surface-raised);
  padding: var(--sp-2) var(--sp-3); font-size: var(--fs-sm); color: var(--text-soft);
}
.gh-alert--ok   { border-left-color: var(--status-ok); }
.gh-alert--warn { border-left-color: var(--status-warn); }
.gh-alert--bad  { border-left-color: var(--status-bad); color: var(--status-bad); }

/* ---------- DATE RANGE ---------- */
/* One control shape for every windowed list (#144): compact, native
   date inputs, and it never occupies space it does not earn. */
.ops-range { display: inline-flex; align-items: center; gap: 6px; flex-wrap: wrap; }
.ops-range-date { width: auto; min-width: 9.5rem; padding: 4px 6px; font-size: var(--fs-sm); }

/* Visible to a screen reader, not on the page: a control whose meaning
   the surrounding layout carries visually still needs a name. */
.gh-sr-only {
  position: absolute; width: 1px; height: 1px; overflow: hidden;
  clip: rect(0 0 0 0); clip-path: inset(50%); white-space: nowrap;
}

/* ---------- DISCLOSURE ---------- */
/* The consistent "there is more here" affordance (ADR-0029 §5, #141).
   Secondary detail folds away by default so a card reads as one decision;
   opening it never changes what the card means. */
.gh-details {
  border: 1px solid var(--line); border-radius: var(--radius-card);
  padding: var(--sp-2); margin: var(--sp-2) 0;
  background: transparent;
}
.gh-details > summary {
  cursor: pointer; font-size: var(--fs-sm); color: var(--text-soft);
  list-style: none;
}
.gh-details > summary::-webkit-details-marker { display: none; }
.gh-details > summary::before {
  content: "\2192"; display: inline-block; margin-right: 8px;
  color: var(--text-dim); transition: transform 120ms ease;
}
.gh-details[open] > summary::before { transform: rotate(90deg); }
.gh-details[open] > summary { color: var(--text); margin-bottom: var(--sp-2); }

/* ---------- EMPTY & LOADING STATES ---------- */
.gh-empty {
  padding: var(--sp-4) var(--sp-3); text-align: center;
  color: var(--text-dim); font-size: var(--fs-sm);
  border: 1px dashed var(--line-strong); border-radius: var(--radius-card);
  background: transparent;
}
.gh-empty .gh-empty-title {
  font-family: var(--serif); font-size: var(--fs-md); font-style: italic;
  color: var(--text-soft); display: block; margin-bottom: 4px;
}
.gh-loading {
  display: inline-flex; align-items: center; gap: 10px;
  color: var(--text-dim); font-size: var(--fs-sm); padding: var(--sp-2) 0;
}
.gh-loading::before {
  content: ""; width: 12px; height: 12px; flex: none;
  border: 1.5px solid var(--line-strong); border-top-color: var(--accent-deep);
  border-radius: 50%; animation: gh-spin .8s linear infinite;
}
@keyframes gh-spin { to { transform: rotate(360deg); } }

/* ---------- DIALOGS (native <dialog>, house-dressed) ---------- */
.gh-dialog {
  border: 1px solid var(--line-strong); border-radius: var(--radius-card);
  background: var(--surface-raised); color: var(--text);
  padding: var(--sp-4); max-width: 480px; width: calc(100% - var(--sp-4));
}
.gh-dialog::backdrop { background: var(--overlay); backdrop-filter: blur(2px); }
.gh-dialog h2 { font-family: var(--serif); font-size: var(--fs-lg); margin-bottom: var(--sp-2); }

/* ---------- KEY/VALUE (the site's hero-aside rows) ---------- */
.gh-kv { display: grid; grid-template-columns: 140px 1fr; gap: var(--sp-1) var(--sp-3); font-size: var(--fs-sm); }
.gh-kv dt {
  font-size: var(--fs-xs); letter-spacing: 0.15em; text-transform: uppercase;
  color: var(--text-dim); padding-top: 2px; margin: 0;
}
.gh-kv dd { margin: 0; color: var(--text); font-feature-settings: "tnum"; overflow-wrap: anywhere; }

/* ---------- UTILITIES ---------- */
.gh-muted { color: var(--text-dim); }
.gh-serif-italic { font-family: var(--serif); font-style: italic; color: var(--accent-ink); }
.gh-tnum { font-feature-settings: "tnum"; }
.gh-row { display: flex; align-items: center; gap: var(--sp-2); flex-wrap: wrap; }
.gh-spread { display: flex; align-items: center; justify-content: space-between; gap: var(--sp-2); flex-wrap: wrap; }
.gh-stack { display: flex; flex-direction: column; gap: var(--sp-2); }
.gh-hairline { border: 0; border-top: 1px solid var(--line); margin: var(--sp-3) 0; }

/* ==========================================================================
   APPLICATION LAYOUT (#262)

   Primitives for an operational workspace, not a centered document. The app
   is used all day on wide monitors, so these fill available width and turn
   into real multi-column compositions on the desktop while collapsing
   cleanly on smaller screens. Pages COMPOSE these; they are not page CSS.
   Every primitive is single-purpose so a later page redesign is assembly.
   ========================================================================== */

/* --- Column groups -------------------------------------------------------
   Two ways to lay out a row of blocks. .gh-autogrid is fluid: it fits as
   many columns of at least --gh-min as the width allows, so a dashboard
   naturally becomes 2/3/4-up as the monitor grows. .gh-cols-N is
   deliberate: exactly N columns on the desktop, stepping down at set
   breakpoints — for compositions where the column count carries meaning. */
.gh-autogrid {
  display: grid; gap: var(--sp-3);
  grid-template-columns: repeat(auto-fill, minmax(var(--gh-min, 280px), 1fr));
  align-items: start;
}
.gh-cols, .gh-cols-2, .gh-cols-3, .gh-cols-4 {
  display: grid; gap: var(--sp-3); align-items: start;
  grid-template-columns: repeat(var(--gh-cols, 2), minmax(0, 1fr));
}
.gh-cols-2 { --gh-cols: 2; }
.gh-cols-3 { --gh-cols: 3; }
.gh-cols-4 { --gh-cols: 4; }
@media (max-width: 1200px) { .gh-cols-4 { --gh-cols: 3; } }
@media (max-width: 960px)  { .gh-cols-3, .gh-cols-4 { --gh-cols: 2; } }
@media (max-width: 680px)  { .gh-cols, .gh-cols-2, .gh-cols-3, .gh-cols-4 { --gh-cols: 1; } }
/* Span helpers for a block that should occupy more than one track. */
.gh-span-2 { grid-column: span 2; }
.gh-span-all { grid-column: 1 / -1; }
@media (max-width: 680px) { .gh-span-2, .gh-span-all { grid-column: auto; } }

/* --- Content + context ---------------------------------------------------
   The application spine: a main working area beside a contextual aside
   (filters, a summary, a record's detail, later a Lex panel). The aside is
   a fixed, comfortable measure; the main area takes the rest. Collapses to
   a single column when there isn't room for both. */
.gh-split {
  display: grid; gap: var(--sp-4); align-items: start;
  grid-template-columns: minmax(0, 1fr) var(--gh-aside, 340px);
}
.gh-split--narrow { --gh-aside: 280px; }
.gh-split--wide   { --gh-aside: 420px; }
.gh-split--start  { grid-template-columns: var(--gh-aside, 340px) minmax(0, 1fr); }
@media (max-width: 1080px) { .gh-split, .gh-split--start { grid-template-columns: minmax(0, 1fr); } }

/* A contextual panel — the aside surface. Sticks under the app header so it
   stays with the operator as the main column scrolls. */
.gh-panel {
  background: var(--surface-raised); border: 1px solid var(--line);
  border-radius: var(--radius-card); padding: var(--sp-3);
  box-shadow: var(--elevation-1);
}
.gh-panel--sticky { position: sticky; top: 84px; }
.gh-panel-title {
  font-size: var(--fs-xs); letter-spacing: 0.15em; text-transform: uppercase;
  color: var(--text-dim); margin-bottom: var(--sp-2);
}

/* --- Toolbar -------------------------------------------------------------
   A dense action/filter row above a table or board. Its spacer pushes what
   follows to the right edge, so primary actions sit where the eye ends. */
.gh-toolbar {
  display: flex; align-items: center; gap: var(--sp-2); flex-wrap: wrap;
  padding: var(--sp-1) 0;
}
.gh-toolbar-spacer { margin-left: auto; }
.gh-toolbar + .gh-table-wrap, .gh-toolbar + .gh-table { margin-top: var(--sp-2); }

/* --- Forms as layouts ----------------------------------------------------
   A form on a wide screen is a grid, not a stack: fields flow into as many
   columns as fit, so a long configuration form stops being a tall ribbon.
   A field opts to full width with --wide (textareas, a lone long value). */
.gh-form-grid {
  display: grid; gap: var(--sp-2) var(--sp-3); align-items: start;
  grid-template-columns: repeat(auto-fill, minmax(var(--gh-field-min, 260px), 1fr));
}
.gh-field--wide { grid-column: 1 / -1; }

/* --- Dense table variant -------------------------------------------------
   Operational tables carry more rows per screen. The dense variant tightens
   row rhythm without touching the readable default. */
.gh-table--dense th { padding: 7px 12px 7px 0; }
.gh-table--dense td { padding: 7px 12px 7px 0; }
/* Numeric columns align on the decimal and read as data. */
.gh-table .gh-cell-num, .gh-table td.gh-cell-num, .gh-table th.gh-cell-num {
  text-align: right; font-variant-numeric: tabular-nums; padding-right: var(--sp-2);
}

/* --- Section density -----------------------------------------------------
   A tighter section rhythm for operational screens, where the airy editorial
   spacing of the default section wastes vertical space. */
.gh-section--tight { padding-top: var(--sp-4); }
.gh-section--tight + .gh-section--tight { margin-top: var(--sp-4); }

/* ==========================================================================
   APPLICATION SHELL (#263)

   The frame every product surface lives in: a full-width top bar, a
   persistent left navigation rail, and a main working area. It is an
   application chrome, not a document header — the operator's context (nav,
   identity, environment, global search) stays put while the work scrolls.
   A page's own contextual aside is composed INSIDE .gh-shell-main with the
   #262 .gh-split / .gh-panel primitives, so the shell itself stays two
   columns and every page assembles the same way. Reference: GitHub / Linear
   / Stripe / Azure operational chromes, not a centered document.
   ========================================================================== */
.gh-shell {
  display: grid;
  grid-template-columns: var(--gh-nav-w, 250px) minmax(0, 1fr);
  grid-template-rows: auto 1fr;
  grid-template-areas:
    "topbar topbar"
    "nav    main";
  min-height: 100vh;
  min-height: 100dvh;
}

/* --- Top bar -------------------------------------------------------------
   Full width, slim, sticky. Brand at the left, global controls at the
   right; a spacer between them. Reuses the app header's frosted treatment. */
.gh-shell-topbar {
  grid-area: topbar; position: sticky; top: 0; z-index: 50;
  display: flex; align-items: center; gap: var(--sp-3);
  height: 56px; padding: 0 var(--sp-3);
  background: color-mix(in srgb, var(--surface) 90%, transparent);
  backdrop-filter: blur(12px); -webkit-backdrop-filter: blur(12px);
  border-bottom: 1px solid var(--line);
}
.gh-shell-topbar .gh-toolbar-spacer { margin-left: auto; }
.gh-topbar-brand {
  display: inline-flex; align-items: baseline; gap: 8px;
  font-weight: 600; font-size: 16px; letter-spacing: -0.02em; color: var(--text);
}
.gh-topbar-brand sup {
  font-size: 9px; letter-spacing: 0.08em; color: var(--accent-ink);
  vertical-align: super; margin-left: 1px;
}
/* The area label beside the brand — "GuideHerd AI · Administration". */
.gh-topbar-context {
  font-size: var(--fs-xs); letter-spacing: 0.16em; text-transform: uppercase;
  color: var(--text-dim); border-left: 1px solid var(--line-strong);
  padding-left: var(--sp-2); align-self: center;
}

/* --- Theme switch --------------------------------------------------------
   The top bar's segmented Light/Dark control. Each segment is a shared
   .gh-btn; aria-pressed carries the state (an explicit persisted choice),
   and the pressed segment inverts — ink fill, surface text — so the state
   reads without color vocabulary of its own. Because the pill container
   clips its corners (overflow hidden would swallow a per-segment ring),
   the shared focus ring is worn by the container while a segment holds
   keyboard focus. */
.gh-theme-switch {
  display: inline-flex; align-items: stretch;
  border: 1px solid var(--line-strong);
  border-radius: var(--radius-pill);
  overflow: hidden;
}
.gh-theme-switch .gh-btn {
  border: 0; border-radius: 0;
  padding: 5px 11px; font-size: var(--fs-xs); font-weight: 500;
  color: var(--text-dim); line-height: 1;
}
.gh-theme-switch .gh-btn + .gh-btn { border-left: 1px solid var(--line-strong); }
.gh-theme-switch .gh-btn:hover { color: var(--text); }
.gh-theme-switch .gh-btn[aria-pressed="true"] {
  background: var(--ink); color: var(--surface);
}
.gh-theme-switch .gh-btn:focus-visible { outline: none; }
.gh-theme-switch:has(.gh-btn:focus-visible) { box-shadow: var(--focus-ring); }

/* --- Identity chip -------------------------------------------------------
   The signed-in person in the top bar: an initials disc beside the name.
   The disc is decorative (the text node carries the identity, so the disc
   is aria-hidden in markup) — a quiet accent wash, never a photo, never
   a color of the page's own. */
.gh-identity { display: inline-flex; align-items: center; gap: 8px; min-width: 0; }
.gh-identity-initials {
  width: 25px; height: 25px; border-radius: 50%; flex: none;
  display: inline-flex; align-items: center; justify-content: center;
  background: color-mix(in srgb, var(--accent) 20%, var(--surface-raised));
  color: var(--accent-ink);
  font-size: var(--fs-xs); font-weight: 600; letter-spacing: 0.02em;
  line-height: 1;
}
.gh-identity-name {
  font-size: var(--fs-sm); color: var(--text-soft);
  white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
}

/* --- Navigation rail -----------------------------------------------------
   Persistent, sticky, its own scroll. Grouped into labelled sections; the
   active item carries a faint accent wash and accent text. --gh-nav-w lets
   a future collapse toggle narrow it without touching the grid. */
.gh-shell-nav {
  grid-area: nav; align-self: start;
  position: sticky; top: 56px; height: calc(100dvh - 56px);
  overflow-y: auto;
  background: var(--surface-raised);
  border-right: 1px solid var(--line);
  padding: var(--sp-2) var(--sp-1);
  display: flex; flex-direction: column; gap: var(--sp-1);
}
.gh-nav-section { display: flex; flex-direction: column; gap: 2px; }
.gh-nav-section + .gh-nav-section { margin-top: var(--sp-2); }
.gh-nav-label {
  font-size: 10px; letter-spacing: 0.15em; text-transform: uppercase;
  color: var(--text-dim); padding: 6px 12px 4px;
}
.gh-nav-item {
  display: flex; align-items: center; gap: 10px;
  padding: 7px 12px; border-radius: 6px;
  color: var(--text-soft); font-size: var(--fs-sm); font-weight: 500;
  line-height: 1.25; white-space: nowrap;
}
.gh-nav-item:hover { background: var(--ink-05); color: var(--text); }
.gh-nav-item[aria-current="page"], .gh-nav-item.is-active {
  background: color-mix(in srgb, var(--accent) 14%, transparent);
  color: var(--accent-ink); font-weight: 600;
}
.gh-nav-item:focus-visible { outline: none; box-shadow: var(--focus-ring); }
/* Optional leading icon slot — pages supply the glyph; the shell only sizes it. */
.gh-nav-item .gh-nav-ico { width: 16px; height: 16px; flex: none; opacity: 0.85; }
.gh-nav-item[aria-current="page"] .gh-nav-ico, .gh-nav-item.is-active .gh-nav-ico { opacity: 1; }
/* A trailing count/badge sits at the right edge of the item. */
.gh-nav-item .gh-nav-meta { margin-left: auto; font-variant-numeric: tabular-nums; color: var(--text-dim); font-weight: 500; }

/* --- Main working area ---------------------------------------------------
   Where the page composes. Its own padding; a page uses .gh-container inside
   only when it wants a max measure — the shell already provides the frame. */
.gh-shell-main { grid-area: main; min-width: 0; padding: var(--sp-3) var(--sp-4) var(--sp-6); }
.gh-shell-main > .gh-container { padding-left: 0; padding-right: 0; }

/* A page header inside the main area — title, subtitle, and row actions on
   one operational line, not a marketing hero. */
.gh-page-head {
  display: flex; align-items: flex-start; justify-content: space-between;
  gap: var(--sp-3); flex-wrap: wrap; margin-bottom: var(--sp-3);
  padding-bottom: var(--sp-2); border-bottom: 1px solid var(--line);
}
.gh-page-title { font-size: var(--fs-xl); font-weight: 600; letter-spacing: -0.015em; color: var(--text); }
.gh-page-sub { font-size: var(--fs-sm); color: var(--text-dim); margin-top: 3px; }

/* --- Responsive ----------------------------------------------------------
   Below the desktop threshold the rail stops being a column and becomes a
   horizontal, scrollable strip under the top bar — no drawer, no JS, and the
   whole app collapses to a single working column. --gh-nav-w can drive an
   icon-rail collapse later. */
@media (max-width: 900px) {
  .gh-shell {
    grid-template-columns: minmax(0, 1fr);
    grid-template-areas: "topbar" "nav" "main";
  }
  .gh-shell-nav {
    position: sticky; top: 56px; height: auto; z-index: 40;
    flex-direction: row; gap: 4px; overflow-x: auto;
    border-right: 0; border-bottom: 1px solid var(--line);
    padding: 8px var(--sp-2);
  }
  .gh-nav-section { flex-direction: row; align-items: center; gap: 4px; }
  .gh-nav-section + .gh-nav-section { margin-top: 0; padding-left: 4px; border-left: 1px solid var(--line); }
  .gh-nav-label { display: none; }
  .gh-nav-item .gh-nav-meta { display: none; }
  .gh-shell-main { padding: var(--sp-3) var(--sp-3) var(--sp-5); }
}

/* ==========================================================================
   INTERACTION STATES (#264)

   The shared vocabulary for the moments between "nothing yet" and "done":
   loading, empty, error, inline validation, and saving. A dependency of
   every page redesign — surfaces compose these rather than re-inventing a
   spinner or a red border each time. Builds on the #261 tokens; nothing
   here is decorative.
   ========================================================================== */

/* --- Skeleton loading ----------------------------------------------------
   A quiet placeholder that mirrors the shape of content still loading, so a
   surface reserves its layout instead of jumping. The shimmer is motion, so
   it yields entirely to prefers-reduced-motion (a flat wash remains). */
.gh-skeleton {
  background: var(--ink-05);
  border-radius: var(--radius-badge);
  background-image: linear-gradient(90deg, transparent 0%, var(--ink-05) 50%, transparent 100%);
  background-size: 200% 100%;
  animation: gh-shimmer 1.3s ease-in-out infinite;
}
.gh-skeleton-line { height: 12px; margin: 8px 0; }
.gh-skeleton-line--sm { width: 40%; }
.gh-skeleton-line--lg { width: 80%; }
.gh-skeleton-block { height: 72px; border-radius: var(--radius-card); }
@keyframes gh-shimmer { to { background-position: -200% 0; } }
@media (prefers-reduced-motion: reduce) { .gh-skeleton { animation: none; } }

/* --- State block ---------------------------------------------------------
   One centered treatment for the three "the list isn't here" cases —
   loading, empty, and error — so they read as the same kind of message and
   differ only by tone. (.gh-empty remains for the plain inline-empty case;
   .gh-state is the full-surface version with an action.) */
.gh-state {
  display: flex; flex-direction: column; align-items: center; text-align: center;
  gap: 6px; padding: var(--sp-5) var(--sp-3);
  border: 1px dashed var(--line-strong); border-radius: var(--radius-card);
  color: var(--text-dim); background: transparent;
}
.gh-state-title { font-size: var(--fs-md); font-weight: 600; color: var(--text-soft); }
.gh-state-msg { font-size: var(--fs-sm); color: var(--text-dim); max-width: 46ch; }
.gh-state-actions { display: flex; gap: var(--sp-1); margin-top: var(--sp-2); flex-wrap: wrap; justify-content: center; }
/* Error tone: a solid edge and a state colour, because an error is not the
   same as "nothing here yet". */
.gh-state--error {
  border-style: solid; border-color: var(--status-bad);
  background: var(--status-bad-bg); color: var(--status-bad);
}
.gh-state--error .gh-state-title { color: var(--status-bad); }
.gh-state--error .gh-state-msg { color: var(--status-bad); }

/* --- Inline validation ---------------------------------------------------
   A field that failed validation carries a red edge AND an owned message,
   never colour alone (WCAG 1.4.1). Pair .gh-input--invalid with
   aria-invalid="true" and describe it with the .gh-field-error id. A hint
   is the neutral helper shown before anything goes wrong. */
.gh-input--invalid,
.gh-input[aria-invalid="true"], .gh-select[aria-invalid="true"], .gh-textarea[aria-invalid="true"] {
  border-color: var(--status-bad);
}
.gh-input--invalid:focus-visible,
.gh-input[aria-invalid="true"]:focus-visible {
  border-color: var(--status-bad);
  box-shadow: 0 0 0 2px var(--surface), 0 0 0 4px var(--status-bad);
}
.gh-field-hint { font-size: var(--fs-xs); color: var(--text-dim); }
.gh-field-error {
  font-size: var(--fs-xs); color: var(--status-bad); display: flex; align-items: center; gap: 6px;
}
.gh-field-error::before { content: ""; width: 5px; height: 5px; border-radius: 50%; background: currentColor; flex: none; }

/* --- Saving / busy -------------------------------------------------------
   A control mid-request reads as busy without losing its width (no layout
   jump): its label dims and a spinner rides alongside. Pair with
   aria-busy="true" and disable the control. */
.gh-btn[aria-busy="true"] { color: transparent !important; position: relative; pointer-events: none; }
.gh-btn[aria-busy="true"]::after {
  content: ""; position: absolute; width: 14px; height: 14px;
  border: 1.5px solid currentColor; border-top-color: transparent; border-radius: 50%;
  color: var(--paper); animation: gh-spin .7s linear infinite;
}
.gh-btn-outline[aria-busy="true"]::after, .gh-btn-ghost[aria-busy="true"]::after { color: var(--accent-ink); }

/* --- Save bar ------------------------------------------------------------
   A settings console should not make an operator hunt for Save. When a form
   has unsaved changes, this bar pins to the bottom of the working area with
   the change count and the two decisions — reducing round-trips (#269). It
   is hidden until there is something to save. */
.gh-save-bar {
  position: sticky; bottom: 0; z-index: 30;
  display: flex; align-items: center; gap: var(--sp-2);
  padding: 12px var(--sp-3); margin-top: var(--sp-3);
  background: color-mix(in srgb, var(--surface-raised) 92%, transparent);
  backdrop-filter: blur(10px); -webkit-backdrop-filter: blur(10px);
  border: 1px solid var(--line-strong); border-radius: var(--radius-card);
  box-shadow: var(--elevation-2);
}
.gh-save-bar-msg { font-size: var(--fs-sm); color: var(--text-soft); }
.gh-save-bar-msg b { color: var(--text); font-weight: 600; font-variant-numeric: tabular-nums; }
.gh-save-bar-actions { margin-left: auto; display: flex; gap: var(--sp-1); }
