/* web/css/style.css — design tokens + components.

   Dark rail, light content (Clark's standing split, matching the CTC C.R.M. shell). A same-day
   detour tried making the whole shell dark violet glass (see git history/memory for that
   round) — Clark, 2026-08-19: "looks too dark and gloomy bring back the light ver. i liked the
   background design we had originally... but still keep the ui and icons, and glow we already
   have implemented its already perfect." So the content plane is light again, the wave is back
   to its original untouched tuning — but backdrop-blur on glass surfaces, the gradient accent
   on the primary button, and the rounder 14px/10px corners from that detour all stayed: real
   frosted glass over a light plane, not the old flat plain-alpha fill.

   The whole app sits over ONE fixed WebGL background (web/js/lib/gradient-waves.js, ported from
   another of Clark's projects, 2026-08-17): a faint animated wave field, colour-switched by
   which screen is showing (see #gradient-waves-bg). Decorative and stays out of dense data
   areas on purpose — a background wash, not something painted behind a table of compliance
   records, so nothing about actually reading the data gets harder.

   Clamp-based spacing throughout, 44px minimum tap targets, no section min-height. */

/* Cross-document View Transitions (Clark, 2026-08-22 — "there is still a screen flash when
   user clicks an org fix that"): switchOrg() (data.js) does a real location.reload(), not a
   same-document hash change — the #view/#nav-items transition below (renderRoute() in app.js)
   never touches it, since that only runs on client-side navigation. This at-rule is the whole
   fix: it opts every same-origin navigation of this page, reloads included, into the browser's
   own before/after crossfade, no JS involved. Chrome/Edge 126+ only for now (Safari/Firefox
   don't yet) — an unsupported browser just ignores the at-rule and reloads exactly as before. */
@view-transition { navigation: auto; }

:root {
  /* --- Sidebar (dark, glass) ------------------------------------------------------------- */
  /* Lightened (Clark, 2026-08-17 — "the nav bar is too dark") from #0d1224 — still reads as
     "dark navy sidebar" against the white content plane, just less black/heavy. --nav-bg-deep
     stays as the actual near-black, used for things that want MORE contrast than the rail
     itself (the login shell, code-ish surfaces). */
  --nav-bg: #161d3a;
  --nav-bg-deep: #070a15;
  --nav-glass-bg: rgba(255, 255, 255, 0.05);
  --nav-glass-border: rgba(255, 255, 255, 0.10);
  --nav-text: #eef0f8;
  --nav-text-muted: rgba(238, 240, 248, 0.55);
  --nav-hover: rgba(255, 255, 255, 0.07);
  --nav-active: rgba(255, 255, 255, 0.06);
  --nav-accent: #e8c84a;
  /* A stable dark ink for text/icons sitting DIRECTLY on a solid --nav-accent fill (badges,
     the "no document" pill, the dashboard exposure ribbon's "due" segment) — found live,
     2026-08-23, adding the dark themes below: those three spots used `color: var(--text)`,
     which is dark in the one theme that existed when they were written but turns LIGHT in
     Dark/Classic Dark, putting near-white text on light gold and making it unreadable. Gold
     itself doesn't move per theme (see each theme block), so its ink shouldn't either. */
  --nav-accent-ink: #070a15;
  --nav-border: rgba(255, 255, 255, 0.10);

  /* --- Content plane (light, background design shows through) ---------------------------- */
  --bg: #f6f7fb;
  /* Frosted, not flat (Clark, 2026-08-19 — kept from the dark-mode detour: "keep... the glow
     we already have implemented its already perfect"): every card/panel/input carries real
     backdrop-filter blur now (see .glass below), not the old plain reduced-alpha fill with no
     blur at all — the landscape/wave still bleeds through, just softened like actual glass
     rather than a flat tint. */
  --surface: rgba(255, 255, 255, 0.84);
  --surface-sunken: rgba(240, 241, 247, 0.86);
  --glass-bg: rgba(255, 255, 255, 0.72);
  --glass-border: rgba(255, 255, 255, 0.6);
  /* A genuinely OPAQUE surface, for the handful of floating layers (popovers, modals, the
     command palette, dropdowns) that were already deliberately solid rather than glass — see
     each one's own comment on why. They used to hardcode `#fff` directly; themes below
     redefine this one token instead, so all of them go dark together. */
  --surface-solid: #ffffff;
  /* Blur amount as a token, not a literal on .glass/.glass-flat below — Classic Dark (further
     down) turns this to 0 for its deliberately flat, no-glow surface; every other theme keeps
     the same blur amount the Midnight Glow pass tuned. */
  --blur-amt: 18px;
  --blur-amt-sm: 14px;
  --border: rgba(30, 41, 66, 0.08);
  --border-strong: rgba(30, 41, 66, 0.16);
  --text: #1c2438;
  --text-secondary: #4c5670;
  --text-muted: #737f9c;

  /* Sky/cyan blue (Clark, 2026-08-17, second pass on this exact token — the first swap from
     purple went to #3562e3, which still read as "too much like indigo"; he wants "powder
     blue/cyan" instead). Matches --tile-blue below, kept as the one accent shared by the
     sidebar icons and the content plane. --accent-grad is kept from the dark-mode detour — a
     gradient treatment for the one true primary moment (the main CTA button), part of the
     "glow" Clark said to keep. */
  --accent: #0284c7;
  --accent-hover: #0369a1;
  --accent-soft: rgba(2, 132, 199, 0.12);
  --accent-grad: linear-gradient(135deg, #38bdf8, #0284c7);

  /* --- Status — unchanged hues from the neumorphic pass (already dark/saturated enough to
     hold contrast on both a solid white surface and the translucent glass one); tone is never
     the only signal — see icons.js for the paired glyph and CLAUDE.md for the CVD reasoning. */
  --st-expired: #b23030;
  --st-valid: #167a16;
  --st-neutral: #6b7690;
  --st-expired-soft: rgba(178, 48, 48, 0.12);
  --st-due-soft: rgba(196, 158, 42, 0.16);
  --st-valid-soft: rgba(22, 122, 22, 0.12);
  --st-neutral-soft: rgba(30, 41, 66, 0.06);

  /* Site palette is sky/cyan blue (--accent, used for buttons/links/focus rings/active nav) +
     gold (--nav-accent) used sparingly. --tile-blue started as "the same blue as --accent, kept
     as its own token because the sidebar dual-tone icons and a couple of nav-only rules
     reference it directly" — that lockstep broke on purpose once themes could change --accent
     (Clark, 2026-08-23: "for side bar nav icons keep them the same even in dark modes dont
     change them"): the theme blocks below deliberately do NOT redefine --tile-blue/
     --tile-blue-light, so the sidebar's active bar/icon tint — and the couple of content-plane
     chips still reading this same token (role-chip--admin, status-chip--completed) — stay this
     one fixed blue in every theme, same as the sidebar itself. --tile-action-bg is a plain dark
     tile background for small utility badges that aren't a nav destination. */
  --tile-blue: #0284c7;
  --tile-blue-light: #eaf6fd;
  --tile-action-bg: #131a30;

  /* --- Type scale (Clark, 2026-08-17 theme overhaul: Corporate Minimalism + High-Density) --
     Named rather than ad-hoc per component — a data-dense enterprise tool reads as
     "systematic" partly because its text sizes are a small closed set, not forty slightly
     different rem values. Existing component rules keep their own clamp()'d sizes where they
     already fit this scale; new/touched rules should reach for one of these first. */
  --fs-xs: 0.68rem;   /* micro-labels: kv-label, chip/badge text */
  --fs-sm: 0.78rem;   /* meta text, secondary lines under a name */
  --fs-base: 0.86rem; /* body default */
  --fs-md: 0.95rem;   /* panel titles, table headers, emphasis */
  --fs-lg: 1.15rem;   /* section headers */
  --fs-xl: clamp(1.2rem, 1.4vw + 0.85rem, 1.6rem); /* page titles */

  /* --- Metrics ------------------------------------------------------------------------ */
  --pad: clamp(0.75rem, 1vw + 0.35rem, 1.25rem);
  --gap: clamp(0.45rem, 0.7vw + 0.2rem, 0.7rem);
  /* Rounder than the original 12px/8px pass — kept from the dark-mode detour along with the
     blur/gradient, part of what Clark called "already perfect." */
  --radius: 14px;
  --radius-sm: 10px;
  --tap-min: 44px;
  /* Ported sidebar sizing (Clark, 2026-08-17, from another project) — a fixed rail width, not
     a viewport-height-scaled item height. A short window is handled by scaling #sidebar-inner
     as one block (scaleSidebarToFit() in app.js) instead of clamping every item's own height. */
  /* Narrowed (Clark, 2026-08-22 — "too wide for no reason, fit the width"): 224px left a lot of
     dead space past the longest nav label ("Plan & Billing"); 188px is close to what that label
     plus its icon tile and padding actually need. */
  --nav-w: clamp(172px, 11vw, 188px);
  --nav-w-collapsed: 60px;
  /* True floating layers (modals, popovers, the command palette) sit ABOVE the surface rather
     than being extruded FROM it — a conventional single soft drop shadow, not a raised/pressed
     pair, is what actually reads as "floating" here. */
  --shadow-sm: 0 2px 6px rgba(20, 28, 48, 0.10);
  --shadow-md: 0 12px 32px rgba(20, 28, 48, 0.18);
  /* Elevation, scoped narrowly on purpose (Clark, 2026-08-18, from the Depth Lab exploration —
     "go with elevation, but dont over do it it can be redundant"): a layered shadow (contact +
     ambient + cast, one shared light source) instead of --shadow-sm/-md's single blur. Used
     ONLY on .tile and .project-card below — the genuine standalone cards that sit directly on
     the page background. Deliberately NOT applied to .md-row: those already sit inside
     .md-list's own --shadow-md container shadow, and stacking a second shadow system onto rows
     already inside a shadowed box is exactly the redundant look this was scoped to avoid.
     Floating layers (popovers/modals/command palette) keep --shadow-md too, per the standing
     "extruded from the surface, not floating above it" reasoning on --shadow-sm/-md themselves. */
  --shadow-elevated: 0 1px 2px rgba(20, 28, 48, 0.06), 0 8px 20px rgba(20, 28, 48, 0.10), 0 20px 40px -14px rgba(20, 28, 48, 0.16);
  --shadow-elevated-hover: 0 2px 4px rgba(20, 28, 48, 0.08), 0 14px 28px rgba(20, 28, 48, 0.14), 0 28px 52px -16px rgba(20, 28, 48, 0.22);
}

/* --- Theme variants (Clark, 2026-08-23 — Settings → Theme, account popover) ------------------
   The block above is the only palette that ships as the bare :root — everything below only
   REDEFINES the same token names, scoped to html[data-theme="…"]. "System" isn't a third
   selector here: applyTheme() (app.js) resolves it to whichever of the two actually matches
   prefers-color-scheme and stamps THAT value as the attribute — same JS-owns-the-single-source-
   of-truth pattern sidebarMode already uses elsewhere in this file, so there's no parallel
   @media block to keep in sync by hand.
   The sidebar rail is deliberately OUT of scope entirely (Clark, 2026-08-23: "for side bar nav
   icons keep them the same even in dark modes dont change them") — no --nav-* token is touched
   here, including --nav-bg, and --tile-blue/--tile-blue-light (the sidebar's own active-bar/
   icon-tint tokens, also read by a couple of content-plane chips — see their own comment by the
   base :root definition) stay fixed too, so the whole rail and anything tinted with its blue
   look identical no matter which theme is picked. The dual-tone nav icon art (icons.js's
   ICONS_SOLID_IMG) is hardcoded pixels regardless and was never going to re-tint anyway. */
html[data-theme="dark"] {
  --bg: #0d1120;
  --surface: rgba(255, 255, 255, 0.045);
  --surface-sunken: rgba(255, 255, 255, 0.03);
  --surface-solid: #141a2c;
  --glass-bg: rgba(255, 255, 255, 0.05);
  --glass-border: rgba(255, 255, 255, 0.1);
  --border: rgba(255, 255, 255, 0.09);
  --border-strong: rgba(255, 255, 255, 0.18);
  --text: #eef0f8;
  --text-secondary: #c3c8db;
  --text-muted: #8991ab;
  /* Brightened from the light theme's #0284c7 — that value is ~2.1:1 on this dark surface,
     under the 3:1 UI-component floor; this one clears it with room to spare. */
  --accent: #4cc3f7;
  --accent-hover: #7ad4fb;
  --accent-soft: rgba(76, 195, 247, 0.16);
  --accent-grad: linear-gradient(135deg, #4cc3f7, #0284c7);
  --st-expired: #e6564f;
  --st-valid: #3ecf6a;
  --st-expired-soft: rgba(230, 86, 79, 0.16);
  --st-due-soft: rgba(232, 200, 74, 0.18);
  --st-valid-soft: rgba(62, 207, 106, 0.14);
  --st-neutral-soft: rgba(255, 255, 255, 0.06);
  --shadow-sm: 0 1px 2px rgba(0, 0, 0, 0.3);
  --shadow-md: 0 12px 32px rgba(0, 0, 0, 0.4);
  --shadow-elevated: 0 1px 2px rgba(0, 0, 0, 0.3), 0 8px 20px rgba(0, 0, 0, 0.32), 0 20px 40px -14px rgba(0, 0, 0, 0.4);
  --shadow-elevated-hover: 0 2px 4px rgba(0, 0, 0, 0.32), 0 14px 28px rgba(0, 0, 0, 0.36), 0 28px 52px -16px rgba(0, 0, 0, 0.46);
}
/* No glass, no blur, no gradient glow — flat solid panels, thin borders, tighter corners.
   Built for the case Dark alone doesn't cover: a foreman's tablet in direct site-office sun,
   where blur just turns into glare, or a long evening session where the least light is the
   best light. Also the cheapest theme to paint (no backdrop-filter layers), which matters on
   older job-site hardware. */
html[data-theme="classic-dark"] {
  --bg: #0a0c14;
  --surface: #12141f;
  --surface-sunken: #0d0f18;
  --surface-solid: #12141f;
  --glass-bg: #12141f;
  --glass-border: #242838;
  --border: #242838;
  --border-strong: #363c52;
  --text: #e7e9f2;
  --text-secondary: #b7bccc;
  --text-muted: #868da3;
  --accent: #3b9eff;
  --accent-hover: #63b2ff;
  --accent-soft: rgba(59, 158, 255, 0.14);
  --accent-grad: #2f8fe0; /* flat, not a gradient — the one deliberate "no glow" break from the others; background: <color> is as valid here as background: <gradient> */
  --st-expired: #e0524f;
  --st-valid: #38b653;
  --st-expired-soft: rgba(224, 82, 79, 0.14);
  --st-due-soft: rgba(209, 165, 58, 0.16);
  --st-valid-soft: rgba(56, 182, 83, 0.13);
  --st-neutral-soft: rgba(255, 255, 255, 0.05);
  --radius: 8px;
  --radius-sm: 6px;
  --blur-amt: 0px;
  --blur-amt-sm: 0px;
  --shadow-sm: 0 1px 2px rgba(0, 0, 0, 0.4);
  --shadow-md: 0 4px 14px rgba(0, 0, 0, 0.4);
  --shadow-elevated: 0 1px 2px rgba(0, 0, 0, 0.4);
  --shadow-elevated-hover: 0 1px 3px rgba(0, 0, 0, 0.5);
}

* { box-sizing: border-box; }

/* Restores the browser's own `[hidden] { display: none }` UA-stylesheet behaviour, which a
   plain class-based `display` (e.g. `.btn-primary { display: inline-flex }`) silently overrides
   any time BOTH apply to the same element — found live 2026-08-22 chasing the Organizations
   page's "+ New organization" toggle button (`el.hidden = true` did nothing visually) and
   `#topbar-account-btn` before it; almost certainly a standing bug on every other `el.hidden =
   true`/`toggleAttribute('hidden', …)` toggle in the app that happens to target a `.btn-*` or
   similar styled element, not just those two. One global, `!important` rule fixes all of them
   at once instead of a one-off `[hidden]{display:none}` override per component as each is
   individually rediscovered. */
[hidden] { display: none !important; }

/* Nothing rendered here is a file to peel off the page (Clark, 2026-09-05: "i dont like users
   being able to hold and drag out the image. and our icons too", then "did you the same for the
   ops console"). Native image dragging let anyone pull the logo, an account avatar or any icon
   out as a loose file, and the translucent drag ghost read as the layout coming apart.

   -webkit-user-drag is the only property that does this, and Firefox does not implement it, so
   the same rule is enforced again in JS -- see the dragstart guard in ops/js/shared.js. Both
   halves are needed; neither covers every browser alone. Same pair as the customer app's
   web/css/style.css + web/js/shared.js; keep them in step.

   Presentation only. Text selection is untouched, and the Kanban board's own cards still drag,
   since they are the element carrying draggable="true", not the icons inside them. */
img, svg, canvas, video { -webkit-user-drag: none; }

/* body's own background is the FALLBACK colour behind #gradient-waves-bg's low-alpha regions —
   dark by default (matching #login-shell, the screen shown before auth resolves), switched to
   the light content-plane colour once gradient-waves.js toggles .app-active on. See
   web/js/lib/gradient-waves.js. */
body {
  margin: 0;
  background: var(--nav-bg);
  color: var(--text);
  font-family: system-ui, -apple-system, 'Segoe UI', Roboto, sans-serif;
  -webkit-font-smoothing: antialiased;
}
body.app-active { background: var(--bg); }

.muted { color: var(--text-muted); }
a { color: var(--accent); }

/* --- Ambient background (Clark, 2026-08-17, ported from another project) -----------------------
   #gradient-waves-bg is created and appended by web/js/lib/gradient-waves.js — a WebGL2
   raymarched wave field behind the whole app, colour-switched between a dark 'login' colorway
   and a light 'app' colorway by which screen is showing. Fades in via .ready once the first real
   frame has drawn (see the script) so there's nothing to flash from a default/wrong colour.
   Decorative and stays out of dense data areas on purpose. */
#gradient-waves-bg {
  position: fixed; inset: 0; z-index: 0; width: 100%; height: 100%;
  opacity: 0; transition: opacity 0.6s ease; pointer-events: none;
}
#gradient-waves-bg.ready { opacity: 1; }

/* --- Glass surfaces (Clark, 2026-08-19 overhaul: backdrop-blur restored) -----------------------
   Every card is translucent, never an opaque flat fill — the (unchanged) wave behind it reads
   faintly through. Blur IS part of the look now (the previous "no blur" rule was specific to
   the old plain-alpha design; the approved Midnight Glow direction is real glassmorphism).
   --glass-surface-bg/-border default to the content plane's dark glass tokens; .sidebar
   overrides them to the rail's own (slightly different opacity, same family), so one set of
   rules works in both zones with no separate variant needed.
     .glass       — the standard card/panel surface
     .glass-flat  — a lighter touch for compact elements (chips, small badges) where the full
                    shadow would overpower the size
   There's no "pressed" state here the way neumorphism had one — glass doesn't sink, it lights
   up: an active/selected element gets an accent-coloured glow border instead (.glass--active). */
:root {
  --glass-surface-bg: var(--glass-bg);
  --glass-surface-border: var(--glass-border);
}
.sidebar {
  --glass-surface-bg: var(--nav-glass-bg);
  --glass-surface-border: var(--nav-glass-border);
}
.glass {
  background: var(--glass-surface-bg);
  border: 1px solid var(--glass-surface-border); border-radius: var(--radius);
  box-shadow: var(--shadow-md);
  backdrop-filter: blur(var(--blur-amt)); -webkit-backdrop-filter: blur(var(--blur-amt));
}
.glass-flat {
  background: var(--glass-surface-bg);
  border: 1px solid var(--glass-surface-border); border-radius: var(--radius-sm);
  box-shadow: var(--shadow-sm);
  backdrop-filter: blur(var(--blur-amt-sm)); -webkit-backdrop-filter: blur(var(--blur-amt-sm));
}
.glass--active {
  border-color: var(--accent);
  box-shadow: var(--shadow-sm), 0 0 0 1px var(--accent-soft), 0 0 18px var(--accent-soft);
}

/* --- Focus-visible ring (Clark, 2026-08-17) -------------------------------------------------
   Keyboard-accessibility pass: every interactive element gets one consistent ring on keyboard
   focus — :focus-visible already limits this to keyboard/programmatic focus, not mouse clicks,
   so it doesn't add a ring around every button a mouse user clicks. One rule for the whole app
   rather than styling focus per component; the dark sidebar rail (and its org-switcher trigger,
   which lives inside it) gets the gold rail accent instead of the content plane's navy blue —
   navy reads at 1.6:1 on the rail's dark background, same reasoning as --nav-accent elsewhere. */
:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; border-radius: 2px; }
.sidebar :focus-visible { outline-color: var(--nav-accent); }

/* --- Icons (Clark, 2026-08-17 theme overhaul) ------------------------------------------------
   One rule for every icon in the app, sourced from web/js/lib/icons.js. Was 13 separate
   "<context> svg { width/height/stroke-width... }" rules at inconsistent stroke-widths
   (1.6–2.0) — now a single stroke convention (Lucide's own default: 2px, round caps/joins),
   sized per context by setting --icon-size on the icon itself or an ancestor (custom
   properties inherit down the cascade, so a wrapper span like .nav-icon can set it once for
   every icon inside). .icon--solid (added automatically by icon()'s `solid` option, only for
   names that actually have a solid glyph) switches to a filled glyph for an active/selected
   state — outline stays the resting state everywhere else. */
.icon {
  width: var(--icon-size, 18px); height: var(--icon-size, 18px);
  stroke: currentColor; fill: none; stroke-width: 2;
  stroke-linecap: round; stroke-linejoin: round;
  flex-shrink: 0;
  /* display: block (Clark, 2026-08-17 — "vertically centre and align the icons in their
     block"): an <svg> with no display set defaults to inline, which carries the usual inline-
     replaced-element baseline gap — a few px of invisible descender space below it. Every
     icon-in-a-square badge in the app (.guide-info-icon, .empty-state-icon, .doc-drop-icon,
     .cmdk-row-icon, etc.) uses display:grid;place-items:center on the wrapper and relied on
     that to centre the icon, but the inline gap was quietly throwing the true centre off by a
     few px in every one of them. This is the actual fix, not a per-component nudge. */
  display: block;
}
.icon--solid { fill: currentColor; stroke: none; }
/* Photo-cutout nav icons (Clark, 2026-08-22 — see icons.js's ICONS_SOLID_IMG): an <img>, not an
   <svg>, so fill/stroke above are no-ops on it — object-fit is the one extra rule it actually
   needs, in case a future asset isn't pre-padded to a perfect square like the current three are. */
.icon--photo { object-fit: contain; }
/* Sidebar nav's own two-tone glyphs (Clark, 2026-08-17 — matched to his reference icon set:
   dark blue primary shape + light blue accent shape, not currentColor/gold). Scoped to
   .nav-icon rather than a blanket .icon--solid override, since ICONS_SOLID is only ever
   rendered there (see icons.js's header comment) — a future solid icon used elsewhere
   shouldn't silently inherit this rail-specific palette. Fixed blue regardless of hover/
   active state, same as the reference — the row background carries the state, not the glyph. */
.nav-icon.icon--solid { fill: var(--tile-blue-light); }
.nav-icon.icon--solid .tile-b { fill: var(--tile-blue); }

/* Thin, unobtrusive scrollbars everywhere something scrolls. Left at OS defaults, Windows
   Chrome draws a wide scrollbar with clickable up/down arrow buttons — that reads as a stray
   control sitting on top of the content. This is the Supabase/Linear/Apollo convention: a
   slim borderless thumb, no track, no arrow buttons. */
* { scrollbar-width: thin; scrollbar-color: var(--border-strong) transparent; }
*::-webkit-scrollbar { width: 9px; height: 9px; }
*::-webkit-scrollbar-track { background: transparent; }
*::-webkit-scrollbar-thumb { background: var(--border-strong); border-radius: 999px; background-clip: padding-box; }
*::-webkit-scrollbar-thumb:hover { background: var(--text-muted); }
*::-webkit-scrollbar-button { display: none; width: 0; height: 0; }
*::-webkit-scrollbar-corner { background: transparent; }

/* --- Login screen ------------------------------------------------------------------- */
/* No background of its own (Clark, 2026-08-17) — body's dark default shows through, with
   #gradient-waves-bg's 'login' colorway visible behind the ::before glow/grid below. z-index:1
   so it still stacks above the canvas regardless of DOM order (the canvas carries an explicit
   z-index:0, same as #app-shell needs — see .app-shell). */
.login-shell {
  min-height: 100vh;
  min-height: 100dvh;
  display: flex;
  align-items: center;
  justify-content: center;
  padding: var(--pad);
  padding-bottom: max(var(--pad), env(safe-area-inset-bottom));
  position: relative;
  z-index: 1;
  overflow: hidden;
}
/* No CSS-only glow/grid here anymore (Clark, 2026-08-18 — the gold glow read as an arbitrary
   second accent, and the grid pattern was unwanted texture). The wave canvas's own 'login'
   colorway (web/js/lib/gradient-waves.js) now carries the screen's depth by itself — a darker
   version of the same blue used on the signed-in app's background, not a separate design. */
.login-card {
  position: relative;
  width: min(400px, 100%);
  background: var(--surface);
  border: 1px solid var(--border);
  border-radius: 16px;
  padding: clamp(1.75rem, 4vw, 2.5rem);
  box-shadow: 0 24px 60px rgba(0, 0, 0, 0.32), 0 2px 8px rgba(0, 0, 0, 0.14);
}
.login-card img { display: block; width: 56px; height: 56px; margin: 0 auto 1rem; border-radius: 8px; }
/* The mark's C is white, and the login card is a light surface in the light theme, so here it
   DOES need the navy chip behind it (the header does not, its box is already navy). Same
   treatment as the customer app's login card. width:auto keeps the 16:9 ratio. */
.login-card img.brand-mark { width: auto; height: 40px; border-radius: 10px; background: #0A1430; padding: 7px 10px; }
.login-card h1 { text-align: center; font-size: 1.4rem; letter-spacing: -0.01em; margin: 0 0 0.3rem; }
.login-card p.sub { text-align: center; color: var(--text-muted); font-size: 0.85rem; margin: 0 0 1.75rem; }
.login-version { text-align: center; font-size: 0.72rem; color: var(--text-muted); margin-top: 1.25rem; }

/* Small leading icons inside the email/password inputs — login-specific (.field--icon), not
   a change to the shared .field used by every other form in the app. */
.field-input-wrap { position: relative; }
.field-icon {
  position: absolute; left: 0.75rem; top: 50%; transform: translateY(-50%);
  --icon-size: 16px; color: var(--text-muted);
  pointer-events: none;
}
/* Specificity note: this must out-rank the generic `.field input` padding shorthand below
   (line ~303) — with equal specificity, source order alone was deciding it, and the generic
   rule's `padding: 0.55rem 0.7rem` shorthand was landing after this and quietly resetting
   padding-left back under the icon (the actual cause of the icon/typed-text overlap, not a
   sizing tweak). `.field.field--icon` (two classes) wins outright regardless of order. */
.field.field--icon input { padding-left: 2.35rem; }

/* Password peek toggle — ported from web/css/style.css (Clark, 2026-08-24). */
.password-peek-toggle {
  position: absolute; right: 0.4rem; top: 50%; transform: translateY(-50%);
  display: inline-flex; align-items: center; justify-content: center;
  width: 28px; height: 28px; padding: 0;
  background: transparent; border: none; border-radius: var(--radius-sm);
  color: var(--text-muted); cursor: var(--cur-hand);
}
.password-peek-toggle:hover { color: var(--text); background: var(--surface-sunken); }
.password-peek-toggle .icon { --icon-size: 16px; }
.field-input-wrap:has(.password-peek-toggle) input { padding-right: 2.1rem; }
@media (pointer: coarse) { .password-peek-toggle { width: var(--tap-min); height: var(--tap-min); right: 0; } }

/* --- Fields & buttons ---------------------------------------------------------------- */
.field { margin-bottom: 0.5rem; }
.field label { display: block; font-size: 0.78rem; font-weight: 600; margin-bottom: 0.2rem; color: var(--text-secondary); }
.field input, .field select, .field textarea {
  width: 100%;
  min-height: var(--tap-min);
  padding: 0.55rem 0.7rem;
  background: var(--surface);
  border: 1px solid var(--border-strong);
  border-radius: var(--radius-sm);
  color: var(--text);
  font-size: 0.9rem;
  font-family: inherit;
}
.field textarea { min-height: 4.5rem; resize: vertical; }
.field input:focus, .field select:focus, .field textarea:focus {
  outline: none; border-color: var(--accent); box-shadow: 0 0 0 3px var(--accent-soft);
}
/* Same look as .field input, for the one spot a text input sits directly in a table row with
   no .field wrapper around it (White-label's brand-domain field, Clark, 2026-08-24 — "fix fill
   in field to match our overall theme and ui style"): that scoping is .field input/select/
   textarea only, so a bare <input> outside one silently fell back to the browser's own white
   default instead of the app's theme. */
.brand-domain-input {
  width: 100%; min-height: var(--tap-min); padding: 0.55rem 0.7rem;
  background: var(--surface); border: 1px solid var(--border-strong); border-radius: var(--radius-sm);
  color: var(--text); font-size: 0.9rem; font-family: inherit;
}
.brand-domain-input:focus {
  outline: none; border-color: var(--accent); box-shadow: 0 0 0 3px var(--accent-soft);
}
.brand-domain-input::placeholder { color: var(--text-muted); }

/* Market Data's review table (Clark, 2026-08-25) — same "bare input outside .field" gap the
   brand-domain-input comment above already describes, generalised into a reusable class since
   this page has several editable cells per row, not just one. */
.mk-cell-input {
  width: 100%; min-height: var(--tap-desktop-zoomed); padding: 0.35rem 0.5rem; /* Fitts: 2.1rem rendered 30px under .content's zoom */
  background: var(--surface); border: 1px solid var(--border-strong); border-radius: var(--radius-sm);
  color: var(--text); font-size: 0.84rem; font-family: inherit;
}
.mk-cell-input:focus { outline: none; border-color: var(--accent); box-shadow: 0 0 0 2px var(--accent-soft); }
.mk-cell-input--num { text-align: right; font-variant-numeric: tabular-nums; }
.mk-review-row--excluded { opacity: 0.5; }
.mk-confidence { font-size: 0.7rem; text-transform: uppercase; letter-spacing: 0.03em; font-weight: 700; }
.mk-confidence--high { color: var(--st-good, #0ca30c); }
.mk-confidence--medium { color: var(--st-warn, #8a6d00); }
.mk-confidence--low { color: var(--st-bad, #d03b3b); }

/* Predecessor picker (planning.js's activityFormFields()) — a plain checkbox list, not a
   native <select multiple> (Clark, 2026-08-23: "allow the user to select more than one
   predecessor at a time" — multi-select already worked via ctrl/cmd-click, but that's an
   undiscoverable gesture nobody reaches for; every option here just toggles on a normal
   click). Scoped overrides for `.field input` above, which would otherwise stretch each
   checkbox to width:100%/min-height:44px like a real text field. */
.check-list {
  display: flex; flex-direction: column; max-height: 150px; overflow-y: auto;
  border: 1px solid var(--border-strong); border-radius: var(--radius-sm); padding: 0.25rem;
  background: var(--surface);
}
.check-list-item {
  display: flex; align-items: center; gap: 0.55rem;
  padding: 0.4rem 0.5rem; border-radius: var(--radius-sm);
  font-size: 0.86rem; cursor: var(--cur-hand); user-select: none;
  transition: background-color 0.12s ease;
}
.check-list-item:hover { background: var(--surface-sunken); }
.check-list-item input[type="checkbox"] {
  width: auto; min-height: 0; flex-shrink: 0; accent-color: var(--accent); cursor: var(--cur-hand);
}

/* Custom select (Clark, 2026-08-22 — "all dropdown make them custome and design to msthc the
   softwares aesthethic"): the browser's own <select> popup can't be restyled at all — generic
   OS font, a blue system highlight, nothing like the rest of the app. fieldSelect()
   (compliance-ui.js) now renders a trigger button + the SAME <select> it's replacing, kept in
   the DOM as .custom-select-native (visually hidden, not display:none, so it stays keyboard-
   tabbable as a real fallback) purely so readForm() and ordinary form semantics (.value,
   required, form.reset()) keep working exactly as before — nothing about how a field's value
   is read anywhere in the app had to change. The open listbox reuses .org-switch-row/
   .org-switch-check wholesale (custom-select.js is the one file that opens it) — the same
   option-row language the org switcher already established, so this reads as the SAME
   dropdown idiom everywhere rather than a one-off. Wired by ONE document-level delegated
   handler (custom-select.js), not per page-call-site, so every fieldSelect() output "just
   works" the instant it's in the DOM — same zero-setup expectation a native <select> always had. */
.custom-select { position: relative; }
.custom-select-native {
  position: absolute; top: 0; left: 0; width: 100%; height: 100%;
  opacity: 0; pointer-events: none;
}
.custom-select-trigger {
  width: 100%; min-height: var(--tap-min); padding: 0.55rem 0.7rem;
  display: flex; align-items: center; justify-content: space-between; gap: 0.5rem;
  background: var(--surface); border: 1px solid var(--border-strong); border-radius: var(--radius-sm);
  color: var(--text); font-size: 0.9rem; font-family: inherit; text-align: left;
  cursor: var(--cur-hand); transition: border-color 0.12s ease, box-shadow 0.12s ease;
}
.custom-select-trigger:hover { border-color: var(--text-muted); }
.custom-select-trigger:focus-visible, .custom-select-trigger[aria-expanded="true"] {
  outline: none; border-color: var(--accent); box-shadow: 0 0 0 3px var(--accent-soft);
}
.custom-select-value { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.custom-select-caret { --icon-size: 16px; color: var(--text-muted); flex-shrink: 0; transition: transform 0.15s ease; }
.custom-select-trigger[aria-expanded="true"] .custom-select-caret { transform: rotate(180deg); }
/* .org-switch-popover's own recipe (position:fixed solid surface, entrance transition, scroll
   when long — see its own comment further down for why solid not translucent), just without
   the fixed 240px width — custom-select.js sets width/position inline to match whichever
   field's trigger opened it. */
.custom-select-listbox {
  position: fixed; z-index: 140; max-width: calc(100vw - 2rem);
  background: var(--surface-solid); border: 1px solid var(--border-strong); border-radius: var(--radius);
  box-shadow: var(--shadow-md); padding: 0.3rem; max-height: min(60vh, 320px); overflow-y: auto;
  opacity: 0; transform: translateY(-6px) scale(0.97); transform-origin: top left;
  transition: opacity 0.14s ease, transform 0.16s cubic-bezier(0.4, 0, 0.2, 1);
}
.custom-select-listbox--up { transform: translateY(6px) scale(0.97); transform-origin: bottom left; }
.custom-select-listbox.popover-open { opacity: 1; transform: translateY(0) scale(1); }
/* <optgroup> label rendered as a non-interactive header row inside the open listbox — same
   micro-label voice as .filter-dropdown-label. */
.custom-select-group {
  padding: 0.45rem 0.6rem 0.15rem; font-size: 0.64rem; font-weight: 700;
  text-transform: uppercase; letter-spacing: 0.05em; color: var(--text-muted);
}
.custom-select-group:not(:first-child) { border-top: 1px solid var(--border); margin-top: 0.25rem; }
/* Compact variant for inline table-row editors — ported from web/css/style.css's own copy
   (Clark, 2026-08-24 — "make sure all dropdown in the website are designed to match
   aesthethic and theme"): Subscriptions' per-row plan select and Costs' Category/Currency
   fields all sit inside a table row or a tight grid, not a full-width form field. min-width
   (not just auto) matters here too — the open listbox matches the trigger's own rendered
   width, and an unpadded trigger sizes to its own label alone while an option row needs a
   bit more (the label plus a checkmark on the active one) — "Enterprise" truncated to
   "Enter..." in Subscriptions' listbox without it. */
.custom-select--compact { width: auto; display: inline-block; }
/* Fitts: this said `min-height: auto`, which beat the 44px on the base rule and rendered a 28px
   target inside a 58px table row. --tap-desktop-zoomed is 36px, landing at 32 once .content's
   zoom: 0.9 scales it. Still compact, just no longer under the floor. */
.custom-select--compact .custom-select-trigger { width: auto; min-width: 150px; min-height: var(--tap-desktop-zoomed); padding: 0.35rem 0.5rem; font-size: 0.82rem; gap: 0.4rem; }
.custom-select--compact .custom-select-caret { --icon-size: 14px; }
/* min-height, and always rendered even empty (fieldText()/fieldSelect(), compliance-ui.js) —
   Clark, 2026-08-23: "check and investigate block alignment issues like this all throughout
   the software ui", starting from the Retention & Turnover form where DLP-start (has a hint)
   and DLP-end (no hint) sat in the same .dt-form-grid row. That grid's `align-items: end`
   bottom-anchors every field, so a field WITH a hint is genuinely taller than one without —
   end-aligning them puts their inputs at two different heights, and the same mismatch recurs
   anywhere a hinted and unhinted field share a row (planning.js's resource/PERT-days forms,
   execution.js's variation order form, monitoring.js's accomplishment/inventory forms all mix
   the two). Reserving this height unconditionally, hint or not, makes every field the same
   total height regardless of which ones happen to carry hint text, so `align-items: end` (or
   plain top alignment in .field-grid, which never had this bug) lines inputs up either way —
   fixed at the CSS/component level instead of patching each call site. */
.field-hint { font-size: 0.72rem; color: var(--text-muted); margin-top: 0.25rem; min-height: 1.05em; }
.error-text { color: var(--st-expired); font-size: 0.8rem; min-height: 1.2em; margin-bottom: 0.4rem; }

.btn-primary, .btn-secondary, .btn-danger-outline {
  display: inline-flex; align-items: center; justify-content: center;
  min-height: var(--tap-min);
  padding: 0.55rem 1.05rem;
  border-radius: var(--radius-sm);
  font-size: 0.88rem;
  font-weight: 600;
  font-family: inherit;
  text-decoration: none; /* these classes land on <a> as often as <button> (e.g. empty-state CTAs) */
  cursor: var(--cur-hand);
  border: 1px solid transparent;
  transition: background-color 0.12s ease, border-color 0.12s ease, color 0.12s ease, transform 0.05s ease;
}
/* The gradient IS the primary action's whole personality (Clark's approved direction) — every
   other button on the page stays quiet glass, so this is the one place boldness gets spent. */
.btn-primary {
  width: 100%; background: var(--accent-grad); color: #fff; box-shadow: 0 4px 18px var(--accent-soft);
  /* background-origin, and it is load-bearing (Clark, 2026-09-06: "hairline light pixels on the
     [left] and hairline dark pixels on the right"). These buttons carry `border: 1px solid
     transparent` from the rule above, and the CSS defaults disagree with each other on a
     gradient: background-origin is padding-box, so the gradient's geometry is measured to the
     PADDING box, while background-clip is border-box, so it is PAINTED across the border box.
     The 1px ring between the two is outside the gradient's own coordinate space, so it is
     filled by extending the nearest stop, which on a 135deg ramp means the light end fringes
     one edge and the dark end fringes the other. Sizing the gradient to the box it is actually
     painted into removes both, with no change to layout, colour or the border itself. */
  background-origin: border-box;
}
.btn-primary:hover { box-shadow: 0 6px 24px rgba(2, 132, 199, 0.38); }
.btn-primary:disabled { opacity: 0.6; cursor: var(--cur-arrow); box-shadow: none; }
.btn-secondary {
  background: var(--glass-surface-bg);
  border-color: var(--glass-surface-border); color: var(--text);
}
.btn-secondary:hover { background: var(--surface-sunken); border-color: var(--border-strong); }
.btn-danger-outline { background: transparent; border-color: var(--st-expired); color: var(--st-expired); }
.btn-danger-outline:hover { background: var(--st-expired-soft); }
.btn-compact { width: auto; padding: 0.45rem 0.85rem; font-size: 0.83rem; min-height: var(--tap-min); }
/* A brief press-down — the small tactile cue Supabase/Linear buttons give on click, so an
   action reads as "acknowledged" the instant it's pressed rather than only once its result
   (a save, a re-render) shows up. Skipped on :disabled so mid-save buttons don't look armed. */
.btn-primary:active:not(:disabled), .btn-secondary:active:not(:disabled), .btn-danger-outline:active:not(:disabled) { transform: scale(0.97); }

/* Von Restorff: a disclosure toggle and the commit button inside the panel it opens were BOTH
   .btn-primary, so an expanded panel put two equally loud buttons on screen and neither read as
   "the" action. Measured on Billing, Costs and Flags. While collapsed the toggle genuinely IS the
   screen's action, so it stays primary; once expanded it has done its job and steps back to let
   the commit stand alone. Market Data deliberately keeps two, because catalog upload and
   indicator recording are independent registers rather than a toggle and its own commit.
   Placed after the .btn-* block on purpose: this stylesheet's source order decides equal-
   specificity ties, and these selectors must win over the plain .btn-primary paint above. */
.btn-primary[data-collapse-toggle][aria-expanded="true"] {
  background: var(--glass-surface-bg);
  border-color: var(--glass-surface-border);
  color: var(--text);
  box-shadow: none;
}
.btn-primary[data-collapse-toggle][aria-expanded="true"]:hover {
  background: var(--surface-sunken); border-color: var(--border-strong); box-shadow: none;
}

/* Fitts, without redesigning the control: a <button class="badge"> toggle (White Label's
   Direct/Reseller switch) is a real target at 45x16, far under the 32px floor. Inflating it would
   turn a badge into a button and change how the table reads, so the VISUAL size is left alone and
   only the hit area grows, through a transparent overlay. -9px vertical is deliberate: the badge
   is ~17.8px in CSS px before .content's zoom: 0.9, so 17.8 + 18 clears the 35.6px needed to
   render at 32. Only badges that are actually buttons get this. */
button.badge { position: relative; }
button.badge::after { content: ''; position: absolute; inset: -9px -6px; border-radius: inherit; }

/* --- App shell: sidebar + content ---------------------------------------------------------
   Locked to the viewport (100dvh, falls back to 100vh — dvh accounts for mobile browser
   chrome so the shell never sits taller than what's actually visible). Sidebar and topbar are
   plain flex children that never scroll; #view is the ONE scrolling region (flex:1 1 auto +
   overflow-y:auto + min-height:0 — the min-height is what stops a flex child from just
   growing past its parent instead of scrolling). This is what keeps the rail and the header
   in place while the page content scrolls, matching Clark's CTC C.R.M reference. */
/* position/z-index above the fixed #gradient-waves-bg canvas (z-index: 0) so the whole shell
   paints over it rather than the stacking order being left to chance. */
.app-shell { display: none; height: 100vh; height: 100dvh; position: relative; z-index: 1; flex-direction: column; }
/* A single column now — the shared header (.top-header) is its own full-width row, and
   .app-body (below) holds the sidebar|content split that used to be .app-shell's own grid
   (Clark, 2026-08-22: "its just a part of the header now like in supabase" — see .top-header's
   own comment). min-height:0 lets .app-body's flex:1 actually shrink to fit instead of
   overflowing the shell, same reasoning as #view's own min-height:0 below. */
.app-shell.visible { display: flex; }
/* margin-top: -1px (Clark, 2026-08-22 — "there is now a visible white line between the header
   and the sidebar"): .top-header's height comes from clamp()-based padding, which doesn't
   always resolve to a whole pixel, so its bottom edge and .sidebar's top edge below it — though
   mathematically flush (confirmed via getBoundingClientRect: identical fractional value) —
   could still rasterize a hairline sub-pixel gap apart. A 1px negative margin pulls .app-body up
   just enough to overlap that seam (it paints on top, being later in DOM order, so nothing about
   .top-header's own content is covered) without colouring the whole shell as a first attempt did
   — that fix bled dark navy through every transparent area of the ENTIRE page, since .content
   itself has no opaque background of its own (the gradient-waves canvas shows through by
   design), not just the seam. */
/* position:relative (Clark, 2026-08-22 — "at the top it intersects and overlays the header
   title... move it down"): .sidebar.nav-auto below is position:absolute, and with nothing
   positioned between it and .app-shell, it was anchoring to .app-shell itself — which also
   contains .top-header ABOVE .app-body — so the hover-expanded overlay started from the very
   top of the shell and covered the header. Anchoring it here instead (.app-body already starts
   below the header, as .top-header's flex sibling) keeps the overlay exactly where the rail
   itself sits, never reaching above it. */
.app-body { flex: 1 1 auto; min-height: 0; margin-top: -1px; display: grid; grid-template-columns: auto minmax(0, 1fr); position: relative; }
/* Auto sidebar mode only (Clark, 2026-08-21 — Supabase-style hover-to-expand, see
   applySidebarMode()/wireSidebarCollapse() in app.js): the first grid track is pinned to the
   collapsed width instead of tracking .sidebar's own width like the rule above does normally —
   this mode needs the rail pulled OUT of grid flow (.sidebar.nav-auto, below) so growing it back
   to full width on hover overlays the content instead of reflowing it, and a track that no
   longer reacts to the rail's width at all is what makes that safe. */
.app-shell.nav-mode-auto .app-body { grid-template-columns: var(--nav-w-collapsed) minmax(0, 1fr); }

/* --- Sidebar (Clark, 2026-08-17, ported from another project) ---------------------------------
   A fixed-size overflow:hidden frame that NEVER scrolls — if #sidebar-inner's natural content
   doesn't fit a short window, it scales down as one block instead (scaleSidebarToFit() in
   app.js), never a scrollbar, never clipped content. width is the one property that actually
   differs between expanded/collapsed on the rail itself; every other visual change collapsing
   drives happens on #sidebar-inner's own children below, and every one of those uses an
   animatable property (width, max-width, opacity, margin, gap, padding) — never display:none
   and never a layout-mode switch (e.g. flex-direction on the nav list), because neither of
   those can transition: they pop/jump instead of sliding. The one accepted exception is the
   header row itself switching row -> column when collapsed (see .sidebar-header below) — that
   only moves two small icons a short distance, not the same thing as the nav list jumping. */
.sidebar {
  width: var(--nav-w);
  height: 100%;
  background: var(--nav-bg);
  color: var(--nav-text);
  border-right: 1px solid var(--nav-border);
  min-height: 0; /* a flex/grid item's auto min-height can otherwise overflow its own row */
  overflow: hidden;
  position: relative;
  transition: width 0.22s cubic-bezier(0.4, 0, 0.2, 1);
}
.sidebar.collapsed { width: var(--nav-w-collapsed); }
/* Set by scaleSidebarToFit() only once the rail has hit its legibility floor and still doesn't
   fit -- see MIN_SIDEBAR_SCALE in app.js. This console carries twenty nav rows across six
   groups, roughly double the customer app's rail, and shrinking far enough to fit them all took
   the labels down to 7px. Scrolling the overflow is the trade; the customer app's rail is short
   enough that it never reaches this and keeps the original never-scrolls behaviour. */
.sidebar.sidebar-scrolls { overflow-y: auto; overscroll-behavior: contain; }
/* Pulled out of grid flow entirely (position:absolute, anchored to .app-shell's own
   position:relative) — the SAME .collapsed class above still drives the width (60px at rest,
   224px when app.js's hover/focus listeners remove it), just now as an overlay rather than a
   layout push, since the grid track it would have occupied is pinned fixed-width instead (see
   .app-shell.nav-mode-auto above). box-shadow only while actually expanded — resting collapsed
   should look identical to plain 'collapsed' mode, not float above the content for no reason. */
.sidebar.nav-auto { position: absolute; top: 0; left: 0; z-index: 30; }
.sidebar.nav-auto:not(.collapsed) { box-shadow: var(--shadow-md); }

/* The thing that actually scales (see scaleSidebarToFit()) — transform-origin: top left is set
   from JS alongside the transform itself, not here, since it only applies while a scale is
   active. */
.sidebar-inner {
  height: 100%;
  padding: clamp(0.6rem, 1.4vh, 0.8rem);
  padding-top: max(clamp(0.6rem, 1.4vh, 0.8rem), env(safe-area-inset-top));
  /* The bottom gets more room than the other three sides, and its own safe-area floor the way
     padding-top already has one (Clark, 2026-09-05: "the account nav on the bottom has no
     sufficient padding from the bottom edge of the screen"). The account button is the last
     child and sits directly on this edge, so this padding is the only thing between it and the
     end of the screen. It is scaled along with everything else by scaleSidebarToFit(), so on a
     short window the visible gap is this value times that scale -- app.js measures the padding
     now, which is what stops the scale from solving it away entirely. */
  padding-bottom: max(clamp(0.9rem, 2.2vh, 1.25rem), env(safe-area-inset-bottom));
  display: flex;
  flex-direction: column;
  gap: 0.35rem;
  transition: padding 0.22s cubic-bezier(0.4, 0, 0.2, 1);
}
.sidebar.collapsed .sidebar-inner { padding-left: 0.6rem; padding-right: 0.6rem; }

/* Brand box — now the first fixed area of .top-header, not the sidebar's own header row (Clark,
   2026-08-22: see .top-header's own comment). Own dark-navy background, matching the sidebar
   column's own width beneath it so the vertical divider lines up with the seam between .sidebar
   and .content-wrap. Tracked .sidebar's collapse state once (deliberately not, per an earlier
   round) but now DOES (Clark, 2026-08-22, later the same day: "make sure the blue part of the
   header also collapses but into its logo form only") — :has(), not a class mirrored onto
   .app-shell by hand: .sidebar and .top-header are cousins (both children of .app-shell, never
   ancestor/descendant of each other), so this is the only way for the brand box to react to
   .sidebar's OWN .collapsed class — including live, on hover, in auto mode — without app.js
   needing to toggle a matching class at every single place .sidebar's own gets toggled. */
.top-header-brand {
  display: flex; align-items: center; flex-shrink: 0;
  width: var(--nav-w);
  /* Right padding trimmed off var(--pad) (18.4px) so "Platform Management" stops being clipped:
     it needs 95px and the stack had 91.4px, three and a half short, so the final "t" was cut.
     The same shortfall was fixed once before by reclaiming the gap (see .nav-brand below) and
     has crept back as --nav-w's clamp floor moved. The LEFT padding is untouched on purpose --
     it is what lines the brand mark up with the nav icons in the rail underneath. The right side
     butts against the box's own divider border, where the loss is invisible. */
  padding: 0 0.75rem 0 var(--pad); background: var(--nav-bg);
  border-right: 1px solid var(--nav-border); /* the divider Clark asked to keep, moved from a horizontal <hr> to a vertical one */
  transition: width 0.22s cubic-bezier(0.4, 0, 0.2, 1);
}
/* gap: 0 too, not just centring (same fix as .sidebar.collapsed .sidebar-account-btn above) —
   .nav-brand's own flex gap still applies next to the now-zero-width .nav-brand-text otherwise,
   quietly pushing the logo left of true centre. */
.app-shell:has(.sidebar.collapsed) .top-header-brand { width: var(--nav-w-collapsed); justify-content: center; padding: 0; }
.app-shell:has(.sidebar.collapsed) .nav-brand { gap: 0; }
.nav-brand {
  /* gap trimmed 0.5rem -> 0.3rem when the brand mark went in: the mark is 16:9, so holding its
     height at 22px makes it 38px wide against the old square icon's 22px, and those 16px came
     straight out of .nav-brand-text, clipping "Platform Management" by 3px (95px needed, 92px
     left). Reclaiming it from the gap keeps the mark at full size and the label complete. */
  display: flex; align-items: center; gap: 0.3rem; min-width: 0;
  text-decoration: none; color: var(--nav-text); cursor: var(--cur-hand);
}
/* Sized down 26px → 22px (Clark, 2026-08-22 — "size the header to be the same size as in
   supabase": measured live off the real supabase.com/dashboard/organizations, its whole header
   bar is 47px tall — mine measured 55.75px, driven almost entirely by this brand box's own
   content height. Shrinking the logo + the title/version stack below is what actually closes
   that gap, since .top-header-brand has no explicit height of its own — it just stretches to
   whatever its tallest content needs (see .top-header's own align-items:stretch). */
.nav-brand img { width: 22px; height: 22px; border-radius: 5px; flex-shrink: 0; }
/* CEDA brand mark (Clark, 2026-09-03: "you also forgot to put the LOGO Mark in the Platform
   Manager"). The console was still showing a flat navy placeholder square with no mark on it.
   Same asset and same reasoning as the customer app: the mark is transparent (white C + gold
   A) and roughly 16:9, so it sits directly on .top-header-brand's navy (var(--nav-bg)) and
   needs no chip. It does NOT copy the app's 34x46 though, because this header was measured
   down to Supabase's 47px bar and that is driven by this box's content height. Holding the
   height at 22px and widening to 38px keeps the mark's own ratio without reopening that gap;
   the square 22x22 + border-radius above would have squashed a 16:9 mark into a box. */
.nav-brand img.brand-mark { width: 38px; height: 22px; border-radius: 0; object-fit: contain; }
/* max-width + opacity, not display:none (same rule every other collapsible label in this app
   follows — see .nav-label) — so the logo settles into the narrower box smoothly instead of
   the text just vanishing a frame before/after the box itself resizes. */
.nav-brand-text { display: flex; flex-direction: column; min-width: 0; line-height: 1.15; max-width: 120px; opacity: 1; overflow: hidden; transition: max-width 0.22s cubic-bezier(0.4, 0, 0.2, 1), opacity 0.15s ease; }
.app-shell:has(.sidebar.collapsed) .nav-brand-text { max-width: 0; opacity: 0; }
/* The wordmark artwork, not the word set in type (Clark, 2026-09-05: "yes do the ops wordmark
   too"), matching the customer app's header. No font has the split C, the three-bar E or the
   gold triangle in the A.

   The span keeps the 14.35px line box the 0.78rem text had, and the image is centred inside it,
   so the two-line stack's height is unchanged and this header stays on the 47px it was measured
   down to against Supabase's own bar (see .nav-brand img's comment above -- that measurement is
   driven entirely by this box's content height, so a taller line here would undo it).

   9px against the mark's 16.8px of ink (113 source rows at 38/256) keeps the mark the clearly
   taller of the two, and lands the letters at the cap height the type had. Explicit width, not
   auto or a percentage: the collapsed rail shrinks .nav-brand-text to zero and must clip this,
   never squash it. */
.nav-brand-title { display: flex; align-items: center; height: 14.35px; }
.nav-brand-title img { display: block; height: 9px; width: 49.3px; max-width: none; }
/* The version disclaimer (Clark, 2026-08-17 — "under the title") */
.nav-brand-version { font-size: 0.58rem; font-weight: 600; color: var(--nav-text-muted); white-space: nowrap; }

/* Nav group heading. Same treatment as the customer app's rail (web/css/style.css): uppercase,
   tracked out, dim and small, so it separates the runs without competing with the destinations
   it labels. Collapses with the labels when the rail does, since a text heading would be the
   only prose left in a 60px column. */
/* --- Fitts's Law: a 32px floor on real hit targets ------------------------------------------
   Clark, 2026-09-03, the ops UX-law pass, matching the customer app's own floor. Measured across
   ten ops screens: this console is mostly read-only dashboards, so it has far fewer targets than
   the app and only two came in short -- the brand link at 25px and the account button at 32px on
   the nose. The 44px coarse-pointer rule still layers on top.

   Same two coordinate systems as the app: `.content` carries zoom: 0.9 here too, so a size
   declared inside it renders 10% smaller and needs 36px to land at 32. The rail and the topbar
   sit outside the zoom. */
:root { --tap-desktop: 32px; --tap-desktop-zoomed: 36px; }
.nav-brand { min-height: var(--tap-desktop); }

/* The coarse-pointer floor has the same zoom problem one step further out, found in the app on
   2026-09-04 and true here for the same reason: `.content` carries zoom: 0.9 (line ~1529), so a
   rule declaring `min-height: var(--tap-min)` INSIDE it has always rendered 39.6px, not 44 --
   every in-content tap rule in this file is quietly 10% short. `.content` is the only zoomed
   container and custom properties inherit, so redefining the token on `.content` itself lifts
   all of them at once instead of restating each rule: 49 * 0.9 = 44.1. Only under a coarse
   pointer, so nothing moves for a mouse.

   Unlike the app's stylesheet, this one has no source-order regression to undo -- the Fitts pass
   here edited the ORIGINAL rules (see .custom-select--compact) rather than stacking a later
   block, which is exactly why it did not silently override the coarse rules the way the app's
   did. Worth preserving that habit. */
@media (pointer: coarse) { .content { --tap-min: 49px; } }
/* .sidebar-account-btn is deliberately NOT given a floor, and the reason is worth keeping.
   It measures 27px on a short window, which looks like a Fitts violation and is not one: the
   rail's own scaleSidebarToFit() puts a transform on .sidebar-inner (0.616 at 758px tall) so the
   rail never scrolls, which is Clark's explicit rule, and a transform scales the rendered result
   whatever min-height says. A floor here cannot win, and if it could it would fight a deliberate
   behaviour. Measured and confirmed it is NOT caused by this pass's group headings: hiding them
   leaves the scale at 0.616 unchanged, because twenty rows do not fit that window either way.
   The 44px coarse-pointer rule still restores real targets on touch, which is where it matters. */
/* The one busy-state style the app grew during its Doherty pass, carried over so a guarded
   action in ops answers the click the same way. */
/* Doherty: the same-tick acknowledgement that a click landed. No `cursor: progress` here,
   it was measured dead: `.btn-primary:disabled { cursor: default }` outranks this single
   class, and `pointer-events: none` means the element's own cursor never resolves anyway. */
.is-busy { opacity: 0.6; pointer-events: none; }

.nav-group-label {
  display: flex; align-items: center;
  margin: 0.85rem 0 0.2rem; padding: 0 0.6rem;
  font-size: 0.62rem; font-weight: 700; letter-spacing: 0.09em; text-transform: uppercase;
  color: var(--nav-text-muted); opacity: 0.75;
  white-space: nowrap; overflow: hidden;
  transition: max-width 0.22s cubic-bezier(0.4, 0, 0.2, 1), opacity 0.15s ease, margin 0.22s ease;
}
#nav-items > .nav-group-label:first-child { margin-top: 0.2rem; }
.app-shell:has(.sidebar.collapsed) .nav-group-label { max-width: 0; opacity: 0; margin-top: 0.55rem; }
@media (max-width: 700px) { .nav-group-label { max-width: 0; opacity: 0; margin-top: 0.55rem; } }

/* Zeigarnik count pill. Sits at the end of the row, so it never shifts the label or the icon and
   the rows stay a uniform stack (Pragnanz). It carries a NUMBER, which is its own label, so this is
   not colour-alone signalling. Hidden when the rail is collapsed rather than floated over the icon
   tile: a two-digit pill on a 40px tile collides with the tile's own corner radius, and collapsed
   is a deliberate icons-only mode, not a place to squeeze text back into. */
.nav-count {
  margin-left: auto;
  min-width: 1.15rem;
  padding: 0.05rem 0.32rem;
  border-radius: 999px;
  background: var(--nav-accent, #e8c84a);
  color: #1b1b1b;
  font-size: 0.62rem;
  font-weight: 700;
  line-height: 1.5;
  text-align: center;
  font-variant-numeric: tabular-nums;
  flex-shrink: 0;
}
.sidebar.collapsed .nav-count { display: none; }
.nav-item {
  display: flex; align-items: center; gap: 0.6rem;
  padding: 0.5rem 0.6rem;
  /* Left corners flush, not rounded (Clark, 2026-08-17 — "i dont want the highlight wrapping
     around... like this for reference", pointing at Netlify's flush-left rows): rounding all
     four corners made the active state's inset left bar curve around the top/bottom-left like
     it was wrapping the pill, instead of sitting as one clean straight edge. Same shape at
     rest and active/hover so the row doesn't change shape when it becomes active — collapsed
     goes back to a plain rounded square (see .sidebar.collapsed .nav-item), where there's no
     bar and a centred icon button reads better fully rounded. */
  border-radius: 0 var(--radius-sm) var(--radius-sm) 0;
  color: var(--nav-text-muted); text-decoration: none;
  font-size: 0.89rem; font-weight: 600;
  white-space: nowrap; overflow: hidden;
  border: none; background: transparent; font-family: inherit; cursor: var(--cur-hand);
  width: 100%; margin: 0;
  transition: background-color 0.12s ease, color 0.12s ease,
    gap 0.22s cubic-bezier(0.4, 0, 0.2, 1), width 0.22s cubic-bezier(0.4, 0, 0.2, 1),
    margin 0.22s cubic-bezier(0.4, 0, 0.2, 1), padding 0.22s cubic-bezier(0.4, 0, 0.2, 1);
}
.nav-item:hover { background: var(--nav-hover); color: var(--nav-text); }
/* A thin left-edge bar, not a filled box (Clark, 2026-08-17, second pass — the flat-fill +
   blue-label version above still read as too much: "like in netlify its just a little bar on
   the side, simple and not too over the top"). inset box-shadow instead of a real border so
   the bar doesn't shift the row's padding/content. Background stays at the same subtle tone
   as :hover — the bar plus full-brightness text is the actual signal, not a filled row. */
.nav-item.active {
  background: var(--nav-hover); color: var(--nav-text);
  box-shadow: inset 3px 0 0 0 var(--tile-blue);
}
.nav-icon { width: 20px; height: 20px; flex-shrink: 0; }
/* Icon tile (Clark, 2026-08-18: "sidebar is kind of flat and boring especially the icons" →
   picked "E" from a set of mockups, then "make the blur more cleaner as its a bit messy") — a
   small rounded-square container behind the EXACT same dual-tone icon art and flush-left bar
   the app already had (Clark: "keep and use the exact dual tone icon designs we have, and
   selection style if possible"), so this only adds a container + an active-state lift, nothing
   about the icons or the bar changed. The active tile's shadow is deliberately a SINGLE layer,
   not the 3-layer stack `--shadow-elevated` uses for light-surface cards — that stack read as
   "messy" here because those three shadows are tuned for a dark shadow falling on a LIGHT
   surface; stacked at low blur on a small 30px tile against the dark rail it just smeared into
   a fuzzy blob. One clean, moderate-blur, accent-tinted shadow is doing the "lifted" job with
   a crisp edge instead. `.nav-item span` below (the label text) and this tile are both bare
   `<span>` children of `.nav-item`, so this needs its own selector specific enough to win over
   that rule's `max-width`/`opacity` — see the collapsed override underneath for the same reason
   (a shared span selector already hid something it shouldn't once before, see
   [[framework-css-lessons-2026-08-17]]'s collapsed-sidebar bugs). */
.nav-item .nav-icon-tile {
  display: grid; place-items: center; flex-shrink: 0;
  width: 30px; height: 30px; border-radius: var(--radius-sm);
  background: rgba(255, 255, 255, 0.05);
  max-width: none; opacity: 1;
  transition: background-color 0.15s ease, box-shadow 0.15s ease;
}
.nav-item.active .nav-icon-tile {
  background: rgba(2, 132, 199, 0.24);
  box-shadow: 0 4px 10px rgba(2, 132, 199, 0.3);
}
/* Explicit, not left to inherit — .sidebar.collapsed .nav-item span (below) targets ANY bare
   span child to hide the label text, which would otherwise also hide this icon tile (the exact
   "shared class hides the wrong thing" bug already hit once on the org switcher). Collapsed
   mode is also where this tile earns its keep the most: the flush-left bar is deliberately
   removed there (see .sidebar.collapsed .nav-item.active below) since it has no flat edge to
   sit against on a fully-rounded button, so the tile fill+shadow becomes the ONLY visible
   active indicator on a collapsed rail — previously that state was only a text-colour change
   that did nothing, since the dual-tone icon's fill is hardcoded, not currentColor. */
.sidebar.collapsed .nav-item .nav-icon-tile { max-width: none; opacity: 1; }
.nav-item span {
  white-space: nowrap; overflow: hidden; text-overflow: ellipsis; min-width: 0;
  max-width: 140px; opacity: 1;
  transition: max-width 0.22s cubic-bezier(0.4, 0, 0.2, 1), opacity 0.15s ease;
}
/* Buttons shrink via width + margin (0 -> auto, to centre), never by switching
   justify-content/align-items directly — alignment properties aren't transitionable either. The
   items sit perfectly flush with zero gap between them (margin stays 0), so vertical spacing
   between icons is entirely this padding's own top/bottom — first tightened from the original
   uniform 0.6rem, then Clark called that too tight ("had too little space. add more padding to
   space them out more") — settled higher than either past value, well past the original. */
.sidebar.collapsed .nav-item { gap: 0; width: 40px; margin: 0 auto; justify-content: center; padding: 0.85rem 0.6rem; border-radius: var(--radius-sm); }
.sidebar.collapsed .nav-item span { max-width: 0; opacity: 0; }
/* The flush-left bar only makes sense against the flush-left corners it was built for — on a
   fully-rounded collapsed icon square it curves around the edge instead of sitting flat, the
   exact "wrapping around" look the bar was designed to avoid in the first place (see the
   .nav-item comment above — collapsed was ALWAYS meant to have no bar, this override was just
   never actually written). Active state reads instead through the icon itself picking up the
   brand blue, on the same subtle fill every hover/active state already uses. (Clark,
   2026-08-18: "the collapse icon design for the highlight and selection is not accurate as
   what we have established for the default expanded version".) */
.sidebar.collapsed .nav-item.active { box-shadow: none; color: var(--tile-blue); }

/* Plain structural wrapper (Clark, 2026-08-19 restructure) — pairs one .nav-item with its own
   .nav-context slot right after it, so a section's sub-pages expand directly under THAT item
   and push whatever's below it (in normal document flow) down, rather than always appearing
   after the last item in the list. No spacing of its own — .nav-item already carries all the
   padding that gives the list its rhythm; adding any here would double it up. */
.nav-item-group { display: block; }

/* Contextual sidebar sub-nav (Clark, 2026-08-18 — "dont keep them hidden inside... add more
   to the sidebar"; restructured + animated 2026-08-19) — a page-specific section (currently
   only a project's Planning stage: Estimate/Schedule/Resources/Subcontractors/Plans) that
   appears/disappears via setSidebarContext() in app.js. Same flush-left-bar active convention
   as .nav-item, but one step quieter — no icon tile, smaller text, indented — so it reads as
   "belongs to the item above it", not a sixth global nav item.

   Grows open/shut via max-height + opacity rather than the old instant `:empty{display:none}`
   cut (Clark: "make all those animated") — display:none can't transition at all, so the section
   used to just pop in/out. max-height animating from 0 to a generous cap (not the exact content
   height, which JS would have to measure) is the same technique this file already uses for the
   sidebar's own collapse elsewhere; a fixed cap over-shoots a short list slightly but the ease
   hides it, and it's what actually lets the surrounding .nav-item rows glide down/up in step —
   ordinary layout reflow during the transition, no separate FLIP animation needed.
   Hidden outright (not animated) in collapsed icons-only mode — these rows have no icon to fall
   back to, and a bare unlabeled row would look broken next to the icon-only main items. */
.nav-context {
  max-height: 0; opacity: 0; overflow: hidden;
  transition: max-height 0.28s cubic-bezier(0.4, 0, 0.2, 1), opacity 0.2s ease, padding 0.28s cubic-bezier(0.4, 0, 0.2, 1);
}
.nav-context:not(:empty) { max-height: 640px; opacity: 1; padding: 0.3rem 0 0.4rem; }
.nav-context-label {
  padding: 0.3rem 0.9rem 0.35rem; font-size: 0.64rem; font-weight: 700;
  text-transform: uppercase; letter-spacing: 0.06em; color: var(--nav-text-muted);
  white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
}
.nav-subitem {
  display: flex; align-items: center; gap: 0.5rem;
  padding: 0.42rem 0.6rem 0.42rem 1.5rem;
  border-radius: 0 var(--radius-sm) var(--radius-sm) 0;
  color: var(--nav-text-muted); text-decoration: none;
  font-size: 0.82rem; font-weight: 600;
  white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
  transition: background-color 0.12s ease, color 0.12s ease;
}
.nav-subitem:hover { background: var(--nav-hover); color: var(--nav-text); }
.nav-subitem-icon { --icon-size: 14px; flex-shrink: 0; opacity: 0.75; }
.nav-subitem.active .nav-subitem-icon { opacity: 1; }
.nav-subitem.active { background: var(--nav-hover); color: var(--nav-text); box-shadow: inset 3px 0 0 0 var(--tile-blue); }
.sidebar.collapsed .nav-context { display: none; }

/* A project row's name is trimmed to whole words by fitOrgNameWords() (app.js), not clipped
   mid-word by CSS ellipsis (Clark, 2026-08-19 — see renderProjectsSidebar()/projectRowLabelHtml()
   in projects.js). text-overflow: clip, not ellipsis — the JS trim already guarantees it fits,
   same reasoning as .org-switch-name. */
.nav-subitem-label { flex: 1 1 auto; min-width: 0; overflow: hidden; white-space: nowrap; text-overflow: clip; }
/* Right-click a project row to rename it in place (Clark, 2026-08-19). The row stays the same
   <a> the whole time — only its content swaps to this input — so no layout jump on entering
   or leaving rename mode. */
.nav-subitem.renaming { background: var(--nav-hover); color: var(--nav-text); cursor: text; }
.nav-rename-input {
  flex: 1 1 auto; min-width: 0;
  background: transparent; border: none; border-bottom: 1px solid var(--nav-accent); outline: none;
  color: var(--nav-text); font: inherit; font-size: 0.82rem; font-weight: 600; padding: 0 0 1px;
}

/* The Projects sub-nav nests a second level (Clark, 2026-08-19 — every registered project as
   a row, the OPEN one's Planning tabs indented beneath it — see renderProjectsSidebar() in
   projects.js). Every project's tree is always in the DOM, collapsed by default, and gets its
   `.open` class toggled rather than being injected/removed — same max-height+opacity grow
   technique as .nav-context above, so switching which project is open (or which of its tabs
   is current) actually animates instead of popping straight to the new shape (Clark: "i dont
   want any snapping navigations anywhere in my ui"). */
.nav-subtree {
  display: flex; flex-direction: column;
  max-height: 0; opacity: 0; overflow: hidden;
  transition: max-height 0.28s cubic-bezier(0.4, 0, 0.2, 1), opacity 0.2s ease;
}
.nav-subtree.open { max-height: 320px; opacity: 1; }
/* Nested tab labels can run long ("Closeout Documents", "Retention & Turnover") and the rail's
   ~180px width (--nav-w) leaves very little room once the icon + this row's own deep indent
   are accounted for (Clark, 2026-08-23 — "content subnav titles is getting cut and not
   fitting. find a alternative"). Shrinking the text further or dropping the icon just delays
   the same problem at the next long label, so these wrap to a second line instead of
   truncating — the base .nav-subitem's nowrap/ellipsis is overridden here, same specificity
   trick font-size already used on this selector. align-items: flex-start (the base rule is
   center) keeps the icon pinned to the first line rather than drifting to a two-line label's
   vertical middle; the max-height above grew 300->320px to give the taller rows room.
   overflow-wrap: anywhere (Clark, 2026-08-23, same ask, next round — "check all sub nav text"
   turned up single-word labels like "Accomplishment"/"Subcontractors" still getting clipped:
   plain white-space:normal only wraps at a space, and a one-word label has none, so it just
   overflowed past the rail and got clipped by .nav-subitem's own overflow:hidden exactly like
   before. This lets a word break mid-word ONLY when it has no other way to fit — multi-word
   labels still wrap at their natural spaces first.
   padding-left cut from 2.35rem -> 1rem (Clark, 2026-08-23, later same day — after several
   rounds of just shortening whichever label didn't fit, "i want to tackle the problems of the
   text not fitting" instead of renaming the next one that trips it): --nav-w itself was
   deliberately narrowed once already (Clark, 2026-08-22 — "too wide for no reason"), so it's
   off the table; this row's OWN indent was the one place still spending width on pure
   hierarchy-signalling rather than the label — the smaller font-size already carries most of
   that "nested" signal on its own. Dropping the icon here was tried next and reverted (Clark:
   "no i dont want to loose the icon") — kept, but padding-right and the icon's own gap are
   trimmed too (icon stays full size, just tighter around it) to claw back the room the icon
   costs; between the three trims "Non-Conformance" fits on one line without wrapping. */
/* align-items: center, not flex-start (Clark, 2026-08-23 — "icons and subnav text is not
   properlly aligned"): flex-start + the icon's own margin-top below were added back when every
   long label here wrapped to 2 lines, to keep the icon pinned to line 1 instead of drifting to
   the block's vertical middle. Since then most of these got short enough to fit ONE line
   (Non-Conformance, Progress, ...) — and on a single-line row, that same top-alignment read as
   the icon sitting visibly too low next to its own text. Centering is correct for the (now
   common) one-line case and still reads fine on the few that still wrap two lines (Purchasing
   & Inventory, Closeout Documents, ...) — the icon just centers across both lines instead of
   pinning to the first, a normal enough look for a nav row. */
.nav-subitem--nested {
  padding-left: 1rem; padding-right: 0.35rem; padding-top: 0.38rem; padding-bottom: 0.38rem;
  gap: 0.35rem; font-size: 0.78rem; white-space: normal; text-overflow: clip; line-height: 1.25;
  overflow-wrap: anywhere; align-items: center;
}
/* A ~1px optical nudge (Clark, 2026-08-23 — "Activities is still slightly misaligned"): these
   icons' own artwork isn't perfectly centered in its 24x24 viewBox (the calendar icon's body,
   for instance, is a rect from y=5 to y=21 — visually weighted a touch low), so even
   box-level align-items:center leaves the drawn shape reading a hair lower than the text next
   to it. This is a blunt, icon-agnostic correction, not a per-glyph fix — good enough to settle
   the ones actually in this nav (calendar, file-text, clock, check-square, users, ...). */
.nav-subitem--nested .nav-subitem-icon { margin-top: -1px; }

/* Personal account button — pinned to the bottom via margin-top: auto, no spacer element
   needed (#sidebar-inner is already a column flex box). Mirrors .org-switch-btn's shape
   (avatar + text + hover/expanded state) but the avatar is the real .user-avatar circle, not
   an initial-in-a-square, and there's no chevron — a popover with real actions inside, not a
   list to pick from. */
.sidebar-account-btn {
  display: flex; align-items: center; gap: 0.5rem; width: 100%; margin-top: auto;
  /* Left padding re-tuned (Clark, 2026-08-18 — "pfp icon on the bottom is not aligned with the
     icons") after the new .nav-icon-tile container (see that rule above) shifted every nav
     icon's own left edge inward by the tile's padding (~5px: a 30px tile centring a 20px icon).
     The avatar wasn't part of that change, so the two drifted out of alignment — measured the
     real gap via getBoundingClientRect() (3.8px) rather than eyeballing it, and added exactly
     that back on top of the original 0.3rem (itself tuned once already, 2026-08-17, for the
     pre-tile icon position — this is that same ongoing tuning, not a one-off fix). */
  padding: 0.4rem 0.45rem 0.4rem 0.54rem; border: 1px solid transparent; border-radius: var(--radius-sm);
  background: transparent; color: var(--nav-text); cursor: var(--cur-hand); text-align: left;
  min-height: var(--tap-min);
}
.sidebar-account-btn:hover, .sidebar-account-btn[aria-expanded="true"] { background: var(--nav-hover); }
.sidebar-account-text { display: flex; flex-direction: column; min-width: 0; line-height: 1.3; }
.sidebar-account-name { font-size: 0.8rem; font-weight: 600; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
/* Top line: name | role, a thin vertical divider between them, not a bullet dot or a chip
   (Clark, 2026-08-22 — landed here after a few passes: chip-beside-email crowded the email
   out; this moves role up next to the name instead, as quiet coloured TEXT rather than a
   badge shape, freeing the second line for the email alone). */
.sidebar-account-top-row { display: flex; align-items: center; gap: 0.4rem; min-width: 0; }
.sidebar-account-divider { width: 1px; height: 10px; background: rgba(255, 255, 255, 0.18); flex-shrink: 0; }
.sidebar-account-role { font-size: 0.68rem; font-weight: 600; color: var(--nav-text-muted); white-space: nowrap; flex-shrink: 0; }
/* Same semantic colours as roleChipClass() (shared.js) uses for the chip everywhere else in
   the app — just applied to plain text here, not a filled/outlined badge shape. */
.sidebar-account-role--owner   { color: var(--nav-accent); }
.sidebar-account-role--admin   { color: #7cc4ff; }
.sidebar-account-role--liaison { color: #6fe090; }
/* Full-width second line now that role moved up to share the top line with the name. */
.sidebar-account-email {
  font-size: 0.68rem; color: var(--nav-text-muted); overflow: hidden; text-overflow: ellipsis;
  white-space: nowrap; min-width: 0;
}
/* gap: 0 too, not just centring (Clark, 2026-08-22 — "the profile icon is not properly
   centred"): .sidebar-account-text shrinks to max-width:0 when collapsed (.nav-label), but the
   button's own 0.5rem flex gap still applies on the empty space next to it — half of that gap
   was quietly pushing the avatar left of true centre even with justify-content:center. */
.sidebar.collapsed .sidebar-account-btn { justify-content: center; padding: 0.4rem 0; gap: 0; }
/* The Organizations-page-only topbar variant (Clark, 2026-08-22 — see index.html's own comment
   on #topbar-account-btn): same button, same popover, just sized to sit among Feedback/Quick
   jump/Help instead of spanning the sidebar's full width. min-height overridden back down to
   match its 32px siblings (Help/Quick jump) — .sidebar-account-btn's own 44px tap-target
   minimum was the ONE thing forcing .top-header's whole align-items:stretch row to 55px tall
   (Clark, 2026-08-22: "size the header to be the same size as in supabase" — measured there at
   47px). Same fine-pointer/coarse-pointer tradeoff the sidebar's own nav items already use
   elsewhere in this file — 44px comes back under @media (pointer: coarse) below. */
.sidebar-account-btn--topbar { width: auto; margin-top: 0; padding: 0.2rem; border-radius: 50%; min-height: 32px; }
.sidebar-account-btn--topbar .user-avatar { width: 28px; height: 28px; font-size: 0.72rem; }
@media (pointer: coarse) {
  .sidebar-account-btn--topbar { min-height: var(--tap-min); }
}
/* No longer needs its own [hidden] override — the global `[hidden] { display: none !important }`
   rule near the top of this file (added once the SAME bug turned up a second time) covers it. */

/* Organizations page: no sidebar at all (Clark, 2026-08-22 — "no need to have the nav bar
   render in like in supabase except for the essential in the header and account settings"),
   toggled by renderNav() in app.js. The content column takes the full row; #topbar-account-btn
   above is what replaces the now-hidden sidebar's own account access. */
.app-body.no-sidebar-page { grid-template-columns: 1fr; }
.app-body.no-sidebar-page .sidebar { display: none; }

/* The popover itself — appended to <body> like .org-switch-popover, for the same overflow:
   hidden-clipping reason, and solid rather than translucent for the same reason too (Clark,
   2026-08-17 — see the comment on .org-switch-popover: this floats over both the dark sidebar
   and the white content with no backdrop behind it, so --glass-bg's translucency reads as
   illegible depending what's underneath). Its own class names (not .org-switch-*): structurally
   this is a header + action rows, not an option list. */
.account-popover {
  position: fixed; z-index: 140; width: 220px; max-width: calc(100vw - 2rem);
  /* max-height + scroll (Clark, 2026-08-23 — added once the Theme picker made this tall enough
     to matter): same fix .topbar-popover already needed for its two-tour Help list. Without it
     a short window could push the popover's bottom off-screen with no way to reach Sign out. */
  max-height: calc(100vh - 24px); overflow-y: auto;
  background: var(--surface-solid); border: 1px solid var(--border-strong); border-radius: var(--radius);
  box-shadow: var(--shadow-md); padding: 0.3rem;
}
/* Entrance/exit animation, shared with .org-switch-popover below (Clark, 2026-08-22: "make
   this popup smooth clean animated not a instant pop up") — every body-level popover in the
   app used to just appear/disappear the instant it was added to/removed from the DOM. The JS
   side (wireAccountMenu()/wireOrgSwitcher()/wireProjectShellSwitcher(), app.js) adds
   `.popover-open` one frame after appending (so the browser registers this closed state
   first), and removes it before removal on close so the exit plays too. Anchored to
   bottom-left by default — the sidebar's own bottom-pinned trigger grows this popover UP, so it
   rises into place (translateY positive → 0). The Organizations page's topbar trigger
   (wireAccountMenu()'s own space-above-vs-below check) instead grows it DOWN, same as
   .org-switch-popover, via the --drop-down modifier below. */
.account-popover { opacity: 0; transform: translateY(6px) scale(0.97); transform-origin: bottom left;
  transition: opacity 0.14s ease, transform 0.16s cubic-bezier(0.4, 0, 0.2, 1); }
.account-popover--drop-down { transform: translateY(-6px) scale(0.97); transform-origin: top left; }
.account-popover.popover-open { opacity: 1; transform: translateY(0) scale(1); }
/* Feedback + Help popovers (Clark, 2026-08-23 "do all" — the two topbar buttons stopped being
   "coming soon" toasts). Same body-level mechanics as .account-popover (JS toggles
   .popover-open a frame after append; fadeOutPopover() on close); these hang off topbar
   triggers so they drop DOWN, right-edge-aligned to their button. Feedback stays backend-less
   by design (Clark, 2026-08-22: "dont build the backend for that yet") — Send composes a
   pre-filled mailto draft instead. */
.topbar-popover {
  position: fixed; z-index: 140; width: 320px; max-width: calc(100vw - 2rem);
  max-height: calc(100vh - 72px); overflow-y: auto; /* Help lists two tours' steps — scroll inside, never off-screen */
  background: var(--surface-solid); border: 1px solid var(--border-strong); border-radius: var(--radius);
  box-shadow: var(--shadow-md); padding: 0.7rem 0.8rem;
  opacity: 0; transform: translateY(-6px) scale(0.97); transform-origin: top right;
  transition: opacity 0.14s ease, transform 0.16s cubic-bezier(0.4, 0, 0.2, 1);
}
.topbar-popover.popover-open { opacity: 1; transform: translateY(0) scale(1); }
.topbar-popover-title { font-size: 0.85rem; font-weight: 800; margin: 0 0 0.45rem; color: var(--text); }
/* Two icon cards, not pill toggles (Clark, 2026-08-23 — "like supabase's where its two icon
   blocks"). Each carries its own colour ("in their respective colours"): gold for Idea (the
   same --nav-accent hue chips already use for "worth doing"), red for Issue (the same
   --st-expired hue every "something's wrong" badge already uses) — a real distinguishing
   colour, not a decoration, since the two cards are otherwise identical in shape. */
.feedback-types { display: grid; grid-template-columns: 1fr 1fr; gap: 0.5rem; margin-bottom: 0.6rem; }
.feedback-type {
  display: flex; flex-direction: column; align-items: center; text-align: center; gap: 0.3rem;
  min-height: var(--tap-min); padding: 0.7rem 0.5rem; border: 1.5px solid var(--border-strong);
  border-radius: var(--radius-sm); background: var(--surface); cursor: var(--cur-hand);
  transition: background-color 0.12s ease, border-color 0.12s ease, box-shadow 0.12s ease;
}
.feedback-type-icon { --icon-size: 22px; width: 36px; height: 36px; border-radius: 50%; display: grid; place-items: center; }
.feedback-type-icon .icon--photo { width: 22px; height: 22px; }
.feedback-type--gold .feedback-type-icon { background: var(--st-due-soft); color: #8a6d00; }
.feedback-type--red .feedback-type-icon { background: var(--st-expired-soft); color: var(--st-expired); }
.feedback-type-title { font-size: 0.82rem; font-weight: 700; color: var(--text); }
.feedback-type-sub { font-size: 0.68rem; color: var(--text-muted); line-height: 1.3; }
.feedback-type:hover { background: var(--surface-sunken); }
.feedback-type--active { border-color: var(--border-strong); box-shadow: 0 0 0 2px var(--surface), 0 0 0 3.5px var(--accent); }
.feedback-type--gold.feedback-type--active { background: var(--st-due-soft); }
.feedback-type--red.feedback-type--active { background: var(--st-expired-soft); }
.topbar-popover textarea {
  width: 100%; min-height: 88px; resize: vertical; font: inherit; font-size: 0.84rem;
  border: 1px solid var(--border-strong); border-radius: var(--radius-sm); padding: 0.45rem 0.55rem;
  background: var(--surface); color: var(--text);
}
.topbar-popover textarea:focus { outline: none; border-color: var(--accent); box-shadow: 0 0 0 3px var(--accent-soft); }
.feedback-actions { display: flex; align-items: center; gap: 0.5rem; margin-top: 0.5rem; }
.feedback-actions .btn-primary { width: auto; flex-shrink: 0; }
.feedback-note { font-size: 0.68rem; color: var(--text-muted); }
.help-row { display: flex; align-items: baseline; gap: 0.5rem; padding: 0.3rem 0; font-size: 0.8rem; color: var(--text-secondary); line-height: 1.5; }
.help-kbd {
  /* No monospace anywhere (Clark's ban, 2026-09-02 — the customer app was migrated then, this
     console was missed). A keycap does not need one: the box, the border and the bold weight
     are what say "this is a key". */
  flex-shrink: 0; font-size: 0.7rem; font-weight: 700; letter-spacing: 0.02em;
  background: var(--surface-sunken); border: 1px solid var(--border-strong); border-radius: 5px;
  padding: 0.1rem 0.4rem; color: var(--text);
}
.help-sub { font-size: 0.72rem; color: var(--text-muted); margin: 0.3rem 0 0; }
/* Help popover's tour section — "Take the tour" plus one quiet row per step, so a user can
   replay the whole thing or jump to just the step they need (Clark, 2026-08-23). */
.help-tour-head { display: flex; align-items: center; justify-content: space-between; gap: 0.5rem; margin: 0.2rem 0 0.35rem; }
.help-tour-head .btn-primary { width: auto; }
.help-tour-steps { display: grid; grid-template-columns: 1fr 1fr; gap: 0.15rem; }
.help-tour-step {
  display: flex; align-items: center; gap: 0.4rem; min-height: 30px; padding: 0.25rem 0.45rem;
  border: none; border-radius: var(--radius-sm); background: transparent; font: inherit; font-size: 0.74rem;
  color: var(--text-secondary); text-align: left; cursor: var(--cur-hand); transition: background-color 0.12s ease, color 0.12s ease;
}
.help-tour-step:hover { background: var(--surface-sunken); color: var(--text); }
.help-tour-step-n { width: 16px; height: 16px; border-radius: 50%; display: grid; place-items: center; flex-shrink: 0; font-size: 0.6rem; font-weight: 800; background: var(--accent-soft); color: var(--accent); }

/* --- Product tour (web/js/lib/tour.js) -------------------------------------------------------
   Three body-level layers: a click shield, the spotlight (a transparent box whose huge spread
   box-shadow dims everything but the target), and the step card. Spot and card both TRANSITION
   their position, so moving between steps glides rather than jumps. z-index sits above every
   popover (140) and the command palette. */
.tour-shield { position: fixed; inset: 0; z-index: 200; }
.tour-spot {
  position: fixed; z-index: 201; pointer-events: none; border-radius: 12px; opacity: 0;
  box-shadow: 0 0 0 2px rgba(255, 255, 255, 0.95), 0 0 0 9999px rgba(22, 29, 58, 0.58);
  transition: top 0.38s cubic-bezier(0.2, 0.8, 0.2, 1), left 0.38s cubic-bezier(0.2, 0.8, 0.2, 1),
              width 0.38s cubic-bezier(0.2, 0.8, 0.2, 1), height 0.38s cubic-bezier(0.2, 0.8, 0.2, 1), opacity 0.3s ease;
}
.tour-spot.tour-open { opacity: 1; }
.tour-card {
  position: fixed; z-index: 202; width: 340px; max-width: calc(100vw - 24px);
  background: var(--surface-solid); border: 1px solid var(--border-strong); border-radius: var(--radius);
  box-shadow: var(--shadow-md), 0 28px 60px -22px rgba(22, 29, 58, 0.5); padding: 1rem 1.1rem 0.9rem;
  opacity: 0; transform: translateY(8px) scale(0.98);
  transition: opacity 0.22s ease, transform 0.26s cubic-bezier(0.2, 0.8, 0.2, 1),
              top 0.38s cubic-bezier(0.2, 0.8, 0.2, 1), left 0.38s cubic-bezier(0.2, 0.8, 0.2, 1);
}
.tour-card.tour-open { opacity: 1; transform: none; }
.tour-card.tour-card--swap { opacity: 0; transform: translateY(4px) scale(0.99); transition-duration: 0.12s, 0.12s, 0.38s, 0.38s; }
.tour-eyebrow { font-size: 0.64rem; font-weight: 800; letter-spacing: 0.1em; text-transform: uppercase; color: var(--accent); }
.tour-title { font-size: 1rem; font-weight: 800; margin: 0.2rem 0 0.35rem; color: var(--text); }
.tour-body { font-size: 0.84rem; line-height: 1.55; color: var(--text-secondary); margin: 0; }
.tour-foot { display: flex; align-items: center; gap: 0.45rem; margin-top: 0.9rem; }
.tour-foot .btn-primary { width: auto; }
.tour-skip { margin-left: auto; font-size: 0.76rem; color: var(--text-muted); }
.tour-dots { display: flex; align-items: center; gap: 4px; }
.tour-dot { width: 6px; height: 6px; border-radius: 3px; background: var(--border-strong); transition: width 0.25s ease, background-color 0.25s ease; }
.tour-dot--done { background: var(--accent); opacity: 0.45; }
.tour-dot--on { width: 18px; background: var(--accent); }
@media (prefers-reduced-motion: reduce) { .tour-spot, .tour-card, .tour-dot { transition: none; } }

/* --- Getting Started card (dashboard.js, Clark 2026-08-23 "premiumly designed and animated") --
   State is derived from real rows every render (a credential exists, a project exists…) —
   nothing is stored except the per-org dismissal. Entrance: the meter fills, the steps stagger
   in, and each completed mark pops — all once, on paint; none of it loops. */
/* flex-shrink: 0 is load-bearing — #view is a column flexbox, and overflow:hidden (needed to
   clip the ::before wash) resets a flex item's automatic min-height from content-size to 0,
   so without it the card gets squashed to its first line whenever the page overflows. */
.gs-card { position: relative; overflow: hidden; flex-shrink: 0; }
.gs-card::before {
  content: ''; position: absolute; inset: 0; pointer-events: none;
  background: radial-gradient(120% 90% at 100% 0%, var(--accent-soft), transparent 58%);
}
.gs-card > * { position: relative; }
.gs-head { display: flex; align-items: center; gap: 0.9rem; }
.gs-eyebrow { font-size: 0.64rem; font-weight: 800; letter-spacing: 0.1em; text-transform: uppercase; color: var(--accent); }
.gs-title { font-size: 1.05rem; font-weight: 800; margin: 0.1rem 0 0.1rem; }
.gs-sub { font-size: 0.8rem; color: var(--text-secondary); }
.gs-ring { margin-left: auto; }
.gs-dismiss {
  width: 32px; height: 32px; border-radius: var(--radius-sm); border: none; background: transparent;
  color: var(--text-muted); font-size: 1rem; cursor: var(--cur-hand); display: grid; place-items: center; flex-shrink: 0;
  transition: background-color 0.12s ease, color 0.12s ease;
}
.gs-dismiss:hover { background: var(--surface-sunken); color: var(--text); }
.gs-meter { height: 6px; border-radius: 4px; background: var(--surface-sunken); overflow: hidden; margin: 0.85rem 0 0.7rem; }
.gs-meter > span { display: block; height: 100%; border-radius: 4px; background: linear-gradient(90deg, var(--accent), #38bdf8); transform-origin: left; animation: gsFill 0.9s cubic-bezier(0.2, 0.8, 0.2, 1) 0.1s both; }
@keyframes gsFill { from { transform: scaleX(0); } }
.gs-steps { list-style: none; margin: 0; padding: 0; display: grid; gap: 0.15rem; }
.gs-step {
  display: flex; align-items: center; gap: 0.75rem; padding: 0.5rem 0.6rem; border-radius: var(--radius-sm);
  animation: gsStepIn 0.45s cubic-bezier(0.2, 0.8, 0.2, 1) both; animation-delay: calc(var(--i) * 70ms + 140ms);
  transition: background-color 0.12s ease;
}
.gs-step:not(.gs-step--done):hover { background: var(--surface-sunken); }
@keyframes gsStepIn { from { opacity: 0; transform: translateY(6px); } }
.gs-mark {
  width: 26px; height: 26px; border-radius: 50%; display: grid; place-items: center; flex-shrink: 0;
  border: 2px solid var(--border-strong); font-size: 0.72rem; font-weight: 800; color: var(--text-muted); --icon-size: 14px;
}
.gs-step--done .gs-mark { background: var(--st-valid); border-color: var(--st-valid); color: #fff; animation: gsPop 0.5s cubic-bezier(0.34, 1.56, 0.64, 1) both; animation-delay: calc(var(--i) * 70ms + 300ms); }
@keyframes gsPop { from { transform: scale(0.5); } }
.gs-step-main { min-width: 0; flex: 1; }
.gs-step-title { font-size: 0.86rem; font-weight: 700; }
.gs-step--done .gs-step-title { color: var(--text-muted); text-decoration: line-through; text-decoration-color: var(--border-strong); }
.gs-step-sub { font-size: 0.74rem; color: var(--text-muted); }
.gs-step-cta { flex-shrink: 0; font-size: 0.76rem; font-weight: 700; color: var(--accent); text-decoration: none; white-space: nowrap; }
.gs-step-cta:hover { text-decoration: underline; }
.gs-step-done { flex-shrink: 0; font-size: 0.7rem; font-weight: 700; color: var(--st-valid); }
.gs-card--leaving { opacity: 0; transform: translateY(-8px); transition: opacity 0.25s ease, transform 0.25s ease; }
@media (prefers-reduced-motion: reduce) { .gs-meter > span, .gs-step, .gs-step--done .gs-mark { animation: none; } }


.account-popover-header { padding: 0.4rem 0.6rem 0.3rem; }
.account-popover-name { font-size: 0.85rem; font-weight: 700; color: var(--text); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.account-popover-email { font-size: 0.74rem; color: var(--text-muted); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.account-popover-divider { border: none; border-top: 1px solid var(--border); margin: 0.25rem 0; }
/* User settings panel (Clark, 2026-08-23 — "start developing user settings": Profile + Change
   password, inside this same popover, not a modal or a new nav item). Reuses .field/.btn-*
   exactly as any other form in the app — the only new rules here are the layout wrapper, the
   bigger avatar preview, and the save-feedback line. */
.account-settings-body { padding: 0.4rem 0.6rem 0.6rem; }
.account-settings-avatar-row { display: flex; align-items: center; gap: 0.65rem; margin-bottom: 0.7rem; }
.account-settings-avatar.user-avatar { width: 52px; height: 52px; font-size: 1.2rem; }
/* The full-page Account settings (Clark, 2026-08-23 — "redirect into another page, like
   supabase's") reuses this same avatar block at a bigger size, since there's real room now. */
.account-settings-avatar--lg.user-avatar { width: 72px; height: 72px; font-size: 1.6rem; }
.account-page { max-width: 640px; display: flex; flex-direction: column; gap: var(--gap); }
.appearance-theme-row { display: flex; gap: 0.5rem; flex-wrap: wrap; }
.appearance-theme-btn {
  min-height: var(--tap-min); padding: 0.5rem 1rem; border-radius: var(--radius-sm);
  border: 1px solid var(--border-strong); background: var(--surface); color: var(--text);
  font: inherit; font-size: 0.85rem; font-weight: 600; cursor: var(--cur-hand);
  transition: border-color 0.12s ease, background-color 0.12s ease;
}
.appearance-theme-btn:hover { border-color: var(--accent); }
.appearance-theme-btn--active { border-color: var(--accent); background: var(--accent-soft); color: var(--accent); }
/* Ported from web/css/style.css (Clark, 2026-08-23 — "no keep it the same as in the os"):
   compact-by-default sizing for the account popover's Theme/Sidebar-behaviour controls, same
   pointer tradeoff as .account-row below. This block didn't exist yet when ops's own copy of
   style.css was first taken (web gained it later the same session) — keep in step by hand. */
.account-popover-section-label { font-size: 0.68rem; font-weight: 700; text-transform: uppercase; letter-spacing: 0.05em; color: var(--text-muted); padding: 0.5rem 0.6rem 0.3rem; }
/* Ops-only (Clark, 2026-08-23 — "hide it inside here instead"): the version number moved out
   of the header subtitle into the account popover, same quiet placement a lot of admin tools
   use for build/version info — present for support, never competing with the header for
   attention. */
.account-popover-version { text-align: center; font-size: 0.68rem; color: var(--text-muted); padding: 0.4rem 0 0.1rem; }
.account-popover .appearance-theme-row--popover { padding: 0 0.6rem 0.5rem; gap: 0.35rem; }
.account-popover .appearance-theme-row--popover .appearance-theme-btn { min-height: 28px; padding: 0.28rem 0.55rem; font-size: 0.76rem; }
.account-popover .field { padding: 0 0.6rem 0.4rem; margin: 0; }
.account-popover .field label { font-size: 0.68rem; font-weight: 700; text-transform: uppercase; letter-spacing: 0.05em; color: var(--text-muted); }
.account-popover .custom-select-trigger { min-height: var(--tap-desktop); padding: 0.3rem 0.55rem; font-size: 0.8rem; } /* outside .content, so unzoomed: 32px really is 32px */
.account-settings-note { font-size: 0.76rem; color: var(--text-muted); line-height: 1.5; margin: 0 0 0.5rem; }
.account-settings-msg { font-size: 0.76rem; color: var(--st-valid); margin: -0.15rem 0 0.5rem; }
.account-settings-msg--error { color: var(--st-expired); }
/* Compact by default, not --tap-min (Clark, 2026-08-23 — "make this pop up smaller no need for
   it to be this big"): the account popover's own Sidebar + Theme option lists together made it
   the tallest popover in the app, each row carrying the full 44px touch minimum a mouse never
   needed. Same fine-pointer/coarse-pointer tradeoff the sidebar rail and .sidebar-account-btn--
   topbar already use elsewhere in this file — 44px comes back under @media (pointer: coarse)
   below, since a touchscreen genuinely does need the bigger target. Shared by .org-switch-row
   right below (the org/project switcher popovers get the same compacting, consistently). */
.account-row {
  display: flex; align-items: center; gap: 0.55rem; width: 100%;
  min-height: 32px; padding: 0.3rem 0.6rem;
  border: none; border-radius: var(--radius-sm); background: transparent;
  color: var(--text); font: inherit; font-size: 0.85rem; text-align: left; cursor: var(--cur-hand);
  text-decoration: none; /* lands on an <a> now too (the "Account settings" link) as often as a <button> */
  transition: background-color 0.12s ease;
}
.account-row:hover { background: var(--surface-sunken); }
.account-row--danger { color: var(--st-expired); }
.account-row--danger:hover { background: var(--st-expired-soft); }
@media (pointer: coarse) {
  .account-row, .org-switch-row { min-height: var(--tap-min); }
  .account-popover .appearance-theme-row--popover .appearance-theme-btn,
  .account-popover .custom-select-trigger { min-height: var(--tap-min); }
}

/* --- Content area ----------------------------------------------------------------------
   Fixed header, one scrolling region. See the .app-shell comment above for the full
   mechanism — content-wrap and topbar never scroll; #view (.content) is flex:1 with its own
   overflow, so the rail and the header stay exactly where Clark's reference has them. */
/* grid-column: 2 explicit, not left to auto-placement (Clark, 2026-08-22 — "in auto collapse
   mode all the ui does not adjust... but is now completely missing"): .app-body's 2nd grid
   track is meant for this element, but in nav-mode-auto .sidebar.nav-auto is position:absolute
   and so isn't a real grid item at all anymore — leaving .content-wrap as the ONLY actual grid
   child, which auto-placement drops into track 1 (the narrow one reserved for the rail) instead
   of track 2, squeezing all page content into ~60px. Pinning the column explicitly is correct
   in every mode, not just auto (.sidebar IS a real grid item in the other modes, occupying
   track 1 same as always), so this isn't gated behind nav-mode-auto specifically. */
.content-wrap { min-width: 0; height: 100%; display: flex; flex-direction: column; overflow: hidden; grid-column: 2; }
/* Reset to column 1 under .no-sidebar-page (Clark, 2026-08-22 — Organizations page pushed to
   the right with a blank gap on the left): that mode collapses .app-body to a single 1fr
   column (see .no-sidebar-page above), so the explicit grid-column: 2 above — needed for
   every OTHER mode, where the sidebar really does occupy column 1 — pointed .content-wrap at a
   column that no longer exists there, auto-placing it into an implicit empty track 2. */
.app-body.no-sidebar-page .content-wrap { grid-column: 1; }

/* One full-width row now (Clark, 2026-08-22 — see .top-header-brand's own comment): the brand
   box, then the org/project switcher + crumb, then Feedback/Quick jump/Help, all as plain flex
   children of this single row instead of a separate sidebar header + a second .topbar element. */
.top-header {
  display: flex; align-items: stretch; gap: var(--gap);
  background: var(--surface);
  border-bottom: 1px solid var(--border);
  flex-shrink: 0;
}
.topbar-left { display: flex; align-items: center; gap: 0.7rem; min-width: 0; flex: 1 1 auto; padding: clamp(0.3rem, 0.5vw, 0.5rem) 0; }
.topbar-crumb { font-size: 0.72rem; font-weight: 700; letter-spacing: 0.07em; text-transform: uppercase; color: var(--text-muted); white-space: nowrap; }
/* Thin on purpose — the sign-out button that used to force the old .topbar tall is gone (moved
   to the sidebar footer), so the only floor left is the profile chip's own line-height. */
.topbar-right { display: flex; align-items: center; gap: 0.5rem; font-size: 0.8rem; color: var(--text-muted); flex-shrink: 0; padding: clamp(0.3rem, 0.5vw, 0.5rem) var(--pad); }
/* Feedback (plain text button, Supabase's own convention) + Help (icon-only circle) — both
   backlogged (Clark, 2026-08-22): no submission form or help centre exists yet, so each just
   toasts an honest "not built" rather than opening something half-finished. */
.topbar-text-btn {
  border: none; background: transparent; color: var(--text-muted); font: inherit; font-size: 0.8rem;
  padding: 0.3rem 0.5rem; border-radius: var(--radius-sm); cursor: var(--cur-hand);
  transition: background-color 0.12s ease, color 0.12s ease;
}
.topbar-text-btn:hover { background: var(--surface-sunken); color: var(--text); }
.topbar-icon-btn {
  display: grid; place-items: center; width: 32px; height: 32px; border-radius: 50%;
  border: 1px solid var(--border); background: var(--surface); color: var(--text-muted); cursor: var(--cur-hand);
  transition: background-color 0.12s ease, border-color 0.12s ease, color 0.12s ease;
}
.topbar-icon-btn:hover { background: var(--surface-sunken); border-color: var(--border-strong); color: var(--text); }
.topbar-icon-btn { --icon-size: 16px; }

/* Org/project switcher, light-content-plane version (Clark, 2026-08-21 — moved off the dark
   rail into the topbar, "so we dont have to battle with name length clipping": the topbar
   spans the full content width, not a 200px rail, so there's no need for fitOrgNameWords()'s
   word-trimming here — text-overflow:ellipsis is enough room to almost never trigger. Shares
   .org-switch-popover/-row/-check (style.css, further down) with the sidebar-era switcher —
   those are already position:fixed and self-positioning off the trigger's own rect, so they
   work unchanged regardless of which button opens them. */
.topbar-switcher { display: flex; min-width: 0; }
.topbar-switch-btn {
  display: flex; align-items: center; gap: 0.5rem; min-width: 0; max-width: 280px;
  padding: 0.32rem 0.7rem; border-radius: var(--radius-sm);
  border: 1px solid var(--border-strong); background: var(--surface);
  color: var(--text); font: inherit; font-weight: 600; font-size: 0.85rem;
  cursor: var(--cur-hand); transition: background-color 0.12s ease;
}
.topbar-switch-btn:hover, .topbar-switch-btn[aria-expanded="true"] { background: var(--surface-sunken); }
.topbar-switch-btn--static { cursor: var(--cur-arrow); }
.topbar-switch-btn--static:hover { background: var(--surface); }
.topbar-switch-avatar {
  display: grid; place-items: center; flex-shrink: 0;
  width: 18px; height: 18px; border-radius: 50%; overflow: hidden;
  background: var(--tile-blue); color: #fff; font-size: 0.6rem; font-weight: 700; --icon-size: 11px;
}
/* A project's own photo (projects.js's mountProjectAvatars()) fills this circle instead of the
   org-initial/folder-icon default — the project switcher only, org switcher always shows the
   initial bubble. */
.topbar-switch-avatar img { width: 100%; height: 100%; object-fit: cover; }
.topbar-switch-name { min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.topbar-switch-chevron { display: flex; align-items: center; flex-shrink: 0; --icon-size: 12px; color: var(--text-muted); }

/* .user-avatar now lives in the sidebar's account button, not the topbar (Clark, 2026-08-17)
   — the id/class stayed the same since auth.js's showApp() just fills whatever's in the DOM,
   only the markup that hosts it moved (see index.html). */
/* Positioning context for .account-hint-dot, below — a separate wrapper, not .user-avatar
   itself, since auth.js's showApp() sets avatar.textContent directly (see index.html's own
   comment) and would wipe out a child element living inside .user-avatar. */
.user-avatar-wrap { position: relative; display: grid; flex-shrink: 0; }
.user-avatar {
  display: grid; place-items: center; flex-shrink: 0;
  /* Sized up (Clark, 2026-08-17 — "make pfp icon bigger") now that this is the sidebar
     account button's own avatar, not a small topbar chip — it carries more visual weight
     down there next to the name/email two-line block. */
  width: 34px; height: 34px; border-radius: 50%;
  background: var(--accent-soft); color: var(--accent);
  font-size: 0.85rem; font-weight: 700; text-transform: uppercase;
  overflow: hidden; /* clips the uploaded-photo <img> below into the circle */
}
.user-avatar img { width: 100%; height: 100%; object-fit: cover; }
/* New-feature dot (Clark, 2026-08-22 — see index.html's own comment on #account-hint-dot):
   a small filled circle at the avatar's edge, gone for good once the account popover's been
   opened once. Decorative-only (no text meaning riding on the colour alone, unlike a status
   badge), so reusing --st-valid (the app's existing "live/good" green, e.g. a Valid credential
   badge) reads fine here rather than needing its own colour. The pulsing ring is a plain ::after
   ping — the dot itself stays put and solid; only the ring scales up and fades. */
.account-hint-dot {
  position: absolute; top: -1px; right: -1px; width: 10px; height: 10px; border-radius: 50%;
  background: var(--st-valid); border: 2px solid var(--nav-bg);
}
.account-hint-dot::after {
  content: ''; position: absolute; inset: -2px; border-radius: 50%; background: var(--st-valid);
  animation: account-hint-pulse 1.8s ease-out infinite;
}
@keyframes account-hint-pulse {
  0% { transform: scale(1); opacity: 0.6; }
  100% { transform: scale(2.3); opacity: 0; }
}
@media (prefers-reduced-motion: reduce) {
  .account-hint-dot::after { animation: none; display: none; }
}

/* Top padding trimmed well below the sides/bottom — the page title sat under a full --pad
   gap that read as dead space and pushed content into an avoidable scrollbar on short
   windows. Every page gets this since .content wraps every route. */
/* A flex column so any page can hand one element `flex:1 1 auto; min-height:0` and have it
   fill whatever room is actually left (see .md-wrap below) instead of guessing a viewport
   fraction. Block children behave exactly as before — flex-direction:column stacks them the
   same way normal flow would. */
/* Content-plane scale (Clark, 2026-08-18: "the scale of all the content is too big and we're
   wasting space, i tried using chrome zoom scale factor to 90% and it looks good... dont touch
   the background wave design, sidebar size and icons, and the header, just the content") —
   `zoom`, not `transform: scale()`: transform shrinks the box visually but keeps its original
   layout footprint (dead space around it), where `zoom` reflows layout in the smaller scale the
   same way Chrome's own page zoom does, so every clamp()/auto-fit/minmax() rule in here
   naturally uses the freed-up room instead of needing separate "make it wider" edits per
   component. Scoped to `.content` only (`main.content`, everything a page renders) — `.sidebar`,
   `.topbar` and `#gradient-waves-bg` are outside `.content`'s subtree in the DOM (see
   index.html's `.content-wrap`), so none of them are affected. */
.content { padding: calc(var(--pad) * 0.5) var(--pad) 0; flex: 1 1 auto; min-width: 0; min-height: 0; overflow-y: auto; display: flex; flex-direction: column; zoom: 0.9; }
/* A flex-column scroll container's own end-edge padding is silently dropped once its content
   overflows — a long-standing Chrome/Firefox quirk. Every page's last element sat flush against
   the viewport bottom the moment it needed to scroll (Clark, 2026-08-17).
   First fix here was margin-bottom on .content's last DOM child (.md-wrap, a panel, whatever
   it happened to be) — worked until a page where a DESCENDANT legitimately overflows its own
   parent's box, so margin added on the parent never reaches the true bottom. A generated flex
   item fixes this properly: .content is display:flex, so a ::after with real content/display IS
   a flex item — an independent sibling with a guaranteed height, not nested inside (and thus
   not at the mercy of) whichever child happens to overflow.
   ONE guaranteed gap, not three stacked ones (Clark, 2026-08-18 — "the bottom margin of the
   whole content's block is too far... make use of all that empty space"): .content's own
   bottom padding above is now 0, and .md-wrap's old margin-bottom is gone (see that rule) —
   both were redundant with this ::after once the master-detail card stopped being able to
   overflow its own box (the record/documents merge bounded .md3-detail's scroll internally, so
   the "descendant escapes its parent" case this whole mechanism was built for can't happen on
   this page anymore). This ::after is now the ONLY reserved bottom space, which is what frees
   the extra room for .md-wrap/.md3 to flex-grow into. */
.content::after { content: ''; display: block; flex-shrink: 0; height: var(--pad); }

/* Page/sidebar entrance animation (Clark, 2026-08-22 — "fix transition between pages instead of
   it being a big flash... the sidebar being a animated clean slide down". A first pass built
   this on the View Transitions API and it kept surfacing new, worse problems — "highlight
   selection delay in the sidebar" (the API freezes the OLD page, active-row highlight included,
   until the ENTIRE callback resolves, so the highlight only moved once the whole destination
   page had finished loading data, not the instant you clicked), "the sidebar load animation is
   not clean... it just overlays the old sidebar nav" and "constant flashing and jitter... same
   problem with sidebar nav icons" (the API tries to geometrically MORPH the before/after
   snapshot into each other, and the team shell's 8-item list vs. a project shell's 6-items-
   plus-subtabs list are different enough heights that the interpolation between two mismatched
   snapshots read as overlap/flicker). Replaced entirely with mountFadeInObserver() (app.js): a
   MutationObserver on each container that replays a plain CSS entrance animation whenever its
   DIRECT CHILDREN actually get replaced. Nothing is ever frozen — renderNav()/page.render() are
   plain unwrapped awaits now, so the active-row highlight (its own separate, already-instant
   CSS transition, see .nav-item.active) moves the moment it's clicked — and since toggling
   `.active`/`aria-current` on an EXISTING row is an attribute mutation, not a childList one, an
   ordinary same-shell click never triggers #nav-items' animation at all; only an actual rebuild
   (buildOrgShellNav()/buildProjectShellNav() replacing its contents wholesale) does. No
   snapshot pairing anywhere, so there's nothing left to mismatch or ghost. */
/* "Focus Pull" (Clark, 2026-08-22 — picked from a set of 3 live-replayable options after
   calling the plain fade/rise above "pretty generic"): starts soft-blurred and a touch zoomed,
   sharpens into place — reads like a camera racking focus onto the new page rather than a flat
   fade. cubic-bezier(0.16,1,0.3,1) ("expo-out") front-loads the motion so it settles without
   ANY overshoot/bounce. First pass ran at 0.38s/6px blur and read as "too fast" — at that
   length the blur barely registers before it's gone, closer to a flicker than a pull; "snappy"
   in motion terms isn't the shortest possible duration, it's a duration long enough to actually
   perceive paired with a decisive ease-out, which is why lengthening it (not shortening) is
   what makes this read as snappy rather than sluggish. Blur/scale bumped up a touch too, so the
   pull itself has more presence at the longer length.
   ONE tier of TWO now, not the only one (Clark, 2026-08-22, two more rounds): the skeleton-to-
   real-data handoff still read as jittery, and separately "no need to fire focus pull animation
   each time [an in-page filter changes, e.g. Audit Log's tabs]... just a clean smooth animated
   transition". mountViewFadeInObserver() (app.js) only ever plays THIS one once per actual page
   navigation — see .view-content-update-in below for everything else. */
@keyframes view-page-transition-in {
  from { opacity: 0; filter: blur(9px); transform: scale(1.025); }
  to { opacity: 1; filter: blur(0); transform: scale(1); }
}
#view.view-page-transition-in { animation: view-page-transition-in 0.5s cubic-bezier(0.16, 1, 0.3, 1); }

/* The quiet tier — a plain, quick crossfade for anything that ISN'T a fresh page landing: the
   skeleton settling into real data, a filter tab, search-as-you-type, any in-page redraw a page
   does on its own. No blur/scale — repeating the heavy effect on every small update would read
   as the page constantly "refocusing" rather than one clean arrival. */
@keyframes view-content-update-in {
  from { opacity: 0; transform: translateY(4px); }
  to { opacity: 1; transform: translateY(0); }
}
#view.view-content-update-in { animation: view-content-update-in 0.16s cubic-bezier(0.4, 0, 0.2, 1); }

/* "Staggered Rows" (Clark, 2026-08-22, same pick): each row eases in a beat after the last
   instead of the whole list dropping as one block — --i (set per-row in web's own
   buildOrgShellNav()/buildProjectShellNav()) drives the delay, so this scales to any row count
   automatically rather than a fixed set of nth-child rules. Ops targets .nav-item directly, not
   .nav-item-group (Clark, 2026-08-24 — "implement all important transitions... to Platform
   manager"): the ops rail is one flat, fixed 18-item list with no org/project shell to switch
   between, so there's no group wrapper here — renderNav() (app.js) builds it once and only
   ever toggles .active afterward, which is what keeps this from replaying on every nav click. */
@keyframes nav-row-in {
  from { opacity: 0; transform: translateY(10px); }
  to { opacity: 1; transform: translateY(0); }
}
#nav-items.nav-items-fade-in > .nav-item {
  animation: nav-row-in 0.36s cubic-bezier(0.16, 1, 0.3, 1) both;
  animation-delay: calc(var(--i, 0) * 28ms);
}

@media (prefers-reduced-motion: reduce) {
  #view.view-page-transition-in, #view.view-content-update-in { animation: none; }
  #nav-items.nav-items-fade-in > .nav-item { animation: none; }
}

.page-title { font-size: var(--fs-xl); margin: 0 0 0.25rem; letter-spacing: -0.01em; }
/* No max-width cap (Clark, 2026-08-20 — was 70ch: this is a one-line subtitle caption under
   a page title, not flowing paragraph prose, so the usual line-length readability cap doesn't
   apply the same way; a 70ch ceiling wrapped it to a second line even with plenty of room
   still free in .content on an ordinary desktop width). Wraps naturally once the real
   container narrows (mobile, a split pane), same as everything else in .content. */
.page-sub { font-size: var(--fs-base); line-height: 1.45; margin: 0 0 0.6rem; color: var(--text-muted); }
.back-link { display: inline-block; color: var(--text-muted); text-decoration: none; font-size: 0.83rem; margin-bottom: 0.6rem; }
.back-link:hover { color: var(--accent); }

.panel {
  background: var(--glass-surface-bg);
  border: 1px solid var(--glass-surface-border);
  border-radius: var(--radius);
  box-shadow: var(--shadow-md);
  /* Tighter than a flat var(--pad) on both — panels stack several deep on content-heavy
     pages (Settings, Company), and the old full-pad gap between every one of them added up
     to real dead space long before the page ran out of actual content. */
  padding: calc(var(--pad) * 0.6);
  margin-bottom: calc(var(--pad) * 0.4);
}
.panel--error { border-color: rgba(178, 48, 48, 0.4); box-shadow: var(--shadow-md), inset 3px 0 0 var(--st-expired); }
/* The raw error string behind "Show technical details" (app.js's renderRoute() catch) — reads
   as a log line, not body copy, so it doesn't look like part of the friendly explanation
   above it. */
/* Reads as a log line rather than body copy without a mono face: smaller, dimmer and with the
   digit alignment that actually helps someone compare a string against a typo. */
.err-detail { font-size: 0.76rem; margin-top: 0.3rem; font-variant-numeric: tabular-nums; letter-spacing: 0.01em; color: var(--text-muted); }
/* An honest "not built yet" placeholder (Clark's standing rule: never a fake page) — plain
   panel, centered, no danger styling since this isn't an error. */
.panel--empty { text-align: center; padding: calc(var(--pad) * 1.4) calc(var(--pad) * 0.6); }
.panel--empty h2 { margin: 0 0 0.4rem; font-size: var(--fs-md); color: var(--text-muted); }
.panel--empty p { margin: 0; max-width: 44ch; margin-inline: auto; }
.panel-title { font-size: var(--fs-md); margin: 0 0 0.4rem; }
.panel-head-row { display: flex; align-items: center; gap: 0.5rem; margin-bottom: 0.4rem; }
.panel-head-row .panel-title { margin-bottom: 0; }
/* Title left, one action button right (Clark, 2026-08-24 — Costs' "Log a cost" form moved
   behind a toggle button on the Ledger panel's own header, same idea as credentials.js's
   .page-head "Import documents" button, just scoped to one panel instead of the whole page). */
.panel-head-row--split { justify-content: space-between; }
.panel-title--standalone { margin: 0.8rem 0 0.35rem; }
/* justify-content: space-between, not the .panel-summary's own default flex flow (Clark,
   2026-08-17): overriding a native <summary>'s display suppresses its default triangle marker
   with no replacement, so without an explicit caret this row gave no visual sign it expands —
   same fix as .req-caret below, at the opposite end of the row. */
.panel-summary {
  width: 100%; min-height: var(--tap-min); display: flex; align-items: center; justify-content: space-between;
  border: none; background: transparent; font: inherit; text-align: left;
  cursor: var(--cur-hand); font-weight: 600; font-size: 0.92rem;
}
.panel-summary-caret { color: var(--text-muted); --icon-size: 16px; transition: transform 0.15s ease; }
.collapse-panel.open > .panel-summary .panel-summary-caret { transform: rotate(90deg); }
/* animateExpand()-driven (shared.js), not a fixed max-height cap — this panel's content
   (a roster table, a form) isn't reliably bounded. max-height:0 here is only the pre-JS
   default; animateExpand sets the real inline max-height on open/close. */
.panel-collapse { max-height: 0; opacity: 0; overflow: hidden; transition: max-height 0.28s cubic-bezier(0.4, 0, 0.2, 1), opacity 0.2s ease; }
/* Covers a panel server-rendered already-open (e.g. re-drawing with rosterOpen already true) —
   animateExpand() only runs on a user-triggered toggle, so a fresh render needs its own way to
   start fully visible instead of clipped at the CSS default above. */
.collapse-panel.open > .panel-collapse { max-height: none; opacity: 1; overflow: visible; }

.callout {
  background: var(--accent-soft);
  border-left: 3px solid var(--accent);
  border-radius: 0 var(--radius-sm) var(--radius-sm) 0;
  padding: 0.7rem var(--pad);
  font-size: 0.85rem;
  line-height: 1.6;
  margin-bottom: var(--pad);
}
.disclaimer {
  font-size: 0.74rem; line-height: 1.5; color: var(--text-muted);
  border-top: 1px solid var(--border); padding-top: 0.6rem; margin-top: 1rem; max-width: 78ch;
}

/* --- Badges & chips --------------------------------------------------------------------- */
/* Status colour never travels alone — the badge always carries its text label.

   Solid fills, not tinted text (Clark, 2026-08-16). A pale-amber-text-on-pale-amber-tint
   badge has to stay dark and desaturated to hold 3:1 contrast on white while also reading as
   distinct from the red "expired" badge — that combination of constraints only has solutions
   in the yellow-olive-brown zone, no matter which exact amber is picked (verified with the
   dataviz skill's validator: shifting the hue warmer to look less "brown" pulls it toward
   red's hue and drops the colour-blind-safe separation below the floor). Solid fill sidesteps
   the whole trade-off: dark text (var(--text), #0b0b0b) on the brand gold (var(--nav-accent),
   already used in the sidebar) clears ~12:1 contrast, and distinctness now comes from a solid
   colour block, not a subtle text hue. */
/* Docket shape (Clark, 2026-08-18 — picked from the Pill Themes exploration): square-left,
   rounded-right, the exact asymmetry already used for the active nav-bar item
   (.nav-item.active's own border-radius) rather than the generic full pill every status
   indicator used before. Applied here and below to .chip/.role-chip and .sev; .badge (already
   solid-filled, no spine needed) gets the shape change only. */
.badge {
  display: inline-flex; align-items: center; gap: 0.3rem;
  padding: 0.18rem 0.55rem;
  border-radius: 3px var(--radius-sm) var(--radius-sm) 3px;
  font-size: 0.72rem; font-weight: 700; white-space: nowrap;
  border: 1px solid transparent;
  --icon-size: 11px;
}
/* Slightly bolder than the app-wide 2px default at this size — a 1px hairline nearly vanishes
   at 11px, and this glyph is doing real work (shape, not just colour, carries the status). */
.badge-icon { stroke-width: 2.5; }
.badge-icon.icon--solid { stroke-width: 0; }
.badge--danger  { background: var(--st-expired); color: #fff; }
.badge--warn    { background: var(--nav-accent); color: var(--nav-accent-ink); }
.badge--ok      { background: var(--st-valid);   color: #fff; }
.badge--neutral { background: var(--st-neutral-soft); color: var(--text-muted); border-color: var(--border); }

.chip {
  display: inline-block; padding: 0.18rem 0.5rem; border-radius: 3px var(--radius-sm) var(--radius-sm) 3px;
  background: var(--surface-sunken); border: 1px solid var(--border); border-left: 3px solid var(--text-muted);
  font-size: 0.7rem; white-space: nowrap; color: var(--text-secondary);
}
/* A chip that's actually a button — click to reveal the control it stands in for (the team
   role select). Same "view first, click to edit" idea as the compliance record toggle, sized
   down to fit a single table cell instead of a whole Edit button. */
.chip--editable {
  display: inline-flex; align-items: center; gap: 0.2rem;
  font: inherit; cursor: var(--cur-hand); min-height: 26px;
  transition: background-color 0.12s ease, border-color 0.12s ease;
}
.chip--editable:hover { background: var(--surface); border-color: var(--border-strong); }
.chip--editable { --icon-size: 11px; }
@media (pointer: coarse) { .chip--editable { min-height: var(--tap-min); } }

/* Foundation marker (Clark, 2026-08-18) — foundationChip() in compliance-ui.js. Blue, not a
   severity/status colour, since it's a structural fact ("N others depend on this"), not an
   urgency judgment. Same tile-blue used for the admin role chip, kept as an outline+spine so
   it reads as its own quiet system next to .sev and .badge rather than competing with either. */
.chip--foundation {
  display: inline-flex; align-items: center; gap: 0.25rem;
  margin-left: 0.4rem; vertical-align: 1px;
  background: var(--accent-soft); border-color: rgba(2, 132, 199, 0.4); border-left-color: var(--tile-blue);
  color: var(--tile-blue); font-weight: 700; font-size: 0.62rem;
  text-transform: uppercase; letter-spacing: 0.02em;
  --icon-size: 10px;
}
.chip-icon { stroke-width: 2.5; }

/* Role chips (Clark, 2026-08-17 — see roleChipClass() in shared.js for which role gets which
   class and why Viewer is left plain). Pale tint + outline, same restrained weight as .sev
   below — a first pass used solid filled pills and it read as too loud/over the top next to
   everything else in this list, which is all quiet outline chips. */
/* Owner: gold border/background but neutral text, not gold text (Clark, 2026-08-17 — "i dont
   like the owner pill being brown") — the same "gold as a dot/border, not text" convention the
   app already uses elsewhere (see .conf--mid, .need--cond): gold text dark enough to hold
   contrast on white unavoidably reads brownish/olive (see the .badge comment on why), so the
   colour carries the accent as a border/fill and the text just stays legible. */
.role-chip--owner   { background: var(--st-due-soft); border-color: rgba(196, 158, 42, 0.6); border-left-color: var(--nav-accent); color: var(--text-secondary); }
.role-chip--admin   { background: var(--accent-soft); border-color: rgba(2, 132, 199, 0.4); border-left-color: var(--tile-blue); color: var(--tile-blue); }
.role-chip--liaison { background: var(--st-valid-soft); border-color: rgba(22, 122, 22, 0.4); border-left-color: var(--st-valid); color: var(--st-valid); }

/* Project status chips (Clark, 2026-08-22 — "colour code them properly... green when its
   active... keep it greyed out if its only a draft"): same outline+left-spine shape as the
   role chips above, same reused status tokens as everywhere else in the app (--st-valid for a
   currently-live state, --st-due/--nav-accent for "paused, not urgent", --tile-blue for a
   calm "finished" that isn't the same as "in progress", --st-expired for a stopped project).
   Draft gets no rule at all — statusChipClass() (projects.js) returns '' for it, so it stays
   the plain neutral .chip. */
.status-chip--active    { background: var(--st-valid-soft); border-color: rgba(22, 122, 22, 0.4); border-left-color: var(--st-valid); color: var(--st-valid); }
.status-chip--on-hold   { background: var(--st-due-soft); border-color: rgba(196, 158, 42, 0.6); border-left-color: var(--nav-accent); color: var(--text-secondary); }
.status-chip--completed { background: var(--accent-soft); border-color: rgba(2, 132, 199, 0.4); border-left-color: var(--tile-blue); color: var(--tile-blue); }
.status-chip--cancelled { background: var(--st-expired-soft); border-color: rgba(178, 48, 48, 0.4); border-left-color: var(--st-expired); color: var(--st-expired); }

/* Severity chips — static importance, distinct from the (filled) status badges. Outline-only
   so the two can sit side by side without reading as the same system. */
.sev {
  display: inline-flex; align-items: center; gap: 0.28rem;
  padding: 0.02rem 0.4rem;
  border-radius: 3px var(--radius-sm) var(--radius-sm) 3px;
  font-size: 0.6rem; font-weight: 700;
  text-transform: uppercase; letter-spacing: 0.03em;
  border: 1px solid; border-left-width: 3px;
  vertical-align: 1px;
  white-space: nowrap;
}
.sev--danger  { color: var(--st-expired); border-color: rgba(178, 48, 48, 0.45); border-left-color: var(--st-expired); }
/* "Important" carries gold as a dot, not as its own text colour — gold text small enough to
   read as a chip label has to go dark/desaturated to stay legible on white (the exact
   dirt-brown problem the badges just moved away from), so the accent lives in a solid dot
   instead and the text stays neutral and easy to read. */
.sev--warn    { color: var(--text-secondary); border-color: rgba(232, 200, 74, 0.6); border-left-color: var(--nav-accent); }
.sev--neutral { color: var(--text-muted); border-color: var(--border-strong); border-left-color: var(--border-strong); }
.sev-dot { width: 5px; height: 5px; border-radius: 50%; background: var(--nav-accent); flex-shrink: 0; }

.md-detail-badges { display: flex; align-items: center; gap: 0.45rem; flex-shrink: 0; }

/* --- Stat tiles -------------------------------------------------------------------------- */
/* Fixed 3 columns, not auto-fit (Clark, 2026-08-17): the dashboard always renders exactly six
   tiles, and auto-fit's column count depends on container width — at several real intermediate
   widths (~560-700px) it lands on 4, 5 or a lonely single tile on its own row instead of a
   balanced set. 3 columns keeps two even rows of three at any width down to the mobile switch;
   auto-fit remains the right tool for lists whose length actually varies (nothing here does). */
.tiles {
  display: grid; grid-template-columns: repeat(3, 1fr);
  gap: var(--gap); margin-bottom: calc(var(--pad) * 0.6);
}
@media (max-width: 560px) { .tiles { grid-template-columns: repeat(2, 1fr); } }
.tile {
  background: var(--glass-surface-bg);
  border: 1px solid var(--glass-surface-border); border-radius: var(--radius);
  box-shadow: var(--shadow-elevated);
  padding: 0.5rem var(--pad);
}
/* Same inset-accent-bar idea as .panel--error. No hover state — these are plain, non-interactive
   divs, and motion on something you can't click would just be noise (Clark's "dont over do it"). */
.tile--danger { box-shadow: var(--shadow-elevated), inset 3px 0 0 var(--st-expired); }
.tile--warn   { box-shadow: var(--shadow-elevated), inset 3px 0 0 var(--nav-accent); }
.tile--ok     { box-shadow: var(--shadow-elevated), inset 3px 0 0 var(--st-valid); }
.tile-value { font-size: 1.55rem; font-weight: 700; line-height: 1.1; }
.tile-label { font-size: 0.75rem; color: var(--text-muted); margin-top: 0.1rem; }

/* --- Alert / mini lists -------------------------------------------------------------------- */
.alert-list, .mini-list, .history-list { list-style: none; margin: 0; padding: 0; }
.alert-link, .mini-row {
  display: flex; align-items: center; justify-content: space-between; gap: var(--gap);
  min-height: var(--tap-min); padding: 0.5rem 0;
  border-bottom: 1px solid var(--border);
  color: var(--text); text-decoration: none; flex-wrap: wrap;
  transition: color 0.12s ease;
}
.alert-list li:last-child .alert-link, .mini-list li:last-child .mini-row { border-bottom: none; }
.alert-link:hover, .mini-row:hover { color: var(--accent); }
.alert-main, .mini-main { display: flex; flex-direction: column; gap: 0.1rem; min-width: 0; }
.alert-name, .mini-name { font-size: 0.89rem; font-weight: 600; }
.alert-where, .mini-main .muted { font-size: 0.76rem; }
.alert-meta, .mini-meta { display: flex; align-items: center; gap: 0.5rem; font-size: 0.76rem; }

/* --- Dashboard revamp (Clark, 2026-08-23 — Concept A "Signal Strip" / "Gate Tracker" + the
   billed-vs-disbursed money chart from Concept B; see the Dashboard Concepts artifact).
   All chart colour here is the already-established status set (--st-expired /
   --nav-accent / --st-valid) or the single accent hue for magnitude — and never colour alone:
   every ribbon segment, ring and bar carries its count/label as text (/dataviz rule). ------- */
/* stretch, not start (Clark, 2026-08-24 — "make by category block fill it's space"): a ribbon+
   legend panel (Costs' "By category", Overview's own "Accounts by plan") is naturally shorter
   than the bar-chart panel beside it, and align-items:start just left it looking under-filled
   next to a taller sibling instead of matching the row's real height. .panel--fill-center (below)
   is what actually centers that panel's own content in the extra height this now gives it. */
.dash-cols { display: grid; grid-template-columns: 1.6fr 1fr; gap: calc(var(--pad) * 0.5); align-items: stretch; }
@media (max-width: 900px) { .dash-cols { grid-template-columns: 1fr; } }
.panel--fill-center { display: flex; flex-direction: column; }
.panel--fill-center > .panel-fill-body { flex: 1 1 auto; display: flex; flex-direction: column; justify-content: center; }

/* Portfolio exposure ribbon — one stacked bar of every tracked credential by expiry state.
   2px gaps between segments (the /dataviz "surface gap between fills" spec) via flex gap. */
.exposure-ribbon { display: flex; gap: 2px; height: 24px; border-radius: 6px; margin: 0.35rem 0 0.5rem; }
/* Segments are real <button>s — clicking drills into that state's credentials (dashboard.js).
   overflow moved off the ribbon so the active ring below isn't clipped at its ends. */
.exposure-seg {
  display: flex; align-items: center; justify-content: center; font-size: 0.66rem; font-weight: 700; min-width: 0;
  border: none; padding: 0; font-family: inherit; cursor: var(--cur-hand); border-radius: 3px;
}
.exposure-ribbon .exposure-seg:first-child { border-radius: 6px 3px 3px 6px; }
.exposure-ribbon .exposure-seg:last-child { border-radius: 3px 6px 6px 3px; }
.exposure-seg.drill-active { box-shadow: 0 0 0 2px var(--surface), 0 0 0 4px var(--accent); }
.exposure-seg--expired { background: var(--st-expired); color: #fff; }
.exposure-seg--due     { background: var(--nav-accent); color: var(--nav-accent-ink); }
.exposure-seg--valid   { background: var(--st-valid); color: #fff; }
.exposure-seg--pending { background: var(--st-neutral); color: #fff; }
.exposure-legend { display: flex; flex-wrap: wrap; gap: 0.4rem 1.1rem; font-size: 0.72rem; font-weight: 600; color: var(--text-secondary); }
.exposure-legend span { display: inline-flex; align-items: center; gap: 0.35rem; }
.exposure-dot { width: 10px; height: 10px; border-radius: 3px; flex-shrink: 0; }

/* Renewals-by-month bars — single-series magnitude, so one hue (accent), no legend, direct
   value labels on every column (they're the data labels, not decoration). Zero months keep a
   grey stub so the timeline reads continuously. */
.dash-bars { display: flex; align-items: flex-end; gap: 0.7rem; height: 110px; padding-top: 0.3rem; }
/* Columns are real <button>s — clicking drills into that month's renewals (dashboard.js);
   empty months are disabled, keeping the timeline continuous but honest about having nothing
   to show. */
.dash-bar-col {
  flex: 1; height: 100%; display: flex; flex-direction: column; align-items: center; justify-content: flex-end; gap: 0.25rem; min-width: 0;
  border: none; background: none; padding: 0; font-family: inherit; cursor: var(--cur-hand); border-radius: var(--radius-sm);
  /* Explicit, not left to the UA button stylesheet (Clark, 2026-08-23 — dark-mode contrast
     sweep): a plain <button> never inherits `color` from the page by default, and Chrome's own
     default for a DISABLED one is a fixed near-black `rgba(16,16,16,.3)` — invisible-adjacent
     on a light page already, fully unreadable once the page itself went dark (found on the
     zero-due months, e.g. "Aug", which render disabled). .dash-bar-val below has no color of
     its own for the same reason — it was always meant to inherit this. */
  color: var(--text);
}
.dash-bar-col:disabled { cursor: var(--cur-arrow); color: var(--text-muted); }
.dash-bar-col:not(:disabled):hover .dash-bar { background: var(--accent-hover); }
.dash-bar-col.drill-active .dash-bar { box-shadow: 0 0 0 2px var(--surface), 0 0 0 4px var(--accent); }
.dash-bar-col.drill-active .dash-bar-label { color: var(--text); }
.dash-bar { width: 100%; max-width: 34px; background: var(--accent); border-radius: 4px 4px 2px 2px; min-height: 3px; }
.dash-bar--zero { background: var(--border-strong); }
.dash-bar-val { font-size: 0.7rem; font-weight: 700; font-variant-numeric: tabular-nums; }
.dash-bar-label { font-size: 0.66rem; color: var(--text-muted); font-weight: 600; }

/* Project rows with readiness rings (org dashboard's Projects panel). The ring's fraction is
   text inside it — the conic fill is reinforcement, never the sole carrier. */
.proj-ring-row { display: flex; align-items: center; gap: 0.8rem; min-height: var(--tap-min); padding: 0.5rem 0; border-bottom: 1px solid var(--border); color: var(--text); text-decoration: none; transition: color 0.12s ease; }
.mini-list li:last-child .proj-ring-row { border-bottom: none; }
.proj-ring-row:hover { color: var(--accent); }
.proj-ring { width: 52px; height: 52px; border-radius: 50%; display: grid; place-items: center; flex-shrink: 0; }
.proj-ring-inner { width: 38px; height: 38px; border-radius: 50%; background: var(--surface); display: grid; place-items: center; font-size: 0.7rem; font-weight: 800; color: var(--text); }
.proj-ring-main { min-width: 0; display: flex; flex-direction: column; gap: 0.1rem; }
.proj-ring-name { font-size: 0.89rem; font-weight: 600; }
.proj-ring-sub { font-size: 0.74rem; color: var(--text-muted); }
.proj-ring-sub--danger { color: var(--st-expired); font-weight: 600; }

/* Small secondary line under a meter or in a fact panel (Money, Key dates). */
.dash-note { font-size: 0.74rem; color: var(--text-secondary); margin-top: 0.25rem; }

/* Inline drill-down list a chart click opens (exposure segment / renewal month bar). */
.dash-drill { margin-top: 0.6rem; border-top: 1px solid var(--border); padding-top: 0.4rem; }
.dash-drill-title { font-size: 0.72rem; font-weight: 700; letter-spacing: 0.04em; text-transform: uppercase; color: var(--text-muted); margin-bottom: 0.1rem; }

/* Quiet jump link in a dashboard panel's head row ("Open Cash Flow →"). */
.dash-jump { margin-left: auto; font-size: 0.74rem; font-weight: 600; white-space: nowrap; text-decoration: none; }
.dash-jump:hover { text-decoration: underline; }

/* Thin single-fill progress bar (money billed %). */
.dash-meter { height: 8px; border-radius: 5px; background: var(--surface-sunken); overflow: hidden; }
.dash-meter > span { display: block; height: 100%; background: var(--accent); border-radius: 5px; }

/* Compact readiness gate strip — the same computeReadiness() data as Initiation's fuller
   .readiness cards, shaped as a 3-across track for the dashboard. */
.gate-strip { display: flex; gap: 0.5rem; }
@media (max-width: 700px) { .gate-strip { flex-direction: column; } }
.gate-step { flex: 1; padding: 0.55rem 0.7rem; border-radius: var(--radius-sm); background: var(--surface-sunken); border: 1px solid var(--border); min-width: 0; }
.gate-step--open { background: var(--st-valid-soft); border-color: rgba(22, 122, 22, 0.25); }
.gate-step--blocked { background: var(--st-due-soft); border-color: rgba(196, 158, 42, 0.4); }
.gate-step-name { font-size: 0.76rem; font-weight: 700; }
.gate-step-state { font-size: 0.7rem; font-weight: 700; margin-top: 0.15rem; }
.gate-step--open .gate-step-state { color: var(--st-valid); }
.gate-step--blocked .gate-step-state { color: #8a6d00; }
.gate-step-needs { font-size: 0.7rem; color: var(--text-secondary); margin-top: 0.3rem; }

/* Billed-vs-disbursed grouped bars (project Money panel) — two series, so a legend is
   mandatory; billed wears the accent, disbursed the neutral border tone. */
.cash-bars { display: flex; align-items: flex-end; gap: 1rem; height: 84px; padding-top: 0.3rem; }
.cash-group { flex: 1; height: 100%; display: flex; flex-direction: column; align-items: center; justify-content: flex-end; gap: 0.25rem; }
.cash-pair { display: flex; align-items: flex-end; gap: 3px; height: 100%; }
.cash-bar { width: 14px; border-radius: 3px 3px 1px 1px; min-height: 2px; }
.cash-bar--billed { background: var(--accent); }
.cash-bar--disb { background: var(--border-strong); }
.cash-label { font-size: 0.64rem; color: var(--text-muted); font-weight: 600; }
.cash-legend { display: flex; gap: 1rem; font-size: 0.7rem; font-weight: 600; color: var(--text-secondary); margin-top: 0.35rem; }
.cash-legend span { display: inline-flex; align-items: center; gap: 0.35rem; }
.cash-sw { width: 10px; height: 10px; border-radius: 3px; flex-shrink: 0; }

/* Organizations page only (Clark, 2026-08-22 — "properly understand the layout" against the
   REAL live supabase.com/dashboard/organizations, not the pasted screenshot): measured there
   via getBoundingClientRect() — heading, search box, cards, and the "New organization" button's
   right edge all land on a consistent ~40px page margin, noticeably wider than .content's own
   --pad (12-20px, shared by every other page in the app, never touched here). 44.44px is the
   pre-zoom value that renders as ~40px on screen once .content's own `zoom: 0.9` scales it down
   (44.44 × 0.9 ≈ 40) — margin, not padding, so it ADDS to whatever --pad already is rather than
   replacing it; `calc(44.44px - var(--pad))` keeps the TOTAL inset at a constant ~40px
   regardless of --pad's own clamp()'d value at any viewport width.
   Second pass, same day: Clark pointed out the content still didn't match — "im talking about
   this content layout not in the same position as in supabase" — the horizontal margin was
   right, but VERTICALLY the heading sat almost flush against the header (measured 6.4px gap)
   where Supabase leaves a real 49px of breathing room below its header before "Your
   Organizations" even starts. margin-top uses the same "target minus what .content's own
   padding already contributes" pattern as the horizontal fix, so the total on-screen gap stays
   a constant ~49px regardless of --pad's own clamp() value. */
.organizations-inset { margin: calc(54.44px - (var(--pad) * 0.5)) calc(44.44px - var(--pad)) 0; }

/* --- List toolbar + card grid (Clark, 2026-08-17 — Supabase project-picker layout; renamed
   from .projects-toolbar 2026-08-22 once organizations.js started reusing it too — search +
   filter/actions + a primary button, the same shape for any card-grid list page in the app). */
.list-toolbar { display: flex; align-items: center; gap: 0.6rem; flex-wrap: wrap; margin-bottom: var(--pad); }
.list-toolbar input[type="search"] {
  min-height: var(--tap-min); padding: 0.5rem 0.8rem; font: inherit; font-size: 0.85rem;
  border: 1px solid var(--border); border-radius: var(--radius-sm);
  background: var(--surface); color: var(--text);
  flex: 1 1 220px; min-width: 0;
}
.list-toolbar input[type="search"]:focus { outline: none; border-color: var(--accent); box-shadow: 0 0 0 3px var(--accent-soft); }
.list-toolbar .filter-chips { flex: 2 1 auto; }
.list-toolbar .btn-primary { width: auto; flex-shrink: 0; }
/* Organizations page's own variant, matching Supabase's reference more closely: a narrow
   search box (not flex-grow filling the row like Projects' own search+filters do) with
   "+ New organization" pushed to the far right, a real gap between them rather than sitting
   right next to it. Placed AFTER the base rules above, not before — equal specificity to
   .list-toolbar's own `input[type="search"]` rule means source order decides the winner, and
   this needs to win. */
.list-toolbar--spread { justify-content: space-between; }
.list-toolbar--spread input[type="search"] { flex: 0 1 280px; }
/* Measured gap, same round as .organizations-inset above: Supabase leaves ~25px between the
   subtitle and the search/button row, not the ~8px .page-sub's own margin-bottom gives every
   other list page. 1.2rem pre-zoom (.content's zoom:0.9) renders as ~17.3px added on top of
   that 8px, landing the toolbar's top at the measured ~126px below the header — scoped to
   .organizations-inset so Projects' own toolbar (no such gap in Supabase's reference) is untouched. */
.organizations-inset .list-toolbar--spread { margin-top: 1.75rem; }
/* auto-fill + a floor, not a fixed column count — the grid uses however much of the viewport
   is actually there, wrapping to more columns on a wide monitor and fewer on a laptop, rather
   than a size that's either cramped or absurdly stretched at either end. */
/* align-items: start, not the grid default of stretch (Clark, 2026-08-22, 2nd round — "dont
   change the card height i want new organisation card height to copy that of the current
   original card"): stretch was forcing every card in a row to the row's TALLEST natural
   height — fine while every card was the same shape, but once the new-organization form
   (naturally taller — a heading, a field, two buttons) joined the row, it stretched the real
   org card taller than its own natural content ever needed. Cards now render at their own
   natural height; organizations.js's syncNewOrgCardHeight() copies a real card's height onto
   the form explicitly instead, so it's an exact copy, not a side effect of a shared row. */
.project-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(260px, 1fr)); gap: var(--gap); align-items: start; }
/* An empty-state illustration rendered INSIDE a card grid (Organizations'/Projects' own
   "No X yet" state, gridHtml() in organizations.js/projects.js) is a single grid item, so it
   only ever got the width of one card track and sat flush left instead of centred on the page
   (Clark, 2026-08-23 screenshot: "have the new organizations yet in the middle of the
   screen"). Span every column and centre its own content once it's the grid's only child. */
.project-grid:has(> .md-empty) { grid-template-columns: 1fr; }
.project-grid > .md-empty { grid-column: 1 / -1; display: flex; flex-direction: column; align-items: center; justify-content: center; min-height: 40vh; }
.project-card {
  display: flex; flex-direction: column; gap: 0.3rem; position: relative;
  padding: var(--pad); text-decoration: none; color: inherit;
  transition: transform 0.12s ease, box-shadow 0.12s ease, opacity 0.12s ease;
  /* Overrides .glass-flat's own --shadow-sm (same specificity, but this rule comes later in the
     cascade) — a real card the user picks one of, not a list row, so it gets the elevated
     resting shadow + a slightly deeper lift on hover rather than staying flat until touched. */
  box-shadow: var(--shadow-elevated);
}
.project-card:hover { transform: translateY(-3px); box-shadow: var(--shadow-elevated-hover); }
/* Organizations' "New organization" form, reusing .project-card purely as a same-size grid
   cell (Clark, 2026-08-22 — see organizations.js's newOrgCardHtml() comment). Not a clickable
   nav card, so the hover-lift .project-card:hover adds is switched off. Card fill/text stay
   the normal card surface (Clark, 3rd round — "dont change the card colour that blue sheen
   gradient... i just want the broken outline to be that colour"): only the dashed border
   carries the accent blue, with bigger, more widely-spaced dashes than a default 1-2px dashed
   rule would give ("make the broke line thicker and bigger and less magnitude" — fewer,
   chunkier dashes, not a busy tight-packed line). 4th round — "too thick make more translucent
   of a border": solid --accent read as too heavy at that width; rgba(2, 132, 199, 0.4) is the
   same translucent-accent border tone already used for role/status chips elsewhere, at a
   trimmed 3px so the dashes stay chunky without overpowering the card. */
.new-org-card { cursor: var(--cur-arrow); border: 3px dashed rgba(2, 132, 199, 0.4); }
.new-org-card:hover { transform: none; }
/* Free drag-to-reorder (Clark, 2026-08-17; reworked to pointer events 2026-08-17 — native HTML5
   drag's own translucent "ghost" preview read as broken, not draggable, and offered no way to
   animate the reflow; shared as wireCardReorder() in card-reorder.js since 2026-08-22, once
   Organizations grew its own reorderable card grid) drags a real cloned block instead —
   touch-action:none so a touch-drag doesn't fight the page's own scroll. */
.project-card { touch-action: none; }
/* The card being dragged stays in the grid FLOW (so the grid still reserves its cell and FLIP
   has a stable box to animate everything else around) but invisible — .project-card-drag-clone
   is what the user actually sees moving. */
.project-card--drag-source { opacity: 0; pointer-events: none; }
/* The floating block itself — a full clone of the card (see makeDragClone() in projects.js),
   so it carries the exact same content/layout, promoted to a solid opaque surface (not the
   grid's own translucent glass-flat) with a lift shadow and a slight scale/tilt — a real block
   being picked up, not a browser drag-ghost. transition:none since its position is driven by
   the pointer every frame, not CSS. */
.project-card-drag-clone {
  position: fixed; z-index: 200; margin: 0; pointer-events: none;
  background: var(--surface); box-shadow: 0 20px 44px rgba(20, 28, 48, 0.3), 0 6px 16px rgba(20, 28, 48, 0.18);
  /* transform-origin: top left, not the default center — makeDragClone() (projects.js) authors
     this box at its pre-zoom ("logical") size and scales it back down to match .content's own
     zoom (Clark, 2026-08-16), so the box's own left/top must land on its true visual top-left
     corner for that math to hold; anchoring at center would shift the visual corner by half the
     zoom-correction's shrink. No static `transform` value here — every scale/rotate this box
     ever shows (the zoom-correction, the "picked up" 1.04 grow, the velocity tilt) is computed
     and combined in JS (see makeDragClone()/onWindowPointerMove()) since they all stack into
     ONE transform, not layered CSS + inline separately. transition applies to whichever value
     JS writes next — left/top move instantly every pointermove (untransitioned) so only the
     tilt/grow ever eases, without adding any lag to the block's actual tracked position. */
  transform-origin: top left; transition: transform 0.15s ease-out; cursor: grabbing;
}
/* padding-right reserves room for .project-card-grip, which now floats absolutely at the
   card's own top-right corner (see its own comment below) instead of sitting in this row's
   flow — without this the status chip (project-card-head-end, on the Projects card) would sit
   directly under the grip's hit-box. */
.project-card-head { display: flex; align-items: center; justify-content: space-between; gap: 0.5rem; margin-bottom: 0.15rem; padding-right: 2.3rem; }
.project-card-icon {
  display: grid; place-items: center; width: 30px; height: 30px; border-radius: var(--radius-sm);
  background: var(--accent-soft); color: var(--accent); --icon-size: 16px; flex-shrink: 0;
  overflow: hidden; /* a photo (mountProjectAvatars(), projects.js) fills this box instead of the icon */
}
.project-card-icon img { width: 100%; height: 100%; object-fit: cover; }
/* A dual-tone photo icon (icon--photo — organizations.js's cluster icon, not an uploaded
   project photo) carries its own pale-blue + solid-blue palette and has real transparent
   padding around the glyph, unlike a full-bleed uploaded photo. `cover` was stretching/
   cropping that padding away, and the tinted --accent-soft badge behind it was nearly the
   same pale blue as the icon's own secondary shapes — together the icon all but disappeared
   into its own badge. `:has()` scopes the fix to only the photo-icon case; an uploaded
   project photo (no .icon--photo child) keeps its cover/accent-soft treatment untouched. */
.project-card-icon:has(.icon--photo) { background: var(--surface); }
.project-card-icon .icon--photo { width: 100%; height: 100%; object-fit: contain; }
/* Organizations card (Clark, 2026-08-22): started as a Supabase-measured horizontal row, then
   Clark asked for real member/project counts — shown a set of 4 mockups built from the app's
   own tokens (org-card-options.html, published as an artifact) and picked "C": header row
   (icon + name/role), plan line, a hairline divider, then a footer of small icon+count meta
   items — taller than the plain row but shorter/wider than the original vertical stack.
   `.project-card--meta` is a scoped modifier on the shared .project-card/.project-card-icon/
   .project-card-head classes so Projects' own cards are untouched. Icon box sized up to 40px
   (from the base 30px) and made circular — our source icon (organizations.png) already carries
   internal padding baked into the asset, same reasoning as the earlier "icon can barely be
   seen" fix. Numbers come from orgCardStats() in data.js, not the catalog. */
.project-card--meta .project-card-head { justify-content: flex-start; margin-bottom: 0; }
/* --icon-size explicit here too (Clark, 2026-08-22, 8th round — "inside this its too small and
   thin and can barely be seen"): the box grew to 40px above, but --icon-size stayed at the
   base .project-card-icon's 30px-box value of 16px — never a problem while this slot only ever
   held the organizations.png asset (sized by .icon--photo, not --icon-size), but the org
   icon-colour picker (organizations.js) can now put a real outline SVG glyph here too, and at
   16px in a 40px circle it read as a faint scribble. 20px matches the app's own ~50% icon-to-
   box ratio elsewhere (.project-header-avatar: 26px/52px; .project-avatar-picker-preview:
   30px/64px), not a one-off number. */
.project-card--meta .project-card-icon { width: 40px; height: 40px; border-radius: 50%; flex-shrink: 0; --icon-size: 20px; }
/* flex: 1 added (Clark, 2026-08-22 — Organizations grew the same drag-grip Projects has):
   .project-card-head is justify-content: flex-start here (not the base card's space-between,
   see the .project-card--meta block above), so without row-text claiming the remaining space
   itself, the newly-added .project-card-head-end grip would sit right after the name instead
   of pinned to the card's far right edge. */
.project-card--meta .project-card-row-text { min-width: 0; flex: 1; }
.project-card--meta .project-card-row-name-line { margin-top: 0.15rem; }
.project-card--meta .project-card-sub { margin: 0.65rem 0 0.75rem; }
.project-card-divider { border: none; border-top: 1px solid var(--border); margin: 0 0 0.7rem; }
.project-card-footer { display: flex; align-items: center; gap: 1.1rem; }
.project-card-meta-item { display: flex; align-items: center; gap: 0.35rem; font-size: var(--fs-sm); color: var(--text-muted); --icon-size: 14px; }
/* Clark, 2026-08-22, after seeing the mockup rendered at its own width: "make it long like
   that" — wider than Projects' 260px floor so the name/plan/footer lines have real breathing
   room instead of wrapping in a near-square box. Scoped to #org-grid only. */
#org-grid { grid-template-columns: repeat(auto-fill, minmax(360px, 1fr)); }

/* Project branding (Clark, 2026-08-21 — "allow the owner... its icon or upload a photo of
   their choice"): the detail header's own avatar, same box as .project-card-icon just bigger,
   and the picker used to change either one. */
.project-header-row { display: flex; align-items: center; gap: 0.9rem; margin-bottom: 0.2rem; }
.project-header-avatar {
  display: grid; place-items: center; width: 52px; height: 52px; border-radius: var(--radius);
  background: var(--accent-soft); color: var(--accent); --icon-size: 26px; flex-shrink: 0; overflow: hidden;
}
.project-header-avatar img { width: 100%; height: 100%; object-fit: cover; }

.project-avatar-picker { display: flex; align-items: flex-start; gap: 1rem; margin-bottom: 1rem; }
.project-avatar-picker-preview {
  display: grid; place-items: center; width: 64px; height: 64px; border-radius: var(--radius);
  background: var(--accent-soft); color: var(--accent); --icon-size: 30px; flex-shrink: 0; overflow: hidden;
}
.project-avatar-picker-preview img { width: 100%; height: 100%; object-fit: cover; }
.project-avatar-picker-body { flex: 1 1 auto; min-width: 0; }
.project-avatar-picker-options { display: flex; flex-wrap: wrap; gap: 0.4rem; margin-bottom: 0.6rem; }
.icon-swatch-btn {
  display: grid; place-items: center; width: 44px; height: 44px; border-radius: var(--radius-sm);
  border: 1.5px solid var(--border); background: var(--surface); color: var(--text-muted);
  --icon-size: 18px; cursor: var(--cur-hand); transition: border-color 0.12s ease, color 0.12s ease;
}
.icon-swatch-btn:hover { border-color: var(--accent); color: var(--accent); }
.icon-swatch-btn--active { border-color: var(--accent); background: var(--accent-soft); color: var(--accent); }
.project-photo-upload-btn { cursor: var(--cur-hand); }
/* Organizations' create-card icon picker (Clark, 5th round — "dont make it tall like that
   still keep the rectangle proportion"): a compact single-row variant of the swatch grid
   above, sized to fit all six options across a ~260px card without wrapping to a second row —
   see newOrgCardHtml()'s own comment in organizations.js for why the full-size picker had to
   be rebuilt rather than just reused. 8th round — "the provided icons we gave are too small":
   28px/14px read as indistinguishable blobs (building vs warehouse vs house all look alike
   that small); sized up to the point six still fit one row without wrapping, tightening the
   gap slightly to buy back the room the larger buttons need. */
.new-org-icon-swatches { display: flex; gap: 0.25rem; margin: 0.3rem 0 0.4rem; }
.icon-swatch-btn--sm { width: 32px; height: 32px; --icon-size: 18px; }
/* The upload badge overlaid on the create-card's icon preview (Clark, 6th round — "where is
   the upload photo option"): .new-org-icon-wrap is a plain positioning shell around the real
   .project-card-icon circle (#new-org-icon-preview, organizations.js) — NOT the same element,
   since .project-card-icon's own overflow:hidden (needed to crop a photo into the circle)
   would clip the badge the instant it pokes past that circle's edge. flex-shrink:0 carries
   forward the value the icon itself used to have as .project-card-head's direct flex child,
   now that this wrapper sits in that slot instead. */
.new-org-icon-wrap { position: relative; flex-shrink: 0; }
.new-org-photo-badge {
  position: absolute; right: -3px; bottom: -3px;
  display: grid; place-items: center; width: 18px; height: 18px; border-radius: 50%;
  background: var(--accent); color: #fff; border: 2px solid var(--surface);
  --icon-size: 10px; cursor: var(--cur-hand); transition: transform 0.12s ease;
}
.new-org-photo-badge:hover { transform: scale(1.1); }
/* Colour swatches (Clark, 7th round — "allow the user to customize and change to what colours
   they want the provided icon assets"): each button's own fill IS the colour it picks (no
   icon glyph inside, unlike .icon-swatch-btn) — the active one gets a ring rather than a
   border-colour change, since a border-colour swap wouldn't read clearly against every fill. */
.new-org-color-swatches { display: flex; gap: 0.3rem; margin: 0 0 0.4rem; }
.color-swatch-btn {
  width: 18px; height: 18px; border-radius: 50%; border: none; padding: 0; cursor: var(--cur-hand);
  box-shadow: 0 0 0 2px var(--surface); transition: transform 0.12s ease, box-shadow 0.12s ease;
}
.color-swatch-btn:hover { transform: scale(1.15); }
.color-swatch-btn--active { box-shadow: 0 0 0 2px var(--surface), 0 0 0 3.5px var(--text); }
.project-card-head-end { display: flex; align-items: center; gap: 0.4rem; }
/* Pinned to the card's own top-right corner (Clark, 2026-08-23 — "make them bigger and align
   them in the top right"), not inline in .project-card-head-end's flex row: on the Projects
   card that row also carries the status chip, which needs to stay put next to the icon, so
   only the grip itself is pulled out of flow — not its wrapper. .project-card is
   `position: relative` (see its own rule above), so this just insets from that box by the
   card's own --pad regardless of where in the header the grip's markup actually sits.
   Own hover/cursor, not the whole card's (Clark, 2026-08-17 — "it automatically triggers drag
   on hover... it should do nothing"): a card-wide grab cursor + hover-revealed grip read as
   "you're about to drag this" the moment you approach it, even though nothing actually engages
   until the hold in wireCardReorder() elapses. Sitting at a constant low opacity makes the
   grip discoverable without hover, and the grab cursor is scoped to just this icon — the rest
   of the card stays a plain, unremarkable click target. Sized up + given its own hit-box
   (Clark, 2026-08-23 — "make them bigger"): 14px read as a faint scribble on its own in the
   corner; 20px + a padded rounded box (same shape as .dt-btn's hover treatment) matches how
   every other icon-only control in the app announces itself. */
.project-card-grip {
  position: absolute; top: var(--pad); right: var(--pad);
  display: grid; place-items: center; --icon-size: 20px; color: var(--text-muted);
  padding: 0.3rem; border-radius: var(--radius-sm);
  opacity: 0.35; transition: opacity 0.12s ease, background-color 0.12s ease;
}
.project-card-grip:hover { opacity: 0.8; cursor: grab; background: var(--surface-sunken); }
/* Edit trigger (Clark, 2026-08-23 — "add a way for the owner user to edit and customize it
   again from the name to the icon pfp"), owner/admin only. Same absolute top-right corner as
   the grip, offset one icon-width further in so the two sit side by side without overlapping;
   same resting-visible-at-low-opacity treatment (not hover-only) for the same discoverability
   reasoning as the grip's own comment above. */
.project-card-edit {
  position: absolute; top: var(--pad); right: calc(var(--pad) + 30px);
  display: grid; place-items: center; --icon-size: 16px; color: var(--text-muted);
  padding: 0.3rem; border-radius: var(--radius-sm); cursor: var(--cur-hand);
  opacity: 0.45; transition: opacity 0.12s ease, background-color 0.12s ease, color 0.12s ease;
}
.project-card-edit:hover { opacity: 1; color: var(--accent); background: var(--surface-sunken); }
.project-card-name { font-size: var(--fs-md); font-weight: 700; line-height: 1.3; }
.project-card-sub { font-size: var(--fs-sm); }
.project-card-meta { display: flex; align-items: center; justify-content: space-between; gap: 0.5rem; margin-top: 0.3rem; font-size: var(--fs-sm); }
.project-card-amount { font-weight: 700; color: var(--text); }
.project-card-flag { margin-top: 0.2rem; }

/* --- Master-detail: one seamless card (Clark, 2026-08-18 redesign, refined many times the
   same day) ---------------------------------------------------------------------------------- */
/* List and detail share ONE bordered/shadowed card — Clark's own CTC CRM reference (list |
   detail, one block, no gap) is the model. Went through several shapes the same day: separate
   record/content cards → merged into one card beside the list → list folded into that same card
   too → the record pane moved to a horizontal strip on top of a second content pane → back down
   to ONE detail pane, record's fields folded into the Documents tab → bounded to a fixed height
   with each column scrolling internally → and finally (Clark: "i dont want internal scrolling
   for the main content make it auto extend depending if which tab and card is selcted") back to
   auto-height, no internal scrollbars at all — the whole card grows or shrinks with whichever
   tab/credential is open, and `.content`'s own page-level scroll (already there for every other
   page) is the only scrolling that ever happens here.
   The record-on-top shape's real bug (not just a preference) was `grid-template-areas: "list
   record" "list content"` — `list` spanned BOTH rows, and CSS Grid's auto-track algorithm
   distributes a multi-row-spanning item's own height need across the tracks it spans, so a long
   list could inflate `content`'s row far past what content itself needed. `.md3` stays the
   single-row, 2-column grid that fixed that (nothing to redistribute across), just with `auto`
   row sizing again instead of a viewport-bounded `1fr` one — align-items:stretch still keeps the
   hairline divider between `.md-list` and `.md3-detail` running the full height of whichever
   column ends up taller, it just no longer clips or scroll-traps the shorter one. See
   [[framework-company-page-redesign-2026-08-18]] for the fuller history. */
/* No margin-bottom here (Clark, 2026-08-18 — removed along with .content's own bottom padding;
   see the .content::after comment for why one guaranteed gap replaced three stacked ones). */
/* flex: 1 0 auto — grow to fill leftover page height when the card is short, but never SHRINK
   below its own natural content (shrink:0) the way the bounded-height version deliberately did;
   shrinking is what forced the internal-scroll model this rule now undoes. */
.md-wrap { flex: 1 0 auto; min-height: 0; display: flex; flex-direction: column; }
.md3 {
  display: grid;
  grid-template-columns: minmax(230px, 300px) minmax(0, 1fr);
  grid-template-areas: "list detail";
  align-items: stretch;
  background: var(--glass-surface-bg);
  border: 1px solid var(--glass-surface-border);
  border-radius: var(--radius); overflow: hidden;
  box-shadow: var(--shadow-md);
  flex: 1 0 auto;
  min-height: 0;
}
/* Between the mobile stack line (700px) and ~960px, a full-width list column leaves detail too
   narrow for its own content (a requirement row's chips, the facts strip) — cap the list
   smaller here so detail keeps enough room. */
@media (min-width: 701px) and (max-width: 960px) {
  .md3 { grid-template-columns: minmax(200px, 220px) minmax(0, 1fr); }
}
@media (max-width: 700px) {
  .md3 { grid-template-columns: 1fr; grid-template-rows: auto auto; grid-template-areas: "list" "detail"; }
  .md-list { border-right: none; border-bottom: 1px solid var(--border); }
}
.md-list {
  grid-area: list;
  border-right: 1px solid var(--border);
  display: flex; flex-direction: column; min-width: 0;
}
/* Search + filter icon, one row (Clark, 2026-08-18 — replaces the row of filter pills that
   used to sit under the search box with a single icon button to its right, opening a dropdown).
   .filter-chip/.filter-chips below are UNCHANGED and still used as-is on Projects — this is a
   separate, new set of classes, not a restyle of the shared ones. */
.md-search-row { display: flex; gap: 0.4rem; padding: 0.6rem var(--pad) 0.5rem; flex-shrink: 0; }
/* Direct-child combinator, not a descendant selector (Clark, 2026-08-18 — "filter dropdown is
   broken", the checkbox rows rendering as huge unstyled squares) — `.filter-wrap`'s dropdown
   (and its own `input[type="checkbox"]` rows) lives nested INSIDE `.md-search-row` in the DOM,
   so a descendant selector here was also matching every checkbox in the open filter dropdown:
   `min-height: var(--tap-min)` and `flex: 1 1 auto` don't compete with the checkbox's own
   `width/height: 15px` rule by specificity (different properties, not the same one), so
   min-height silently won as a floor and flex-grow silently stretched it across the row. `>`
   scopes this to the row's own search box, which really is a direct child, and leaves anything
   nested deeper (the whole filter dropdown) alone. */
.md-search-row > input {
  min-height: var(--tap-min); padding: 0.4rem 0.6rem; font-size: 0.83rem; font-family: inherit;
  border: 1px solid var(--border); border-radius: var(--radius-sm);
  background: var(--surface-sunken); color: var(--text);
  flex: 1 1 auto; min-width: 0;
  /* Placeholder text overrunning the box showed as a raw clipped glyph, not a clean cutoff
     (Clark, 2026-08-18) — text-overflow needs overflow:hidden + white-space:nowrap to actually
     take effect (its default no-op condition otherwise), and inputs don't wrap anyway. */
  overflow: hidden; white-space: nowrap; text-overflow: ellipsis;
}
.md-search-row > input:focus { outline: none; border-color: var(--accent); box-shadow: 0 0 0 3px var(--accent-soft); }
.filter-wrap { position: relative; flex-shrink: 0; }
.filter-btn {
  min-height: var(--tap-min); min-width: var(--tap-min); display: inline-flex; align-items: center;
  justify-content: center; border: 1px solid var(--border); border-radius: var(--radius-sm);
  background: var(--surface-sunken); color: var(--text-secondary); cursor: var(--cur-hand); position: relative;
  --icon-size: 16px;
}
.filter-btn:hover { background: var(--surface); border-color: var(--border-strong); }
/* Same accent as .filter-chip--active — a filter that isn't "All" is still an accent-level
   state, just carried by an icon button now instead of a highlighted pill. */
.filter-btn--active { border-color: var(--accent); color: var(--accent); background: var(--accent-soft); }
.filter-badge {
  position: absolute; top: -4px; right: -4px; min-width: 15px; height: 15px; padding: 0 3px;
  border-radius: 999px; background: var(--accent); border: 1.5px solid var(--surface);
  display: block;
}
/* Explicit, not left to the [hidden] attribute's own UA-stylesheet default — an author rule
   with equal specificity to [hidden] wins the cascade regardless of source order (origin is
   compared before specificity), so `.filter-badge { display: block }` above would otherwise
   silently defeat the hidden attribute rather than the other way around. */
.filter-badge[hidden] { display: none; }
/* Solid #fff, not var(--surface) (Clark, 2026-08-18 — "too translucent"): same floating-surface
   rule as .account-popover/.org-switch-popover/.cmdk-panel — this sits over dynamic list
   content, not the fixed content-plane background var(--glass-surface-bg) is meant for, so
   translucency let the rows underneath bleed straight through it. */
/* Two columns, not stacked groups (Clark, 2026-08-18 — "i dont like it the filter dropwdown
   being a internal scroll", chose the two-column mockup over a chip row or tighter single
   column): Status and Severity side by side means all 8 options fit in one short dropdown, so
   there's nothing left to scroll — `max-height`/`overflow-y:auto` are gone along with the
   scrollbar they existed for. Widened accordingly (230px → ~390px) to fit two real columns
   instead of a cramped one. */
/* left:0, not right:0 (Clark's own screenshot caught this live: the wider two-column dropdown
   opening flush with the filter BUTTON's right edge extended leftward straight into/behind the
   sidebar, since the list column it sits in is only 230-300px wide — far narrower than the
   dropdown's own 390px). Opening from the left edge instead extends rightward into the detail
   pane, which always has the room. Fine at every breakpoint: the filter button sits at the
   right end of the search row regardless of list-column width, so its left edge is always the
   list column's own left edge or later — there's nothing narrower to the right of it to run
   into except open content-plane space. */
.filter-dropdown {
  position: absolute; top: calc(100% + 6px); left: 0; width: 390px; background: var(--surface-solid);
  border: 1px solid var(--border-strong); border-radius: var(--radius); box-shadow: var(--shadow-md);
  padding: 0.4rem; z-index: 5;
  /* Animated open/close (Clark, 2026-08-20 — "all the dropdown expansions... not an instant
     snap popout"): was a plain `hidden` attribute toggle, which is a hard display:none/block
     flip with nothing to transition. `visibility` + a delayed transition on the CLOSE direction
     only (via the two-value `transition` list below) keeps it out of tab order and unclickable
     while closed, but still lets the opacity/transform animate both ways — `visibility` itself
     can't be eased, so it flips at the very end of the closing animation and at the very start
     of the opening one. */
  opacity: 0; transform: translateY(-6px) scale(0.98); visibility: hidden; pointer-events: none;
  transition: opacity 0.16s ease, transform 0.16s cubic-bezier(0.4, 0, 0.2, 1), visibility 0s linear 0.16s;
}
.filter-dropdown.open {
  opacity: 1; transform: translateY(0) scale(1); visibility: visible; pointer-events: auto;
  transition: opacity 0.16s ease, transform 0.16s cubic-bezier(0.4, 0, 0.2, 1);
}
.filter-dropdown-cols { display: flex; }
.filter-dropdown-group { flex: 1 1 50%; min-width: 0; padding: 0 0.3rem; }
.filter-dropdown-group:first-child { border-right: 1px solid var(--border); }
.filter-dropdown-label {
  padding: 0.4rem 0.6rem 0.2rem; font-size: 0.66rem; font-weight: 700; text-transform: uppercase;
  letter-spacing: 0.05em; color: var(--text-muted);
}
/* A real <label>+checkbox pair, not a button standing in for one — multi-select (Clark,
   2026-08-18: "allow the user to select more than one") reads more honestly as checkboxes than
   as buttons toggling an --active class, and it's free keyboard/screen-reader semantics. */
.filter-dropdown-item {
  display: flex; align-items: center; gap: 0.55rem; width: 100%; text-align: left;
  padding: 0.5rem 0.6rem; border-radius: var(--radius-sm); font-size: 0.84rem; color: var(--text);
  cursor: var(--cur-hand); min-height: var(--tap-min);
}
.filter-dropdown-item:hover { background: var(--surface-sunken); }
/* Custom-drawn checkbox (Clark, 2026-08-18: "i dont like it being check boxes... make it
   designed well in line with the spatial ui theme and professional os software theme") — the
   bare native `accent-color` checkbox read as an unstyled OS control dropped into a designed
   app. `.cb-input` is the real, still-fully-functional/accessible checkbox — kept in the DOM
   for keyboard/screen-reader semantics and the existing change-event wiring — just visually
   hidden (`opacity:0`, stacked under `.cb-box` via `position:absolute`, not `display:none`,
   which would break tabbing to it). `.cb-box` is what's actually seen: a plain symmetric
   rounded square, not the app's asymmetric Docket chip shape — Docket's spine-and-notch shape
   is a STATUS/category marker (chips, badges, pills), and forcing that same shape onto an
   interactive binary toggle would borrow meaning that doesn't apply here; a small rounded
   square with a checkmark is the correct, universal convention this app doesn't need to
   reinvent (macOS/Windows/Linear/Notion all agree on it) — "in line with... professional OS
   software" specifically points at that established shape, not a novel one. */
.cb-input {
  position: absolute; width: 1px; height: 1px; margin: -1px; opacity: 0; cursor: var(--cur-hand);
}
.cb-box {
  display: grid; place-items: center; flex-shrink: 0; width: 16px; height: 16px;
  border-radius: 4px; border: 1.5px solid var(--border-strong); background: var(--surface-solid);
  transition: background-color 0.12s ease, border-color 0.12s ease;
}
.cb-check {
  --icon-size: 11px; color: #fff; opacity: 0; transform: scale(0.5);
  transition: opacity 0.1s ease, transform 0.1s ease;
}
.cb-input:checked ~ .cb-box { background: var(--accent); border-color: var(--accent); }
.cb-input:checked ~ .cb-box .cb-check { opacity: 1; transform: scale(1); }
.cb-input:hover ~ .cb-box { border-color: var(--accent); }
.cb-input:focus-visible ~ .cb-box { box-shadow: 0 0 0 3px var(--accent-soft); }
.filter-dropdown-clear {
  display: block; width: 100%; text-align: center; border: none; border-top: 1px solid var(--border);
  background: none; font: inherit; font-size: 0.8rem; font-weight: 600; color: var(--text-secondary);
  padding: 0.55rem 0.6rem 0.25rem; margin-top: 0.3rem; cursor: var(--cur-hand); min-height: var(--tap-min);
}
.filter-dropdown-clear:hover { color: var(--accent); }
.filter-dropdown-clear:disabled { color: var(--text-muted); cursor: var(--cur-arrow); }
/* Pill filter toggles (Linear/Notion/Supabase convention) below the search box, in place of
   a <select> — the active filter is visible at a glance instead of hidden until opened. */
.filter-chips { display: flex; gap: 0.35rem; flex-wrap: wrap; padding: 0 var(--pad) 0.6rem; border-bottom: 1px solid var(--border); flex-shrink: 0; }
.filter-chip {
  min-height: 30px; padding: 0.28rem 0.75rem;
  border: 1px solid var(--border-strong); border-radius: 999px;
  background: var(--surface); color: var(--text-secondary);
  font: inherit; font-size: 0.76rem; font-weight: 600; white-space: nowrap; cursor: var(--cur-hand);
  transition: background-color 0.12s ease, border-color 0.12s ease, color 0.12s ease, box-shadow 0.12s ease;
}
.filter-chip:hover { background: var(--surface-sunken); }
/* Active glows in the accent hue rather than a flat solid fill — the Aurora system's own
   "this one, right now" convention (see .glass--active), reused here at chip scale. */
.filter-chip--active {
  background: var(--accent); border-color: var(--accent); color: #fff;
  box-shadow: 0 0 14px var(--accent-soft);
}
.filter-chip--active:hover { background: var(--accent-hover); }
/* Same fine/coarse-pointer split as .copy-btn and the nav rail — compact next to the search
   box on a mouse, restored to the 44px tap minimum on touch. */
@media (pointer: coarse) { .filter-chip { min-height: var(--tap-min); } }
/* Used standalone in a toolbar (Projects) rather than under a search box inside a card — no
   border-bottom/padding to separate it from anything, and each chip carries its own count,
   Apollo's own "Total / Net New / Saved" habit of a filter doubling as a number. */
.filter-chips--inline { border-bottom: none; padding: 0; }
.filter-chip-count { opacity: 0.7; font-weight: 700; margin-left: 0.15rem; }
.filter-chip--attention { --icon-size: 11px; display: inline-flex; align-items: center; gap: 0.3rem; border-color: rgba(178, 48, 48, 0.4); color: var(--st-expired); }
.filter-chip--attention.filter-chip--active { background: var(--st-expired); border-color: var(--st-expired); color: #fff; box-shadow: 0 0 14px var(--st-expired-soft); }
/* No overflow-y here (Clark, 2026-08-18 — "i dont want internal scrolling for the main content
   make it auto extend"): the list shows every row in full now, same as .md3-detail below; the
   card grows to fit and .content's own page-level scroll (already there for every page) takes
   over if the whole thing runs past the viewport. min-height is still a floor, not a scroll
   trigger — it just keeps the list from ever reading as a sliver next to a short detail pane. */
.md-scroll { flex: 1 1 auto; min-height: 220px; padding-bottom: var(--pad); }

.md-group-title {
  display: flex; align-items: center; justify-content: space-between; gap: 0.5rem;
  padding: 0.4rem var(--pad) 0.3rem;
  font-size: 0.66rem; font-weight: 700; text-transform: uppercase; letter-spacing: 0.06em;
  color: var(--text-secondary);
  /* Solid, not --surface-sunken's translucent tint (Clark, 2026-08-17 — "when it scroll it
     goes past and under the title... in tier 1 title"): this is position:sticky over a
     scrolling list, so a translucent background lets the row that just scrolled underneath
     show through instead of being hidden by it. */
  /* var(--surface-solid), not a hardcoded #f2f3f8 (Clark, 2026-08-24 — "same issue look at the
     light tier bars fix them to match darkmode"): the literal never got redefined per theme, so
     it stayed a light-mode grey even under dark mode — same bug already found and fixed in
     web/css/style.css's own copy of this class. */
  background: var(--surface-solid);
  border-bottom: 1px solid var(--border);
  border-left: 3px solid var(--accent);
  position: sticky; top: 0; z-index: 1;
}
.md-group-count {
  font-size: 0.66rem; font-weight: 700; letter-spacing: 0;
  color: var(--text-muted);
  background: var(--surface); border: 1px solid var(--border);
  border-radius: 999px; padding: 0.08rem 0.45rem;
}
/* Denser than a card list, short of a cramped spreadsheet: min-height still guarantees the
   44px tap target, but the padding around it is tighter and the right-hand column has a fixed
   width so status badges line up into an implied column down the list (Apollo/CRM row shape). */
.md-row {
  display: flex; align-items: center; justify-content: space-between; gap: 0.5rem;
  width: 100%; min-height: var(--tap-min);
  padding: 0.4rem var(--pad);
  border: none; border-bottom: 1px solid var(--border); border-left: 3px solid transparent;
  background: transparent; color: var(--text); font-family: inherit; text-align: left; cursor: var(--cur-hand);
  transition: background-color 0.12s ease, border-left-color 0.12s ease, box-shadow 0.12s ease;
}
.md-row:hover { background: var(--surface-sunken); }
.md-row--active { background: var(--accent-soft); border-left-color: var(--accent); }
.md-row--untracked .md-row-name { color: var(--text-muted); font-weight: 500; }
.md-row-main { display: flex; flex-direction: column; gap: 0.05rem; min-width: 0; }
.md-row-name { font-size: 0.85rem; font-weight: 600; line-height: 1.3; }
.md-row-sub { font-size: 0.72rem; color: var(--text-muted); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.md-row-ref {
  /* Digit alignment, not a mono family: what a reference number needs is for its columns to
     line up so a scanned string is easy to tell from a typo. */
  font-variant-numeric: tabular-nums; letter-spacing: 0.01em;
  color: var(--text-muted);
}
.md-row-meta { display: flex; flex-direction: column; align-items: flex-end; gap: 0.15rem; flex-shrink: 0; min-width: 6.5rem; }
.md-row-days { font-size: 0.68rem; color: var(--text-muted); white-space: nowrap; }
/* The checklist's row is an accordion (click expands it in place) with nothing else saying so —
   same caret convention as .req-caret, rotating on open (Clark, 2026-08-17). Only the project
   checklist's rows use this; the master-detail's own .md-row selects into a side pane instead
   of expanding, so it has nothing to signal here. */
.md-row-trail { display: flex; align-items: center; gap: 0.5rem; flex-shrink: 0; }
.md-row-caret { color: var(--text-muted); --icon-size: 15px; transition: transform 0.15s ease; }
.md-row--active .md-row-caret { transform: rotate(90deg); }

/* .md3-detail: the one right-hand pane. No scroll region of its own anymore (Clark, 2026-08-18
   — "i dont want internal scrolling for the main content make it auto extend depending if which
   tab and card is selcted"): header, tabs and tab-body just stack and the pane takes whatever
   height its current tab's content needs, growing or shrinking as the selected credential/tab
   changes. Padding stays on the individual children rather than .md3-detail itself only because
   that's what already matches .md-list's own padding shape (search row / rows each carry their
   own) — not for scroll reasons anymore, since nothing here scrolls independently now. */
.md3-detail { grid-area: detail; display: flex; flex-direction: column; min-width: 0; }
.md3-detail > .detail-head { padding: calc(var(--pad) * 0.85) calc(var(--pad) * 0.85) 0; flex-shrink: 0; }
.md3-detail > .seg-tabs { padding: 0 calc(var(--pad) * 0.85); flex-shrink: 0; }
.md3-detail > .tab-body { padding: 0 calc(var(--pad) * 0.85) calc(var(--pad) * 0.85); }
/* The record's own form, opened above the document itself inside the Documents tab (Clark,
   2026-08-18: "for the details merge it into the documents tab instead") — a plain rule, not a
   card, since it's one continuous "here's everything about this credential" reading, not two
   separate blocks glued together. */
.detail-tab-divider { border-top: 1px solid var(--border); margin: 1rem 0; }
/* Nothing selected (Clark, 2026-08-23: "does not need to be down there but up higher so its
   visually centred in the viewport without needing to scroll down"). `.md3-detail` is a grid
   item sharing a ROW with `.md-list` — a long credential list stretches that row far past one
   screenful, and centering within the item's own (stretched-to-match) height put the
   illustration below the fold. `align-self: start` opts this one item out of the stretch, so
   its box reverts to min-height (below) instead of the full row height, and — since the page
   always loads scrolled to the top — that alone puts it in view with no scrolling needed.
   position:sticky was tried here first and reverted: `.md3` carries its own `overflow:hidden`
   (load-bearing, for its border-radius clip), which becomes the nearest clipping ancestor for
   sticky's own positioning math instead of the real scrolling element (.content) — the empty
   state ends up pinned to `.md3`'s own (still row-tall) box and scrolls away with it exactly
   like a non-sticky element would, just from a different starting point. Not worth restructuring
   `.md3` to fix; align-self alone already satisfies the actual ask (visible without scrolling
   on load) without it. */
.md3-detail:has(> .md-empty) {
  align-items: center; justify-content: center;
  align-self: start;
  min-height: min(60vh, 420px);
}
.md-empty { color: var(--text-muted); font-size: 0.88rem; padding: clamp(1rem, 4vh, 2rem) var(--pad); text-align: center; }

/* Empty states get a spot illustration + a short explanation rather than one line of grey
   text — reused for "nothing selected", "no search matches", and similar spots. `.md-empty`
   still owns the padding/centring; this just lays out its contents. The old 40px dark
   icon-tile (.empty-state-icon) was replaced by the generated illustrations on 2026-08-23
   (Clark's "premium experience" pass) — see EMPTY_ILLUSTRATIONS in compliance-ui.js. */
.empty-state { display: flex; flex-direction: column; align-items: center; gap: 0.5rem; }
.empty-state-title { font-size: 0.9rem; font-weight: 600; color: var(--text-secondary); }
.empty-state-sub { font-size: 0.8rem; line-height: 1.5; max-width: 32ch; }
/* The illustration itself — shared by .empty-state and .panel--empty. A gentle idle float so
   empty screens feel alive rather than dead-ended; killed under prefers-reduced-motion.
   (A ChatGPT frame-by-frame sprite loop was tried on top of this on 2026-08-23 and scrapped
   the same day — backlogged for when Clark generates animation assets with ElevenLabs.) */
.empty-illustration { display: block; width: 116px; height: 116px; margin: 0 auto; animation: emptyFloat 4.5s ease-in-out infinite; }
.panel--empty .empty-illustration { margin-bottom: 0.35rem; }
@keyframes emptyFloat { 0%, 100% { transform: translateY(0); } 50% { transform: translateY(-6px); } }
@media (prefers-reduced-motion: reduce) { .empty-illustration { animation: none; } }

/* Skeleton rows shown the instant a list is about to load, so the pane never flashes blank —
   same idea as Apollo/Linear's loading placeholders. Purely visual; swapped out by the first
   real drawList(). */
.skeleton-row { display: flex; align-items: center; justify-content: space-between; gap: 0.5rem; min-height: var(--tap-min); padding: 0.4rem var(--pad); border-bottom: 1px solid var(--border); }
.skeleton-bar {
  height: 9px; border-radius: 4px;
  background: linear-gradient(90deg, var(--surface-sunken) 25%, var(--border) 50%, var(--surface-sunken) 75%);
  background-size: 200% 100%; animation: skeleton-shimmer 1.4s ease-in-out infinite;
}
@keyframes skeleton-shimmer { 0% { background-position: 200% 0; } 100% { background-position: -200% 0; } }

/* --- Credential detail: header + facts + segmented tabs -------------------------------------
   Supabase-style hierarchy: a small uppercase eyebrow (the agency) sits above the title instead
   of below it, so the title reads first and the agency reads as its category — not a caption. */
.detail-head { margin-bottom: 0.6rem; }
.detail-eyebrow { font-size: 0.7rem; font-weight: 700; text-transform: uppercase; letter-spacing: 0.06em; color: var(--text-muted); margin-bottom: 0.15rem; }
.detail-title-row { display: flex; align-items: center; gap: 0.6rem; flex-wrap: wrap; }
.detail-title { font-size: 1.15rem; font-weight: 700; letter-spacing: -0.01em; margin: 0; flex: 1 1 auto; min-width: 0; }
.detail-badges { display: inline-flex; align-items: center; gap: 0.4rem; flex-shrink: 0; }

/* A flat definition row with hairline dividers between fields, not a boxed panel — the boxed
   treatment is reserved for the states that need to visibly stand out (empty, no-document). */
.facts-strip {
  display: flex; flex-wrap: wrap; align-items: center; gap: 0.5rem 0;
  margin-top: 0.6rem;
  padding: 0.5rem 0 0;
  border-top: 1px solid var(--border);
}
.facts-strip .kv:not(:last-child) { margin-right: 1.4rem; padding-right: 1.4rem; border-right: 1px solid var(--border); }
.facts-strip--empty {
  justify-content: space-between;
  background: var(--surface-sunken); border: 1px solid var(--border); border-radius: var(--radius-sm);
  padding: 0.6rem 0.8rem;
}

/* Untracked credential: the way in is document-first. Upload is the primary CTA; manual
   entry is a quiet link for people who don't have the paper yet. */
.doc-first { display: flex; align-items: center; justify-content: space-between; gap: var(--gap); flex-wrap: wrap; width: 100%; }
.doc-first-text { display: flex; flex-direction: column; gap: 0.15rem; font-size: 0.86rem; flex: 1 1 260px; min-width: 0; }
.doc-first-text .muted { font-size: 0.8rem; line-height: 1.5; }
.doc-first-actions { display: flex; align-items: center; gap: 0.6rem; flex-wrap: wrap; }
.btn-link {
  background: none; border: none; padding: 0.4rem 0.2rem; min-height: var(--tap-min);
  font: inherit; font-size: 0.8rem; color: var(--accent); cursor: var(--cur-hand); text-decoration: underline;
  text-underline-offset: 2px;
}
.btn-link:hover { color: var(--accent-hover); }
label.btn-link { display: inline-flex; align-items: center; }

/* Record with no stored document behind it — flagged, never silent. Gold, since it is a
   gap in the firm's own records rather than a lapse in the licence. Boxed (unlike the default
   flat facts-strip) because this state needs to visibly stand out — a soft gold wash for the
   surrounding box, matching the solid-fill badges for the pill itself (see .badge's comment
   on why gold works as a fill/dot but not as small text). */
.facts-strip--nodoc {
  border: 1px solid rgba(232, 200, 74, 0.5); border-radius: var(--radius-sm);
  background: var(--st-due-soft); padding: 0.55rem 0.8rem;
}
.nodoc-flag {
  display: inline-flex; align-items: center; gap: 0.4rem;
  margin-left: auto; min-height: var(--tap-min);
  padding: 0.3rem 0.7rem; border-radius: 999px;
  border: 1px solid transparent; background: var(--nav-accent);
  color: var(--nav-accent-ink); font: inherit; font-size: 0.76rem; font-weight: 700; cursor: var(--cur-hand);
}
.nodoc-flag:hover { filter: brightness(0.94); }
.nodoc-dot { width: 8px; height: 8px; border-radius: 50%; background: currentColor; flex-shrink: 0; }
.nodoc-inline { color: var(--text-secondary); font-weight: 600; }
.doc-drop--slim { padding: 0.6rem var(--pad); font-size: 0.82rem; }
.kv { font-size: 0.86rem; font-weight: 600; }
.kv-label {
  display: block; font-size: 0.62rem; font-weight: 700;
  text-transform: uppercase; letter-spacing: 0.04em;
  color: var(--text-muted); margin-bottom: 0.1rem;
}
.kv-extra { font-weight: 400; font-size: 0.76rem; margin-left: 0.3rem; }

/* ERROR STATE (Clark, 2026-09-03: "in the error loading data pages create custom error screen
   and animate it too. and one for dark and light variant and with the error have a button for
   the user to click to trigger a refresh").

   LIGHT AND DARK COME FROM TOKENS, not from two assets. The illustration is inline SVG painted
   entirely from custom properties, so the dark variant is the four overrides below rather than a
   second PNG that has to be regenerated whenever the brand moves. That is also why this is not
   built on icons/empty/*.png like the empty states: a raster illustration serves the same pixels
   to both themes, which is the reason those look wrong on a dark page.

   THE MOTION IS THE MESSAGE. A dot travels the wire and stops dead at the break, over and over.
   It says "something was on its way and did not arrive", which is exactly what happened, and it
   reads as still-trying rather than broken-forever — true, because the retry button is right
   there. Everything is transform/opacity only, so it composites on the GPU and costs nothing. */
.error-state {
  --err-ink: var(--text);
  --err-line: var(--border-strong);
  --err-card: var(--surface);
  --err-break: var(--st-expired);
  display: flex; flex-direction: column; align-items: center; text-align: center;
  gap: 0.15rem;
  padding: clamp(1.4rem, 4vw, 2.6rem) 1.2rem;
}
.err-art { width: 168px; max-width: 60%; height: auto; overflow: visible; margin-bottom: 0.5rem; }
.err-art-card { fill: var(--err-card); stroke: var(--err-line); stroke-width: 2; }
.err-art-line { fill: var(--err-line); opacity: 0.55; }
.err-art-line--short { opacity: 0.35; }
.err-art-wire { stroke: var(--err-line); stroke-width: 2.5; stroke-linecap: round; fill: none; }
.err-art-break { stroke: var(--err-break); stroke-width: 2.5; stroke-linecap: round; stroke-linejoin: round; fill: none; }
.err-art-shadow { fill: var(--err-line); opacity: 0.18; }

/* The card breathes; the shadow answers it in the opposite direction, which is what stops the
   float reading as the whole graphic sliding rather than the card lifting off the page. */
.err-art-float { animation: errFloat 3.2s ease-in-out infinite; transform-origin: 84px 49px; }
.err-art-shadow { animation: errShadow 3.2s ease-in-out infinite; transform-origin: 84px 103px; }
/* Travels the wire, stops at the break, waits, starts over. The pause at the end is deliberate:
   a continuous loop reads as a loading spinner, and this is the opposite of one. */
.err-art-spark { fill: var(--err-break); animation: errSpark 2.6s cubic-bezier(0.4, 0, 0.6, 1) infinite; }
.err-art-break { animation: errBreakPulse 2.6s ease-in-out infinite; }

@keyframes errFloat {
  0%, 100% { transform: translateY(0); }
  50%      { transform: translateY(-5px); }
}
@keyframes errShadow {
  0%, 100% { transform: scaleX(1); opacity: 0.18; }
  50%      { transform: scaleX(0.88); opacity: 0.11; }
}
@keyframes errSpark {
  0%      { transform: translateX(0); opacity: 0; }
  10%     { opacity: 1; }
  46%     { transform: translateX(26px); opacity: 1; }
  56%     { transform: translateX(26px); opacity: 0; }  /* stops dead at the break */
  100%    { transform: translateX(26px); opacity: 0; }
}
@keyframes errBreakPulse {
  0%, 40%  { opacity: 0.45; }
  56%      { opacity: 1; }
  80%, 100% { opacity: 0.45; }
}

.error-state-title { font-size: 1.05rem; font-weight: 700; margin: 0; }
.error-state-msg {
  font-size: 0.86rem; color: var(--text-secondary); margin: 0.25rem 0 0.9rem;
  max-width: 30rem; line-height: 1.5;
}
.error-state [data-error-retry] { min-height: var(--tap-min); }
.error-state-detail { margin-top: 0.9rem; }
.error-state .err-detail { font-size: 0.78rem; word-break: break-word; }

/* DARK. Measured rather than assumed, which changed what this rule says: --surface in dark is
   already rgba(255,255,255,0.045), a raised overlay on a dark ground, so it themes correctly on
   its own and the first version of this rule was actively wrong -- it swapped the card to
   --surface-sunken (0.03), making it FAINTER than the default when the intent was the opposite.
   --border-strong likewise flips to a light hairline by itself.
   So exactly ONE token genuinely needs overriding: --st-expired (#b23030) is a deep red chosen
   to sit on white, and on a near-black ground it goes muddy rather than reading as a break. */
html[data-theme="dark"] .error-state { --err-break: #ff6b6b; }
html[data-theme="classic-dark"] .error-state { --err-break: #ff6b6b; }

/* Clark's standing rule, and the honest reading of this animation: it is decoration on top of a
   message that must work without it. Nothing here conveys anything the text does not. */
@media (prefers-reduced-motion: reduce) {
  .err-art-float, .err-art-shadow, .err-art-spark, .err-art-break { animation: none; }
  .err-art-spark { opacity: 1; }
}

/* SUPPLIER CLAIM REVIEW (ops/js/pages/supplier-claims.js). Three columns because a decision is
   made by COMPARING three things: the registry entry being claimed, what the merchant's own
   listing says about themselves, and who filed it. Side by side, a mismatch between the registry
   name and the listing name is visible at a glance; stacked, the reviewer has to hold one in
   their head while scrolling to the next, which is how a wrong approval happens. Collapses to one
   column under 760px, where three columns would be too narrow to read rather than easier. */
.claim-review-grid {
  display: grid; gap: 0.8rem;
  grid-template-columns: repeat(auto-fit, minmax(210px, 1fr));
  margin-top: 0.5rem;
}
.claim-review-grid .kv-value { display: block; font-size: 0.88rem; font-weight: 600; line-height: 1.35; }
.claim-review-docs { list-style: none; margin: 0.3rem 0 0; padding: 0; display: flex; flex-direction: column; gap: 0.25rem; }
.claim-review-docs li { display: flex; gap: 0.5rem; align-items: baseline; font-size: 0.8rem; flex-wrap: wrap; }
.claim-review-doc-kind { font-weight: 600; }
.claim-review + .claim-review { margin-top: 0.8rem; }
/* Reference and permit numbers read as codes, not prose. This used a monospace face, which
   Clark banned outright on 2026-09-02; the customer app was migrated that day and this console
   was missed. What a reference number actually needs is DIGIT ALIGNMENT, which tabular-nums
   gives in the app's own face. Renamed off "mono" so nothing reintroduces the family behind a
   name that promises it — matching web/css/style.css's own .kv-code. */
.kv-code { font-variant-numeric: tabular-nums; letter-spacing: 0.01em; font-size: 0.82rem; font-weight: 600; }

/* Underline tabs (Supabase/Linear convention) rather than a pill-in-a-tray — one hairline
   under the whole row, the active tab picked out by its own 2px accent underline. */
.seg-tabs {
  display: flex; gap: 1.2rem; max-width: 100%;
  border-bottom: 1px solid var(--border);
  margin-bottom: 0.7rem;
  /* overflow-y: hidden is NOT redundant here (Clark, 2026-08-17 — "does not need to have that
     empty scroll indication that a glitch"): per spec, setting only overflow-x to a
     non-visible value computes overflow-y to auto too, not visible — so this row was getting
     an unwanted vertical scrollbar track from a few px of sub-pixel rounding, on a row that
     never needed to scroll vertically at all. */
  overflow-x: auto; overflow-y: hidden;
  /* flex-shrink: 0 (Clark, 2026-08-20) — needed the moment .seg-tabs sits as a direct child of
     `.content`'s own flex column (the new stage-level tab strip in projects.js does; the
     credential detail's copy already had this via `.md3-detail > .seg-tabs`, scoped instead of
     global, which is why this only surfaced now). Per the flexbox spec, any non-`visible`
     overflow drops a flex item's automatic minimum size to 0 — combined with default
     flex-shrink:1, that let this whole row collapse to ~1px (children still full height,
     clipped away) the instant the page's total content exceeded the viewport. Tab strips
     should never shrink below their content in any container. */
  flex-shrink: 0;
}
.seg-tab {
  display: inline-flex; align-items: center; gap: 0.35rem;
  min-height: var(--tap-min);
  padding: 0 0.05rem;
  margin-bottom: -1px;
  border: none; border-bottom: 2px solid transparent;
  background: transparent;
  font: inherit; font-size: 0.85rem; font-weight: 600;
  color: var(--text-muted);
  text-decoration: none;
  cursor: var(--cur-hand);
  white-space: nowrap;
  transition: color 0.12s ease, border-color 0.12s ease;
}
.seg-tab:hover { color: var(--text); }
.seg-tab--active {
  color: var(--accent);
  border-bottom-color: var(--accent);
}
.seg-tab-icon { --icon-size: 15px; }

/* Stage step-through nav (Clark, 2026-08-20) — sits directly under a stage's .seg-tabs, in
   the persistent outer shell alongside the tab strip (see renderStageTabStrip() in
   projects.js for why it can't live inside #stage-content). "N of M" plus Prev/Next keeps
   the five-tab set feeling like one guided pass through the stage, not five unrelated pages
   only reachable from the sidebar. */
.stage-stepper {
  display: flex; align-items: center; justify-content: space-between; gap: 0.6rem;
  margin: 0.6rem 0 1rem;
  font-size: 0.8rem;
  flex-shrink: 0;
}
.stage-step {
  display: inline-flex; align-items: center; gap: 0.3rem;
  min-height: var(--tap-min);
  padding: 0 0.3rem;
  color: var(--text-muted);
  text-decoration: none;
  font-weight: 600;
  border-radius: var(--radius-sm);
  transition: color 0.12s ease, background 0.12s ease;
}
.stage-step:hover { color: var(--accent); background: var(--accent-soft); }
.stage-step--next { margin-left: auto; }
.stage-step-icon { --icon-size: 14px; }
.stage-step-icon--prev { transform: scaleX(-1); }
.stage-step-count { color: var(--text-muted); font-variant-numeric: tabular-nums; }

/* A brief settle-in on the tab body's content — every tab switch and every row selection
   renders a fresh .tab-fade element (it's part of the replaced innerHTML, not a class toggled
   on a persisting node), so the animation restarts on its own with no JS retrigger needed.
   A touch of blur added (Clark, 2026-08-23 — "i want it to be a smooth transition... just do a
   focus pull"): a small taste of #view's own "Focus Pull" (view-page-transition-in above) so a
   stage-content swap reads as gently resolving into focus rather than a flat opacity cut, but
   MUCH lighter (3px, not 9px) and no scale — this fires far more often (every tab AND every row
   selection), and the design intent behind the two-tier system above is explicit that the full
   heavy effect is for one real page landing, not every small in-page redraw. */
@keyframes tab-fade-in { from { opacity: 0; filter: blur(3px); transform: translateY(3px); } to { opacity: 1; filter: blur(0); transform: translateY(0); } }
.tab-fade { animation: tab-fade-in 0.2s ease both; }
@media (prefers-reduced-motion: reduce) {
  .tab-fade { animation: none; }
}

/* Scoped filter/search redraws — redrawSmooth() in shared.js (Clark, 2026-08-22, Projects and
   Audit Log's filter chips both: "just disappears and reappears instantly"). Same idea as
   .tab-fade above, just class-toggled (not a fresh element each time) since the call sites
   reuse one persisting node rather than replacing their own wrapper. */
@keyframes content-swap-in { from { opacity: 0; transform: translateY(4px); } to { opacity: 1; transform: translateY(0); } }
.content-swap-in { animation: content-swap-in 0.2s cubic-bezier(0.4, 0, 0.2, 1); }
@media (prefers-reduced-motion: reduce) {
  .content-swap-in { animation: none; }
}

/* redrawSmoothList()'s per-row entrance (shared.js) — Clark, 2026-08-22, "C: height eases AND
   rows stagger in" from the filter-transition options. Same expo-out easing as the sidebar's
   own staggered rows / the page-level Focus Pull, so all of this session's motion reads as one
   consistent language rather than a different feel per screen. */
@keyframes list-row-stagger-in {
  from { opacity: 0; transform: translateY(6px); }
  to { opacity: 1; transform: translateY(0); }
}
.list-row-stagger-in { animation: list-row-stagger-in 0.26s cubic-bezier(0.16, 1, 0.3, 1) both; }
@media (prefers-reduced-motion: reduce) {
  .list-row-stagger-in { animation: none; }
}
/* The reverse — a single card's own exit, not a redrawSmoothList() stagger (Clark, 2026-08-22,
   9th round — "when i click cancel the animation as it goes back... is not made, make it clean
   and smooth too not snappy"): redrawSmoothList() eases the GRID's own height down on close,
   but the closing card itself was vanishing INSTANTLY the moment its innerHTML got swapped out
   — the exact "snappy" Clark meant. organizations.js's toggleCreating(false) plays this on the
   card first and waits for it before ever calling redrawSmoothList, so the card visibly fades
   out before the grid collapses around the space it leaves, rather than a hard cut. */
@keyframes list-row-stagger-out {
  from { opacity: 1; transform: translateY(0) scale(1); }
  to { opacity: 0; transform: translateY(4px) scale(0.97); }
}
.list-row-stagger-out { animation: list-row-stagger-out 0.16s cubic-bezier(0.4, 0, 1, 1) both; }
@media (prefers-reduced-motion: reduce) {
  .list-row-stagger-out { animation: none; }
}

.row-detail { padding: 0.2rem var(--pad) var(--pad); border-top: 1px solid var(--border); }
/* Entrance for a freshly-expanded checklist row (Clark, 2026-08-23 — "dropdowns in stage 1 is
   not animated cleanly"). The whole list repaints via innerHTML on every toggle, so nothing
   here can rely on a CSS transition from a previous state — the detail is born already-open.
   An opacity/translate keyframe, NOT a max-height animation: the credential detail is exactly
   the tall-flex-panel case where animateExpand() reflows siblings every frame (documented
   v0.10.0 gotcha — reserve height animation for small bounded collapsibles). The classes are
   added by draw() only when the expanded row actually CHANGED, so tab switches and saves
   inside the open row don't replay the entrance. The caret needs the same treatment for the
   same reason — its .15s transform transition never runs on an element painted already-rotated. */
.row-detail--enter { animation: rowDetailIn 0.22s cubic-bezier(0.4, 0, 0.2, 1); }
@keyframes rowDetailIn { from { opacity: 0; transform: translateY(-5px); } }
.md-row--enter .md-row-caret { animation: mdCaretIn 0.2s ease; }
@keyframes mdCaretIn { from { transform: rotate(0deg); } }

/* --- Forms ------------------------------------------------------------------------------------ */
/* Capped at 280px, not 1fr (Clark, 2026-08-17): on a wide panel, a handful of short fields
   ("Status", "On", "Contractor") stretching to fill the row leaves them looking sparse, and a
   field count that doesn't divide evenly into the fitted column count strands the last row
   mostly empty (Reminders' 5 fields in a 4-column row was the worst case). Fixed-width columns
   left-align instead, so the panel reads as dense at any width; matched between the form and
   its own view-mode grid below so toggling Edit doesn't shift field widths under the user. */
.field-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(200px, 280px)); gap: 0 var(--pad); }
.form-section { font-size: 0.7rem; text-transform: uppercase; letter-spacing: 0.06em; color: var(--text-muted); margin: 1rem 0 0.5rem; }
.item-actions { display: flex; flex-wrap: wrap; gap: 0.45rem; margin-top: 0.35rem; }

/* --- Record view (Clark, 2026-08-17): read-only by default, "Edit" swaps in the form above --- */
.record-view-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(200px, 280px)); gap: 0.75rem var(--pad); margin-bottom: 0.7rem; }
.record-view-field { font-size: 0.88rem; }
.record-view-value { display: block; margin-top: 0.15rem; font-weight: 600; }
.record-view-notes { margin-bottom: 0.7rem; font-size: 0.86rem; max-width: 74ch; }
.record-view-notes p { margin: 0.2rem 0 0; white-space: pre-wrap; line-height: 1.55; font-weight: 400; }

.renewal-block {
  margin-top: 1rem; padding: 0.2rem var(--pad) 0.6rem;
  border: 1px dashed rgba(232, 200, 74, 0.45);
  border-radius: var(--radius-sm); background: var(--st-due-soft);
}
.history { margin-top: 1rem; }
.history-list li {
  display: flex; justify-content: space-between; gap: var(--gap); flex-wrap: wrap;
  font-size: 0.8rem; padding: 0.35rem 0; border-bottom: 1px solid var(--border);
}

/* --- Project stage pipeline (Clark, 2026-08-17) -----------------------------------------------
   Read-only "you are here" strip, not the Kanban board — see the comment on renderStagePipeline()
   in projects.js. Wraps on narrow widths rather than scrolling horizontally (row-gap covers the
   wrap case since flex-wrap breaks the single connecting line into per-row segments anyway). */
.stage-pipeline { display: flex; align-items: center; margin: 0.3rem 0 1rem; flex-wrap: wrap; row-gap: 0.5rem; }
.stage-step {
  display: flex; align-items: center; gap: 0.4rem; flex-shrink: 0;
  text-decoration: none; padding: 0.2rem 0.3rem 0.35rem; margin: -0.2rem -0.3rem 0; border-radius: 6px;
  border-bottom: 2px solid transparent;
}
.stage-step:hover { background: var(--surface-hover, var(--surface)); }
.stage-step--viewed { border-bottom-color: var(--accent); }
.stage-step-dot {
  display: grid; place-items: center; flex-shrink: 0; --icon-size: 15px;
  width: 30px; height: 30px; border-radius: 50%; font-size: 0.8rem; font-weight: 700;
  border: 2px solid var(--border-strong); color: var(--text-muted); background: var(--surface);
}
/* The current stage gets a soft halo on top of the solid fill (Clark, 2026-08-23 — "make it
   reflect what stage the project is currently in") so "you are here" reads even when no
   earlier stage is done yet and the whole track is otherwise grey. */
.stage-step--current .stage-step-dot { border-color: var(--accent); background: var(--accent); color: #fff; box-shadow: 0 0 0 4px var(--accent-soft); }
.stage-step--done .stage-step-dot { border-color: var(--st-valid); background: var(--st-valid); color: #fff; }
.stage-step-label { font-size: 0.78rem; font-weight: 600; color: var(--text-muted); white-space: nowrap; }
.stage-step--current .stage-step-label { color: var(--text); }
.stage-step--done .stage-step-label { color: var(--text-secondary); }
/* Thicker rounded track so the walked part of the pipeline reads as a progress FILL, not a
   hairline (same Clark request). --done = between two completed stages; --tocurrent = the
   segment arriving AT the current stage, green fading into the current dot's accent blue so
   the fill visibly hands over from "finished" to "in progress". */
.stage-step-line { flex: 1 1 28px; min-width: 16px; height: 4px; border-radius: 999px; background: var(--border); margin: 0 0.3rem; }
.stage-step-line--done { background: var(--st-valid); }
.stage-step-line--tocurrent { background: linear-gradient(90deg, var(--st-valid), var(--accent)); }

/* --- Project detail facts -------------------------------------------------------------------- */
.facts-row { display: grid; grid-template-columns: repeat(auto-fit, minmax(140px, 1fr)); gap: var(--gap); margin-bottom: calc(var(--pad) * 0.6); }
.fact { background: var(--surface-sunken); border: 1px solid var(--border); border-radius: var(--radius-sm); padding: 0.55rem var(--pad); font-size: 0.86rem; }
.fact-key { display: block; font-size: 0.68rem; color: var(--text-muted); margin-bottom: 0.1rem; }

/* --- Checklist summary + groups (project permits) --------------------------------------------- */
.checklist-summary {
  display: flex; align-items: center; gap: var(--gap); flex-wrap: wrap;
  font-size: 0.85rem; padding: 0.6rem var(--pad);
  background: var(--surface-sunken); border: 1px solid var(--border);
  border-radius: var(--radius); margin-bottom: var(--gap);
}
.group { margin-bottom: 0.8rem; }
.group-title { font-size: 0.88rem; margin: 0 0 0.15rem; }
.group-blurb { font-size: 0.76rem; margin: 0 0 0.5rem; }
/* The project checklist's own tier lists (renderComplianceChecklist(), compliance-ui.js) never
   had the Company page's .md3 wrapper — that page's master-detail groups everything into ONE
   persistent card, this one groups by severity tier instead, so each tier gets its own list.
   .md-row's own background:transparent (its base rule) assumes a solid ancestor supplies the
   backing, and without one every row here sat directly on the page's own gradient background
   with nothing behind it (Clark, 2026-08-22 — "ui bars for project documentation... they are
   just clear, update to match aesthethic and theme of all ui"). Same card recipe as .md3
   itself, scoped to .group .md-list specifically — the Company page's OWN .md-list already
   sits inside .md3's identical treatment, and nesting a second one there would just double the
   border/shadow. */
.group .md-list {
  background: var(--glass-surface-bg); border: 1px solid var(--glass-surface-border);
  border-radius: var(--radius); box-shadow: var(--shadow-md); overflow: hidden;
}

/* --- Portal tab --------------------------------------------------------------------------------- */
.portal-links { display: flex; flex-wrap: wrap; gap: 0.45rem; }
.portal-link {
  display: inline-flex; align-items: center; gap: 0.3rem;
  min-height: var(--tap-min);
  padding: 0.4rem 0.8rem;
  background: var(--surface);
  border: 1px solid var(--border-strong);
  border-radius: var(--radius-sm);
  color: var(--accent);
  text-decoration: none;
  font-size: 0.83rem;
  font-weight: 600;
}
.portal-link:hover { border-color: var(--accent); background: var(--accent-soft); }
.portal-form { margin-top: 0.6rem; }
.portal-form-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); gap: 0 var(--pad); }
.portal-form .field { margin-bottom: 0.6rem; }
.portal-note { font-size: 0.72rem; line-height: 1.5; flex: 1 1 240px; align-self: center; }

/* --- Guide (a tab in the credential detail — never a separate page) ----------------------------- */
/* An "at a glance" info card — icon + label + value per row — in place of a plain definition
   list, so where/website/validity/deadline read as one scannable unit rather than four loose
   lines. Reused (smaller, via .req-body below) inside an expanded requirement's cross-ref. */
.guide-info-card { border: 1px solid var(--border); border-radius: var(--radius); background: var(--surface); margin-bottom: 0.8rem; overflow: hidden; }
/* align-items: center, not flex-start (Clark, 2026-08-18 — "icons... not properly vertically
   centred... looks quite annoying"): flex-start pinned the icon to the top of the row, level
   with the label line, but the label+value text block is two lines tall — against that the icon
   visibly sat above centre instead of balanced against the whole block. The old flex-start +
   a guessed `margin-top` nudge on the icon was papering over that same misalignment rather than
   fixing it. */
.guide-info-row { display: flex; align-items: center; gap: 0.65rem; padding: 0.6rem 0.85rem; font-size: 0.82rem; line-height: 1.5; border-bottom: 1px solid var(--border); }
.guide-info-row:last-child { border-bottom: none; }
.guide-info-icon { display: grid; place-items: center; flex-shrink: 0; width: 22px; height: 22px; border-radius: var(--radius-sm); background: var(--tile-action-bg); color: #fff; }
.guide-info-icon { --icon-size: 13px; }
.guide-info-text { min-width: 0; }
.guide-info-label { display: block; font-size: 0.66rem; font-weight: 700; text-transform: uppercase; letter-spacing: 0.04em; color: var(--text-muted); margin-bottom: 0.1rem; }
.guide-link { color: var(--accent); word-break: break-all; }
.guide-h { font-size: 0.7rem; text-transform: uppercase; letter-spacing: 0.06em; color: var(--text-muted); margin: 0.6rem 0 0.35rem; }
.guide-h-hint { font-weight: 400; text-transform: none; letter-spacing: 0; margin-left: 0.4rem; }
/* Numbered steps as a small timeline — a circular badge per step with a connecting line —
   instead of the browser's plain decimal list marker. Pure CSS (counters), so it needed no
   markup change: <ol class="guide-steps"><li> stayed exactly what it was. */
.guide-steps { list-style: none; margin: 0; padding: 0; counter-reset: guide-step; display: flex; flex-direction: column; gap: 0.55rem; }
.guide-steps li { counter-increment: guide-step; position: relative; padding-left: 2.15rem; min-height: 1.5rem; max-width: 74ch; font-size: 0.84rem; line-height: 1.5; padding-top: 0.1rem; }
.guide-steps li::before {
  content: counter(guide-step); position: absolute; left: 0; top: 0;
  width: 1.5rem; height: 1.5rem; display: grid; place-items: center; border-radius: 50%;
  background: var(--accent-soft); color: var(--accent); font-size: 0.7rem; font-weight: 700;
}
.guide-steps li:not(:last-child)::after { content: ''; position: absolute; left: 0.75rem; top: 1.5rem; bottom: -0.55rem; width: 1px; background: var(--border); }
.guide-steps--sm { margin: 0.4rem 0 0.6rem; gap: 0.4rem; }
.guide-steps--sm li { font-size: 0.8rem; padding-left: 1.85rem; min-height: 1.3rem; }
.guide-steps--sm li::before { width: 1.3rem; height: 1.3rem; font-size: 0.62rem; }
.guide-steps--sm li:not(:last-child)::after { left: 0.65rem; top: 1.3rem; }

/* Requirements: each row is a prerequisite you can expand to see where/how to get it. Rows
   that reference another catalog credential carry that credential's LIVE status badge, so the
   list doubles as a "which of these do I actually hold" checklist. */
.guide-reqs { list-style: none; margin: 0; padding: 0; }
.req { border: 1px solid var(--border); border-radius: var(--radius-sm); margin-bottom: 0.35rem; background: var(--surface); }
.req--text { display: flex; align-items: center; justify-content: space-between; gap: 0.6rem; padding: 0.55rem 0.8rem; font-size: 0.84rem; color: var(--text); }

/* Need level: required rows are the plain default; conditional and supporting are visibly
   secondary so the mandatory set reads as one block. */
.req--conditional, .req--supporting { background: var(--surface-sunken); }
.req--conditional .req-label, .req--supporting .req-label { color: var(--text-secondary); }
/* Fixed short label now (Clark, 2026-08-17 — see needChip() in compliance-ui.js), so no
   max-width/ellipsis machinery is needed — a short static string can't overflow its row. */
/* Docket shape (Clark, 2026-08-18 — missed in the v0.6.3 sweep that moved .badge/.chip/.sev
   off the generic 999px pill; caught when he circled this exact chip still looking like the
   old shape). Same square-left/rounded-right + colour-spine treatment as .sev, not a bare
   outline pill. */
.need {
  display: inline-flex; align-items: center; gap: 0.3rem;
  padding: 0.05rem 0.5rem; border-radius: 3px var(--radius-sm) var(--radius-sm) 3px;
  font-size: 0.66rem; font-weight: 700; white-space: nowrap; border: 1px solid; border-left-width: 3px;
  vertical-align: middle;
}
/* Same reasoning as .sev--warn: gold shows as a dot, the text stays neutral and legible. */
.need--cond { color: var(--text-secondary); border-color: rgba(232, 200, 74, 0.6); border-left-color: var(--nav-accent); background: var(--st-due-soft); }
.need--supp { color: var(--text-muted); border-color: var(--border-strong); background: var(--surface); }
.req-when { margin: 0 0 0.45rem; font-size: 0.8rem; line-height: 1.55; color: var(--text-secondary); }
/* A plain button + max-height collapse, not native <details>/<summary> (Clark, 2026-08-20 —
   see renderRequirementRow() in compliance-ui.js for the full reasoning) — same technique as
   the sidebar's .nav-subtree. `.req-details.open` replaces `details[open]` throughout. */
.req-summary {
  display: flex; align-items: center; justify-content: space-between; gap: 0.6rem;
  flex-wrap: wrap; /* a long label + several chips (need/status/caret) can't always share one
                       row in a narrow detail pane — wrap rather than overflow it sideways. */
  width: 100%;
  min-height: var(--tap-min);
  padding: 0.45rem 0.8rem;
  border: none; background: transparent; font: inherit; text-align: left;
  cursor: var(--cur-hand);
  font-size: 0.84rem;
  transition: background-color 0.12s ease;
}
.req-summary:hover { background: var(--surface-sunken); }
.req-label { flex: 1 1 auto; min-width: 0; }
/* The kind glyph (text / doc / cross-ref) — muted, small, never the only signal (the label
   and expanded body still say exactly what it is). */
.req-icon { --icon-size: 15px; color: var(--text-muted); }
.req-meta { display: inline-flex; align-items: center; gap: 0.5rem; flex-shrink: 0; }
.req-caret { color: var(--text-muted); --icon-size: 15px; transition: transform 0.15s ease; }
.req-details.open .req-caret { transform: rotate(90deg); }
.req-details.open .req-summary { border-bottom: 1px solid var(--border); background: var(--surface-sunken); }
.req-collapse {
  max-height: 0; opacity: 0; overflow: hidden;
  /* This is the CLOSING direction (the .open rule below carries its own). The fixed 640px cap
     means the first stretch of a close animates max-height through air above the real content
     height — nothing visibly moves until the cap crosses it. A faster opacity fade (0.12s)
     empties the box before that dead stretch is perceptible (Clark, 2026-08-23 dropdown pass). */
  transition: max-height 0.22s cubic-bezier(0.4, 0, 0.2, 1), opacity 0.12s ease;
}
.req-details.open .req-collapse { max-height: 640px; opacity: 1; transition: max-height 0.28s cubic-bezier(0.4, 0, 0.2, 1), opacity 0.2s ease; }
.req-body { padding: 0.6rem 0.8rem 0.75rem; }
.req-title-row { display: flex; flex-direction: column; gap: 0.1rem; margin-bottom: 0.4rem; font-size: 0.86rem; }
.req-title-row .muted { font-size: 0.76rem; }
.req-how { margin: 0 0 0.5rem; font-size: 0.83rem; line-height: 1.6; color: var(--text-secondary); max-width: 74ch; }
.req-actions { display: flex; flex-wrap: wrap; gap: 0.4rem; margin-top: 0.4rem; }
.req-body .guide-info-card { margin-bottom: 0.4rem; }
.req-body .guide-info-row { padding: 0.45rem 0.7rem; font-size: 0.78rem; }
.portal-link--sm { padding: 0.3rem 0.65rem; font-size: 0.78rem; } /* keeps the 44px min-height from .portal-link */
.guide-verify {
  margin-top: 1rem; padding: 0.6rem var(--pad);
  background: var(--surface-sunken); border-radius: var(--radius-sm);
  font-size: 0.78rem; line-height: 1.6; max-width: 76ch;
}

/* --- Documents: the vault + upload → (stored) → review left/right → apply --------------------- */
.doc-drop {
  border: 2px dashed var(--border-strong);
  border-radius: var(--radius);
  background: var(--surface-sunken);
  padding: clamp(0.7rem, 2.2vh, 1.3rem) var(--pad);
  text-align: center;
  transition: border-color 0.15s ease, background 0.15s ease;
}
.doc-drop--over { border-color: var(--accent); background: var(--accent-soft); }
.doc-drop--busy { opacity: 0.8; }
.doc-drop-inner { display: flex; flex-direction: column; align-items: center; gap: 0.45rem; }
.doc-drop-title { font-weight: 600; font-size: 0.95rem; }
/* The upload icon on a full (non-slim) drop zone — same circle-badge convention as
   .empty-state-icon, just on the drop zone's own sunken background instead of the surface. */
.doc-drop-icon {
  width: 40px; height: 40px; display: grid; place-items: center; flex-shrink: 0;
  border-radius: var(--radius-sm); background: var(--tile-action-bg); color: #fff;
  box-shadow: 0 1px 3px rgba(20, 28, 48, 0.18);
}
.doc-drop-icon { --icon-size: 20px; }

.doc-vault { margin-top: 1rem; }
.doc-list { list-style: none; margin: 0; padding: 0; }
.doc-row {
  display: flex; align-items: center; justify-content: space-between; gap: 0.6rem; flex-wrap: wrap;
  padding: 0.5rem 0.75rem; border: 1px solid var(--border); border-radius: var(--radius-sm);
  background: var(--surface); margin-bottom: 0.35rem;
  transition: background-color 0.12s ease;
}
.doc-row:hover { background: var(--surface-sunken); }
.doc-row-main { display: flex; flex-direction: column; gap: 0.1rem; min-width: 0; flex: 1 1 200px; }
.doc-row-name { font-size: 0.86rem; font-weight: 600; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.doc-row-sub { font-size: 0.74rem; }
/* A row can carry up to four buttons (Open / Re-read / Make current / Delete) — quiet by
   default and revealed on hover, like Supabase's row actions, rather than four buttons
   competing for attention on every row all the time. :focus-within keeps it reachable by
   keyboard (Tab lands inside before hover would ever reveal it), and touch has no hover to
   rely on, so coarse pointers always show them. */
.doc-row-actions { display: flex; align-items: center; gap: 0.35rem; flex-wrap: wrap; opacity: 0; transition: opacity 0.12s ease; }
.doc-row:hover .doc-row-actions, .doc-row:focus-within .doc-row-actions { opacity: 1; }
@media (pointer: coarse) { .doc-row-actions { opacity: 1; } }

.doc-review {
  border: 1px solid var(--border); border-radius: var(--radius);
  background: var(--surface); padding: 0.7rem var(--pad) var(--pad);
  margin-bottom: 0.9rem;
}
.doc-review-head { display: flex; align-items: center; justify-content: space-between; gap: 0.6rem; flex-wrap: wrap; margin-bottom: 0.6rem; font-size: 0.86rem; }

/* Previews sized to the document. A single PDF page is exactly its own aspect ratio at the
   container width — no scroll. Multi-page stacks its pages in a capped, scrollable box with a
   page counter. Images use their natural ratio. */
.doc-preview {
  position: relative; width: 100%;
  border: 1px solid var(--border); border-radius: var(--radius-sm);
  background: var(--surface-sunken); overflow: hidden;
}
.doc-preview--multi { overflow-y: auto; }
.doc-pages { display: flex; flex-direction: column; gap: 6px; }
.doc-page { display: block; width: 100%; height: auto; background: #fff; box-shadow: 0 1px 3px rgba(11,11,11,0.12); }
.doc-preview--multi .doc-page:not(:last-child) { border-bottom: 1px solid var(--border); }
.doc-page-count {
  position: sticky; bottom: 0.4rem; margin: 0 0.4rem 0.4rem auto; width: max-content;
  padding: 0.15rem 0.55rem; border-radius: 999px;
  background: rgba(22, 33, 62, 0.85); color: #fff; font-size: 0.7rem; font-weight: 700;
  pointer-events: none;
}
.doc-preview-loading { display: flex; align-items: center; gap: 0.5rem; padding: 1.2rem; font-size: 0.82rem; }
.doc-preview-img { display: block; width: 100%; height: auto; }
.doc-preview-none { padding: 1.2rem; text-align: center; font-size: 0.84rem; }
.record-doc { margin-top: 1.1rem; }
.record-doc-head { display: flex; align-items: center; justify-content: space-between; gap: 0.6rem; }
.doc-print-btn { display: inline-flex; align-items: center; gap: 0.3rem; --icon-size: 14px; }
.record-prov { font-size: 0.78rem; margin: 0 0 0.7rem; padding: 0.4rem 0.7rem; border-left: 3px solid var(--st-valid); background: var(--st-valid-soft); border-radius: 0 var(--radius-sm) var(--radius-sm) 0; }

/* Current vs archived documents */
.doc-tag {
  display: inline-block; padding: 0.05rem 0.5rem; margin-right: 0.4rem; border-radius: 999px;
  font-size: 0.66rem; font-weight: 700; vertical-align: 1px; border: 1px solid;
}
.doc-tag--current { color: var(--st-valid); border-color: rgba(22, 122, 22, 0.4); background: var(--st-valid-soft); }
.doc-tag--archive { color: var(--text-muted); border-color: var(--border-strong); background: var(--surface); }
.doc-row--archived { background: var(--surface-sunken); }
.doc-row--archived .doc-row-name { color: var(--text-secondary); font-weight: 500; }

/* Split viewer (Clark, 2026-08-18, reordered same day) — the Documents tab's own layout, used
   inside the master-detail's wide content pane (renderDocumentsSplitView() in documents.js).
   Details (name, tags, kind/size/date, actions) sit in one full-width header ON TOP now
   (Clark: "have the document details be on top and the documents ro[w]... below it" — a first
   pass put the metadata under the preview, competing for space in the narrower right column);
   the file list + preview split sits below it. The narrow project-checklist context keeps the
   older stacked renderDocumentsTab(). */
.doc-detail-head { margin-bottom: 1rem; }
.doc-detail-title { font-size: 0.95rem; font-weight: 700; }
.doc-detail-sub { font-size: 0.8rem; margin: 0.2rem 0 0.6rem; }
.doc-split { display: grid; grid-template-columns: minmax(160px, 200px) minmax(0, 1fr); gap: var(--gap); align-items: start; margin-bottom: 1rem; }
.doc-split-list { display: flex; flex-direction: column; gap: 0.3rem; }
.doc-split-row {
  display: flex; align-items: center; gap: 0.5rem; padding: 0.45rem 0.55rem; border-radius: var(--radius-sm);
  border: 1px solid transparent; cursor: var(--cur-hand); font: inherit; text-align: left; background: transparent; color: var(--text);
}
.doc-split-row:hover { background: var(--surface-sunken); }
.doc-split-row--active { background: var(--accent-soft); border-color: var(--accent); }
.doc-split-row-icon { --icon-size: 15px; color: var(--text-muted); flex-shrink: 0; }
.doc-split-name { font-size: 0.78rem; font-weight: 600; margin-top: 0.15rem; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; max-width: 130px; }

.doc-upload-choice { margin-top: 0.4rem; }
/* Underline style, matching .seg-tabs (Clark, 2026-08-17) — was its own pill segmented
   control; now the same tab convention as the credential detail's Record/Guide/Portal row. */
.doc-mode-tabs { margin-bottom: 0.5rem; }
.doc-archive-form { display: flex; align-items: center; gap: 0.5rem; flex-wrap: wrap; justify-content: center; margin-bottom: 0.3rem; }
.doc-archive-form label.muted { font-size: 0.82rem; }
.doc-archive-form input[type="text"] {
  min-height: var(--tap-min); width: 11rem; padding: 0.4rem 0.6rem; font: inherit; font-size: 0.86rem;
  border: 1px solid var(--border-strong); border-radius: var(--radius-sm); background: var(--surface); color: var(--text);
}
.doc-archive-form input:focus { outline: none; border-color: var(--accent); box-shadow: 0 0 0 3px var(--accent-soft); }
.scan-choose { display: inline-flex; align-items: center; cursor: var(--cur-hand); margin-top: 0.3rem; }
/* "Choose files" + "Choose folder" side by side on bulk import's drop zone (Clark, 2026-08-20). */
.bulk-choose-row { display: flex; gap: 0.5rem; flex-wrap: wrap; justify-content: center; }
.bulk-choose-row .scan-choose { margin-top: 0; }
.scan-note { font-size: 0.78rem; line-height: 1.6; margin: 0.7rem 0 0; max-width: 72ch; }

.scan-split { display: grid; grid-template-columns: minmax(260px, 1fr) minmax(300px, 1fr); gap: var(--gap); align-items: start; }
.scan-left { min-width: 0; }
.scan-file-row { display: flex; align-items: center; gap: 0.6rem; flex-wrap: wrap; margin-bottom: 0.5rem; font-size: 0.82rem; }
.scan-file-name { font-weight: 600; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; max-width: 100%; }
.scan-preview {
  border: 1px solid var(--border); border-radius: var(--radius-sm);
  background: var(--surface-sunken); overflow: hidden;
}

.scan-right { min-width: 0; }
.scan-status { display: flex; align-items: center; gap: 0.6rem; font-size: 0.88rem; color: var(--text-secondary); padding: 0.6rem 0; }
.spinner {
  width: 16px; height: 16px; border-radius: 50%;
  border: 2px solid var(--border-strong); border-top-color: var(--accent);
  animation: spin 0.8s linear infinite;
}
@keyframes spin { to { transform: rotate(360deg); } }

.scan-review-head { display: flex; flex-direction: column; gap: 0.15rem; margin-bottom: 0.7rem; font-size: 0.86rem; }
.scan-review-head .muted { font-size: 0.78rem; }
.scan-warnings {
  margin: 0 0 0.7rem; padding: 0.5rem 0.8rem 0.5rem 1.8rem;
  background: var(--st-due-soft); border-left: 3px solid var(--nav-accent); border-radius: 0 var(--radius-sm) var(--radius-sm) 0;
  font-size: 0.78rem; line-height: 1.55;
}
.scan-fields { display: flex; flex-direction: column; gap: 0.45rem; }
.scan-field {
  display: grid; grid-template-columns: minmax(150px, 0.9fr) minmax(0, 1.4fr); gap: 0.4rem 0.7rem; align-items: start;
  padding: 0.5rem 0.65rem; border: 1px solid var(--border); border-radius: var(--radius-sm); background: var(--surface);
}
.scan-field--empty { opacity: 0.6; }
.scan-field-accept { display: flex; align-items: center; gap: 0.5rem; min-height: var(--tap-min); font-size: 0.83rem; font-weight: 600; cursor: var(--cur-hand); }
.scan-field-accept input { width: 18px; height: 18px; accent-color: var(--accent); flex-shrink: 0; }
.scan-field-main input {
  width: 100%; min-height: var(--tap-min); padding: 0.45rem 0.6rem;
  border: 1px solid var(--border-strong); border-radius: var(--radius-sm);
  font: inherit; font-size: 0.86rem; background: var(--surface); color: var(--text);
}
.scan-field-main input:focus { outline: none; border-color: var(--accent); box-shadow: 0 0 0 3px var(--accent-soft); }
.scan-field-meta { display: flex; align-items: center; gap: 0.5rem; margin-top: 0.3rem; font-size: 0.72rem; }
.scan-diff { color: var(--text-muted); }

.conf { display: inline-flex; align-items: center; gap: 0.28rem; padding: 0.05rem 0.45rem; border-radius: 999px; font-size: 0.66rem; font-weight: 700; border: 1px solid; }
.conf--hi  { color: var(--st-valid);   border-color: rgba(22, 122, 22, 0.4); }
.conf--mid { color: var(--text-secondary); border-color: rgba(232,200,74,0.6); } /* gold as a dot, not text — see .sev--warn */
.conf--lo  { color: var(--st-expired); border-color: rgba(208,59,59,0.4); }

.scan-excerpt { margin-top: 0.8rem; font-size: 0.78rem; }
.scan-excerpt > .panel-summary { color: var(--text-muted); font-size: inherit; }
.scan-excerpt pre {
  margin: 0; padding: 0.6rem 0.8rem; max-height: 180px; overflow: auto;
  white-space: pre-wrap; word-break: break-word;
  background: var(--surface-sunken); border: 1px solid var(--border); border-radius: var(--radius-sm);
  font-size: 0.72rem; line-height: 1.5;
}

/* --- Bulk import: drop many → sorted → review table → import ------------------------------------ */
.page-head { display: flex; align-items: flex-start; justify-content: space-between; gap: var(--gap); flex-wrap: wrap; flex-shrink: 0; }
.page-head > div { min-width: 0; flex: 1 1 320px; }
.page-head > .btn-secondary { flex-shrink: 0; margin-top: 0.2rem; }
.page-head--section { margin-top: 1.4rem; }
.page-head--section .panel-title--standalone { margin-top: 0; }
/* animateExpand()-driven open/close (Clark, 2026-08-20), replacing a `hidden` attribute
   toggle — the review table inside can hold dozens of rows, so a fixed max-height cap would
   clip it; see animateExpand() in shared.js. */
.bulk-panel {
  flex-shrink: 0; max-height: 0; opacity: 0; overflow: hidden;
  transition: max-height 0.28s cubic-bezier(0.4, 0, 0.2, 1), opacity 0.2s ease;
}
.bulk-panel.open { margin-bottom: var(--pad); }
/* "Register a project" panel (Clark, 2026-08-22, 2 rounds later — "fix dropdown load animation
   for this is jitery and snappy its getting hanged up... fix the scaling of the whole block
   and content... too big"): the first version animated open with a .bulk-panel-style
   max-height transition, but at this form's real content height (~1000px — Identification's
   icon/colour pickers, then the full Contract section) that forced the FLEX-COLUMN layout
   (.content, with #project-grid as this panel's own sibling right below it) to reflow every
   single animation frame — the project cards visibly overlapped the form's own fields for the
   whole ~280ms, which is what actually read as "jittery... hanged up... too big", not the
   custom <select> the wording named (this panel is the thing that "drops down"). Replaced with
   the same trick .content-swap-in uses elsewhere: `hidden` toggles LAYOUT instantly (nothing
   here ever animates a property that affects box size, so there is nothing left for a sibling
   to reflow around mid-transition) and only opacity/transform — compositor-only, zero layout
   cost — animate the reveal. See projects.js's own toggle/cancel handlers for the two-frame
   sequencing this needs (add `hidden`-removal and the `--open` class a frame apart, so the
   browser commits the closed state before animating away from it). */
.new-project-panel {
  opacity: 0; transform: translateY(-8px);
  transition: opacity 0.22s ease, transform 0.22s cubic-bezier(0.4, 0, 0.2, 1);
}
.new-project-panel--open { opacity: 1; transform: translateY(0); }
/* Scaled down (Clark, 2026-08-23 — "too big scale it down this is too unnecessary"): the icon/
   colour picker in particular was sized for an EXISTING project's own branding page, where
   picking a look is the point of the visit; on the CREATE form it's a minor first step before
   the fields that actually matter, so it gets the same compact treatment the org create-card's
   own icon picker already uses (icon-swatch-btn--sm) rather than a bespoke size. Scoped to
   .new-project-panel only — the Details page's own (existing-project) picker is untouched. */
.new-project-panel .project-avatar-picker { gap: 0.7rem; margin-bottom: 0.7rem; }
.new-project-panel .project-avatar-picker-preview { width: 40px; height: 40px; --icon-size: 20px; }
.new-project-panel .icon-swatch-btn { width: 32px; height: 32px; --icon-size: 16px; }
.new-project-panel .project-avatar-picker-options { gap: 0.3rem; margin-bottom: 0.4rem; }
.new-project-panel .form-section { margin: 0.7rem 0 0.35rem; }
.new-project-panel .form-section:first-of-type { margin-top: 0; }
.new-project-panel .field { margin-bottom: 0.35rem; }
.new-project-panel .field textarea { min-height: 3rem; }
.bulk-drop {
  border: 2px dashed var(--border-strong); border-radius: var(--radius);
  background: var(--surface-sunken); padding: clamp(1rem, 3vh, 1.8rem) var(--pad); text-align: center;
  transition: border-color 0.15s ease, background 0.15s ease;
}
.bulk-drop.doc-drop--over { border-color: var(--accent); background: var(--accent-soft); }
.bulk-drop .doc-drop-inner .muted { max-width: 64ch; }

.bulk-review {
  background: var(--surface); border: 1px solid var(--border); border-radius: var(--radius);
  padding: var(--pad);
}
.bulk-review-head { display: flex; align-items: flex-start; justify-content: space-between; gap: var(--gap); flex-wrap: wrap; margin-bottom: 0.7rem; font-size: 0.88rem; }
.bulk-review-head .muted { font-size: 0.8rem; }
.bulk-review-actions { display: flex; gap: 0.4rem; flex-shrink: 0; }

.table-scroll { overflow-x: auto; }
.bulk-table { width: 100%; border-collapse: collapse; font-size: 0.83rem; }
.bulk-table th {
  text-align: left; font-size: 0.66rem; text-transform: uppercase; letter-spacing: 0.05em;
  color: var(--text-muted); font-weight: 700; padding: 0.4rem 0.5rem; border-bottom: 1px solid var(--border);
  white-space: nowrap;
}
.bulk-table td { padding: 0.5rem; border-bottom: 1px solid var(--border); vertical-align: top; }
.bulk-table tbody tr { transition: background-color 0.12s ease; }
.bulk-table tbody tr:hover { background: var(--surface-sunken); }
.bulk-row--reading { opacity: 0.75; }
.bulk-row--error { background: var(--st-expired-soft); }
.bulk-row--unsorted { background: var(--st-due-soft); }
.bulk-row--wrongscope { background: var(--st-neutral-soft); }
.bulk-row--done { opacity: 0.5; }
.bulk-col-inc { width: 2.2rem; text-align: center; }
.bulk-col-inc input { width: 18px; height: 18px; accent-color: var(--accent); margin-top: 0.4rem; }
.bulk-col-file { min-width: 160px; max-width: 260px; }
.bulk-file-name { font-weight: 600; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.bulk-file-sub { font-size: 0.72rem; margin-top: 0.1rem; }
.bulk-col-code { min-width: 220px; }
.bulk-select {
  width: 100%; min-height: var(--tap-min); padding: 0.35rem 0.5rem; font: inherit; font-size: 0.82rem;
  border: 1px solid var(--border-strong); border-radius: var(--radius-sm); background: var(--surface); color: var(--text);
}
.bulk-code-meta { margin-top: 0.25rem; font-size: 0.7rem; }
.bulk-col-fields { min-width: 200px; }
.bulk-kv { display: inline-block; margin: 0 0.9rem 0.2rem 0; font-size: 0.8rem; font-weight: 600; }
.bulk-kv .kv-label { display: inline; margin-right: 0.3rem; }
/* Reference numbers read as codes, same convention as .kv-code above. */
.bulk-kv--code { font-variant-numeric: tabular-nums; letter-spacing: 0.01em; }
.bulk-col-act { white-space: nowrap; text-align: right; }
.bulk-col-act .btn-link { font-size: 0.76rem; }
.spinner--inline { display: inline-block; width: 11px; height: 11px; border-width: 2px; vertical-align: -1px; }

.bulk-foot { display: flex; align-items: center; justify-content: space-between; gap: var(--gap); flex-wrap: wrap; margin-top: 0.8rem; font-size: 0.78rem; }
.bulk-foot .muted { flex: 1 1 320px; max-width: 70ch; }

.bulk-done { background: var(--surface); border: 1px solid var(--border); border-radius: var(--radius); padding: var(--pad); }
.bulk-done-head { display: flex; flex-direction: column; gap: 0.2rem; font-size: 0.9rem; margin-bottom: 0.6rem; }
.bulk-done-head .muted { font-size: 0.8rem; }
.bulk-done-list { margin: 0 0 0.6rem; padding-left: 1.2rem; font-size: 0.8rem; line-height: 1.6; color: var(--text-secondary); }
.bulk-done-list li { margin-bottom: 0.25rem; }
.bulk-done-list--check { border-left: 3px solid var(--nav-accent); background: var(--st-due-soft); padding: 0.4rem 0.6rem 0.4rem 1.6rem; border-radius: 0 var(--radius-sm) var(--radius-sm) 0; list-style-position: outside; }
.scan-warnings--sanity { margin-top: 0.6rem; }

@media (max-width: 700px) {
  /* Below the mobile line the review table stacks each file as a card — no horizontal
     scroll (Clark's rule), and the credential dropdown gets the full width it needs. */
  .bulk-table, .bulk-table tbody, .bulk-table tr, .bulk-table td { display: block; width: 100%; }
  .bulk-table thead { display: none; }
  .bulk-table tr.bulk-row { border: 1px solid var(--border); border-radius: var(--radius-sm); margin-bottom: 0.5rem; padding: 0.4rem 0.6rem 0.5rem; }
  .bulk-table td { border-bottom: none; padding: 0.25rem 0; }
  .bulk-col-inc { width: auto; text-align: left; }
  .bulk-col-inc input { margin: 0.2rem 0.4rem 0 0; }
  .bulk-col-file, .bulk-col-code, .bulk-col-fields { min-width: 0; max-width: none; }
  .bulk-col-act { text-align: left; }
  /* label the stacked cells so they still read as a table */
  .bulk-col-code::before { content: 'Sorted to'; display: block; font-size: 0.66rem; text-transform: uppercase; letter-spacing: 0.05em; color: var(--text-muted); font-weight: 700; margin-bottom: 0.15rem; }
  .bulk-col-fields::before { content: 'Read from it'; display: block; font-size: 0.66rem; text-transform: uppercase; letter-spacing: 0.05em; color: var(--text-muted); font-weight: 700; margin-bottom: 0.15rem; }
}

/* --- Project readiness gates ------------------------------------------------------------------ */
.readiness { display: grid; grid-template-columns: repeat(auto-fit, minmax(240px, 1fr)); gap: var(--gap); margin-bottom: calc(var(--pad) * 0.6); }
.gate {
  background: var(--surface); border: 1px solid var(--border); border-left: 3px solid var(--nav-accent);
  border-radius: var(--radius-sm); padding: 0.6rem 0.8rem; font-size: 0.82rem;
}
.gate--open { border-left-color: var(--st-valid); }
.gate-head { display: flex; align-items: center; gap: 0.45rem; flex-wrap: wrap; }
.gate-dot { width: 9px; height: 9px; border-radius: 50%; background: var(--nav-accent); flex-shrink: 0; }
.gate--open .gate-dot { background: var(--st-valid); }
.gate-label { font-weight: 700; flex: 1 1 auto; }
.gate-state { font-size: 0.74rem; font-weight: 700; color: var(--text-secondary); }
.gate--open .gate-state { color: var(--st-valid); }
.gate-missing { margin-top: 0.3rem; font-size: 0.78rem; }
.gate-missing .btn-link { padding: 0.15rem 0.3rem; font-size: 0.78rem; } /* keeps the 44px min-height from .btn-link */
.gate-others { margin-top: 0.2rem; font-size: 0.72rem; }

/* --- Org switcher (sidebar footer) ------------------------------------------------------------
   A trigger button + a popover appended to <body> (a Supabase-style project switcher), not a
   plain <select> — the popover is body-level so it isn't clipped by .sidebar's overflow:hidden
   (the rail scales rather than scrolls; see .sidebar's own comment), and position:fixed with
   JS-computed coordinates means it opens correctly regardless of rail width or collapse state. */
.org-switch-btn {
  display: flex; align-items: center; gap: 0.4rem; width: 100%;
  min-height: var(--tap-min); padding: 0.3rem 0.35rem;
  border: 1px solid var(--nav-border); border-radius: var(--radius-sm);
  background: var(--nav-hover); color: var(--nav-text);
  font: inherit; font-size: 0.78rem; font-weight: 600; text-align: left; cursor: var(--cur-hand);
  transition: background-color 0.12s ease, gap 0.22s cubic-bezier(0.4, 0, 0.2, 1), padding 0.22s cubic-bezier(0.4, 0, 0.2, 1);
}
.org-switch-btn:hover, .org-switch-btn[aria-expanded="true"] { background: var(--nav-active); }
/* The one-membership case (renderOrgFooter() in app.js) — same bubble, but a plain div with
   nothing to click: no pointer cursor, no hover state, no chevron in the markup at all. */
.org-switch-btn--static { cursor: var(--cur-arrow); }
.org-switch-btn--static:hover { background: var(--nav-hover); }
.org-switch-avatar {
  display: grid; place-items: center; flex-shrink: 0;
  width: 18px; height: 18px; border-radius: 50%;
  background: var(--nav-accent); color: var(--nav-bg-deep);
  font-size: 0.6rem; font-weight: 700;
}
/* A standalone label class (distinct from .nav-item/.nav-brand's bare `span` selectors above)
   since the org switcher's own markup isn't part of the ported pattern — kept as its own class
   so it still fades in step with everything else on collapse without needing that markup
   rewritten too. */
.nav-label {
  max-width: 108px; opacity: 1; overflow: hidden; white-space: nowrap;
  transition: max-width 0.22s cubic-bezier(0.4, 0, 0.2, 1), opacity 0.15s ease;
}
.sidebar.collapsed .nav-label { max-width: 0; opacity: 0; }
/* One line, whole words only (Clark, 2026-08-17 — after trying both shrink-to-fit and wrap-
   to-two-lines and disliking both: "just one liner... cut and use the first two or one word of
   the company name if it fits"). fitOrgNameWords() in app.js does the actual word-trimming at
   the button's real font-size — this is just the box: no shrink, no wrap, no ellipsis (the
   trimming already guarantees it fits, so "..." should never actually be needed). max-width:
   none overrides .nav-label's generic 108px cap, same reasoning as before. */
.org-switch-name.nav-label {
  flex: 1 1 auto; min-width: 0; max-width: none;
  overflow: hidden; white-space: nowrap; text-overflow: clip;
}
.org-switch-chevron.nav-label { display: flex; align-items: center; flex-shrink: 0; max-width: 13px; --icon-size: 12px; }
.sidebar.collapsed .org-switch-btn { justify-content: center; gap: 0; padding: 0.3rem 0; }

/* Solid, not the reusable translucent --glass-bg (Clark, 2026-08-17 — "the dropdown being
   that translucent its hard to see"): every other glass-surface panel in the app always sits
   on the same content-plane background, but this one floats via position:fixed and can
   overlap the dark sidebar AND the white content at once — a translucent fill there reads as
   two different, both-illegible colours depending what's directly behind it. */
.org-switch-popover {
  position: fixed; z-index: 140; width: 240px; max-width: calc(100vw - 2rem);
  background: var(--surface-solid);
  border: 1px solid var(--border-strong); border-radius: var(--radius);
  box-shadow: var(--shadow-md); padding: 0.3rem; max-height: 60vh; overflow-y: auto;
}
/* Entrance/exit animation — see .account-popover's own comment above for the full mechanism
   (shared JS pattern, `.popover-open` toggled a frame after append). This one grows DOWN from a
   topbar trigger, so it drops into place (translateY negative → 0) instead of rising. */
.org-switch-popover { opacity: 0; transform: translateY(-6px) scale(0.97); transform-origin: top left;
  transition: opacity 0.14s ease, transform 0.16s cubic-bezier(0.4, 0, 0.2, 1); }
.org-switch-popover.popover-open { opacity: 1; transform: translateY(0) scale(1); }
.org-switch-row {
  display: flex; align-items: center; justify-content: space-between; gap: 0.5rem; width: 100%;
  min-height: 32px; padding: 0.3rem 0.6rem; /* compact by default — see .account-row's own comment on why, and its coarse-pointer restore right after it */
  border: none; border-radius: var(--radius-sm); background: transparent;
  color: var(--text); font: inherit; font-size: 0.85rem; text-align: left; cursor: var(--cur-hand);
  transition: background-color 0.12s ease;
}
.org-switch-row:hover { background: var(--surface-sunken); }
.org-switch-row--active { background: var(--accent-soft); }
/* Keyboard focus while a custom-select.js listbox is open — same visual as :hover (arrow-key
   navigation should read exactly like moving the mouse over the options), just driven by JS
   since the option buttons stay unfocused (the trigger keeps real DOM focus throughout, same
   as a native <select> never moves focus onto its own popped-open options either). */
.org-switch-row--focused { background: var(--surface-sunken); }
.org-switch-row-main { display: flex; align-items: center; gap: 0.5rem; min-width: 0; }
.org-switch-row-name { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; font-weight: 600; }
.org-switch-check { --icon-size: 16px; color: var(--accent); }

/* --- Settings: team table, invite --------------------------------------------------------------- */
.team-table { width: 100%; border-collapse: collapse; font-size: 0.85rem; }
.team-table th { text-align: left; font-size: 0.66rem; text-transform: uppercase; letter-spacing: 0.05em; color: var(--text-muted); font-weight: 700; padding: 0.3rem 0.5rem; border-bottom: 1px solid var(--border); }
.team-table td { padding: 0.4rem 0.5rem; border-bottom: 1px solid var(--border); vertical-align: middle; }
.team-table tbody tr { transition: background-color 0.12s ease; }
.team-table tbody tr:hover { background: var(--surface-sunken); }
.team-row--pending { background: var(--surface-sunken); }
.team-row--busy { opacity: 0.6; }
.team-name { font-weight: 600; }
.team-email { font-size: 0.76rem; }
.team-role { min-height: var(--tap-min); padding: 0.35rem 0.5rem; font: inherit; font-size: 0.82rem; border: 1px solid var(--border-strong); border-radius: var(--radius-sm); background: var(--surface); color: var(--text); }
/* Quiet until the row is hovered/focused — same reveal-on-hover convention as the document
   vault's row actions. Coarse pointers (no hover) always show it. */
.team-actions { text-align: right; white-space: nowrap; opacity: 0; transition: opacity 0.12s ease; }
.team-table tbody tr:hover .team-actions, .team-table tbody tr:focus-within .team-actions { opacity: 1; }
@media (pointer: coarse) { .team-actions { opacity: 1; } }
.invite-form { margin-top: 0.5rem; }
.invite-row { display: flex; align-items: flex-end; gap: 0.6rem; flex-wrap: wrap; }
.invite-row .field { margin-bottom: 0; flex: 1 1 200px; }
/* fieldSelect() (the Role dropdown, converted from a raw <select> 2026-08-23) always renders a
   .field-hint; the hand-written Email field beside it has none — under align-items:flex-end an
   empty hint would float the Role control ~1em above its neighbours. No hints are used here. */
.invite-row .field-hint { display: none; }
.invite-row .btn-primary { flex-shrink: 0; margin-bottom: 0; }
/* Below the mobile line the team table stacks each member as a card, same convention as
   .bulk-table — dropping columns into labelled stacked rows rather than a horizontal scroll. */
@media (max-width: 700px) {
  .team-table, .team-table tbody, .team-table tr, .team-table td { display: block; width: 100%; }
  .team-table thead { display: none; }
  .team-table tbody tr { border: 1px solid var(--border); border-radius: var(--radius-sm); margin-bottom: 0.5rem; padding: 0.5rem 0.65rem; }
  .team-table td { border-bottom: none; padding: 0.25rem 0; }
  .team-role { width: 100%; }
  /* Label the stacked cells so they still read as a table — the Actions cell doesn't need
     one, the Remove button already says what it does. */
  .team-col-role::before {
    content: 'Role'; display: block; font-size: 0.62rem; text-transform: uppercase;
    letter-spacing: 0.05em; color: var(--text-muted); font-weight: 700; margin-bottom: 0.15rem;
  }
  .team-actions { text-align: left; }
}

/* --- Dialogs ------------------------------------------------------------------------------------ */
.modal-overlay {
  position: fixed; inset: 0; z-index: 100;
  display: flex; align-items: center; justify-content: center;
  background: rgba(11, 15, 29, 0.45);
  padding: var(--pad); opacity: 0; transition: opacity 0.2s ease;
}
.modal-overlay--open { opacity: 1; }
/* Solid, not --glass-bg — same reasoning as .cmdk-panel above: sitting at 72% opacity on top
   of its own dimmed backdrop still lets the dimmed page bleed through (Clark, 2026-08-17). */
.modal-panel {
  width: min(420px, 100%); background: var(--surface-solid);
  border: 1px solid var(--border-strong);
  border-radius: var(--radius); padding: var(--pad); box-shadow: var(--shadow-md);
}
.app-dialog-message { margin: 0 0 1.1rem; font-size: 0.92rem; line-height: 1.5; white-space: pre-line; }
.modal-actions { display: flex; justify-content: flex-end; gap: 0.5rem; }
.modal-actions .btn-primary { width: auto; }

/* --- Command palette (Cmd+K quick jump, Clark, 2026-08-17) ---------------------------------------- */
.cmdk-trigger {
  display: inline-flex; align-items: center; gap: 0.4rem;
  min-height: 32px; padding: 0 0.6rem; border: 1px solid var(--border); border-radius: var(--radius-sm);
  background: var(--surface); color: var(--text-muted); font: inherit; font-size: 0.78rem; cursor: var(--cur-hand);
  transition: background-color 0.12s ease, border-color 0.12s ease;
}
.cmdk-trigger:hover { background: var(--surface-sunken); border-color: var(--border-strong); }
.cmdk-trigger { --icon-size: 14px; }
.cmdk-trigger-key {
  /* Flex-centered AND clipped (Clark, 2026-08-23, two rounds — first "not centred", then "the
     command icon is clearly overlapping the bounds of the outline on its tops part", then "dont
     remove the commands icon keep it just resize it to fit"): ⌘ has no glyph in Consolas/
     Liberation Mono (this app's fallback stack on Windows, which has no SF Mono), so the
     browser draws it from some OTHER installed font than the plain "K" right next to it — and
     that fallback font's ⌘ glyph is taller than its own reported line box, so centering alone
     still let it poke out past the border on top. Flexbox centers the box; a fixed `height` +
     `overflow: hidden` guarantees whatever the real glyph ink does, it gets cropped AT the
     padding edge rather than drawn over the border — the one fix that holds regardless of which
     fallback font Windows/macOS/Linux each pick, without swapping the symbol for plain text. */
  display: inline-flex; align-items: center; justify-content: center; line-height: 1;
  height: 18px; overflow: hidden;
  /* No font-family at all now (Clark's monospace ban, 2026-09-02; this console was missed).
     Dropping it actually HELPS the ⌘ problem described above rather than hurting it: the mono
     stack was the reason the browser had to source ⌘ from a different font than the K beside
     it, since Consolas and Liberation Mono have no ⌘ glyph. The app's own face is far more
     likely to carry both, and the height + overflow clip stays as the guarantee either way. */
  font-size: 0.7rem; letter-spacing: 0.02em; color: var(--text-muted); border: 1px solid var(--border-strong); border-radius: 4px; padding: 0.05rem 0.3rem;
}
@media (max-width: 900px) { .cmdk-trigger-label { display: none; } }
@media (max-width: 700px) { .cmdk-trigger { display: none; } } /* the sidebar already covers nav on mobile */

.cmdk-overlay {
  position: fixed; inset: 0; z-index: 200;
  display: flex; align-items: flex-start; justify-content: center;
  background: rgba(11, 15, 29, 0.45);
  padding: 12vh var(--pad) var(--pad); opacity: 0; transition: opacity 0.15s ease;
}
.cmdk-overlay--open { opacity: 1; }
/* Solid, not --glass-bg (Clark, 2026-08-17 — full visual pass turned this up: a stray "1/2"
   from a tier count badge was bleeding through a row's own text). .cmdk-overlay's dark backdrop
   dims the page behind it, but the panel itself sitting at 72% opacity ON TOP of that dimmed
   page still let it show through — same root cause as .org-switch-popover/.account-popover,
   just discovered later since this one only shows over already-dimmed content. */
.cmdk-panel {
  width: min(560px, 100%); max-height: 70vh; display: flex; flex-direction: column;
  background: var(--surface-solid);
  border: 1px solid var(--border-strong);
  border-radius: var(--radius); box-shadow: var(--shadow-md); overflow: hidden;
}
.cmdk-search { display: flex; align-items: center; gap: 0.5rem; padding: 0.7rem var(--pad); border-bottom: 1px solid var(--border); flex-shrink: 0; --icon-size: 16px; color: var(--text-muted); }
.cmdk-search input { flex: 1 1 auto; border: none; background: transparent; font: inherit; font-size: 0.92rem; color: var(--text); min-width: 0; }
.cmdk-search input:focus { outline: none; }
.cmdk-esc {
  /* A keycap needs no mono face: the box and border are what say "this is a key". */
  font-size: 0.68rem; letter-spacing: 0.02em; color: var(--text-muted); border: 1px solid var(--border-strong); border-radius: 4px; padding: 0.1rem 0.35rem; flex-shrink: 0;
}
.cmdk-list { overflow-y: auto; padding: 0.4rem; }
.cmdk-row {
  display: flex; align-items: center; gap: 0.6rem; width: 100%;
  min-height: var(--tap-min); padding: 0.4rem 0.6rem; border: none; border-radius: var(--radius-sm);
  background: transparent; text-align: left; cursor: var(--cur-hand); font: inherit;
}
.cmdk-row--active, .cmdk-row:hover { background: var(--surface-sunken); }
.cmdk-row-icon {
  display: grid; place-items: center; flex-shrink: 0; width: 26px; height: 26px; border-radius: var(--radius-sm);
  background: var(--tile-action-bg); color: #fff;
}
.cmdk-row--active .cmdk-row-icon { background: var(--accent-soft); color: var(--accent); }
.cmdk-row-icon { --icon-size: 14px; }
.cmdk-row-text { display: flex; flex-direction: column; min-width: 0; }
.cmdk-row-label { font-size: 0.86rem; font-weight: 600; color: var(--text); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.cmdk-row-sub { font-size: 0.74rem; }
.cmdk-empty { padding: 1rem; text-align: center; font-size: 0.85rem; }

/* --- Toasts --------------------------------------------------------------------------------------- */
/* Below the topbar, right-aligned — clear of both the dev widget (bottom-right) and the
   sidebar (left), and it survives an SPA route change since it's appended to <body>, not
   the page container that route changes replace. */
.toast-host {
  position: fixed; top: calc(var(--pad) + 44px); right: var(--pad); z-index: 150;
  display: flex; flex-direction: column; gap: 0.5rem;
  pointer-events: none;
}
.toast {
  min-width: 180px; max-width: 340px;
  padding: 0.6rem 0.9rem; border-radius: var(--radius-sm);
  background: var(--nav-bg-deep); color: var(--nav-text);
  border: 1px solid var(--nav-glass-border);
  font-size: 0.85rem; line-height: 1.4; box-shadow: var(--shadow-md), 0 0 20px rgba(2, 132, 199, 0.15);
  opacity: 0; transform: translateY(-6px);
  transition: opacity 0.18s ease, transform 0.18s ease;
}
.toast--in { opacity: 1; transform: translateY(0); }
.toast--error { background: var(--st-expired); color: #fff; }

/* --- Copy-to-clipboard ---------------------------------------------------------------------------- */
.copy-btn {
  display: inline-flex; align-items: center; justify-content: center;
  width: 24px; height: 24px; margin-left: 0.3rem; padding: 0; vertical-align: -6px;
  border: none; background: transparent; border-radius: var(--radius-sm);
  color: var(--text-muted); cursor: var(--cur-hand);
  transition: background-color 0.12s ease, color 0.12s ease;
}
.copy-btn:hover { background: var(--surface-sunken); color: var(--text); }
.copy-btn { --icon-size: 14px; }
/* Compact next to inline text on a mouse, restored to the 44px tap minimum on touch. */
@media (pointer: coarse) { .copy-btn { width: var(--tap-min); height: var(--tap-min); } }

/* --- Dev widget ---------------------------------------------------------------------------------- */
.dev-widget { position: fixed; right: 16px; bottom: 16px; z-index: 200; }
.dev-widget-chip {
  min-height: var(--tap-min); padding: 0.5rem 0.9rem; border-radius: 999px;
  background: var(--nav-bg-deep); border: 1px solid var(--nav-border);
  color: var(--nav-text); font-size: 0.82rem; font-family: inherit; cursor: var(--cur-hand);
  box-shadow: var(--shadow-md);
}
.dev-widget-panel {
  position: absolute; right: 0; bottom: calc(100% + 8px); width: 220px;
  background: var(--surface); border: 1px solid var(--border);
  border-radius: var(--radius); padding: 0.7rem; box-shadow: var(--shadow-md);
}
.dev-widget-title { font-size: 0.7rem; color: var(--text-muted); margin-bottom: 0.45rem; text-transform: uppercase; letter-spacing: 0.05em; }
.dev-widget-btn {
  display: block; width: 100%; min-height: var(--tap-min); margin-bottom: 0.35rem;
  padding: 0.5rem 0.65rem; border-radius: var(--radius-sm);
  background: var(--surface-sunken); border: 1px solid var(--border);
  color: var(--text); font-size: 0.83rem; font-family: inherit; cursor: var(--cur-hand); text-align: left;
}
.dev-widget-btn--danger { color: var(--st-expired); border-color: rgba(208,59,59,0.35); }

/* --- Responsive ----------------------------------------------------------------------------------- */
/* Master-detail stacks and the rail forces icons-only at Clark's 700px line. */
@media (max-width: 700px) {
  /* Mobile forces icons-only permanently — a different, intentional pattern from the desktop
     never-scroll/scale-to-fit concern (scaleSidebarToFit() itself already skips below this same
     700px line), not the same thing. .sidebar's own width is forced regardless of its collapse
     mode (there's no mode picker on mobile at all — the account popover's own "Sidebar" section
     still opens, it just has no visible effect down here). .nav-brand-title/-version are NOT in
     this list — they live in .top-header now, not .sidebar, and stay visible everywhere per
     Clark's own "its just a part of the header now" rule, mobile included. */
  .sidebar { width: var(--nav-w-collapsed) !important; }
  .nav-item span, .nav-label { max-width: 0; opacity: 0; }

  /* .md3's own column/area stack for mobile lives in the master-detail section above (it needs
     to, for cascade ordering against the desktop rules there) — nothing to repeat here now that
     desktop ALSO auto-extends with no internal scroll (Clark, 2026-08-18); mobile and desktop
     share the same "whole page scrolls" behaviour today, so there's no separate override left
     to make. */
  .doc-split { grid-template-columns: 1fr !important; }
  .doc-split-list { flex-direction: row; overflow-x: auto; }
  .field-grid { grid-template-columns: 1fr; }
  .facts-strip .kv:not(:last-child) { margin-right: 1rem; padding-right: 1rem; }
  .scan-split { grid-template-columns: 1fr; }
  .doc-preview--multi { max-height: 55vh !important; }
  .scan-field { grid-template-columns: 1fr; }
  /* The underline tabs' gap is sized for desktop's wider detail pane — at phone width three
     or four tabs (Record / Documents (N) / How to renew / Portal) don't fit it and the strip
     scrolls sideways. Tighten the gap and type size so the full set fits without scrolling. */
  .seg-tabs { gap: 0.5rem; }
  .seg-tab { font-size: 0.76rem; }
}

/* --- Stage 2: Planning (Clark, 2026-08-18) ------------------------------------------------------
   BOQ / schedule / resources / subcontractors / plans all share this one small component set —
   a dense editable table, an inline add/edit row, small icon row-actions, and (schedule only) a
   CSS-only Gantt bar chart. Follows .bulk-table's proven shape rather than inventing a second
   dense-table system, but under a generic name since these lists have nothing to do with
   bulk import. */
.planning-toolbar { display: flex; align-items: center; justify-content: space-between; gap: var(--gap); flex-wrap: wrap; margin-bottom: 0.6rem; }
.planning-toolbar h2 { margin: 0; }
.planning-toolbar .btn-secondary { width: auto; flex-shrink: 0; }

.dense-table { width: 100%; border-collapse: collapse; font-size: 0.83rem; }
.dense-table th {
  text-align: left; font-size: 0.66rem; text-transform: uppercase; letter-spacing: 0.05em;
  color: var(--text-muted); font-weight: 700; padding: 0.4rem 0.5rem; border-bottom: 1px solid var(--border);
  white-space: nowrap;
}
.dense-table td { padding: 0.5rem; border-bottom: 1px solid var(--border); vertical-align: middle; }
.dense-table tbody tr:hover { background: var(--surface-sunken); }
.dense-table tfoot td { border-bottom: none; padding-top: 0.6rem; }
.dt-num { text-align: right; font-variant-numeric: tabular-nums; white-space: nowrap; }
.dt-group-row td { background: var(--surface-sunken); font-weight: 700; font-size: 0.78rem; padding: 0.4rem 0.5rem; }
.dt-total-row td { font-weight: 700; }
.dt-row--critical { background: var(--st-expired-soft); }
.dt-col-actions { width: 4.5rem; text-align: right; white-space: nowrap; }
.dt-empty td { text-align: center; color: var(--text-muted); padding: 1.2rem 0.5rem; }

/* Small icon buttons for row edit/delete — same visual family as .copy-btn. */
.dt-btn {
  display: inline-flex; align-items: center; justify-content: center;
  width: 26px; height: 26px; padding: 0; border: none; background: transparent;
  border-radius: var(--radius-sm); color: var(--text-muted); cursor: var(--cur-hand); --icon-size: 14px;
  transition: background-color 0.12s ease, color 0.12s ease;
}
.dt-btn:hover { background: var(--surface-sunken); color: var(--text); }
.dt-btn--danger:hover { background: var(--st-expired-soft); color: var(--st-expired); }
@media (pointer: coarse) { .dt-btn { width: var(--tap-min); height: var(--tap-min); } }

/* The inline add/edit row — a form laid across the table's own columns via colspan, so
   editing never opens a modal (per CLAUDE.md: data entry is inline, never a modal). */
.dt-form-row td { padding: 0.6rem 0.5rem; background: var(--accent-soft); }
/* align-items: start, not end (Clark, 2026-08-23, alignment-sweep round 2 — "didnt i say to
   double check the entirety of all ui for alignment issues with blocks"): end-alignment
   bottom-anchored every field to the row's TALLEST item, so a row mixing a hinted field
   (taller) with plain ones (shorter) put their INPUTS at visibly different heights — the same
   root cause as the DLP-form bug fixed earlier this session, just resurfacing wherever
   .dt-form-actions (no label of its own) shares a row with 3-4 real fields instead of 1-2.
   Reserving hint space (.field-hint's min-height, fixed earlier) only solved field-vs-field;
   it never touched field-vs-button, since the button has no label/hint structure at all.
   Top-alignment sidesteps that: every field's input now starts at the same Y regardless of
   what trails below it. */
.dt-form-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(140px, 1fr)); gap: 0.5rem 0.6rem; align-items: start; }
/* Own tinted backdrop (Clark, 2026-08-23 — "same alignment issues with blocks and texfield
   here", diagnosed as .btn-secondary's own "glass" background (--glass-surface-bg, near-white)
   reading as an invisible, unstyled button — the Save/Cancel row was never actually
   misaligned, it just had no visible button box to line up AGAINST). Every OTHER .dt-form-grid
   lives inside a <tr class="dt-form-row"><td> (own rule below already gives THAT its tinted
   background + padding), so the glass button always had a backdrop to sit on — except this one
   usage (closing.js's standalone Retention & Turnover date form), which isn't a table row at
   all and sits directly on the plain white .panel behind it. A modifier class, not added to
   .dt-form-grid itself, so the table-row usages don't double up their own <td> padding. */
.dt-form-grid--standalone { background: var(--accent-soft); padding: 0.6rem 0.5rem; border-radius: var(--radius-sm); }
.dt-form-grid .field { margin: 0; }
.dt-form-grid label { font-size: 0.68rem; }
.dt-form-actions { display: flex; gap: 0.4rem; margin-top: 0.5rem; }
/* Full-width row of its own, not squeezed into whatever auto-fit column happened to be left
   (Clark, 2026-08-23, same round — "the cancel button is getting clipped in content
   viewport"): Save/Delete/Cancel together were wider than a single ~140px grid column once a
   row form's Delete button was in the mix, and flex children don't wrap on their own, so
   Cancel just ran off the edge instead of dropping to a new line. Spanning the full grid width
   also settles the alignment question above for free — actions no longer share a row with any
   field at all, so there's nothing left to align them against. */
.dt-form-grid > .dt-form-actions { grid-column: 1 / -1; flex-wrap: wrap; margin-top: 0.3rem; }

/* Small pill filters (Resources tab's kind switch) — plain toggle buttons, not .seg-tabs
   (that's for whole-workspace navigation, this is a filter within one tab). */
.pill-filter { display: flex; gap: 0.4rem; flex-wrap: wrap; margin-bottom: 0.7rem; }
.pill-filter-btn {
  min-height: var(--tap-min); padding: 0.3rem 0.8rem; border-radius: 999px;
  border: 1px solid var(--border-strong); background: var(--surface); color: var(--text-muted);
  font: inherit; font-size: 0.8rem; font-weight: 600; cursor: var(--cur-hand);
  transition: background-color 0.12s ease, color 0.12s ease, border-color 0.12s ease;
}
.pill-filter-btn:hover { border-color: var(--accent); }
.pill-filter-btn--active { background: var(--accent); border-color: var(--accent); color: #fff; }

/* --- Gantt (CSS bars over a percent-of-duration track, no charting library) ---------------- */
.gantt { border: 1px solid var(--border); border-radius: var(--radius); overflow: hidden; }
.gantt-row {
  display: flex; align-items: center; border-bottom: 1px solid var(--border);
  cursor: var(--cur-hand); transition: background 0.12s ease;
}
.gantt-row:last-child { border-bottom: none; }
.gantt-row:nth-child(odd) { background: var(--surface-sunken); }
.gantt-row:hover, .gantt-row:focus-visible { background: var(--accent-soft); outline: none; }
.gantt-row--editing { background: var(--accent-soft); }
.gantt-label {
  flex: 0 0 clamp(9rem, 22%, 15rem); padding: 0.45rem 0.6rem; font-size: 0.78rem; font-weight: 600;
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap; border-right: 1px solid var(--border);
}
.gantt-row--critical .gantt-label { color: var(--st-expired); }
.gantt-track { flex: 1 1 auto; position: relative; height: 2.1rem; padding: 0 0.4rem; }
.gantt-bar {
  position: absolute; top: 0.4rem; height: 1.3rem; min-width: 3px; border-radius: 4px;
  background: var(--accent); box-shadow: var(--shadow-sm);
}
.gantt-bar--critical { background: var(--st-expired); }
.gantt-bar-label {
  position: absolute; left: 0.4rem; right: 0.4rem; top: 50%; transform: translateY(-50%);
  font-size: 0.68rem; font-weight: 700; color: #fff; white-space: nowrap; overflow: hidden;
  pointer-events: none;
}
.gantt-legend { display: flex; gap: 1rem; align-items: center; margin-top: 0.5rem; font-size: 0.76rem; color: var(--text-muted); flex-wrap: wrap; }
.gantt-legend-swatch { display: inline-block; width: 0.7rem; height: 0.7rem; border-radius: 3px; margin-right: 0.35rem; vertical-align: -1px; }
/* The ES/EF/LS/LF and O/M/P/TE acronym key (planning.js's renderNetwork()) — a second, quieter
   line under the main legend rather than crammed into the same row, since it's reference text
   read once, not a recurring status key like the swatches above. */
.gantt-legend--keys { margin-top: 0.15rem; font-size: 0.72rem; }

/* --- PERT/CPM network — freeform canvas (Clark, 2026-08-20; see renderNetwork()/
   drawPertEdges()/fitNetworkView() in planning.js) -----------------------------------------------
   `.pert-canvas` is the fixed-size VIEWPORT (fits the panel, never grows) with the white grid
   background baked on ITSELF rather than the inner layer — a `background-position` offset
   equal to the pan translate keeps the grid visually anchored to canvas-space, not diagram-
   space, which reads as "the grid is the desk, the diagram slides across it" rather than the
   grid stretching/tearing under zoom. `.pert-canvas-inner` carries the one CSS transform
   (`translate(tx,ty) scale(s)`, written inline by planning.js) that IS the pan/zoom — nodes and
   the SVG edge overlay are both children of it, so everything moves together for free. */
.pert-toolbar { display: flex; align-items: center; gap: 0.6rem; flex-wrap: wrap; margin-bottom: 0.6rem; }
.pert-zoom-group {
  display: flex; align-items: center; gap: 0.35rem;
  border: 1px solid var(--border-strong); border-radius: var(--radius-sm); padding: 0.15rem 0.3rem;
}
.pert-zoom-btn {
  display: grid; place-items: center; width: 1.6rem; height: 1.6rem; border-radius: 4px;
  border: none; background: transparent; color: var(--text-secondary); font-size: 1rem; font-weight: 700;
  cursor: var(--cur-hand); transition: background-color 0.12s ease;
}
.pert-zoom-btn:hover { background: var(--surface-sunken); }
.pert-zoom-pct { font-size: 0.74rem; color: var(--text-muted); width: 2.6em; text-align: center; font-variant-numeric: tabular-nums; }
.btn-connector--active { background: var(--accent-soft); border-color: var(--accent); color: var(--accent); }

.pert-canvas {
  position: relative; height: min(62vh, 560px); overflow: hidden;
  border: 1px solid var(--border); border-radius: var(--radius);
  background-color: #fff;
  background-image:
    linear-gradient(to right, var(--border) 1px, transparent 1px),
    linear-gradient(to bottom, var(--border) 1px, transparent 1px);
  background-size: 28px 28px;
  cursor: grab;
  /* Every drag gesture on this canvas (pan, node move, resize, drag-to-connect) is a plain
     mousedown+move — with nothing to stop it, the browser reads that as a text-selection drag
     across whatever node labels/numbers it crosses, which paints as a blue highlight flash
     sweeping the canvas on every drag (Clark, 2026-08-20: "the screen flashes every time").
     user-select:none on the whole canvas (inherited by every node) stops the browser from ever
     starting that selection in the first place — the fix belongs here, not as a preventDefault
     in the drag handlers, since selection can start on mousedown before any handler runs. */
  user-select: none; -webkit-user-select: none;
}
.pert-canvas--panning { cursor: grabbing; }
/* Active-resize state (Clark, 2026-08-20 — refine pass): forces the resize cursor across the
   whole canvas (not just the 14px grip) for the duration of a resize drag. Without this, a fast
   drag routinely outruns the grip's own hover box — the grip's opacity:0 default then hides it
   mid-drag and the cursor snaps back to "grab", which reads as the resize having silently
   stopped even though startResizeDrag()'s pointermove listener is still very much live. Same
   "--panning"-style toggle, paired open/close around the drag in planning.js. */
.pert-canvas--resizing, .pert-canvas--resizing * { cursor: nwse-resize !important; }
.pert-canvas-inner { position: absolute; top: 0; left: 0; transform-origin: 0 0; }
.pert-edges { position: absolute; top: 0; left: 0; z-index: 0; pointer-events: none; overflow: visible; }

/* Minimap (Clark, 2026-08-20 — "add a little/mini viewport on the bottom left showing where it
   is currently zoomed in overall", then "the user can also use the mini viewport's box to
   navigate... it also shows up while the user is changing zooms [and] if [no] action is done
   it simply just hides away") — a sibling of .pert-canvas-inner, not a child of it, so it sits
   fixed in the viewport's own corner instead of panning/zooming away with the diagram. A peek
   overlay, not a permanent fixture: starts `hidden`, fades in via the `.pert-minimap--visible`
   class the moment any zoom/pan interaction happens (peekMinimap() in planning.js), and fades
   back out ~1.5s after the last one. Interactive (click/drag to jump the main viewport there),
   so — unlike a purely decorative overlay — this one needs real pointer-events. */
.pert-minimap {
  position: absolute; z-index: 4; left: 10px; bottom: 10px;
  background: rgba(255, 255, 255, 0.92); border: 1px solid var(--border-strong);
  border-radius: var(--radius-sm); box-shadow: var(--shadow-md);
  padding: 2px; cursor: var(--cur-hand); backdrop-filter: blur(2px);
  opacity: 0; transition: opacity 0.18s ease;
  touch-action: none; /* it owns its own pointer drag, not the browser's scroll gesture */
}
.pert-minimap--visible { opacity: 1; }
.pert-minimap svg { display: block; }
.pert-minimap-node { fill: var(--border-strong); }
.pert-minimap-viewport { fill: var(--accent-soft); stroke: var(--accent); stroke-width: 1.5; cursor: grab; }
/* Cursor feedback while the box is actually being dragged (Clark, 2026-08-20 — refine pass) —
   without this the cursor stays "grab" for the whole gesture, which reads as if the drag never
   registered. Toggled on the wrapper in the pointerdown/up handlers, same "--panning"-style
   active-state class as `.pert-canvas--panning` uses for the main canvas. */
.pert-minimap--dragging, .pert-minimap--dragging .pert-minimap-viewport { cursor: grabbing; }

/* Edit/connector/block-properties forms as an IN-CANVAS popover (Clark, 2026-08-20 — "dont
   make it a seperate dropdown below outside just make it popout/dropwdown inside the
   viewport") — a right-side drawer inside `.pert-canvas` itself (clipped by its own
   `overflow:hidden`), not a block pushed below the whole canvas+legend the way Gantt/List's
   forms still are. Solid, not `--glass-surface-bg` — it floats over the canvas's own dynamic
   grid/node content, same "floating surfaces must be solid" rule as every other overlay in
   this app (.account-popover, .cmdk-panel, …). The nested `.panel` these three form functions
   already return gets its own chrome stripped here so it doesn't double up with the
   popover's. */
.pert-popover {
  position: absolute; z-index: 6; top: 10px; right: 10px; bottom: 10px;
  width: min(360px, calc(100% - 20px));
  background: var(--surface-solid); border: 1px solid var(--border-strong); border-radius: var(--radius);
  box-shadow: var(--shadow-elevated); overflow-y: auto; padding: calc(var(--pad) * 0.6);
  animation: pert-popover-in 0.16s ease both;
}
@keyframes pert-popover-in { from { opacity: 0; transform: translateX(10px); } to { opacity: 1; transform: translateX(0); } }
.pert-popover .panel { background: transparent; border: none; box-shadow: none; padding: 0; margin: 0; }

.pert-node {
  position: absolute; z-index: 1; width: 190px; height: 118px; overflow: hidden;
  background: var(--surface); border: 1.5px solid var(--border-strong); border-radius: var(--radius-sm);
  padding: 0.55rem 0.7rem; cursor: grab; text-align: left;
  box-shadow: var(--shadow-sm);
  transition: border-color 0.12s ease, box-shadow 0.12s ease, background-color 0.12s ease;
  touch-action: none; /* this element owns its own pointer drag, not the browser's scroll/zoom gesture */
}
.pert-node:hover { border-color: var(--accent); box-shadow: var(--shadow-md); }
/* Active-drag state (Clark, 2026-08-20 — refine pass): `.pert-canvas` already switches to
   "grabbing" while panning, but a block being repositioned stayed "grab" for the whole drag —
   an inconsistency once you notice the canvas does it right next door. Toggled once real
   movement is detected (the same >4px threshold that already distinguishes a drag from a
   click), not on pointerdown, so a plain click never flashes it. */
.pert-node--dragging { cursor: grabbing; border-color: var(--accent); box-shadow: var(--shadow-md); }
.pert-node--critical { border-color: var(--st-expired); }
.pert-node--critical:hover { border-color: var(--st-expired); }
.pert-node--editing { background: var(--accent-soft); }
.pert-node--pickable { cursor: var(--cur-hand); }
.pert-node--pickable:hover { border-color: var(--nav-accent); box-shadow: 0 0 0 2px var(--st-due-soft); }
.pert-node--picked { border-color: var(--nav-accent); box-shadow: 0 0 0 2px var(--st-due-soft); }
.pert-node-pick-hint { display: block; margin-top: 0.3rem; font-size: 0.66rem; font-weight: 700; color: var(--nav-accent); text-transform: uppercase; letter-spacing: 0.03em; }
.pert-node-head { display: flex; align-items: center; justify-content: space-between; gap: 0.4rem; margin-bottom: 0.35rem; }
.pert-node-name { font-size: 0.82rem; font-weight: 700; color: var(--text); }
.pert-node-grid {
  display: grid; grid-template-columns: 1fr 1fr; gap: 0.15rem 0.5rem;
  font-size: 0.7rem; color: var(--text-muted); font-variant-numeric: tabular-nums;
}
.pert-node-grid--pert { grid-template-columns: 1fr 1fr 1fr; }
.pert-node-grid b { color: var(--text-secondary); font-weight: 700; }
.pert-node-foot { margin-top: 0.35rem; padding-top: 0.3rem; border-top: 1px solid var(--border); font-size: 0.7rem; color: var(--text-muted); }
.pert-node--critical .pert-node-foot { color: var(--st-expired); }

.pert-edge { fill: none; stroke: var(--border-strong); stroke-width: 1.5; }
.pert-edge--critical { stroke: var(--st-expired); stroke-width: 2; }
/* A wide, fully transparent path drawn under the visible connector line — the visible one is
   only ~2px, an unreasonably thin target on a pannable/zoomable canvas, so this widens the
   actual clickable area without widening what's seen. */
.pert-connector-hit { fill: none; stroke: transparent; stroke-width: 14; pointer-events: stroke; cursor: var(--cur-hand); }
.pert-connector-line { fill: none; stroke-width: 2; pointer-events: none; }
.pert-connector-label { font-size: 11px; font-weight: 700; pointer-events: none; }
.pert-connector:hover .pert-connector-line { stroke-width: 3; }
/* The live line while dragging from an edge handle (startConnectorDrag() in planning.js) —
   dashed so it reads as "not committed yet" versus a real connector's solid line. */
.pert-edge-preview { fill: none; stroke: var(--accent); stroke-width: 2; stroke-dasharray: 5 4; pointer-events: none; }

/* Edge/connection handles (Clark, 2026-08-20 — "on block edges whether it be the top bottom or
   sides to add a connector") — one small dot centred on each side, hidden until the block is
   hovered so four extra dots don't clutter every node at rest. Each is a bit larger than it
   looks (14px) so it's a comfortable drag target even at a zoomed-out scale. */
/* The hit area is bigger than the visible dot (Clark, 2026-08-23: "the half circle for adding
   connectors is too small and the user most of the time drags the block"): same "widen the
   invisible target, keep the visible mark small" trick .pert-connector-hit below uses for
   connector LINES — a real 14px dot was an unreasonably precise target to land a drag on right
   at a node's edge, so most attempts missed and dragged the block instead. The 28px box is the
   click/touch target; ::after draws the actual dot, unchanged size, centered inside it. */
.pert-node-handle {
  position: absolute; z-index: 2; width: 28px; height: 28px;
  display: grid; place-items: center;
  opacity: 0; transition: opacity 0.12s ease;
  cursor: crosshair; touch-action: none;
}
.pert-node-handle::after {
  content: ''; width: 14px; height: 14px; border-radius: 50%;
  background: var(--accent); border: 2px solid #fff; box-shadow: var(--shadow-sm);
  transition: transform 0.12s ease;
}
.pert-node:hover .pert-node-handle, .pert-node-handle:hover { opacity: 1; }
.pert-node-handle:hover::after { transform: scale(1.25); }
.pert-node-handle--top    { top: -14px;  left: 50%; margin-left: -14px; }
.pert-node-handle--bottom { bottom: -14px; left: 50%; margin-left: -14px; }
.pert-node-handle--left   { left: -14px; top: 50%; margin-top: -14px; }
.pert-node-handle--right  { right: -14px; top: 50%; margin-top: -14px; }
/* A node currently hovered as a valid drop target while dragging a connector from another
   block's handle — same accent-glow language as .pert-node--pickable/--picked use for the
   click-two-nodes flow, so both ways of starting a connector read as the same affordance. */
.pert-node--drop-target { border-color: var(--accent); box-shadow: 0 0 0 3px var(--accent-soft); }

/* Free resize (Clark, 2026-08-20 — "allow the user to drag also resize the block to their
   desired size too"): one grip at the bottom-right corner, same hidden-until-hovered treatment
   as the connection handles so it doesn't clutter a node at rest. Checked before the node-body
   drag in the shared pointerdown handler, so grabbing the grip never also starts a reposition. */
/* Resize from any edge or corner, not just the one bottom-right grip (Clark, 2026-08-23:
   "the resize window resize logic as app windows in WindowsOS" — a Windows/Mac window lets you
   drag any of its 4 edges for a single-axis resize, or a corner for both axes at once, with
   only a cursor change to mark it — no permanent handle drawn on every edge). Corners keep a
   fixed 14px hit box; edges stretch the full side MINUS the two corners' own width so nothing
   overlaps and every pixel of the border is grabbable. Only the SE corner keeps the small
   visible bracket mark (below) — same discoverability affordance this already had — the other
   7 stay invisible-but-functional, exactly like a real OS window border. */
.pert-node-resize { position: absolute; z-index: 3; opacity: 0; transition: opacity 0.12s ease; touch-action: none; }
.pert-node:hover .pert-node-resize, .pert-node-resize:hover { opacity: 1; }
.pert-node-resize--n  { top: -4px; left: 14px; right: 14px; height: 8px; cursor: ns-resize; }
.pert-node-resize--s  { bottom: -4px; left: 14px; right: 14px; height: 8px; cursor: ns-resize; }
.pert-node-resize--e  { right: -4px; top: 14px; bottom: 14px; width: 8px; cursor: ew-resize; }
.pert-node-resize--w  { left: -4px; top: 14px; bottom: 14px; width: 8px; cursor: ew-resize; }
.pert-node-resize--ne { top: -4px; right: -4px; width: 14px; height: 14px; cursor: nesw-resize; }
.pert-node-resize--nw { top: -4px; left: -4px; width: 14px; height: 14px; cursor: nwse-resize; }
.pert-node-resize--sw { bottom: -4px; left: -4px; width: 14px; height: 14px; cursor: nesw-resize; }
.pert-node-resize--se { bottom: -4px; right: -4px; width: 14px; height: 14px; cursor: nwse-resize; }
.pert-node-resize--se::after {
  content: ''; position: absolute; right: 3px; bottom: 3px; width: 8px; height: 8px;
  border-right: 2px solid var(--accent); border-bottom: 2px solid var(--accent); border-radius: 0 0 2px 0;
}

.node-props-connectors { list-style: none; margin: 0.4rem 0 0; padding: 0; display: flex; flex-direction: column; gap: 0.3rem; }
.node-props-connectors li { display: flex; align-items: center; gap: 0.5rem; font-size: 0.8rem; padding: 0.3rem 0.1rem; }
.node-props-connectors li > span:nth-child(2) { flex: 1 1 auto; min-width: 0; }
.connector-swatch-dot--sm { width: 12px; height: 12px; flex-shrink: 0; }
.connector-swatch-dot--auto {
  background: repeating-conic-gradient(var(--border-strong) 0% 25%, var(--surface) 0% 50%) 0 0 / 10px 10px;
}

/* Connector colour picker — a radio group of colour dots, same "real input kept for a11y,
   visually replaced" technique as .cb-input/.cb-box's custom checkbox elsewhere in this file. */
.connector-swatches { display: flex; gap: 0.5rem; margin-top: 0.3rem; }
.connector-swatch { position: relative; cursor: var(--cur-hand); }
.connector-swatch input { position: absolute; width: 1px; height: 1px; margin: -1px; opacity: 0; }
.connector-swatch-dot {
  display: block; width: 24px; height: 24px; border-radius: 50%;
  border: 2px solid transparent; box-shadow: inset 0 0 0 1px rgba(11, 15, 29, 0.12);
  transition: border-color 0.12s ease, transform 0.12s ease;
}
.connector-swatch input:checked ~ .connector-swatch-dot { border-color: var(--text); transform: scale(1.12); }
.connector-swatch input:focus-visible ~ .connector-swatch-dot { box-shadow: 0 0 0 3px var(--accent-soft); }

/* --- Row status spine + progress cell (Clark, 2026-08-19, Planning/Execution "Command Board"
   redesign) — a dense-table row's own status gets an inset accent bar, same technique .tile's
   --danger/--warn/--ok variants already use (inset 3px 0 0 <color>), applied to the row's
   first cell since box-shadow on <tr> itself isn't reliably painted cross-browser. Distinct
   from .dt-row--critical above (that one flags "on the PERT-CPM critical path", a schedule-
   network fact; this is the row's own workflow status) — a row can carry both at once. */
.dt-spine-ok td:first-child { box-shadow: inset 3px 0 0 var(--st-valid); }
.dt-spine-warn td:first-child { box-shadow: inset 3px 0 0 var(--nav-accent); }
.dt-spine-danger td:first-child { box-shadow: inset 3px 0 0 var(--st-expired); }
.dt-spine-neutral td:first-child { box-shadow: inset 3px 0 0 var(--border-strong); }
.dt-progress { display: flex; align-items: center; gap: 0.5rem; }
.dt-progress-track { flex: 1 1 auto; height: 6px; background: var(--surface-sunken); border-radius: 999px; overflow: hidden; min-width: 48px; }
.dt-progress-fill { height: 100%; border-radius: 999px; background: var(--accent); }
.dt-progress-fill--danger { background: var(--st-expired); }
.dt-progress-pct { font-size: 0.72rem; font-weight: 700; color: var(--text-secondary); width: 2.2em; text-align: right; flex-shrink: 0; font-variant-numeric: tabular-nums; }
/* The row a standalone edit panel (below the table, not embedded in it — see
   activityFormPanel() in execution.js) currently belongs to — a plain highlight, same idea as
   .dt-row:hover but persistent while the panel is open. */
.dt-row--editing { background: var(--accent-soft); }
.dt-btn--active { background: var(--accent-soft); color: var(--accent); }

/* --- S-curve (Clark, Stage 4 Monitoring, 2026-08-21) — a hand-drawn line chart, not a
   library, per /dataviz's "line vs. baseline" form: Planned is chart chrome (dashed, muted —
   it needs no series-strength colour of its own since it's a reference, not a competing
   identity), Actual is the one real accent line. Text never wears the series colour (marks-
   and-anatomy.md) — end labels and axis text stay in text tokens throughout; identity comes
   from the legend swatch + the dash pattern, not from colouring the numbers. */
.scurve { margin: 0.6rem 0 1rem; }
.scurve-svg { width: 100%; height: auto; display: block; }
.scurve-grid { stroke: var(--border); stroke-width: 1; }
.scurve-axis-label { font-size: 0.62rem; fill: var(--text-muted); }
.scurve-line { fill: none; stroke-width: 2; stroke-linecap: round; stroke-linejoin: round; }
.scurve-line--planned { stroke: var(--text-muted); stroke-dasharray: 6 4; }
.scurve-line--actual { stroke: var(--accent); }
.scurve-marker { fill: var(--accent); stroke: #fff; stroke-width: 2; }
.scurve-marker--live { fill: var(--accent-hover); opacity: 0.85; }
.scurve-end-label { font-size: 0.74rem; font-weight: 700; fill: var(--text-secondary); }
/* Legend swatches were built at the line's own 2px stroke-weight (Clark, 2026-08-21: "too thin
   and small... can barely be made out") — fine inside the chart where a hairline reads as
   "the line", but at legend scale that's an almost-invisible sliver, worse on the dashed one.
   4px + a longer, higher-contrast dash pattern (dedicated to the swatch, not reused from
   .scurve-line--planned) plus a bigger label make this a legend you can actually read at a
   glance instead of one that only makes sense once you already know what it says. */
.scurve-legend { display: flex; flex-wrap: wrap; gap: 1.25rem; margin-top: 0.6rem; font-size: 0.86rem; font-weight: 600; color: var(--text-secondary); }
.scurve-legend-item { display: inline-flex; align-items: center; gap: 0.55rem; }
.scurve-legend-swatch { width: 26px; height: 4px; border-radius: 2px; display: inline-block; flex-shrink: 0; }
.scurve-legend-swatch--planned { background-color: transparent; background-image: repeating-linear-gradient(90deg, var(--text-muted) 0 8px, transparent 8px 13px); }
.scurve-legend-swatch--actual { background: var(--accent); }

/* --- Kanban board (Clark, 2026-08-19) — CLAUDE.md's own standing rule, only just applied:
   "small fixed-status workflows (4-6 stages) use a Kanban board, drag to advance." Shared by
   Planning's Subcontractors (contracts) and Execution's Variation Orders/QC Checklist/Labor
   Compliance tabs via renderKanbanBoard() in shared.js — one drag implementation, not four.
   Cards get .tile's elevation treatment (they're card-like surfaces, same reasoning that
   already scopes --shadow-elevated to .tile/.project-card and nowhere else). Native HTML5
   drag-and-drop — desktop only; every card is also a normal click target into its full edit
   form (with a status field), so touch users always have a way to move one. */
.kanban { display: grid; grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); gap: 0.7rem; align-items: start; }
.kanban-col-head { display: flex; align-items: center; justify-content: space-between; padding: 0 0.15rem 0.5rem; }
.kanban-col-head > div { display: flex; align-items: center; min-width: 0; }
.kanban-col-head strong { font-size: 0.72rem; font-weight: 800; text-transform: uppercase; letter-spacing: 0.04em; color: var(--text-secondary); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.kanban-col-head .count { font-size: 0.68rem; font-weight: 700; color: var(--text-muted); background: var(--surface-sunken); padding: 0.05rem 0.42rem; border-radius: 999px; flex-shrink: 0; margin-left: 0.4rem; }
.kanban-col-head .dot { width: 7px; height: 7px; border-radius: 50%; display: inline-block; margin-right: 0.45rem; flex-shrink: 0; }
.kanban-col-head .dot--ok { background: var(--st-valid); }
.kanban-col-head .dot--warn { background: var(--nav-accent); }
.kanban-col-head .dot--danger { background: var(--st-expired); }
.kanban-col-head .dot--neutral { background: var(--st-neutral); }
.kanban-col-body {
  background: var(--surface-sunken); border-radius: var(--radius); padding: 0.5rem;
  min-height: 64px; display: flex; flex-direction: column; gap: 0.5rem;
  transition: background-color 0.12s ease;
}
.kanban-col-body.drag-over { background: var(--accent-soft); }
.kb-card {
  background: var(--surface); border: 1px solid var(--glass-surface-border); border-radius: var(--radius-sm);
  padding: 0.6rem 0.7rem; box-shadow: var(--shadow-elevated); cursor: grab;
}
.kb-card:active { cursor: grabbing; }
.kb-card.dragging { opacity: 0.4; }
.kb-card .kb-ref { font-variant-numeric: tabular-nums; letter-spacing: 0.01em; font-size: 0.66rem; color: var(--text-muted); }
.kb-card .kb-title { font-size: 0.82rem; font-weight: 700; margin: 0.15rem 0 0.5rem; line-height: 1.32; }
.kb-card .kb-meta { display: flex; gap: 0.6rem; flex-wrap: wrap; align-items: center; }
.kb-card .kb-delta { font-size: 0.72rem; font-weight: 800; font-variant-numeric: tabular-nums; }
.kb-card .kb-delta.pos { color: var(--st-valid); }
.kb-card .kb-delta.neg { color: var(--st-expired); }
.kb-add-btn {
  width: 100%; text-align: left; background: transparent; border: 1px dashed var(--border-strong);
  border-radius: var(--radius-sm); padding: 0.5rem 0.6rem; color: var(--text-muted);
  font: inherit; font-size: 0.78rem; font-weight: 700; cursor: var(--cur-hand);
  display: flex; align-items: center; gap: 0.35rem; min-height: var(--tap-min);
  transition: border-color 0.12s ease, color 0.12s ease;
}
.kb-add-btn:hover { border-color: var(--accent); color: var(--accent); }

@media (max-width: 700px) {
  .dense-table { font-size: 0.78rem; }
  .gantt-label { flex-basis: 6.5rem; }
  .kanban { grid-template-columns: 1fr; }
}

/* --- Print (Clark, 2026-08-17) -----------------------------------------------------------------
   Printing the document vault's stored certificate, not the whole app shell — see the
   data-action="print-doc" handler in shared.js. Classic single-element print technique:
   visibility (not display) so the target keeps its already-computed layout, then pulled to the
   page origin since a hidden-but-visibility'd sidebar/topbar would otherwise still hold their
   layout space. @page { size: auto } always, per CLAUDE.md — never a fixed size like A4. */
@media print {
  @page { size: auto; }
  body.print-doc-active * { visibility: hidden; }
  body.print-doc-active [data-print-target],
  body.print-doc-active [data-print-target] * { visibility: visible; }
  body.print-doc-active [data-print-target] {
    position: absolute; inset: 0 auto auto 0; width: 100%; margin: 0; padding: 0; border: none; box-shadow: none;
  }
  body.print-doc-active .no-print { display: none !important; }
}

/* ============================================================================
   Ops console additions (Clark, 2026-08-23) — everything above this line is a
   straight copy of web/css/style.css, kept in step by hand (see ops/README.md).
   Below is new, ops-only: a dense admin table and a couple of small helpers the
   customer app never needed.
   ============================================================================ */
/* Header height parity (Clark, 2026-08-23 — "the header is completely different in size...
   cause content to be squished"): CEDA's .top-header has no explicit height of its own —
   it's the ambient result of the org-switcher chip's padding + 18px avatar sitting in
   .topbar-left (see .topbar-switch-btn above). Ops has nothing to switch between (one admin
   console, no orgs), so .topbar-left/.topbar-right stay empty and the row naturally collapses
   to ~27px instead of CEDA's own ~44px — which pushes the whole page up and reads as
   "squished" next to the real app. Rather than fake a switcher chip just to hold the row open,
   pin the height directly to CEDA's own measured resting value. Remeasure and update this
   if CEDA's header height ever changes (getBoundingClientRect() on #top-header there). */
.top-header { min-height: 43.75px; }

/* Reserved, UNSCALED bottom breathing room for the account row (Clark, 2026-08-23 — "the
   profile button and its highlight is too close to the bottom now without proper padding like
   the original fix"): ops's own scaleSidebarToFit() (app.js) — ported from CEDA's, but
   CEDA's own sidebar rarely needs to actually compress since it has far fewer nav items —
   shrinks #sidebar-inner (content AND its own padding-bottom) to fit exactly, so at a real
   scale-down the account row's edge lands flush against .sidebar's own bottom edge: the padding
   scaled down along with everything else instead of staying put. This padding lives on
   .sidebar itself, the UNSCALED container, so scaleSidebarToFit() measures a shorter
   availableHeight and fits content inside it — a genuine reserved gap that survives compression
   at any nav length, not one more scaled pixel indistinguishable from zero. */
.sidebar { padding-bottom: 8px; }

/* flex-shrink: 0 (Clark, 2026-08-23 — "content on the bottom is getting cut out"): .content is
   a flex column with overflow-y:auto — the intended scroll container — but a plain block child
   defaults to flex-shrink:1, so once Users' grouped list (or any other long .admin-table)
   outgrew the visible viewport, flexbox compressed the box down to fit instead of letting it
   grow past the fold, and this element's own overflow:hidden (for the rounded-corner border,
   not meant to hide real rows) clipped whatever didn't fit — .content's own scrollbar never
   even saw the excess, since the child reported a squeezed height, not its true one. */
.admin-table { background: var(--surface); border: 1px solid var(--border); border-radius: var(--radius); box-shadow: var(--shadow-elevated); overflow: hidden; flex-shrink: 0; }
.admin-table-head, .admin-table-row {
  display: grid; grid-template-columns: 1.8fr 0.9fr 1fr 0.8fr 0.5fr; gap: var(--gap);
  align-items: center; padding: 0.6rem var(--pad); border-bottom: 1px solid var(--border);
}
.admin-table-row:last-child { border-bottom: none; }
.admin-table-head { font-size: var(--fs-xs); font-weight: 700; text-transform: uppercase; letter-spacing: 0.03em; color: var(--text-muted); }
.admin-org-name { font-weight: 700; }
.admin-org-sub { font-size: var(--fs-xs); color: var(--text-muted); margin-top: 1px; }
.admin-usage-bar { height: 5px; border-radius: 3px; background: var(--border); overflow: hidden; margin-top: 3px; }
.admin-usage-fill { height: 100%; background: var(--accent); border-radius: 3px; }
.admin-feedback-row { display: flex; gap: 0.6rem; padding: 0.6rem 0; border-bottom: 1px solid var(--border); align-items: flex-start; }
.admin-feedback-row:last-child { border-bottom: none; }

/* System Health (Clark, 2026-08-23 — "i want it to be infographic card and graphs", "copy
   other professional software and platform management uis"): a real status-page pattern
   (Cloudflare/Vercel/Stripe-style) — a proportional ring built from a single CSS conic-gradient
   (no chart library needed for one summary shape) plus a card grid with a colour-matched icon
   chip and a latency gauge bar per service, instead of a plain data table. */
.health-summary { display: flex; align-items: center; gap: 1.4rem; margin-bottom: var(--gap); }
.health-ring {
  width: 96px; height: 96px; border-radius: 50%; flex-shrink: 0;
  display: grid; place-items: center;
}
.health-ring-center {
  width: 68px; height: 68px; border-radius: 50%; background: var(--surface-solid);
  display: flex; flex-direction: column; align-items: center; justify-content: center;
  box-shadow: var(--shadow-elevated);
}
.health-ring-count { font-size: 1.05rem; font-weight: 700; color: var(--text); font-variant-numeric: tabular-nums; }
.health-ring-label { font-size: 0.66rem; text-transform: uppercase; letter-spacing: 0.05em; color: var(--text-muted); }
.health-overall-label { font-size: 1.15rem; font-weight: 700; }
.health-cards { display: grid; grid-template-columns: repeat(auto-fill, minmax(220px, 1fr)); gap: var(--gap); }
.health-card {
  background: var(--surface); border: 1px solid var(--border); border-radius: var(--radius);
  box-shadow: var(--shadow-elevated); padding: 1rem; display: flex; flex-direction: column; gap: 0.6rem;
  transition: transform 0.12s ease, box-shadow 0.12s ease;
}
.health-card:hover { transform: translateY(-1px); box-shadow: var(--shadow-md); }
.health-card-top { display: flex; align-items: center; justify-content: space-between; }
.health-card-icon {
  width: 34px; height: 34px; border-radius: 50%; display: grid; place-items: center; flex-shrink: 0;
}
.health-card-icon-svg { width: 17px; height: 17px; color: #fff; }
.health-card-name { font-size: 0.92rem; font-weight: 600; color: var(--text); }
.health-gauge { height: 6px; border-radius: 4px; background: var(--surface-sunken); overflow: hidden; }
.health-gauge-fill { height: 100%; border-radius: 4px; transition: width 0.3s ease; }
.health-card-latency { font-size: 0.76rem; color: var(--text-muted); font-variant-numeric: tabular-nums; }
@media (max-width: 700px) { .health-summary { flex-direction: column; align-items: flex-start; } }


/* --- The disguise (see ops/js/knock.js) ----------------------------------------------------
   Styled to look like nothing: a default-ish serif-free 404 on a plain white ground, with none
   of this stylesheet's own tokens, no brand navy and no dark mode. It has to be believable as a
   host that simply has nothing at this path, and every design token used here would be a tell
   that something built is sitting behind it. The colours are literal for the same reason. */
.ops-404 {
  position: fixed; inset: 0; z-index: 50;
  display: flex; flex-direction: column; align-items: center; justify-content: center;
  gap: 0.5rem; background: #fff; color: #000;
  font-family: system-ui, -apple-system, 'Segoe UI', Roboto, sans-serif;
}
.ops-404[hidden] { display: none; }
.ops-404 h1 { margin: 0; font-size: 1.5rem; font-weight: 500; letter-spacing: -0.01em; }
.ops-404 p { margin: 0; font-size: 0.875rem; color: #555; }


/* --- THE CURSOR (Clark, 2026-09-05: "i want our custom cursor to be the one we had in our
   Marketing Video") -------------------------------------------------------------------------
   The exact arrow the film uses, lifted from marketing/ceda-saas-video/beat-quad.html: the same
   path, the same navy fill and the same white 1.5 stroke with round joins. Nothing redrawn, so
   the pointer in a demo video and the pointer on the real product are the same object.

   SVG rather than PNG because it is the only form that stays crisp on a 2x display -- a cursor
   image is not re-rasterised from a @2x file the way a background-image is. Two things the SVG
   must carry for it to render at all: explicit width/height (a viewBox alone is not enough), and
   URL-encoding rather than base64, so `#` becomes %23. The trailing `5 3` is the hotspot, sitting
   exactly on the arrow's tip at 5,2.6 -- get that wrong and every click lands slightly off where
   the user aimed. A native keyword closes each list, so any browser that declines SVG cursors
   falls back to the normal one rather than to nothing.

   Data URIs and not files, deliberately: web/_headers ships `img-src 'self' data:`, so this is
   allowed in production, and inlining means no extra request, nothing to add to sw.js's
   STATIC_ASSETS, and no cursor that goes missing offline.

   TWO STATES, ONE SILHOUETTE. Interactive elements get the same arrow inverted -- white fill,
   navy stroke -- rather than a different shape. It reads on any background either way round
   (whichever half disappears, the other carries it), and it keeps the hover affordance the
   native hand provides. That affordance is why `cursor: pointer` is not simply left alone
   throughout: every one of them now points at --cur-hand, so nothing silently reverts to the
   system hand halfway through the page. `cursor: default` points at --cur-arrow for the same
   reason -- it means "not clickable", not "use the OS arrow".

   Left NATIVE on purpose: text (an I-beam is a real affordance and nobody thanks you for
   restyling it), not-allowed, grab/grabbing, and the resize cursors. Those say something about
   what is about to happen, and a branded lookalike would only make them harder to read.

   Keep this block identical in web/css/style.css and ops/css/style.css -- ops copies web's
   stylesheet by hand (see ops/README.md). */
:root {
  --cur-arrow: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='24' height='24' viewBox='0 0 24 24'%3E%3Cpath d='M5 2.6 19.4 11.6 12.6 12.6 9.4 19z' fill='%230E1730' stroke='%23fff' stroke-width='1.5' stroke-linejoin='round'/%3E%3C/svg%3E") 5 3, auto;
  --cur-hand: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='24' height='24' viewBox='0 0 24 24'%3E%3Cpath d='M5 2.6 19.4 11.6 12.6 12.6 9.4 19z' fill='%23fff' stroke='%230E1730' stroke-width='1.5' stroke-linejoin='round'/%3E%3C/svg%3E") 5 3, pointer;
}
html { cursor: var(--cur-arrow); }
/* The disguise must NOT be branded. A "404 Not Found" page flying a custom navy-and-white CEDA
   cursor answers the question the whole thing exists to leave unanswered -- see ops/js/knock.js.
   Native cursor until the console is actually unlocked. */
.ops-404, .ops-404 * { cursor: auto; }

/* --- Vendors: the third-party dependency register (Clark, 2026-09-06) -----------------------
   ops-only, so it lives at the end rather than in the block ops copies from web/css/style.css.

   Colour never carries meaning alone here, same rule as everywhere else in CEDA: the dot and
   the card's left edge repeat what the badge already says in words, and the criticality chip is
   text. A card read with no colour perception loses nothing.

   Rows are hidden by the filter with the `hidden` attribute, which works because of the global
   `[hidden] { display: none !important; }` near the top of this file -- without it, .vendor-row's
   own `display: flex` would out-rank the UA stylesheet and a "hidden" row would stay visible.

   REDESIGNED to dense rows (Clark, 2026-09-06: "so i can see more things at a glance and reduce
   my need to scroll down"). The card grid it replaced cost about three screens for fourteen
   entries. Nothing was removed to shorten it: every field the card carried now lives in the row's
   expansion, and the row surfaces the three things that make someone open this page at all --
   health, cost, and how close the next charge is. */

/* --nav-accent is the RAIL's gold (#e8c84a) and is one value that does not swap by theme. On the
   white content plane it measures about 1.6:1 -- the exact trap CLAUDE.md calls out: legible on
   navy, nearly invisible on white. So status amber gets its own theme-aware token rather than
   borrowing the rail's. #8a6d00 is the warn value already validated against the white content
   surface; the rail gold is kept for dark, where it is correct. Declared here in the ops-only
   block, so none of the token blocks ops copies by hand from web/css/style.css has to change. */
:root { --vendor-warn: #8a6d00; }
html[data-theme="dark"], html[data-theme="classic-dark"] { --vendor-warn: #e8c84a; }

.vendor-summary {
  display: flex; flex-wrap: wrap; align-items: center; justify-content: space-between;
  gap: var(--pad);
}
.vendor-summary-head { display: flex; align-items: flex-start; gap: 0.7rem; min-width: 0; }
.vendor-overall { font-size: 1.02rem; font-weight: 650; letter-spacing: -0.01em; }
.vendor-overall-sub { font-size: 0.78rem; margin-top: 0.2rem; max-width: 62ch; }

.vendor-dot {
  width: 10px; height: 10px; border-radius: 50%; flex: none; margin-top: 0.34rem;
}
.vendor-dot--lg { width: 14px; height: 14px; margin-top: 0.3rem; }

.vendor-counts { display: flex; gap: 1.4rem; flex-wrap: wrap; }
.vendor-count { display: flex; flex-direction: column; align-items: flex-start; }
.vendor-count-val { font-size: 1.35rem; font-weight: 680; line-height: 1.1; letter-spacing: -0.02em; }
.vendor-count-label { font-size: 0.68rem; text-transform: uppercase; letter-spacing: 0.06em; color: var(--text-muted); }
/* Three ordinal tiers, each with a shape as well as a hue: red and amber cannot clear CVD
   separation on their own at any step, which is why the dot is drawn filled for danger and
   hollow for warn, and why every tile keeps its written label. */
.vendor-count--danger .vendor-count-val { color: var(--st-expired); }
.vendor-count--warn .vendor-count-val { color: var(--vendor-warn); }
.vendor-count--ok .vendor-count-val { color: var(--st-valid); }
.vendor-count-dot {
  display: inline-block; width: 6px; height: 6px; border-radius: 50%;
  margin-right: 0.3rem; vertical-align: 0.05em;
}
.vendor-count--danger .vendor-count-dot { background: var(--st-expired); }
.vendor-count--danger .vendor-count-label { color: var(--st-expired); }
.vendor-count--warn .vendor-count-dot { background: transparent; box-shadow: inset 0 0 0 2px var(--vendor-warn); }

/* --- filter row --- */
.vendor-filter { display: flex; align-items: center; gap: var(--pad); flex-wrap: wrap; margin: var(--pad) 0; }
.vendor-search {
  flex: 1 1 260px; min-width: 0;
  min-height: var(--tap-min);
  padding: 0.55rem 0.7rem;
  background: var(--surface);
  border: 1px solid var(--border-strong);
  border-radius: var(--radius-sm);
  color: var(--text);
  font: inherit; font-size: 0.86rem;
}
.vendor-search::placeholder { color: var(--text-muted); }
.vendor-toggle {
  display: inline-flex; align-items: center; gap: 0.45rem;
  min-height: var(--tap-min);
  font-size: 0.82rem; color: var(--text-secondary);
  cursor: var(--cur-hand); user-select: none;
}
.vendor-toggle input { width: 15px; height: 15px; cursor: var(--cur-hand); accent-color: var(--accent); }

/* --- groups --- */
.vendor-group + .vendor-group { margin-top: calc(var(--pad) * 1.6); }
.vendor-group-head { margin-bottom: 0.6rem; }
.vendor-group-title { font-size: 0.74rem; text-transform: uppercase; letter-spacing: 0.08em; color: var(--text-muted); margin: 0; }
.vendor-group-blurb { font-size: 0.79rem; color: var(--text-muted); margin: 0.15rem 0 0; }

/* --- page head + the two summary panels ---------------------------------------------------- */
.vendor-page-head { display: flex; align-items: flex-start; justify-content: space-between; gap: var(--pad); flex-wrap: wrap; }
.vendor-page-head .page-sub { margin-bottom: 0; }

/* Status on the left, money on the right, so the two questions this page answers sit side by side
   instead of one below the other. 1.15fr/1fr rather than equal halves: the status panel carries
   five counts and a two-line sentence, the money panel carries a number and four small tiles. */
.vsum-cols { display: grid; grid-template-columns: 1.15fr 1fr; gap: var(--gap); align-items: stretch; }
.vsum-cols > .panel { margin-bottom: 0; }

.vsum-money { display: flex; flex-direction: column; gap: 0.5rem; }
.vsum-money-head { display: flex; align-items: flex-start; justify-content: space-between; gap: 0.5rem; }
.vsum-money-val { font-size: 1.35rem; font-weight: 680; line-height: 1.15; letter-spacing: -0.02em; font-variant-numeric: tabular-nums; }
.vsum-money-link {
  display: inline-flex; align-items: center; gap: 0.2rem;
  font-size: 0.76rem; font-weight: 600; text-decoration: none; color: var(--accent);
  min-height: var(--tap-desktop-zoomed); cursor: var(--cur-hand);
}
.vsum-money-link:hover { text-decoration: underline; }
.vsum-money-sub { display: flex; flex-wrap: wrap; gap: 0.3rem; margin: 0; }
.vsum-chip {
  font-size: 0.7rem; padding: 0.12rem 0.45rem; border-radius: 999px;
  background: var(--surface-sunken); border: 1px solid var(--border); color: var(--text-secondary);
}
.vsum-chip--warn { color: var(--vendor-warn); border-color: var(--vendor-warn); }
.vsum-none { font-size: 0.76rem; margin: 0; }
.vsum-ledger { font-size: 0.72rem; line-height: 1.45; color: var(--text-muted); margin: auto 0 0; }
.vsum-fx { margin: 0; }
/* The converted half of a pair. Quieter than the native figure on purpose: one of the two is what
   appears on the statement and the other is a convenience, and they should not read as equals.
   The separator is supplied HERE rather than baked into the string, so the same markup can read
   inline in a wide panel and stacked on two lines inside a narrow list column. */
/* The PHP half of every ops figure. Quieter than the AUD one on purpose: AUD is the headline
   this console leads with and PHP is the conversion beside it, and they must not read as equals.
   Separator supplied here so the same markup can sit inline or stack. */
.ops-fx { color: var(--text-muted); font-weight: 500; font-size: 0.86em; }
.ops-fx::before { content: '\00b7'; margin: 0 0.32rem; color: var(--border-strong); }

.vr-fx { color: var(--text-muted); font-weight: 500; }
.vr-fx::before { content: '·'; margin: 0 0.3rem; color: var(--border-strong); }
/* Where a figure came from. Quiet by design: it qualifies the number beside it rather than
   competing with the status badge, which is the only thing on a row allowed to shout. */
.vr-sync {
  display: inline-block; margin-left: 0.35rem; padding: 0.02rem 0.3rem;
  font-size: 0.6rem; font-weight: 650; text-transform: uppercase; letter-spacing: 0.05em;
  border-radius: 999px; border: 1px solid currentColor; vertical-align: 0.08em;
}
.vr-sync--ok { color: var(--st-valid); }
.vr-sync--warn { color: var(--vendor-warn); }
.vr-sync--muted { color: var(--text-muted); }

/* The renewal strip. Four at most: a fifth would push the panel taller than its neighbour, and
   the whole point of the redesign is that this fits without scrolling. */
.vsum-renewals { display: grid; grid-template-columns: repeat(auto-fit, minmax(120px, 1fr)); gap: 0.4rem; }
.vsum-renewal {
  display: flex; flex-direction: column; gap: 0.22rem;
  padding: 0.4rem 0.5rem; text-align: left;
  background: var(--surface-sunken); border: 1px solid var(--border); border-radius: var(--radius-sm);
  color: inherit; font: inherit; cursor: var(--cur-hand);
  transition: border-color 0.15s ease, background 0.15s ease;
}
.vsum-renewal:hover { border-color: var(--border-strong); background: var(--surface-solid); }
.vsum-renewal-top { display: flex; align-items: baseline; justify-content: space-between; gap: 0.3rem; }
.vsum-renewal-name { font-size: 0.74rem; font-weight: 600; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.vsum-renewal-days { font-size: 0.74rem; font-weight: 700; font-variant-numeric: tabular-nums; flex: none; }
.vsum-renewal-days--soon { color: var(--vendor-warn); }
.vsum-renewal-days--over { color: var(--st-expired); }
.vsum-renewal-sub { font-size: 0.66rem; color: var(--text-muted); }

/* The cycle meter. Shape as well as colour: it fills left to right as the cycle runs out, so the
   "how close are we" reading survives with no colour perception at all -- the same rule the
   status badges follow by always carrying their text label. */
.vr-meter { display: block; height: 3px; border-radius: 999px; background: var(--border); overflow: hidden; }
.vr-meter-fill { display: block; height: 100%; background: var(--accent); border-radius: 999px; }
.vr-meter-fill--soon { background: var(--vendor-warn); }

.vendor-migrate {
  font-size: 0.79rem; line-height: 1.5; color: var(--text-secondary);
  border-left: 3px solid var(--vendor-warn);
}

/* --- filter chips -------------------------------------------------------------------------- */
/* --tap-desktop-zoomed throughout this section, never a raw px floor: .content carries
   zoom: 0.9, so a 32px rule renders 28.8px and quietly sits under the desktop floor. Measured on
   the rendered page, which is the only place this class of bug is visible at all. */
.vendor-chips { display: flex; flex-wrap: wrap; gap: 0.3rem; }
.vendor-chip {
  min-height: var(--tap-desktop-zoomed); padding: 0.28rem 0.6rem;
  font: inherit; font-size: 0.75rem; font-weight: 550;
  color: var(--text-secondary); background: var(--surface); cursor: var(--cur-hand);
  border: 1px solid var(--border); border-radius: 999px;
  transition: background 0.15s ease, border-color 0.15s ease, color 0.15s ease;
}
.vendor-chip:hover { border-color: var(--border-strong); color: var(--text); }
.vendor-chip.is-active { background: var(--accent); border-color: var(--accent); color: #fff; }
.vendor-chip--ghost { margin-left: auto; }

/* --- vendor marks --------------------------------------------------------------------------
   The glyph itself is inlined by js/lib/vendor-logos.js (a CDN logo is silently blocked by this
   console's own CSP in production). The COLOUR is handed over as --mark / --mark-dark and picked
   here rather than in JS, so flipping the theme recolours every mark immediately instead of
   leaving GitHub's near-black logo invisible on the dark rail until the next render. */
.vendor-logo {
  flex: none; display: inline-flex; align-items: center; justify-content: center;
  border-radius: var(--radius-sm); overflow: hidden;
}
.vendor-logo--mark { color: var(--mark, var(--text)); }
html[data-theme="dark"] .vendor-logo--mark,
html[data-theme="classic-dark"] .vendor-logo--mark { color: var(--mark-dark, var(--mark, var(--text))); }
.vendor-logo svg { width: 100%; height: 100%; display: block; }

/* The fallback, for a vendor with no permissively published mark and for PhilGEPS, where CEDA's
   standing rule forbids agency logo artwork. Neutral rather than a guessed brand colour: an
   invented hex is the same class of mistake as an invented renewal date. */
.vendor-logo--monogram {
  background: var(--surface-sunken); border: 1px solid var(--border); color: var(--text-muted);
  font-weight: 700; letter-spacing: 0.02em;
}
/* A raster mark cannot be recoloured, and inverting somebody's logo to suit our dark rail would
   be altering their mark. So it sits on a white tile instead, which is how the same icon appears
   in a browser tab anyway. Runway's is near-black and would otherwise vanish entirely. */
.vendor-logo--raster { background: #fff; border: 1px solid var(--border); }
.vendor-logo--raster img { width: 100%; height: 100%; object-fit: contain; display: block; padding: 2px; box-sizing: border-box; }

.vendor-logo--sm { width: 17px; height: 17px; font-size: 0.55rem; }
.vendor-logo--lg { width: 30px; height: 30px; font-size: 0.72rem; }
/* The 17px tile leaves a raster mark almost nothing after its padding, so it loses the inset. */
.vendor-logo--sm.vendor-logo--raster img { padding: 1px; }

/* --- list / cards switch -------------------------------------------------------------------- */
.vendor-viewswitch {
  display: inline-flex; gap: 2px; padding: 2px;
  background: var(--surface-sunken); border: 1px solid var(--border); border-radius: 999px;
}
.vendor-view-btn {
  display: inline-flex; align-items: center; gap: 0.3rem;
  min-height: calc(var(--tap-desktop-zoomed) - 4px); padding: 0.2rem 0.65rem;
  font: inherit; font-size: 0.74rem; font-weight: 600;
  color: var(--text-muted); background: none; border: none; border-radius: 999px;
  cursor: var(--cur-hand);
  transition: background 0.15s ease, color 0.15s ease;
}
.vendor-view-btn:hover { color: var(--text); }
.vendor-view-btn.is-active { background: var(--surface-solid); color: var(--text); box-shadow: var(--shadow-sm); }
.vendor-view-icon { --icon-size: 13px; }

/* --- cards ----------------------------------------------------------------------------------
   A card is a COLLAPSED summary, not an opened one. Built the other way round first, and fourteen
   cards ran to about seven screens, which is the problem the whole redesign was meant to remove.
   It now shows what a row shows, stacked rather than columned, and expands in place on click
   through exactly the same handler.

   auto-FILL, not auto-fit: auto-fit collapses the empty tracks, so a group holding a single card
   (Data and AI, once filtered) would stretch that card the full width of the page and make it read
   as more important than its neighbours. min(...) keeps the floor from overflowing a narrow
   window, which a bare 260px would. */
.vendor-grid {
  display: grid; gap: var(--gap);
  grid-template-columns: repeat(auto-fill, minmax(min(288px, 100%), 1fr));
  align-items: start;
}
/* START-aligned, and levelled by reserving two lines for the NAME instead (see .vc-name below).
   Stretching the row looked identical while every card was collapsed and then went wrong the
   moment one was opened: the expanded card dragged both its neighbours to its own height and
   left them mostly empty. Reserving the space makes collapsed cards uniform anyway, and an
   expansion then grows only the card that was clicked. */
.vendor-card {
  display: flex; flex-direction: column;
  background: var(--surface);
  border: 1px solid var(--border);
  border-left: 3px solid var(--st-neutral);
  border-radius: var(--radius);
  box-shadow: var(--shadow-sm);
  overflow: hidden;
}
.vendor-card--operational, .vendor-card--configured { border-left-color: var(--st-valid); }
.vendor-card--degraded, .vendor-card--not_configured { border-left-color: var(--vendor-warn); }
.vendor-card--down { border-left-color: var(--st-expired); }
.vendor-card.is-open { background: var(--surface-sunken); }

/* The whole summary is the toggle, so the target is the card rather than a caret. */
.vendor-card .vr-head { flex: 1 1 auto; display: flex; }
.vc-main {
  display: flex; flex-direction: column; gap: 0.38rem;
  flex: 1 1 auto; width: 100%; padding: 0.6rem 0.7rem 0.55rem;
  background: none; border: none; color: inherit; font: inherit; text-align: left;
  cursor: var(--cur-hand);
  transition: background 0.15s ease;
}
.vc-main:hover { background: var(--surface-solid); }
.vendor-card.is-open .vc-main:hover { background: transparent; }

/* The portal link is absolutely positioned over this row's right edge (it cannot live inside the
   toggle button: a nested <a> in a <button> is invalid markup and would not open). Reserve its
   width here, or it lands on top of the status badge, which is exactly what it did. */
.vc-top { display: flex; align-items: center; gap: 0.45rem; min-width: 0; padding-right: 1.5rem; }
.vc-title { flex: 1 1 auto; min-width: 0; display: flex; flex-direction: column; gap: 0.05rem; }
.vc-name {
  font-size: 0.82rem; font-weight: 650; letter-spacing: -0.005em; line-height: 1.25;
  display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical; overflow: hidden;
  overflow-wrap: anywhere;
  /* Exactly two lines, whether the name needs them or not. This is what keeps a row of collapsed
     cards level without stretching them to each other. */
  min-height: 2.5em;
}
.vc-tier { font-size: 0.6rem; text-transform: uppercase; letter-spacing: 0.07em; font-weight: 650; }
.vc-tier.vendor-crit--critical { color: var(--text); font-weight: 700; }
.vc-tier.vendor-crit--important { color: var(--text-secondary); }
.vc-tier.vendor-crit--situational { color: var(--text-muted); font-weight: 500; }
.vc-top .vendor-dot { margin-top: 0; }
.vc-top .badge { flex: none; font-size: 0.63rem; padding: 0.08rem 0.38rem; }

/* Two lines at most. "Affects" is the one field the expansion states in full anyway, so clamping
   it here costs nothing and is what keeps every card the same height. */
.vc-affects {
  font-size: 0.72rem; line-height: 1.35; color: var(--text-secondary);
  display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical; overflow: hidden;
  min-height: 1.95em;
}

.vc-money {
  display: flex; align-items: center; flex-wrap: wrap; gap: 0.2rem 0.5rem;
  font-size: 0.74rem; font-variant-numeric: tabular-nums;
}
.vc-money-cost { font-weight: 650; }
.vc-money-next { font-weight: 650; }
.vc-money .vr-meter { flex: 1 1 40px; min-width: 34px; }

.vc-foot {
  display: flex; align-items: flex-end; justify-content: space-between; gap: 0.4rem;
  border-top: 1px solid var(--border); padding-top: 0.35rem; margin-top: auto;
}
/* The check cell is a column in a row and a single line on a card: there is no column here for it
   to align to, and stacked it would add height to every card for nothing. */
.vc-check { display: flex; align-items: baseline; gap: 0.3rem; min-width: 0; }
.vc-check .vr-check-kind, .vc-check .vr-check-sub { white-space: nowrap; }
.vc-foot .vr-caret { color: var(--text-muted); }
.vendor-card.is-open .vr-caret-icon { transform: rotate(180deg); }

/* Anchored to the card's top-right, not centred on it: centring against .vr-head would put the
   globe halfway down an expanded card, which is the bug the row version already hit. */
.vendor-card .vr-jump--card { top: 0.5rem; right: 0.4rem; transform: none; }

.vendor-card .vr-panel-inner { padding: 0.2rem 0.7rem 0.7rem; border-top-style: solid; }
/* One column inside a card. The row's expansion has the full page width for its two meta columns;
   a ~268px card does not, and measured on the rendered page the second column broke "A Supabase
   outage is a total outage" to one word per line. */
.vendor-card .vr-panel-cols { grid-template-columns: 1fr; gap: var(--gap); }
.vendor-card .vendor-meta-row { grid-template-columns: 4.6rem 1fr; }

/* The dependency register, sitting under the health cards on the merged System Health page. A
   rule and a gap rather than a panel: the two halves are one page about one subject, and boxing
   the lower half would put them back in the two containers the merge just removed. */
.vendor-register-host {
  margin-top: calc(var(--pad) * 1.5);
  padding-top: calc(var(--pad) * 1.2);
  border-top: 1px solid var(--border);
}
.vendor-section-title { font-size: 1.15rem; }

/* --- provider bundles ------------------------------------------------------------------------
   Entries that are ONE account at the same company, drawn as one block so five rows stop reading
   as five unrelated dependencies. A tinted inset and a tag, not a heavier border: the block is a
   grouping, and giving it a stronger edge than the status colours already on each row would put
   the emphasis on the wrong thing. */
.vendor-cluster {
  position: relative;
  background: color-mix(in srgb, var(--accent) 5%, transparent);
  box-shadow: inset 2px 0 0 color-mix(in srgb, var(--accent) 32%, transparent);
}
.vendor-cluster-tag {
  display: block; padding: 0.28rem 0.7rem 0.1rem 1.05rem;
  font-size: 0.6rem; text-transform: uppercase; letter-spacing: 0.07em;
  color: var(--text-muted);
}
/* The rows inside a bundle lose their dividers: the block is the unit now, and an internal rule
   would cut it back into the separate things it is not. */
.vendor-cluster > .vendor-row { border-top: none; }
.vendor-cluster + .vendor-row, .vendor-row + .vendor-cluster { border-top: 1px solid var(--border); }

/* In the card grid the bundle is its own grid item spanning as many columns as it holds, with an
   inner grid of the same track size. `min()` keeps it from demanding more columns than the grid
   has at a narrow width, which would push it out of the container. */
.vendor-cluster--cards {
  grid-column: span min(var(--cluster-span), 3);
  border-radius: var(--radius);
  padding: 0 0.35rem 0.35rem;
}
.vendor-cluster-cards {
  display: grid; gap: var(--gap);
  grid-template-columns: repeat(var(--cluster-span), minmax(0, 1fr));
  align-items: start;
}
@media (max-width: 900px) {
  .vendor-cluster--cards { grid-column: span 1; }
  .vendor-cluster-cards { grid-template-columns: 1fr; }
}

/* --- the list ------------------------------------------------------------------------------ */
/* One shared grid template on the head and every row, so the columns line up without a table and
   without fixed pixel widths. minmax(0, …) on the flexible tracks is load-bearing: a grid track's
   default `min-width: auto` refuses to shrink below its content, and one long "affects" sentence
   would otherwise widen the whole list past the page and bring back the horizontal scroll this
   layout exists to avoid. */
.vendor-list {
  --vr-cols: minmax(0, 1.5fr) 7.5rem 5.2rem minmax(0, 1.7fr) 8.5rem 7.5rem 6.5rem 1.6rem;
  border: 1px solid var(--border); border-radius: var(--radius);
  background: var(--surface); overflow: hidden;
}
.vendor-list-head, .vr-main {
  display: grid; grid-template-columns: var(--vr-cols);
  gap: 0.6rem; align-items: center;
}
.vendor-list-head {
  padding: 0.35rem 0.7rem;
  background: var(--surface-sunken); border-bottom: 1px solid var(--border);
  font-size: 0.63rem; text-transform: uppercase; letter-spacing: 0.07em; color: var(--text-muted);
}

.vendor-row { position: relative; border-top: 1px solid var(--border); }
.vendor-row:first-of-type { border-top: none; }
.vendor-row.is-open { background: var(--surface-sunken); }

/* The whole row is the toggle, so the click target is the full width rather than a caret. */
.vr-main {
  width: 100%; padding: 0.42rem 2.9rem 0.42rem 0.7rem;
  background: none; border: none; color: inherit; font: inherit; text-align: left;
  cursor: var(--cur-hand); min-height: 38px;
  transition: background 0.15s ease;
}
.vr-main:hover { background: var(--surface-solid); }
.vendor-row.is-open .vr-main:hover { background: transparent; }

/* Colour never carries meaning alone: the left edge repeats what the badge already says in words. */
.vendor-row::before {
  content: ''; position: absolute; left: 0; top: 0; bottom: 0; width: 3px;
  background: var(--st-neutral);
}
.vendor-row--operational::before, .vendor-row--configured::before { background: var(--st-valid); }
.vendor-row--degraded::before, .vendor-row--not_configured::before { background: var(--vendor-warn); }
.vendor-row--down::before { background: var(--st-expired); }

.vendor-row--flash { animation: vendor-flash 1.2s ease; }
@keyframes vendor-flash {
  0%, 60% { background: color-mix(in srgb, var(--accent) 14%, transparent); }
  100% { background: transparent; }
}

.vr-cell { min-width: 0; display: block; }
.vr-name { display: flex; align-items: center; gap: 0.45rem; }
.vr-name-text {
  font-size: 0.82rem; font-weight: 600; letter-spacing: -0.005em;
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
.vendor-list-head .vr-name { padding-left: 1.05rem; } /* clears the dot column on the rows below */
.vr-main .vendor-dot { margin-top: 0; }

.vr-status .badge { font-size: 0.66rem; padding: 0.1rem 0.42rem; }
.vr-crit { font-size: 0.63rem; text-transform: uppercase; letter-spacing: 0.06em; font-weight: 650; }
.vendor-list-head .vr-crit, .vendor-list-head .vr-bill, .vendor-list-head .vr-renew, .vendor-list-head .vr-check { color: var(--text-muted); font-weight: 400; letter-spacing: 0.07em; }

.vr-affects {
  font-size: 0.74rem; color: var(--text-secondary);
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}

/* Two lines, like NEXT CHARGE and CHECKED beside it. A dual-currency figure plus a sync chip does
   not fit one dense line, and widening the column would cost the "affects" text instead. */
.vr-bill {
  display: flex; flex-direction: column; align-items: flex-start; gap: 0.05rem;
  font-size: 0.75rem; font-variant-numeric: tabular-nums; line-height: 1.25; min-width: 0;
}
.vr-bill .vr-amt, .vr-bill .ops-amt { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; max-width: 100%; }
.vr-bill .vr-fx, .vr-bill .ops-fx { font-size: 0.66rem; }
.vr-bill .vr-fx::before, .vr-bill .ops-fx::before { content: none; }
.vr-bill .vr-sync { margin-left: 0; margin-top: 0.08rem; }
.vr-bill--paid { font-weight: 650; color: var(--text); }
.vr-bill--ok { color: var(--text-secondary); }
.vr-bill--muted { color: var(--text-muted); }
.vr-bill--warn { color: var(--vendor-warn); }

.vr-renew { display: flex; flex-direction: column; gap: 0.1rem; line-height: 1.25; }
.vr-renew-days { font-size: 0.75rem; font-weight: 650; font-variant-numeric: tabular-nums; }
.vr-renew-days--soon { color: var(--vendor-warn); }
.vr-renew-days--over { color: var(--st-expired); }
.vr-renew-date { font-size: 0.64rem; color: var(--text-muted); }
.vr-dim { font-size: 0.72rem; color: var(--text-muted); }

.vr-check { display: flex; flex-direction: column; gap: 0.1rem; line-height: 1.25; }
.vr-check-kind { font-size: 0.72rem; color: var(--text-secondary); }
.vr-check-kind--ok { color: var(--st-valid); }
.vr-check-kind--todo { color: var(--vendor-warn); font-weight: 600; }
.vr-check-sub { font-size: 0.64rem; color: var(--text-muted); font-variant-numeric: tabular-nums; }

.vr-caret { display: flex; justify-content: flex-end; color: var(--text-muted); }
.vr-caret-icon { --icon-size: 14px; transition: transform 0.2s ease; }
.vendor-row.is-open .vr-caret-icon { transform: rotate(180deg); }

/* The primary portal link, absolutely positioned so it sits INSIDE the row's padding without
   being inside the toggle button -- a nested <a> in a <button> is invalid and would not open.
   Its containing block is .vr-head and NOT .vendor-row: measured on the rendered page, anchoring
   it to the row centred it against the row PLUS its open panel, so opening a vendor slid the
   globe down into the middle of the expanded detail. */
.vr-head { position: relative; }
.vr-jump {
  position: absolute; right: 0.45rem; top: 50%; transform: translateY(-50%);
  display: inline-flex; align-items: center; justify-content: center;
  width: var(--tap-desktop-zoomed); height: var(--tap-desktop-zoomed); border-radius: 999px;
  color: var(--text-muted); text-decoration: none; cursor: var(--cur-hand);
  transition: background 0.15s ease, color 0.15s ease;
}
.vr-jump:hover { background: var(--surface-solid); color: var(--accent); }
.vr-jump svg { --icon-size: 14px; }

/* --- shared detail styling, used inside the expansion --------------------------------------- */
.vendor-purpose { font-size: 0.82rem; line-height: 1.5; color: var(--text-secondary); margin: 0; }

.vendor-meta { display: flex; flex-direction: column; gap: 0.28rem; }
.vendor-meta-row { display: grid; grid-template-columns: 4.6rem 1fr; gap: 0.5rem; align-items: baseline; }
.vendor-meta-key {
  font-size: 0.66rem; text-transform: uppercase; letter-spacing: 0.06em;
  color: var(--text-muted); padding-top: 0.1rem;
}
.vendor-meta-val { font-size: 0.79rem; line-height: 1.45; color: var(--text); min-width: 0; overflow-wrap: anywhere; }
.vendor-unknown { color: var(--text-muted); font-style: italic; }
.vendor-secret { display: block; font-size: 0.76rem; color: var(--text-secondary); }
.vendor-secret b { font-weight: 650; color: var(--text); letter-spacing: 0.01em; }
.vendor-secret + .vendor-secret { margin-top: 0.15rem; }

.vendor-check {
  background: var(--surface-sunken);
  border-radius: var(--radius-sm);
  padding: 0.5rem 0.6rem;
}
.vendor-check-key {
  display: block;
  font-size: 0.64rem; text-transform: uppercase; letter-spacing: 0.07em;
  color: var(--text-muted); margin-bottom: 0.2rem;
}
.vendor-check-val { font-size: 0.76rem; line-height: 1.45; color: var(--text-secondary); margin: 0; }
.vendor-check-detail { font-size: 0.76rem; line-height: 1.45; color: var(--text); margin: 0.25rem 0 0; overflow-wrap: anywhere; }
.vendor-latency { color: var(--text-muted); }

/* --text-secondary, not --text-muted: the note carries the caveat that actually matters (do not
   overwrite the MX records, a 405 means healthy), so it is content, not a label. Measured at
   4.00:1 on the light surface with the muted token, under AA for its size. */
.vendor-note {
  font-size: 0.76rem; line-height: 1.5; color: var(--text-secondary);
  margin: 0; padding-left: 0.6rem;
  border-left: 2px solid var(--border-strong);
}

.vendor-links { display: flex; flex-wrap: wrap; align-items: center; gap: 0.35rem; }
.vendor-link {
  display: inline-flex; align-items: center; gap: 0.3rem;
  min-height: var(--tap-desktop-zoomed); padding: 0.25rem 0.55rem;
  font-size: 0.76rem; font-weight: 550; text-decoration: none;
  color: var(--text); background: var(--surface-sunken);
  border: 1px solid var(--border); border-radius: 999px;
  cursor: var(--cur-hand);
  transition: background 0.15s ease, border-color 0.15s ease;
}
.vendor-link:hover { background: var(--surface-solid); border-color: var(--border-strong); }
.vendor-link-icon { --icon-size: 13px; color: var(--text-muted); }

/* Tier is ranked by TONE, not hue. Red and amber are spoken for on this page: they mean
   something is wrong right now. A permanent red "CRITICAL" on five healthy rows would compete
   with the one badge that actually needs attention and teach the eye to skip both. So the tier
   column ranks by weight and lightness, and the colour vocabulary stays with status. */
.vr-crit.vendor-crit--critical { color: var(--text); font-weight: 700; }
.vr-crit.vendor-crit--important { color: var(--text-secondary); }
.vr-crit.vendor-crit--situational { color: var(--text-muted); font-weight: 500; }

/* ...and it takes a HUE only when that entry is actually broken, so the colour means "this one,
   now" rather than "this one is important in general". Three classes, which is what lets it win
   over the two-class tone rules above without !important. Situational is deliberately absent:
   there is no urgent version of it. */
.vr-crit.vendor-crit--alert.vendor-crit--critical,
.vc-tier.vendor-crit--alert.vendor-crit--critical { color: var(--st-expired); font-weight: 700; }
.vr-crit.vendor-crit--alert.vendor-crit--important,
.vc-tier.vendor-crit--alert.vendor-crit--important { color: var(--vendor-warn); font-weight: 700; }

/* --- the expansion ------------------------------------------------------------------------- */
.vr-panel { overflow: hidden; transition: max-height 0.28s ease, opacity 0.22s ease; }
.vr-panel-inner {
  display: flex; flex-direction: column; gap: 0.55rem;
  padding: 0.2rem 0.9rem 0.85rem 1.05rem;
  border-top: 1px dashed var(--border);
}
.vr-panel-cols { display: grid; grid-template-columns: 1fr 1fr; gap: var(--gap) 1.4rem; }
.vr-panel-inner .vendor-meta-row { grid-template-columns: 5.4rem 1fr; }
.vr-verify-line { font-size: 0.74rem; line-height: 1.45; color: var(--text-secondary); margin: 0.35rem 0 0; }
.vr-verify-line--stale { color: var(--vendor-warn); }

/* --- the tracking form --------------------------------------------------------------------- */
.vr-record { border-top: 1px solid var(--border); padding-top: 0.5rem; }
.vr-record-summary {
  font-size: 0.76rem; font-weight: 600; color: var(--accent);
  cursor: var(--cur-hand); min-height: var(--tap-desktop-zoomed); display: flex; align-items: center;
  list-style: none;
}
.vr-record-summary::-webkit-details-marker { display: none; }
.vr-record-summary::before { content: '+ '; margin-right: 0.15rem; }
.vr-record[open] .vr-record-summary::before { content: '\2212 '; }
.vr-record-body { padding-top: 0.4rem; }
.vr-record-hint { font-size: 0.73rem; line-height: 1.5; color: var(--text-muted); margin: 0 0 0.5rem; }
.vr-record-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(150px, 1fr)); gap: 0 var(--gap); }
.vr-record-actions { margin-top: 0.5rem; }

.vendor-empty { color: var(--text-muted); font-size: 0.85rem; padding: var(--pad) 0; }

/* --- Costs: the vendor half of the ledger (Clark, 2026-09-06) -------------------------------
   Three panels rather than .dash-cols' two, because "By vendor" earns a place beside the trend
   and the category split: it is the column the Vendors register reconciles against. */
.cost-cols { display: grid; grid-template-columns: 1.5fr 1fr 1fr; gap: calc(var(--pad) * 0.5); align-items: stretch; }
@media (max-width: 1100px) { .cost-cols { grid-template-columns: 1fr 1fr; } }
@media (max-width: 760px) { .cost-cols { grid-template-columns: 1fr; } }

/* Horizontal bars, not a pie: these are ranked magnitudes with long labels, and a bar's length
   is read accurately where a wedge's angle is not. Direct labels on every row, so the figure
   never depends on matching a colour to a legend. */
.cost-vendor-bars { display: flex; flex-direction: column; gap: 0.3rem; }
.cost-vendor-bars + .cost-vendor-bars { margin-top: 0.6rem; }
.cost-vendor-cur { font-size: 0.63rem; text-transform: uppercase; letter-spacing: 0.07em; color: var(--text-muted); }
.cost-vendor-row { display: grid; grid-template-columns: minmax(0, 1fr) 3.5rem auto; gap: 0.45rem; align-items: center; }
.cost-vendor-name { font-size: 0.74rem; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.cost-vendor-track { height: 6px; border-radius: 999px; background: var(--border); overflow: hidden; }
.cost-vendor-fill { display: block; height: 100%; background: var(--tile-blue); border-radius: 999px; }
.cost-vendor-val { font-size: 0.74rem; font-weight: 600; font-variant-numeric: tabular-nums; }

/* 44px tap minimum on coarse pointers, the standing rule for every interactive element. */
@media (pointer: coarse) {
  .vendor-view-btn { min-height: calc(var(--tap-min) - 6px); }
  .vendor-link { min-height: var(--tap-min); padding: 0.4rem 0.75rem; }
  .vendor-toggle input { width: 18px; height: 18px; }
  .vr-main { min-height: var(--tap-min); }
  .vendor-chip { min-height: var(--tap-min); }
  .vr-jump { width: var(--tap-min); height: var(--tap-min); }
  .vr-main { padding-right: calc(var(--tap-min) + 0.3rem); }
  .vsum-money-link, .vr-record-summary { min-height: var(--tap-min); }
}

/* Columns are DROPPED as the window narrows rather than the list scrolling sideways, the
   standing rule for every dense table in this app. Order of sacrifice runs from the least to the
   most load-bearing: "affects" first (it is the one field the expansion states in full anyway),
   then "checked", then the tier chip. Cost and next-charge survive to the narrowest width,
   because they are half the reason the page was redesigned. */
@media (max-width: 1180px) {
  .vendor-list { --vr-cols: minmax(0, 1.6fr) 7.5rem 5.2rem 8.5rem 7.5rem 6.5rem 1.6rem; }
  .vr-affects { display: none; }
}
@media (max-width: 960px) {
  .vendor-list { --vr-cols: minmax(0, 1.6fr) 7.5rem 5.2rem 8.5rem 7.5rem 1.6rem; }
  .vr-check { display: none; }
  .vsum-cols { grid-template-columns: 1fr; }
  .vr-panel-cols { grid-template-columns: 1fr; }
}
@media (max-width: 760px) {
  .vendor-list { --vr-cols: minmax(0, 1.5fr) 7rem 7.5rem 6.8rem 1.6rem; }
  .vr-crit { display: none; }
}

/* One column below the split point, matching how every other ops grid collapses. Below this the
   grid is abandoned entirely: five tracks in 340px is five unreadable ones, so each row becomes
   a small stack. It is taller per row, which is the correct trade at a width where a dense table
   cannot be read at all. */
@media (max-width: 620px) {
  .vendor-grid { grid-template-columns: 1fr; }
  .vendor-summary { flex-direction: column; align-items: flex-start; }
  .vendor-counts { gap: 1rem; }
  .vendor-list-head { display: none; }
  .vr-main {
    display: flex; flex-wrap: wrap; align-items: center; gap: 0.3rem 0.5rem;
    padding: 0.55rem 2.6rem 0.55rem 0.7rem;
  }
  .vr-name { flex: 1 1 100%; }
  .vr-crit { display: inline; }
  .vr-renew, .vr-check { flex-direction: row; align-items: baseline; gap: 0.3rem; }
  .vr-meter { display: none; }        /* no room, and the day count says the same thing */
  .vr-caret { position: absolute; right: 2.1rem; top: 0.85rem; }
  .vendor-chip--ghost { margin-left: 0; }
}


/* --- Not found ---------------------------------------------------------------------------
   The customer app's "Not on file" card (web/css/style.css), carried over so a stale link reads
   the same in both consoles. Copied rather than shared, for the same reason ops copies its
   Pages Function auth helper: two separate deployables cannot import across origins.

   ONE real difference from the customer app's copy: there is no --font-display token here and no
   Sora is shipped at all, so size, weight and tracking do the work a display face does over
   there. Everything else comes through the tokens and follows whichever theme is active, which
   is why the shadow below is the only hardcoded value -- the app's copy leans on a --shadow-md
   tuned for a light plane, and this console's default surface is dark. */
.not-found {
  flex: 1 1 auto;
  align-self: stretch;
  display: grid;
  place-items: center;
  padding: clamp(1.4rem, 4vw, 3rem) 1.2rem;
}
.nf-record {
  position: relative;
  width: min(40rem, 100%);
  background: var(--surface-solid);
  border: 1px solid var(--border);
  border-radius: var(--radius);
  box-shadow: 0 1px 2px rgba(0, 0, 0, 0.35), 0 20px 44px -24px rgba(0, 0, 0, 0.75);
  padding: clamp(1.7rem, 4vw, 2.9rem);
  /* Hovers, slowly and shallowly (Clark, 2026-09-06). Transform only, so it composites on the
     GPU and repaints nothing. The shadow rides along rather than counter-animating: that needs a
     separate element, and any child put behind this card would paint on top of its own
     background, because animating transform makes this element a stacking context. */
  animation: nfFloat 6s ease-in-out infinite;
}
@keyframes nfFloat {
  0%, 100% { transform: translateY(0); }
  50%      { transform: translateY(-8px); }
}
.nf-tag {
  font-size: 0.68rem; font-weight: 700; letter-spacing: 0.15em; text-transform: uppercase;
  color: var(--text-muted);
}
.nf-title { font-size: clamp(1.6rem, 3.4vw, 2.5rem); font-weight: 800; letter-spacing: -0.02em;
  margin: 0.4rem 0 0.35rem; }
.nf-msg { font-size: clamp(0.95rem, 1.35vw, 1.12rem); color: var(--text-secondary);
  line-height: 1.55; margin: 0; max-width: 48ch; }
/* The address, set small and tracked rather than in a code face. It is whatever was in the URL,
   so it has to be able to be long: it wraps mid-token instead of pushing the card wider. */
.nf-path {
  display: inline-block; margin: 0.9rem 0 0;
  font-size: clamp(0.8rem, 1.05vw, 0.94rem); font-weight: 500; letter-spacing: 0.04em;
  color: var(--text-secondary);
  background: var(--surface-sunken);
  border: 1px solid var(--border);
  border-radius: var(--radius-sm, 6px);
  padding: 0.28rem 0.6rem;
  max-width: 100%; overflow-wrap: anywhere;
}
.nf-actions { display: flex; flex-wrap: wrap; gap: 0.6rem; margin-top: clamp(1.3rem, 2.4vw, 1.8rem); }
.nf-actions .btn-primary, .nf-actions .btn-secondary {
  width: auto; padding: 0.62rem 1.25rem; font-size: clamp(0.88rem, 1.1vw, 0.98rem);
}
/* Rotated, offset past the card's own corner, and it lands ONCE. A stamp that keeps animating
   stops being a stamp. Not a colour-only signal either: it carries its words, the same rule
   every status badge in this console follows. */
.nf-stamp {
  position: absolute; top: clamp(1.1rem, 2.2vw, 1.7rem); right: -1.15rem;
  transform: rotate(-11deg);
  font-size: clamp(0.98rem, 1.5vw, 1.32rem); font-weight: 800; letter-spacing: 0.14em;
  text-transform: uppercase;
  color: var(--st-expired);
  border: 3px solid var(--st-expired);
  border-radius: 6px;
  padding: 0.42rem 0.9rem;
  opacity: 0.85;
  /* Worn ink (Clark, 2026-09-06: "add some texture to the stamp it looks like a stamp"). A real
     rubber stamp never prints solid: the pad is unevenly loaded and the paper is not perfectly
     flat, so coverage breaks up. This masks the whole element with fractal noise, so the border
     and the letters lose ink TOGETHER and it reads as one impression rather than as a bordered
     label containing faded text.

     Deterministic, not random: seed='7' fixes the pattern, so it is the same impression on every
     load and can be judged once. The alpha floor is 0.28 -- noise plus that constant, clamped --
     which mottles without ever dropping a stroke far enough to make the word hard to read, and
     the word is the part that carries the meaning. The grain was coarsened once after
     looking at it: at baseFrequency 0.8 the speckle was finer than the stroke weight and
     simply read as a slightly lighter red, not as ink.

     An SVG data URI rather than a PNG, so nothing is fetched and nothing can be blocked: both
     consoles ship `img-src 'self' data:`, which is the directive a CSS mask is checked against.
     Fixed mask-size rather than 100%, so the grain stays the same size as the stamp scales with
     the viewport instead of stretching with it. */
  -webkit-mask-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='90' height='45'%3E%3Cfilter id='s'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='0.45' numOctaves='3' seed='7'/%3E%3CfeColorMatrix type='matrix' values='0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 0 0 0 0.28'/%3E%3C/filter%3E%3Crect width='90' height='45' filter='url(%23s)'/%3E%3C/svg%3E");
          mask-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='90' height='45'%3E%3Cfilter id='s'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='0.45' numOctaves='3' seed='7'/%3E%3CfeColorMatrix type='matrix' values='0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 0 0 0 0.28'/%3E%3C/filter%3E%3Crect width='90' height='45' filter='url(%23s)'/%3E%3C/svg%3E");
  -webkit-mask-size: 90px 45px;
          mask-size: 90px 45px;
  animation: nfStamp 0.45s cubic-bezier(0.2, 1.5, 0.4, 1) both;
}
@keyframes nfStamp {
  from { transform: rotate(-11deg) scale(1.5); opacity: 0; }
}

@media (prefers-reduced-motion: reduce) {
  .nf-record, .nf-stamp { animation: none; }
}

/* --- Proper-name casing hint (Clark, 2026-09-06) --------------------------------------------
   Shown under a field whose capitalisation was corrected on blur. It exists so the change is
   never silent: the app rewrote what somebody typed, so it says so. There is nothing to press --
   the casing is locked, and an undo button would make the lockdown a suggestion. See
   properName() in js/lib/name-case.js. */
.name-case-hint {
  font-size: 0.72rem; line-height: 1.4; color: var(--text-secondary);
  margin: 0.2rem 0 0;
}
