/* ---------------------------------------------------------------------------
 * SMART Workspace web console — design system + base components.
 *
 * Visual reference: Tailscale console. Clean light theme, generous
 * whitespace, brand accent (default #ff1c76 / 76dev magenta) only where
 * it earns attention (primary actions, brand strip, focus rings).
 *
 * White-labeling: operators override brand tokens via env vars in
 * vmshell. The base.html template injects them as :root custom
 * properties so a single CSS variable change cascades everywhere.
 * --------------------------------------------------------------------------- */


/* ---- Design tokens ------------------------------------------------------ */

:root {
  /* Native form controls (select dropdowns + their <option> popups, date
     pickers, scrollbars) follow the active theme. In dark mode this gives them
     dark UA chrome — light text on a dark field — so the option list, which CSS
     can't reliably restyle, stays readable. Flipped in the dark blocks below. */
  color-scheme: light;

  /* Brand — overrideable per-deployment from vmshell env vars.
     `--brand-primary` is the only one operators usually change. */
  /* 76DEV magenta. --brand-primary is the AA-safe Magenta Text #d11062: white
     button text on it is 5.1:1 and it is 5.1:1 as link text on cream/white, so
     it is safe for normal-size text. --brand-accent is the vivid Signal Magenta
     #ff1c76 (only 3.6:1 on cream) — large display, icons, focus rings, borders,
     and the logo ONLY, never normal text (docs/CONSOLE_UI_REFRESH.md).
     Operators white-labeling should keep --brand-primary ≥4.5:1 with white. */
  --brand-primary:        #d11062;
  --brand-primary-hover:  #a30c4e;
  --brand-primary-ring:   #ff1c7633;
  --brand-accent:         #ff1c76;   /* vivid Signal Magenta — non-text only */
  --brand-link:           #d11062;   /* links/normal magenta text on light */
  --brand-link-inverse:   #ff8ab4;   /* links/magenta text on Ink Navy */

  /* Surfaces — 76DEV Canvas Cream page, white cards */
  --bg-page:      #fcfaf6;   /* Canvas Cream */
  --bg-surface:   #ffffff;
  --bg-muted:     #f3f0e9;   /* warm cream-muted panel */
  --bg-hover:     #f3f0e9;
  --bg-sunken:    #f3f0e9;   /* recessed surface (Q4) — mirrors bg-muted in light */

  /* Text — Body Ink / Ink Navy heading; grays AA on cream */
  --text-primary:   #303030;  /* Body Ink */
  --text-heading:   #263342;  /* Ink Navy (headings/nav) */
  --text-secondary: #4a4a4a;  /* Gray 700 */
  --text-muted:     #6b6b6b;  /* Gray 500 — lightest AA body gray on cream */
  --text-disabled:  #9a9a9a;
  /* Text on the brand fill (.btn--primary only). Deliberately NOT overridden
     in dark mode: `brand-primary` does not flip, so flipping this to a dark
     value put dark navy on a mid-blue button (2.03:1). Enforced by the
     (text-inverse, brand-primary) pair in theme.py AA_PAIRS. */
  --text-inverse:   #fcfaf6;
  --text-faint:     #6b6b6b;  /* fourth text step (Q4) — mirrors text-muted in light */

  /* Borders — Gray 300 (borders/dividers only, never text) */
  --border:         #c9c9c9;
  --border-strong:  #b0b0b0;
  --border-subtle:  #c9c9c9;  /* subtle divider (Q4) — mirrors border in light */

  /* Header — Ink Navy bar in BOTH themes. A dedicated role (not --bg-surface):
     the header is always navy regardless of page surface, so the nav reads as
     a branded bar on cream and stays distinct from the navy page in dark. */
  --header-bg:        #263342;   /* Ink Navy */
  --header-fg:        #fcfaf6;   /* cream — 12.3:1 on navy */
  --header-fg-muted:  #c9d1d9;   /* muted cream — idle nav, 8.3:1 on navy */
  --header-border:    #1b2530;   /* a step darker than the bar */

  /* Danger (destructive actions) — error red, theme-flipped so the label
     stays AA on whichever surface the button sits on. */
  --danger-fg:      #c8102e;   /* 5.9:1 on white */
  --danger-border:  #e3a6ad;

  /* Semantic colors for status pills */
  --status-ok-bg:    #ecfdf5;
  --status-ok-fg:    #065f46;
  --status-warn-bg:  #fef3c7;
  --status-warn-fg:  #78350f;
  --status-err-bg:   #fef2f2;
  --status-err-fg:   #7f1d1d;
  --status-info-bg:  #eff6ff;
  --status-info-fg:  #1e3a8a;

  /* Spacing — 8px grid. Use these rather than raw pixels in component CSS. */
  --space-1:  4px;
  --space-2:  8px;
  --space-3:  12px;
  --space-4:  16px;
  --space-5:  24px;
  --space-6:  32px;
  --space-7:  48px;
  --space-8:  64px;

  --layout-max-width: 1400px;  /* page content max width (header + main). Spatial
                                  audit P3: was 1200px → ~40% dead margin on wide
                                  monitors; 1400 reclaims width for the tables. */

  /* Type — 76DEV Fira Sans / Fira Mono (self-hosted, see @font-face below) */
  --font-sans: "Fira Sans", "Segoe UI", system-ui, -apple-system,
               Helvetica, Arial, sans-serif;
  --font-mono: "Fira Mono", ui-monospace, "SF Mono", Menlo, Consolas, monospace;
  --text-xs: 11px;
  --text-sm: 13px;
  --text-md: 14px;
  --text-lg: 16px;
  --text-xl: 18px;
  --text-2xl: 22px;
  --text-3xl: 28px;

  /* Radius + elevation — 76DEV scale (sm/md/lg/pill) */
  --radius-sm: 4px;
  --radius-md: 8px;
  --radius-lg: 16px;
  --radius-pill: 999px;
  --shadow-sm: 0 1px 2px rgba(38, 51, 66, 0.05);
  --shadow-md: 0 4px 12px rgba(38, 51, 66, 0.08);
}

/* ---- Dark theme (76DEV navy surfaces) ----------------------------------- */
/* The token ROLES flip; components built against the roles follow for free.
   `[data-theme="dark"]` = explicit user choice (theme cookie, set server-side
   so there's no flash). The media block covers OS preference when the user has
   made no explicit choice (:root:not([data-theme])). --brand-primary/-accent
   (magenta) are unchanged — magenta fills + the vivid accent read fine on navy;
   only link text flips to the on-navy AA magenta #ff8ab4. */
[data-theme="dark"] {
  color-scheme: dark;          /* dark native selects/options + scrollbars */
  --bg-page:        #263342;   /* Ink Navy */
  --bg-surface:     #2f3e50;   /* navy card */
  --bg-muted:       #38485c;
  --bg-hover:       #38485c;
  --bg-sunken:      #1d2733;   /* recessed surface (Q4) */
  --text-primary:   #fcfaf6;   /* Canvas Cream */
  --text-heading:   #fcfaf6;
  --text-secondary: #c9d1d9;
  --text-muted:     #aab4bf;
  --text-faint:     #8a97a5;   /* fourth text step (Q4) */
  --border:         #44546a;
  --border-strong:  #5a6b80;
  --border-subtle:  #324050;   /* subtle divider (Q4) */
  --header-bg:      #2f3e50;   /* navy surface — distinct from the navy page */
  --danger-fg:      #ff8f9b;   /* 5.0:1 on the navy card */
  --danger-border:  #5a3038;
  --brand-link:     #ff8ab4;   /* magenta on navy (5.84:1 AA) */
  --status-ok-bg:   #16351f; --status-ok-fg:   #5fd38a;
  --status-warn-bg: #3a2e10; --status-warn-fg: #ffcf66;
  --status-err-bg:  #3a1820; --status-err-fg:  #ff7a8a;
  --status-info-bg: #16263a; --status-info-fg: #7fb6ef;
}
@media (prefers-color-scheme: dark) {
  :root:not([data-theme]) {
    color-scheme: dark;
    --bg-page: #263342; --bg-surface: #2f3e50; --bg-muted: #38485c;
    --bg-hover: #38485c; --bg-sunken: #1d2733;
    --text-primary: #fcfaf6; --text-heading: #fcfaf6;
    --text-secondary: #c9d1d9; --text-muted: #aab4bf;
    --text-faint: #8a97a5;
    --border: #44546a; --border-strong: #5a6b80; --border-subtle: #324050;
    --brand-link: #ff8ab4;
    --header-bg: #2f3e50; --danger-fg: #ff8f9b; --danger-border: #5a3038;
    --status-ok-bg: #16351f; --status-ok-fg: #5fd38a;
    --status-warn-bg: #3a2e10; --status-warn-fg: #ffcf66;
    --status-err-bg: #3a1820; --status-err-fg: #ff7a8a;
    --status-info-bg: #16263a; --status-info-fg: #7fb6ef;
  }
}


/* ---- Reset + base ------------------------------------------------------- */

*, *::before, *::after { box-sizing: border-box; }

html, body { margin: 0; padding: 0; }

body {
  font-family: var(--font-sans);
  font-size: var(--text-md);
  line-height: 1.5;
  color: var(--text-primary);
  background: var(--bg-page);
}

a { color: var(--brand-primary); text-decoration: none; }
a:hover { color: var(--brand-primary-hover); text-decoration: underline; }
/* Links inside prose need a non-color cue (underline) so they're distinguishable
   without relying on color — WCAG 1.4.1. UI/action links (.btn, .link) stay
   underline-free; only links embedded in paragraph text are underlined. */
p a:not(.btn):not(.link) { text-decoration: underline; }

code, pre { font-family: var(--font-mono); font-size: var(--text-sm); }
pre {
  margin: 0;
  padding: var(--space-3) var(--space-4);
  background: #0b1020;
  color: #d1d5db;
  border-radius: var(--radius-md);
  overflow-x: auto;
  user-select: all;
}

h1, h2, h3, h4 { margin: 0 0 var(--space-3) 0; line-height: 1.25; }
h1 { font-size: var(--text-3xl); font-weight: 600; }
h2 { font-size: var(--text-xl); font-weight: 600; }
h3 { font-size: var(--text-md); font-weight: 600; }


/* ---- Layout: header + main --------------------------------------------- */

.app-header {
  background: var(--header-bg);
  border-bottom: 1px solid var(--header-border);
  /* The brand strip — a thin line of vivid Signal Magenta across the very top
     of the Ink Navy bar. Non-text use, so the raw accent #ff1c76 is in bounds. */
  border-top: 3px solid var(--brand-accent);
}

.app-header__inner {
  max-width: var(--layout-max-width);
  margin: 0 auto;
  padding: var(--space-3) var(--space-5);
  display: flex;
  align-items: center;
  gap: var(--space-5);
  /* Wrap at every width, not only inside the responsive band. Nav links are
     nowrap, so above the band they cannot give way under compression — without
     this the row would overflow the header instead of dropping to a second
     line. Costs nothing while the content fits, and it means the header
     degrades on its own rather than on a breakpoint being estimated
     correctly. */
  flex-wrap: wrap;
}

.app-header__brand {
  display: flex;
  align-items: center;
  gap: var(--space-3);
  color: var(--header-fg);
}
.app-header__brand:hover { text-decoration: none; }

.app-header__logo {
  width: 28px; height: 28px;
  color: var(--brand-accent); /* vivid Signal Magenta logo on navy (if inline SVG) */
}
.app-header__logo svg, .app-header__logo img {
  width: 100%; height: 100%; display: block;
}

.app-header__brand-text {
  display: flex; flex-direction: column; line-height: 1.1;
}
.app-header__brand-name {
  font-weight: 600; font-size: var(--text-md);
  color: var(--header-fg);
}
.app-header__brand-tagline {
  font-size: var(--text-xs); color: var(--header-fg-muted);
}

.app-header__nav {
  display: flex; gap: var(--space-4); margin-left: var(--space-5);
}
.app-header__nav a {
  color: var(--header-fg-muted);
  font-size: var(--text-sm);
  font-weight: 500;
  padding: var(--space-2) 0;
  border-bottom: 2px solid transparent;
  transition: color .1s, border-color .1s;
  /* Break between items, never inside one. Under compression "SSH keys" and
     "App feedback" wrapped to two ragged lines each, which was half of what
     the header looked cluttered with (feedback report 7). */
  white-space: nowrap;
}

/* Above the responsive band the secondary links sit inline like the rest, so
   the trigger that collapses them is not needed. The band below flips these
   two. Exactly one copy of those links is ever displayed. */
.app-header__more { display: none; }
.app-header__nav a:hover {
  color: var(--header-fg);
  text-decoration: none;
}
.app-header__nav a.active {
  color: var(--header-fg);
  border-bottom-color: var(--brand-accent);
}

/* Admin items collapsed into an "Administration" disclosure dropdown. Native
   <details>/<summary> — no JS. The trigger matches a nav link; the open panel
   surfaces below it, themed as a header extension via the same --header-* tokens. */
.app-header__admin,
.app-header__more {
  position: relative;
}
.app-header__admin > summary,
.app-header__more > summary {
  list-style: none;
  cursor: pointer;
  color: var(--header-fg-muted);
  font-size: var(--text-sm);
  font-weight: 500;
  padding: var(--space-2) 0;
  border-bottom: 2px solid transparent;
  transition: color .1s, border-color .1s;
}
.app-header__admin > summary::-webkit-details-marker,
.app-header__more > summary::-webkit-details-marker { display: none; }
.app-header__admin > summary::after,
.app-header__more > summary::after { content: " \25BE"; }  /* ▾ caret */
.app-header__admin > summary:hover,
.app-header__more > summary:hover { color: var(--header-fg); }
.app-header__admin > summary.active,
.app-header__more > summary.active {
  color: var(--header-fg);
  border-bottom-color: var(--brand-accent);
}
.app-header__admin-menu,
.app-header__more-menu {
  position: absolute;
  top: 100%;
  left: 0;
  margin-top: var(--space-2);
  z-index: 50;
  display: flex;
  flex-direction: column;
  gap: var(--space-3);
  min-width: 11rem;
  padding: var(--space-3) var(--space-4);
  background: var(--header-bg);
  border: 1px solid var(--border);
  border-radius: var(--radius-md);
  box-shadow: var(--shadow-md);
}
.app-header__admin-menu a,
.app-header__more-menu a {
  color: var(--header-fg-muted);
  font-size: var(--text-sm);
  font-weight: 500;
  white-space: nowrap;
}
.app-header__admin-menu a:hover,
.app-header__more-menu a:hover { color: var(--header-fg); text-decoration: none; }

/* Open-feedback count in the admin nav. Sits on the collapsed "Administration"
   summary as well as the Feedback link, so an open report is visible without
   opening the menu — the first report this product received went unnoticed for
   six hours because nothing surfaced it. Hidden entirely when the count is
   zero or unknown, so it only ever means "there is something waiting". */
.nav-badge {
  display: inline-block;
  min-width: 1.25em;
  padding: 0 .4em;
  margin-left: .35em;
  border-radius: 999px;
  background: var(--status-warn-bg);
  color: var(--status-warn-fg);
  font-size: .72rem;
  font-weight: 700;
  line-height: 1.5;
  text-align: center;
  vertical-align: middle;
}
.app-header__admin-menu a.active,
.app-header__more-menu a.active { color: var(--header-fg); }

.app-header__user {
  margin-left: auto;
  display: flex; align-items: center; gap: var(--space-3);
  color: var(--header-fg-muted); font-size: var(--text-sm);
}
.app-header__user > span { color: var(--header-fg); }
/* Sign-out reads as a cream ghost on the navy bar, not a heavy white block. */
.app-header .btn--outline {
  background: transparent;
  border-color: var(--header-fg-muted);
  color: var(--header-fg);
}
.app-header .btn--outline:hover {
  background: rgba(255, 255, 255, 0.10);
  color: var(--header-fg);
}

/* Spatial audit P1 (docs/CONSOLE_SPATIAL_AUDIT.md): below ~768px the single
   non-wrapping header row (brand + 7 nav links + icons + user + Sign out)
   overflowed horizontally, stranding Sign out off-screen and forcing a page
   scroll. Collapse it: brand + user/Sign-out stay on row one (user right via
   margin-left:auto), the nav drops to its own full-width row and wraps. Pure
   CSS, no JS — the minimum that stops the overflow and keeps every control
   reachable. A hamburger is a later polish. */
/* Spatial audit P2 (docs/CONSOLE_SPATIAL_AUDIT.md): the workspace detail config
   cards (SSH, Share, Egress profile, Share folder, Egress allowlist, Backups)
   were full-width cards each holding one small control — ~75% empty horizontal
   space stacked into a long scroll. Two columns on wide viewports halves the
   scroll and uses the width; single column below (the cards are block by
   default, so no rule needed there). */
@media (min-width: 1024px) {
  /* Masonry-style column pack, not a 1fr 1fr grid: a grid aligns each row to its
     tallest card, so a short card (SSH, Backups) paired with a tall one (Share,
     Egress allowlist) leaves a big gap below it. CSS columns flow the cards top-
     to-bottom and pack them tightly regardless of height. The card keeps its
     default margin-bottom for vertical spacing within a column. */
  .config-grid {
    column-count: 2;
    column-gap: var(--space-4);
    /* multicol clips the last card's bottom margin, so the grid would touch the
       full-width Smarties cards below it — add the gap on the container. */
    margin-bottom: var(--space-4);
  }
  .config-grid > .card { break-inside: avoid; }
}

/* The header's responsive band. Was max-width: 768px, which is BELOW iPad
   portrait (834px) — so the tablet this was written for never got it and fell
   through to a desktop header needing roughly 1100px of content width. Under
   that compression labels wrapped mid-phrase and the thumbs form was squeezed
   until its buttons stacked vertically (feedback report 7). 1024px covers iPad
   portrait and landscape, and any desktop window narrow enough to have the
   same problem. */
@media (max-width: 1024px) {
  .app-header__inner {
    flex-wrap: wrap;
    gap: var(--space-2) var(--space-3);
    padding: var(--space-2) var(--space-4);
  }
  .app-header__nav {
    order: 3;
    flex-basis: 100%;
    margin-left: 0;
    flex-wrap: wrap;
    gap: var(--space-2) var(--space-4);
  }
  .app-header__user {
    flex-wrap: wrap;
  }
  /* Collapse the measured tail behind "More" (see _app_header.html for the
     numbers). Swap, never both: the inline copies go, the trigger arrives. */
  .app-header__nav-secondary { display: none; }
  .app-header__more { display: block; }
  /* The thumbs are one control, not three. Without this the row is a shrinkable
     flex item and compression stacks the buttons into a column — which is what
     the report's screenshot shows. */
  .sat-widget { flex: 0 0 auto; }
}

/* Spatial audit P4 (docs/CONSOLE_SPATIAL_AUDIT.md): the list-page tables had no
   mobile treatment — long filenames wrapped to 5-6 lines and rows ballooned.
   Reflow the opted-in .data--stack tables (workspaces + shared files only, not
   the admin tables) into stacked cards: hide the header row, make each row a
   bordered block, and prefix each cell with its column label. */
@media (max-width: 768px) {
  table.data--stack thead { display: none; }
  table.data--stack,
  table.data--stack tbody,
  table.data--stack tr,
  table.data--stack td { display: block; }
  table.data--stack tr {
    border: 1px solid var(--border);
    border-radius: var(--radius-md);
    padding: var(--space-2) var(--space-3);
    margin-bottom: var(--space-3);
  }
  table.data--stack tr:hover { background: transparent; }
  table.data--stack td {
    padding: var(--space-1) 0;
    border-bottom: none;
  }
  table.data--stack td[data-label]::before {
    content: attr(data-label);
    display: inline-block;
    min-width: 5.5em;
    color: var(--text-muted);
    font-weight: 600;
    font-size: var(--text-xs);
    text-transform: uppercase;
    letter-spacing: 0.04em;
  }
  table.data--stack td.actions {
    padding-top: var(--space-2);
    white-space: normal;   /* override the desktop nowrap so buttons wrap */
  }
}

/* Tablet console (docs/plans/2026-06-21-003-feat-tablet-console-copilot-plan.md):
   a touch-target + input pass for coarse-pointer devices. Bell icons and
   .btn--sm sit below the ~44px touch floor; form fields below 16px trigger iOS
   Safari auto-zoom on focus. Gate on pointer: coarse, not width — a small
   desktop window is still a mouse, and width alone over-fires. */
@media (pointer: coarse) {
  .btn--sm, .bell-btn { min-height: 44px; min-width: 44px; }
  /* 16px stops iOS Safari from zooming the page when a field gains focus.
     Covers type=number (the admin quota row) as well as text/select/textarea. */
  form input, form select, form textarea { font-size: 16px; }
  /* The admin Users quota row (3 number inputs + Save) wraps badly once the
     fields grow to 16px; keep it on one line and let it scroll horizontally
     instead of restructuring the deliberately-wide admin table. */
  td.actions form[action$="/quota"] {
    display: flex; flex-wrap: nowrap; gap: var(--space-2);
    overflow-x: auto; max-width: 100%;
  }
  td.actions form[action$="/quota"] input { width: 6em; flex: 0 0 auto; }
}

.app-main {
  max-width: var(--layout-max-width);
  margin: 0 auto;
  padding: var(--space-6) var(--space-5);
}


/* ---- Page header (inside main) ----------------------------------------- */

.page-header {
  display: flex; align-items: center; justify-content: space-between;
  gap: var(--space-4);
  margin-bottom: var(--space-5);
}
.page-header__title { margin: 0; }
.page-header__subtitle {
  color: var(--text-secondary); font-size: var(--text-sm);
  margin-top: var(--space-1);
}
.page-header__actions { display: flex; gap: var(--space-2); }


/* ---- Cards ------------------------------------------------------------- */

.card {
  background: var(--bg-surface);
  border: 1px solid var(--border);
  border-radius: var(--radius-lg);
  padding: var(--space-5);
  margin-bottom: var(--space-4);
  box-shadow: var(--shadow-sm);   /* refined-flat: restrained lift off the cream */
}
.card__header {
  display: flex; justify-content: space-between; align-items: baseline;
  margin-bottom: var(--space-4);
}
.card__header h2 { margin: 0; }
.card__subtitle {
  color: var(--text-secondary); font-size: var(--text-sm);
  margin-top: var(--space-1);
}


/* ---- Buttons ----------------------------------------------------------- */

.btn {
  display: inline-flex; align-items: center; justify-content: center;
  gap: var(--space-2);
  padding: var(--space-2) var(--space-4);
  border-radius: var(--radius-md);
  font-family: inherit; font-size: var(--text-sm); font-weight: 500;
  line-height: 1.2;
  border: 1px solid transparent;
  background: var(--bg-surface);
  color: var(--text-primary);
  cursor: pointer; user-select: none;
  text-decoration: none;
  transition: background .1s, border-color .1s, color .1s;
}
.btn:hover { background: var(--bg-hover); text-decoration: none; }
.btn:focus-visible {
  /* Solid ring in the vivid Signal Magenta accent — a focus indicator is a UI
     component (3:1 bar): #ff1c76 is 3.7:1 on white and 3.5:1 on the navy bar,
     both clearing WCAG 1.4.11/2.4.11. (Brand reserves the accent for rings.) */
  outline: 2px solid var(--brand-accent); outline-offset: 2px;
}

.btn--primary {
  background: var(--brand-primary); color: var(--text-inverse);
  border-color: var(--brand-primary);
}
.btn--primary:hover {
  background: var(--brand-primary-hover); color: var(--text-inverse);
  border-color: var(--brand-primary-hover);
}

.btn--outline {
  background: var(--bg-surface); border-color: var(--border-strong);
  color: var(--text-primary);
}
.btn--outline:hover { background: var(--bg-hover); }

.btn--danger {
  background: var(--bg-surface); border-color: var(--danger-border);
  color: var(--danger-fg);
}
.btn--danger:hover { background: var(--status-err-bg); }

.btn--sm {
  padding: var(--space-1) var(--space-3);
  font-size: var(--text-xs);
}

.btn:disabled { opacity: .5; cursor: not-allowed; }


/* ---- Tables ----------------------------------------------------------- */

table.data {
  width: 100%;
  border-collapse: collapse;
  font-size: var(--text-sm);
}
table.data th {
  text-align: left;
  font-size: var(--text-xs);
  text-transform: uppercase; letter-spacing: 0.04em;
  color: var(--text-muted); font-weight: 600;
  padding: var(--space-2) var(--space-3);
  border-bottom: 1px solid var(--border);
}
table.data td {
  padding: var(--space-3);
  border-bottom: 1px solid var(--border);
  vertical-align: middle;
}
table.data tr:last-child td { border-bottom: none; }
table.data tr:hover { background: var(--bg-muted); }
table.data td.actions {
  text-align: right; white-space: nowrap;
  /* Spatial audit P3: shrink the actions cell to its buttons so it stops
     absorbing the table's slack and floating far right (the mid-row gap). The
     slack falls to the Name column instead, keeping the action cluster next to
     the row's status. */
  width: 1%;
}

/* Software page: one table with category sub-header rows, so the
   Software / Version / License columns line up across every group. */
table.software tr.group th {
  text-transform: none;
  letter-spacing: normal;
  font-size: var(--text-sm);
  color: var(--text-primary);
  font-weight: 600;
  background: var(--bg-muted);
  padding: var(--space-2) var(--space-3);
}


/* ---- Status pills ------------------------------------------------------ */

.pill {
  display: inline-flex; align-items: center;
  padding: 2px var(--space-2);
  border-radius: 999px;
  font-size: var(--text-xs); font-weight: 500;
  background: var(--bg-muted); color: var(--text-secondary);
  line-height: 1.4;
}
.pill--ok    { background: var(--status-ok-bg);   color: var(--status-ok-fg); }
.pill--warn  { background: var(--status-warn-bg); color: var(--status-warn-fg); }
.pill--err   { background: var(--status-err-bg);  color: var(--status-err-fg); }
.pill--info  { background: var(--status-info-bg); color: var(--status-info-fg); }
/* Running-smarty pills in the workspaces Status column — blue, distinct from the
   green running-status pill; small gap between adjacent pills in a cell. */
.pill--smarty { background: var(--status-info-bg); color: var(--status-info-fg); }
td .pill + .pill { margin-left: var(--space-1); }

/* Audit-log action families. The label format is `<family>.<verb>`
   (workspace.create, user.sshkey-add, smarty.install, …). Each family
   gets its own pill color so the eye can group events at a glance. */
.pill--family-workspace { background: var(--status-info-bg); color: var(--status-info-fg); }
.pill--family-user      { background: var(--status-warn-bg); color: var(--status-warn-fg); }
.pill--family-smarty    { background: var(--status-ok-bg);   color: var(--status-ok-fg); }


/* ---- Audit log table -------------------------------------------------- */

table.audit pre.audit-details {
  font-size: var(--text-xs);
  padding: var(--space-2) var(--space-3);
  margin: var(--space-2) 0 0 0;
  max-width: 600px;
  white-space: pre-wrap;
  word-break: break-word;
}
table.audit details summary { cursor: pointer; }

/* Column headers stay visible when you've paged deep into a long log. Sticks to
   the viewport, not a nested scroll container — a scroll wrapper would need a
   fixed height and trap the sticky. z-index below the action-picker dropdown
   (z-20) so a suggestion list still overlays the header. */
table.audit thead th {
  position: sticky;
  top: 0;
  z-index: 5;
  background: var(--bg-surface);
}

/* Timestamps read as a column: fixed-width figures, never wrapping mid-value. */
.audit-ts { font-variant-numeric: tabular-nums; white-space: nowrap; }

/* Keyset pagination footer: loaded-count pinned left, the Load-more control (or
   the end-of-log marker) centered under the table. */
.audit-foot {
  display: flex;
  align-items: center;
  justify-content: center;
  gap: var(--space-3);
  padding: var(--space-3);
  position: relative;
}
.audit-foot__status { position: absolute; left: var(--space-3); }
.audit-foot__end { letter-spacing: 0.03em; }


/* ---- Audit filter ribbon ---------------------------------------------- */

/* Compact one-row filter bar, Tailscale-console style. Wraps to the next
   line on narrow viewports instead of breaking into a stacked form. */
.audit-ribbon {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: var(--space-2);
  padding: var(--space-2) var(--space-3);
  margin-bottom: var(--space-3);
  background: var(--bg-surface);
  border: 1px solid var(--border);
  border-radius: var(--radius-md);
}
/* width:auto overrides the generic `form input/select { width: 100% }`
   rule above; without it each control inflates to fill the line and
   the ribbon wraps to N rows instead of one. */
.audit-ribbon input,
.audit-ribbon select {
  margin: 0;
  padding: 4px 8px;
  font-size: var(--text-sm);
  height: 30px;
  width: auto;
  box-sizing: border-box;
  flex: 0 0 auto;
}
.audit-ribbon input[type="text"] { width: 180px; }
.audit-ribbon select { min-width: 140px; }
.audit-ribbon .btn--sm { height: 30px; }

/* Action picker (htmx active search). The wrapper is the positioning context
   so the suggestion list can overlay the page instead of pushing the ribbon
   open on every keystroke. */
.action-suggest { position: relative; flex: 0 0 auto; }
.action-suggest__results {
  position: absolute;
  z-index: 20;
  top: calc(100% + 4px);
  left: 0;
  min-width: 260px;
  max-height: 280px;
  overflow-y: auto;
  background: var(--bg-surface);
  border: 1px solid var(--border);
  border-radius: var(--radius-md);
  box-shadow: var(--shadow-md, 0 4px 12px rgb(0 0 0 / 18%));
}
.action-suggest__results:empty { display: none; }
.action-suggest__list { list-style: none; margin: 0; padding: 4px; }
.action-suggest__item {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--space-2);
  width: 100%;
  padding: 5px 8px;
  background: none;
  border: 0;
  border-radius: var(--radius-sm);
  font: inherit;
  text-align: left;
  cursor: pointer;
}
.action-suggest__item:hover { background: var(--bg-subtle, rgb(127 127 127 / 12%)); }
/* Keyboard focus must be as obvious as hover — arrow keys move focus here. */
.action-suggest__item:focus-visible {
  outline: 2px solid var(--accent, currentColor);
  outline-offset: -2px;
  background: var(--bg-subtle, rgb(127 127 127 / 12%));
}
.action-suggest__empty { margin: 0; padding: 8px; }
/* Actor/target facet values are plain text (no pill) and can be long emails —
   truncate so the count on the right stays visible. min-width:0 lets the flex
   item shrink below its content width, which is what enables the ellipsis. */
.audit-facet-value {
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
  min-width: 0;
}


/* ---- Banners (flash messages) ----------------------------------------- */

.banner {
  display: flex; gap: var(--space-3);
  padding: var(--space-3) var(--space-4);
  border-radius: var(--radius-md);
  font-size: var(--text-sm);
  margin-bottom: var(--space-4);
}
.banner.ok  { background: var(--status-ok-bg);  color: var(--status-ok-fg);
              border-left: 4px solid #10b981; }
.banner.err { background: var(--status-err-bg); color: var(--status-err-fg);
              border-left: 4px solid #ef4444; }
.banner.critical { background: #7f1d1d; color: #fef2f2;
              border-left: 4px solid #fca5a5; font-size: var(--text-base); }
.banner.critical code { background: rgba(255,255,255,.18); padding: 0 .25em;
              border-radius: var(--radius-sm); }


/* ---- Quota usage banner ----------------------------------------------- */

.quota {
  display: flex; gap: var(--space-4); align-items: center;
  flex-wrap: wrap;
  background: var(--bg-surface);
  border: 1px solid var(--border);
  border-radius: var(--radius-md);
  /* Spatial audit P5: slim the vertical padding so the quota strip is a compact
     bar, not a full-height card — reclaims above-the-fold space for the list.
     The "+ New workspace" CTA stays in the bar (above the fold). */
  padding: var(--space-2) var(--space-4);
  margin-bottom: var(--space-4);
  font-size: var(--text-sm); color: var(--text-secondary);
}
.quota strong { color: var(--text-primary); }
.quota .quota__cta { margin-left: auto; }


/* ---- Forms ------------------------------------------------------------ */

form label {
  display: block; margin: var(--space-3) 0 var(--space-1);
  font-size: var(--text-sm); font-weight: 500; color: var(--text-primary);
}
form input[type=text], form select, form textarea {
  width: 100%; padding: var(--space-2) var(--space-3);
  border: 1px solid var(--border-strong);
  border-radius: var(--radius-md);
  font-family: inherit; font-size: var(--text-sm);
  background: var(--bg-surface);
  color: var(--text-primary);
  box-sizing: border-box;
}
form input:focus, form select:focus, form textarea:focus {
  outline: none;
  border-color: var(--brand-accent);
  box-shadow: 0 0 0 3px var(--brand-primary-ring);
}
form textarea {
  font-family: var(--font-mono); min-height: 80px;
  resize: vertical;
}
form .field-help {
  font-size: var(--text-xs); color: var(--text-muted);
  margin: var(--space-1) 0 0;
}
/* Live per-rule name feedback (#609): the same help line turns red on the
   unmet rule and green when valid. Theme-aware via the status-* vars. */
form .field-help--err { color: var(--status-err-fg); font-weight: 500; }
form .field-help--ok  { color: var(--status-ok-fg);  font-weight: 500; }
form .field-help--ok::before  { content: "✓ "; }
form .field-help--err::before { content: "✗ "; }
form .field-label {
  display: block; margin: var(--space-3) 0 var(--space-1);
  font-size: var(--text-sm); font-weight: 500; color: var(--text-primary);
}
form .form-actions {
  display: flex; gap: var(--space-2);
  margin-top: var(--space-5);
}
form.inline { display: inline; margin: 0; }
/* Profile/share dropdowns (egress-profile, share-mode) sit inline with their
   Apply button. The generic `form select { width:100% }` stretched them
   full-width and wrapped Apply to a new line; shrink to content + align. */
form.inline select {
  width: auto;
  vertical-align: middle;
  margin-right: var(--space-2);
}
form.inline .btn { vertical-align: middle; }

/* Files page: visibility editor. Collapsed to just the pill (the <summary>);
   opening reveals one checkbox per workspace (checked = may see the file),
   stacked vertically so it stays tidy at iPad-and-down widths where the
   .data--stack reflow is active. */
.restrict > summary {
  list-style: none; cursor: pointer; display: inline-flex;
  align-items: center; gap: 4px;
}
.restrict > summary::-webkit-details-marker { display: none; }
.restrict > summary::after { content: " \25BE"; color: var(--text-muted); font-size: var(--text-sm); }
.restrict[open] > summary::after { content: " \25B4"; }  /* ▴ */
.restrict-form { margin-top: var(--space-2); }
.restrict-check {
  display: flex; align-items: center; gap: 6px;
  margin-bottom: 4px; font-size: var(--text-sm); cursor: pointer;
}
.restrict-check input[type="checkbox"] { margin: 0; }
.restrict-form .btn { margin-top: 4px; }

/* Compliance evidence-pack date range: fixed-width labels so the From/To
   inputs share a left edge (vertically aligned). */
.compliance-range {
  display: flex; flex-direction: column;
  gap: var(--space-2); align-items: flex-start;
}
.compliance-range label {
  display: flex; align-items: center; gap: var(--space-3);
  color: var(--text-muted); font-size: var(--text-sm);
}
.compliance-range label > span { width: 3rem; text-align: right; }
.compliance-range button { margin-top: var(--space-2); }

/* The download form reads as a distinct action panel, set off from the
   page title with its own surface + border. */
.download-panel {
  border: 1px solid var(--border);
  border-radius: var(--radius-md);
  background: var(--bg-muted);
  padding: var(--space-4);
  min-width: 300px;
}
.download-panel .small { margin: var(--space-2) 0 0; }

/* ---- Compliance: coverage proportion bars ----------------------------- */

/* A stacked bar per baseline so the implemented / partial / inherited / owed
   split reads at a glance, instead of as a row of bare numbers. The four
   segments always sum to the baseline total (every control is exactly one). */
.coverage { display: flex; flex-direction: column; gap: var(--space-5); }
.coverage__row { display: flex; flex-direction: column; gap: var(--space-2); }
.coverage__head {
  display: flex; align-items: baseline; justify-content: space-between;
  gap: var(--space-3);
}
.coverage__name { font-weight: 600; font-size: var(--text-md); }
.coverage__pct { color: var(--text-secondary); font-size: var(--text-sm); }
.coverage__pct strong { color: var(--text-primary); }
.coverage__bar {
  display: flex; height: 14px; border-radius: 999px; overflow: hidden;
  background: var(--bg-muted); box-shadow: inset 0 0 0 1px var(--border);
}
.coverage__seg { height: 100%; }
.coverage__seg--ok   { background: #10b981; }
.coverage__seg--warn { background: #f59e0b; }
.coverage__seg--info { background: #3b82f6; }
.coverage__seg--owed { background: var(--border-strong); }

/* Legend / status key — colored dots shared by the bars and the explorer
   pills, so a reader can decode either without a separate caption. */
.coverage__legend, .status-legend {
  display: flex; flex-wrap: wrap; gap: var(--space-4);
  font-size: var(--text-sm); color: var(--text-secondary);
}
.coverage__legend span, .status-legend span {
  display: inline-flex; align-items: center; gap: var(--space-2);
}
.dot { width: 9px; height: 9px; border-radius: 999px; flex: 0 0 auto; }
.dot--ok   { background: #10b981; }
.dot--warn { background: #f59e0b; }
.dot--info { background: #3b82f6; }
.dot--owed { background: var(--border-strong); }

/* Owed = not-yet-addressed, the ISSO's worklist — a neutral pill, NOT an
   error. (Red would misread a coverage gap as a failure.) */
.pill--owed { background: var(--bg-muted); color: var(--text-secondary); }

/* ---- Control explorer table ------------------------------------------- */

/* Break the long control list into per-family blocks so 287 rows are
   scannable. Same sub-header pattern as the software page's category rows. */
.ctrl-table tr.fam th {
  text-transform: none; letter-spacing: normal;
  font-size: var(--text-sm); font-weight: 600; color: var(--text-primary);
  background: var(--bg-muted);
  padding: var(--space-2) var(--space-3);
  border-top: 1px solid var(--border);
}
.ctrl-table tr.fam th .muted { font-weight: 400; }
.ctrl-table td code { font-weight: 600; color: var(--text-primary); }
.ctrl-table .claim + .claim { margin-top: var(--space-2); }
.ctrl-table .claim-stmt { color: var(--text-muted); font-size: var(--text-xs); }

/* Owed / inherited rows have no platform claim — instead of a blank cell, an
   actionable lead + a csf.tools reference. The status-colored left border and
   absence of a bold component name set it apart from a real claim. */
.ctrl-table .lead {
  border-left: 2px solid var(--border-strong);
  padding-left: var(--space-3);
  color: var(--text-secondary);
  font-size: var(--text-xs);
  line-height: 1.5;
  max-width: 58ch;
}
.ctrl-table .lead--inherited { border-left-color: #3b82f6; }
.ctrl-table .lead__ref {
  display: inline-block;
  margin-top: var(--space-1);
  white-space: nowrap;
  font-weight: 500;
}


/* ---- Smarties cards (per-workspace detail page) ---------------------- */

.installed-card {
  border: 1px solid var(--border);
  border-radius: var(--radius-md);
  padding: var(--space-4);
  background: var(--bg-surface);
  margin-bottom: var(--space-3);
}
.installed-card:last-child { margin-bottom: 0; }
.installed-card__head { margin-bottom: var(--space-2); }
.installed-card__head code {
  font-size: var(--text-xs); color: var(--text-muted);
}
.installed-card__actions { margin-top: var(--space-3); }

.connect-block { margin: var(--space-2) 0; }
.connect-block h3 {
  margin: 0 0 var(--space-1) 0;
  font-size: var(--text-xs); font-weight: 600;
  color: var(--text-secondary);
  text-transform: uppercase; letter-spacing: 0.05em;
}
pre.connect-line {
  margin: 0 0 var(--space-1) 0;
  padding: var(--space-2) var(--space-3);
  font-size: var(--text-xs);
}

.smarty-grid {
  display: grid; gap: var(--space-3);
  grid-template-columns: repeat(auto-fill, minmax(240px, 1fr));
}
.smarty-card {
  border: 1px solid var(--border); border-radius: var(--radius-md);
  padding: var(--space-3) var(--space-4); background: var(--bg-muted);
}
.smarty-card h3 { margin: 0 0 var(--space-1) 0; font-size: var(--text-md); }
.smarty-card p { margin: 0; }
/* Name on the left, Install at the top-right (the title row had dead space). */
.smarty-card__head {
  display: flex;
  justify-content: space-between;
  align-items: flex-start;
  gap: var(--space-2);
  margin-bottom: var(--space-1);
}
.smarty-card__head h3 { margin: 0; }
.smarty-card__head form { flex: 0 0 auto; }

/* Confirmation dialog — the friendly replacement for native confirm(). */
.confirm-dialog {
  border: 1px solid var(--border);
  border-radius: var(--radius-lg);
  padding: var(--space-5);
  max-width: 28rem;
  width: calc(100% - var(--space-6));
  background: var(--bg-surface);
  color: var(--text-primary);
  box-shadow: 0 12px 40px rgba(0, 0, 0, 0.25);
}
.confirm-dialog::backdrop { background: rgba(0, 0, 0, 0.45); }
.confirm-dialog__msg {
  margin: 0 0 var(--space-5); font-size: var(--text-md); line-height: 1.45;
}
.confirm-dialog__actions {
  display: flex; justify-content: flex-end; gap: var(--space-2);
}


/* ---- Copy-row (CLI token, etc.) -------------------------------------- */

.copy-row {
  display: flex; gap: var(--space-2); align-items: stretch;
}
.copy-row pre { flex: 1; margin: 0; }
.copy-row button { white-space: nowrap; }

/* The share URL is a link (feedback report 10). Scoped to #share-url so the
   other .copy-row users (cli-token, admin invite) keep plain <pre> text.
   The padding moves off the <pre> and onto the anchor so the whole panel —
   padding included — is the tap target: an inline anchor is only as tall as
   its text, well under the 44px floor the coarse-pointer block sets for
   .btn--sm, and this was reported from a 328px phone. */
#share-url { padding: 0; }
#share-url a {
  display: block;
  padding: var(--space-3) var(--space-4);
  color: inherit;
  text-decoration: underline;
  /* `pre` sets user-select: all, which on touch turns a tap into
     select-the-whole-block instead of following the link. */
  user-select: text;
}


/* ---- Drag-and-drop upload zone --------------------------------------- */

#dropzone {
  border: 2px dashed var(--border-strong);
  border-radius: var(--radius-md);
  padding: var(--space-5);
  text-align: center;
  color: var(--text-muted);
  transition: border-color .1s, background .1s;
  margin: var(--space-3) 0;
}
#dropzone.dragover {
  border-color: var(--brand-primary);
  background: var(--brand-primary-ring);
  color: var(--text-primary);
}
#dropzone p { margin: 0; }
/* A rejected upload (e.g. malware scan) shows the server's reason in red. */
#dropzone.dropzone--error {
  border-color: var(--status-err-fg);
  color: var(--status-err-fg);
}
.link { color: var(--brand-primary); cursor: pointer; }
.link input[type=file] { display: none; }


/* ---- Utilities -------------------------------------------------------- */

.muted   { color: var(--text-muted); }
.small   { font-size: var(--text-xs); }
.mono    { font-family: var(--font-mono); }
.center  { text-align: center; }

/* Screen-reader-only text (accessible name without visual weight). */
.visually-hidden {
  position: absolute !important; width: 1px; height: 1px;
  padding: 0; margin: -1px; overflow: hidden; clip: rect(0 0 0 0);
  white-space: nowrap; border: 0;
}

/* Skip-to-content link — off-screen until focused, then visible (2.4.1). */
.skip-link {
  position: absolute; left: var(--space-3); top: -4rem; z-index: 100;
  padding: var(--space-2) var(--space-4);
  background: var(--bg-surface); color: var(--text-primary);
  border: 1px solid var(--border-strong); border-radius: var(--radius-md);
  box-shadow: var(--shadow-md);
  transition: top .12s ease;
}
.skip-link:focus { top: var(--space-3); text-decoration: none; }
.empty   { color: var(--text-muted); padding: var(--space-6); text-align: center; }
.stack-sm > * + * { margin-top: var(--space-3); }


/* ---- Bell icon (feedback opener in header) --------------------------- */

.bell-btn {
  display: inline-flex; align-items: center; justify-content: center;
  background: transparent; color: var(--header-fg-muted);
  border: 1px solid transparent;
  border-radius: var(--radius-md);
  padding: var(--space-1);
  cursor: pointer;
  line-height: 1;
}
.bell-btn:hover {
  color: var(--header-fg);
  border-color: transparent;
  background: rgba(255, 255, 255, 0.10);
}

/* Satisfaction widget: the two thumbs, and the post-vote confirmation that
   htmx swaps in. The confirmation reads as cream header text on the navy bar. */
.sat-widget { display: inline-flex; align-items: center; gap: var(--space-1); }
/* The 3-level thumb buttons: Netflix-style circular icons in the brand color.
   Shared by the console header and the web terminal (both load this file). */
.sat-thumb {
  width: 30px; height: 30px; padding: 0; border-radius: 50%;
  display: inline-flex; align-items: center; justify-content: center;
  background: transparent; border: 1px solid transparent;
  color: var(--brand-primary); cursor: pointer;
  transition: background .12s ease, color .12s ease;
}
.sat-thumb svg { width: 15px; height: 15px; display: block; }
.sat-thumb:hover, .sat-thumb:focus-visible {
  background: var(--brand-primary); color: #fff; outline: none;
}
/* Thumb-down is the thumb-up glyph rotated 180°. */
.sat-thumb--down svg { transform: rotate(180deg); }
/* Double-up: two smaller thumbs, slightly overlapped. */
.sat-thumb--double svg { width: 12px; }
.sat-thumb--double svg + svg { margin-left: -3px; }

/* Static brand thumb glyph for admin readouts (the icon, not a button). */
.sat-ico { display: inline-flex; align-items: center; color: var(--brand-primary); vertical-align: middle; }
.sat-ico svg { width: 16px; height: 16px; display: block; }
.sat-ico--down svg { transform: rotate(180deg); }
.sat-ico--double svg { width: 13px; }
.sat-ico--double svg + svg { margin-left: -3px; }

/* Satisfaction is a glance, not the focus — a thin collapsible bar above the
   feedback queue. Expand it for the breakdown + recent ratings. */
.sat-summary {
  border: 1px solid var(--border); border-radius: var(--radius-md);
  margin-bottom: var(--space-5);
}
.sat-summary > summary {
  cursor: pointer; list-style: none;
  display: flex; align-items: center; gap: var(--space-3);
  padding: var(--space-2) var(--space-4); font-size: var(--text-sm);
}
.sat-summary > summary::-webkit-details-marker { display: none; }
.sat-summary > summary::after {
  content: "\25be"; color: var(--text-muted); font-size: 11px;
  transition: transform .15s ease;
}
.sat-summary[open] > summary { border-bottom: 1px solid var(--border); }
.sat-summary[open] > summary::after { transform: rotate(180deg); }
.sat-score-inline { color: var(--brand-primary); font-weight: 700; }
.sat-counts { display: inline-flex; gap: var(--space-4); margin-left: auto; }
.sat-count { display: inline-flex; align-items: center; gap: 4px; }
.sat-detail { padding: var(--space-3) var(--space-4) var(--space-4); }
.sat-widget--done {
  color: var(--header-fg); font-size: var(--text-sm); font-weight: 500;
  padding: 0 var(--space-2);
}
.sat-widget--err { color: var(--header-fg-muted); }


/* ---- Feedback dialog (native <dialog>) ------------------------------- */

.feedback-dialog {
  width: min(520px, 92vw);
  padding: var(--space-5);
  border: 1px solid var(--border);
  border-radius: var(--radius-md);
  background: var(--bg-surface);
  color: var(--text-primary);
  box-shadow: 0 10px 30px rgba(0, 0, 0, 0.15);
}
.feedback-dialog::backdrop {
  background: rgba(0, 0, 0, 0.4);
}
.feedback-dialog h2 {
  margin: 0 0 var(--space-2) 0;
  font-size: var(--text-lg);
}
.feedback-dialog textarea {
  width: 100%;
  min-height: 120px;
  margin-top: var(--space-3);
  box-sizing: border-box;
}


/* ---- Feedback admin page --------------------------------------------- */

.feedback-tabs {
  display: flex; gap: var(--space-2);
  margin-bottom: var(--space-3);
}
.feedback-tab {
  padding: 6px 14px;
  border-radius: var(--radius-md);
  font-size: var(--text-sm);
  color: var(--text-secondary);
  text-decoration: none;
  border: 1px solid var(--border);
  background: var(--bg-surface);
}
.feedback-tab:hover { color: var(--text-primary); }
.feedback-tab--active {
  background: var(--brand-primary);
  color: #fff;
  border-color: var(--brand-primary);
}
.feedback-tab--active:hover { color: #fff; }

/* Captured terminal viewport on a report (#249). Tokenised, not hardcoded:
   this block previously set background:var(--surface-sunken,#0b1020) — a token
   that does not exist, so every render fell through to the dark hex and the
   block was unreadable in light theme. The real token is --bg-sunken. */
.fb-terminal {
  max-height: 24rem;
  overflow: auto;
  padding: var(--space-3);
  border: 1px solid var(--border);
  border-radius: var(--radius-md);
  background: var(--bg-sunken);
  color: var(--text-primary);
  font-family: var(--font-mono);
  font-size: var(--text-xs);
  line-height: 1.5;
  white-space: pre;
}

/* The screenshot, demoted behind a disclosure when the report also carries
   terminal text — the shot shows where in the UI, the text shows what was on
   screen, and only the text survives the WebGL canvas readback. */
.fb-shot > summary { cursor: pointer; color: var(--text-secondary); }
.fb-shot__img {
  max-width: 100%;
  border: 1px solid var(--border);
  border-radius: var(--radius-md);
}

/* Report detail: content column + metadata rail (report #11). The card was
   pinned at 900px inside a much wider .app-main, so everything ran down one
   narrow column while half the screen sat empty.

   minmax(0, 1fr), not 1fr: the terminal <pre> is white-space:pre, so its
   min-content width is the longest line. A plain 1fr track refuses to shrink
   below that and pushes the rail off-screen, giving the whole page a
   horizontal scrollbar. minmax(0, …) lets the track shrink and the <pre>
   scroll inside itself, which is what .fb-terminal's overflow is for. */
.fb-detail { display: grid; gap: 0 var(--space-6); }
.fb-detail__head { grid-column: 1 / -1; }
.fb-detail__head h2 { margin: 0 0 var(--space-4); }

@media (min-width: 64rem) {
  .fb-detail { grid-template-columns: minmax(0, 1fr) 20rem; }
  /* The rail reads as a distinct surface, not a third content column. */
  .fb-detail__side {
    border-left: 1px solid var(--border-subtle);
    padding-left: var(--space-5);
  }
}
.fb-detail__side h3 { font-size: var(--text-sm); margin: 0 0 var(--space-2); }
.fb-detail__side h3 + * { margin-top: 0; }
.fb-detail__side ul { margin: 0 0 var(--space-4); padding-left: var(--space-4); }
/* A long report URL must wrap rather than widen the rail track. */
.fb-detail__url { overflow-wrap: anywhere; }
.fb-detail__resolve { margin-top: var(--space-4); }
/* The resolver's message field (feedback #17): full-width above the button,
   so the placeholder — the default that lands if it stays empty — is
   readable. */
.fb-detail__resolve input[name="message"] {
  width: 100%; margin-bottom: var(--space-2);
}
/* What the resolver said, shown under the "Resolved … by …" line. */
.fb-detail__resolution {
  color: var(--text-muted); font-size: var(--text-sm);
  border-left: 3px solid var(--border); padding-left: var(--space-3);
  margin-top: var(--space-2);
}

/* Diagnostics key/value grid on a report. */
.fb-diag {
  display: grid;
  grid-template-columns: max-content 1fr;
  gap: 2px var(--space-4);
  margin: 0 0 var(--space-4);
}
.fb-diag dt { color: var(--text-muted); font-size: var(--text-sm); }
.fb-diag dd { margin: 0; color: var(--text-primary); font-size: var(--text-sm); }

/* Report list: one row per report, artefacts on the row. */
.fb-list { list-style: none; margin: 0; padding: 0; }
.fb-row {
  display: flex;
  align-items: flex-start;
  gap: var(--space-3);
  padding: var(--space-3) 0;
  border-top: 1px solid var(--border-subtle);
}
.fb-row:first-child { border-top: 0; }
.fb-row__main { flex: 1; min-width: 0; }
.fb-row__note {
  display: -webkit-box;
  -webkit-line-clamp: 2;          /* clamp for layout, not truncation — the */
  -webkit-box-orient: vertical;   /* full note stays in the DOM and on the  */
  overflow: hidden;               /* detail view */
  color: var(--text-primary);
  font-weight: 600;
  text-decoration: none;
}
.fb-row__note:hover { text-decoration: underline; }
.fb-row__meta {
  display: flex;
  flex-wrap: wrap;
  gap: var(--space-2);
  margin-top: 2px;
  color: var(--text-muted);
  font-size: var(--text-xs);
}
.fb-row__tags { display: flex; flex-wrap: wrap; gap: 4px; flex: 0 0 auto; }
.fb-row__action {
  display: flex;
  align-items: center;
  gap: var(--space-2);
  flex: 0 0 auto;
}
.fb-tag {
  padding: 1px 8px;
  border-radius: var(--radius-pill);
  border: 1px solid var(--border);
  background: var(--bg-muted);
  color: var(--text-secondary);
  font-size: var(--text-xs);
  white-space: nowrap;
}
.fb-tag--terminal { background: var(--status-info-bg); color: var(--status-info-fg); border-color: transparent; }
.fb-tag--err      { background: var(--status-err-bg);  color: var(--status-err-fg);  border-color: transparent; }

@media (max-width: 640px) {
  .fb-row { flex-wrap: wrap; }
  .fb-row__main { flex: 1 0 100%; }
}

/* ---- Satisfaction scorecard ----------------------------------------- */

.sat-scorecards {
  display: flex; gap: var(--space-3);
  margin-bottom: var(--space-3);
}
.sat-score {
  display: flex; flex-direction: column; gap: var(--space-1);
  min-width: 200px;
}
.sat-score__label {
  font-size: var(--text-sm); color: var(--text-secondary);
}
.sat-score__value {
  font-size: var(--text-2xl); font-weight: 600; line-height: 1.1;
}


table.feedback-table .feedback-message {
  white-space: pre-wrap;
  word-break: break-word;
  max-width: 480px;
}


/* ---- Share panel ----------------------------------------------------- */

form.share-form .share-fieldset {
  border: 0; padding: 0; margin: 0; min-width: 0;
}
form.share-form .share-fieldset legend {
  padding: 0; margin: var(--space-3) 0 var(--space-1);
  font-size: var(--text-sm); font-weight: 500; color: var(--text-primary);
}
form.share-form .share-radio {
  display: flex; align-items: flex-start; gap: var(--space-2);
  margin: var(--space-2) 0;
  font-size: var(--text-sm);
  font-weight: 400;          /* override the bold label default */
  color: var(--text-secondary);
}
form.share-form .share-radio input[type="radio"] {
  margin: 4px 0 0 0;
  width: auto;
  flex: 0 0 auto;
}
form.share-form .share-radio strong { color: var(--text-primary); }

/* Port + Visibility side by side so the Share card uses its width instead of
   leaving the right half empty. Each column min ~13rem; they wrap (stack) on a
   narrow card. The port number input fills its column (it isn't caught by the
   generic text-input width:100% rule), so no dead space beside it either. */
form.share-form .share-grid {
  display: flex;
  flex-wrap: wrap;
  gap: var(--space-3) var(--space-6);
  align-items: flex-start;
}
form.share-form .share-grid > * { flex: 1 1 13rem; min-width: 0; }
form.share-form .share-grid__port input { width: 100%; }

/* Fira Sans / Fira Mono (OFL, self-hosted, latin subset) — the 76DEV typeface.
   See docs/CONSOLE_UI_REFRESH.md. */
@font-face { font-family: "Fira Sans"; font-style: normal; font-weight: 400;
  font-display: swap; src: url("/_static/fonts/fira/fira-sans-400.woff2") format("woff2"); }
@font-face { font-family: "Fira Sans"; font-style: normal; font-weight: 500;
  font-display: swap; src: url("/_static/fonts/fira/fira-sans-500.woff2") format("woff2"); }
@font-face { font-family: "Fira Sans"; font-style: normal; font-weight: 600;
  font-display: swap; src: url("/_static/fonts/fira/fira-sans-600.woff2") format("woff2"); }
@font-face { font-family: "Fira Sans"; font-style: normal; font-weight: 700;
  font-display: swap; src: url("/_static/fonts/fira/fira-sans-700.woff2") format("woff2"); }
@font-face { font-family: "Fira Mono"; font-style: normal; font-weight: 400;
  font-display: swap; src: url("/_static/fonts/fira/fira-mono-400.woff2") format("woff2"); }
@font-face { font-family: "Fira Mono"; font-style: normal; font-weight: 500;
  font-display: swap; src: url("/_static/fonts/fira/fira-mono-500.woff2") format("woff2"); }

/* Brand footer (base.html) — Fira Sans, muted, centered. */
.app-footer {
  text-align: center;
  padding: 24px 16px 32px;
  color: var(--text-muted);
  font-size: var(--text-sm);
  font-family: var(--font-sans);
}
.app-footer a { color: var(--text-muted); text-decoration: none; }
.app-footer a:hover { color: var(--brand-primary); text-decoration: underline; }
.app-footer__brand { font-weight: 700; letter-spacing: 0.02em; }

/* Release-notes list on /admin/software. */
.release-notes { margin: 0; padding: 12px 16px 16px 32px; }
.release-notes li { margin: 4px 0; color: var(--text-primary); }

/* ============================================================================
   Front door — the unauthenticated console root (templates/front_door.html).
   Standalone priming page (Variant C: split, Ink Navy). Page-specific layout;
   deliberately always dark (Q4) -- every --fd-* custom property comes from
   the active theme's DARK map, injected unconditionally by front_door.html,
   so the panel stays navy under every theme and every light/dark mode.
   Contrast verified AA (508): muted greys >= 5.07:1 on the navy panels (the
   two Q4-snapped greys moved from 5.18:1/7.91:1 to 6.11:1/8.32:1 -- upward
   only, so this floor still holds).
   ============================================================================ */
.fd { min-height: 100vh; display: flex; flex-direction: column; background: var(--fd-bg-page);
      color: var(--fd-text-primary); font-family: var(--font-sans); }
/* Brand bar: lockup on the left; motto and both CTAs on the right. Sign-in is
   on screen before any copy, which is what a console people return to owes
   them. Wordmark in white (the neutral), motto in the theme's link tone. */
.fd__bar { display: flex; align-items: center; justify-content: space-between; gap: 16px;
      padding: 20px 5vw; background: var(--fd-bg-sunken);
      border-bottom: 1px solid var(--fd-border-subtle); }
.fd__brand-lockup { display: flex; align-items: center; gap: 12px; }
.fd__brand-lockup img { height: 40px; width: auto; }
.fd__brand-name { font-weight: 700; font-size: 23px; line-height: 1.1; color: #fff; letter-spacing: -0.01em; }
.fd__brand-tag { font-size: 12px; font-weight: 700; letter-spacing: 0.14em; text-transform: uppercase;
      color: var(--fd-brand-link); white-space: nowrap; margin-right: 8px; }
.fd__bar-cta { display: flex; align-items: center; gap: 10px; }
.fd__bar .fd__btn { padding: 11px 20px; font-size: 15px; }
/* Hero band: kicker, headline, and the rest of the subhead on the left; the
   claim card (the subhead's opening sentences) on the right, so the one
   sentence the page exists for is what the eye lands on. */
.fd__hero { display: grid; grid-template-columns: 1.15fr 0.85fr; gap: 24px 64px;
      padding: 44px 5vw 36px; align-items: start; }
.fd__lead { display: flex; flex-direction: column; gap: 16px; }
.fd__kicker { font-size: 20px; font-weight: 600; line-height: 1.35;
      color: var(--fd-text-secondary); max-width: 42ch; margin: 0 0 -2px; }
.fd h1 { font-size: clamp(28px, 3vw, 44px); font-weight: 700; color: #fff;
      line-height: 1.12; max-width: 24ch; margin: 0; }
.fd__sub { font-size: 18px; color: var(--fd-text-secondary); line-height: 1.55; max-width: 46ch; margin: 0; }
.fd__actions { display: flex; gap: 10px; }
.fd__actions--mobile { display: none; margin-top: 4px; }
.fd__claim { background: var(--fd-bg-sunken); border: 1px solid var(--fd-border-subtle);
      border-radius: 14px; padding: 26px 28px; display: flex; flex-direction: column;
      gap: 12px; margin-top: 30px; }
.fd__claim-cap { margin: 0; font-size: 11px; font-weight: 700; letter-spacing: 0.13em;
      text-transform: uppercase; color: var(--fd-brand-link); }
.fd__claim p { margin: 0; font-size: 18px; line-height: 1.5; color: var(--fd-text-primary); }
/* Capabilities: one row across the full width. auto-fit so a white-labeled
   list of three or five items still fills the row. */
.fd__caps { display: grid; grid-template-columns: repeat(auto-fit, minmax(220px, 1fr));
      gap: 28px; padding: 0 5vw 34px; list-style: none; margin: 0; }
.fd__cap { display: flex; flex-direction: column; gap: 12px; }
.fd__cap-n { flex: none; width: 30px; height: 30px; border-radius: 8px;
      background: var(--fd-cap-bg); display: flex; align-items: center;
      justify-content: center; color: var(--fd-brand-link); font-weight: 700; margin-top: 2px; }
.fd__cap h3 { font-size: 17px; font-weight: 600; color: #fff; margin: 0 0 3px; }
.fd__cap p { font-size: 15px; color: var(--fd-text-muted); line-height: 1.5; margin: 0; }
.fd__btn { display: inline-block; font: inherit; font-weight: 600; font-size: 16px;
      border-radius: 10px; padding: 14px 26px; text-decoration: none;
      border: 2px solid transparent; text-align: center; }
.fd__btn--primary { background: var(--fd-brand-primary); color: #fff;
      box-shadow: 0 6px 20px var(--fd-primary-shadow); }
.fd__btn--ghost { background: transparent; color: var(--fd-brand-link); border-color: var(--fd-brand-link); }
.fd__fine { font-size: 13px; color: var(--fd-text-faint); }
/* Proof line: the shipped-control regime behind the ATO evidence, pinned to
   the bottom edge of the viewport by margin-top: auto. */
.fd__foot { margin-top: auto; display: flex; align-items: center; gap: 18px;
      padding: 18px 5vw 26px; border-top: 1px solid var(--fd-border-subtle); }
.fd__proof-cap { margin: 0; font-size: 11px; font-weight: 700; letter-spacing: 0.13em;
      text-transform: uppercase; color: var(--fd-text-faint); white-space: nowrap; }
.fd__proof { display: flex; flex-wrap: wrap; gap: 8px; list-style: none; padding: 0; margin: 0; }
.fd__proof li { font-size: 12.5px; font-weight: 600; color: var(--fd-text-secondary);
      border: 1px solid var(--fd-border-subtle); border-radius: 999px; padding: 5px 12px; }
/* Phone: the bar keeps lockup + Log in only; both CTAs repeat under the
   subhead; hero and capabilities go single-column with the number beside the
   title again; the proof line stacks. */
@media (max-width: 860px) { .fd__bar { padding: 14px 20px; }
      .fd__bar .fd__brand-tag, .fd__bar .fd__btn--ghost { display: none; }
      .fd__brand-lockup img { height: 32px; }
      .fd__brand-name { font-size: 20px; }
      .fd__hero { grid-template-columns: 1fr; gap: 18px; padding: 28px 20px 24px; }
      .fd__actions--mobile { display: flex; }
      .fd__actions--mobile .fd__btn { flex: 1; padding: 13px 18px; font-size: 15px; }
      .fd__claim { margin-top: 0; padding: 20px; }
      .fd__claim p { font-size: 16px; }
      .fd__caps { grid-template-columns: 1fr; gap: 18px; padding: 0 20px 28px; }
      .fd__cap { flex-direction: row; gap: 14px; align-items: flex-start; }
      .fd__cap p { font-size: 14px; }
      .fd__foot { flex-direction: column; align-items: flex-start; gap: 10px; padding: 18px 20px 28px; } }

/* ---------------------------------------------------------------------------
   Admin users page — Tailscale-style infra list.

   One calm row per person carrying its real state; routine controls revealed
   on demand, destructive ones behind a per-row menu. Every disclosure is a
   <details>, the same zero-JS primitive .app-header__admin already uses, so
   this adds no client-side behaviour to a server-rendered page.
   --------------------------------------------------------------------------- */

/* Disclosure — the invite form and each row's quota editor. */
.disclosure > summary.disclosure__summary {
  list-style: none;
  cursor: pointer;
  display: flex; align-items: baseline; gap: var(--space-2);
  font-weight: 600;
  color: var(--text-heading);
  padding: var(--space-3) var(--space-4);
}
.disclosure > summary.disclosure__summary::-webkit-details-marker { display: none; }
.disclosure > summary.disclosure__summary::before {
  content: "\25B8";                       /* ▸, rotates when open */
  color: var(--text-muted);
  transition: transform .12s ease;
}
.disclosure[open] > summary.disclosure__summary::before { transform: rotate(90deg); }
.disclosure > summary.disclosure__summary:hover { background: var(--bg-hover); }
.disclosure__summary--sm {
  font-weight: 500;
  font-size: var(--text-sm);
  color: var(--text-secondary);
  padding: var(--space-2) 0 !important;
}
.disclosure__body { padding: 0 var(--space-4) var(--space-4); }
.disclosure--inline { border: none; background: none; }
.disclosure--inline .disclosure__body { padding: var(--space-2) 0 var(--space-3); }

/* The quota field cluster — shared by the invite form and the row editors, so
   the two can never drift into different shapes. */
.qedit {
  display: flex; flex-wrap: wrap; align-items: flex-end;
  gap: var(--space-3);
}
.qedit__field { display: flex; flex-direction: column; gap: var(--space-1); }
.qedit__field--grow { flex: 1 1 16rem; }
.qedit__field label {
  font-size: var(--text-xs); font-weight: 600;
  text-transform: uppercase; letter-spacing: 0.04em;
  color: var(--text-muted);
}
.qedit__field input { width: 100%; min-width: 7rem; }
.qedit__field--grow input { min-width: 0; }

/* Usage cells: the number stays primary, the bar is the glance. */
.usage__text { display: block; font-size: var(--text-sm); white-space: nowrap; }
.usage__bar { height: 6px; margin-top: var(--space-1); min-width: 4.5rem; }
td.usage { min-width: 7rem; }

.user__email { font-weight: 500; }

/* The quota-editor row hangs off the row above it — no top border, so the two
   read as one unit rather than two list entries. */
tr.qedit-row > td { border-top: none; padding-top: 0; }

/* Per-row overflow menu. Right-aligned so it can't drift under the viewport
   edge on the last column. */
.rowmenu { position: relative; display: inline-block; }
.rowmenu > summary.rowmenu__trigger {
  list-style: none; cursor: pointer;
  display: inline-flex; align-items: center; justify-content: center;
  width: 2rem; height: 2rem;
  border: 1px solid transparent; border-radius: var(--radius-sm);
  color: var(--text-secondary);
  font-size: var(--text-lg); line-height: 1;
}
.rowmenu > summary.rowmenu__trigger::-webkit-details-marker { display: none; }
.rowmenu > summary.rowmenu__trigger:hover {
  background: var(--bg-hover); border-color: var(--border);
  color: var(--text-heading);
}
.rowmenu[open] > summary.rowmenu__trigger {
  background: var(--bg-hover); border-color: var(--border);
}
.rowmenu__menu {
  position: absolute; top: 100%; right: 0; z-index: 40;
  margin-top: var(--space-1);
  min-width: 10rem;
  display: flex; flex-direction: column;
  padding: var(--space-1);
  background: var(--bg-surface);
  border: 1px solid var(--border);
  border-radius: var(--radius-md);
  box-shadow: var(--shadow-md);
  text-align: left;
}
.rowmenu__item {
  display: block; width: 100%; text-align: left;
  padding: var(--space-2) var(--space-3);
  background: none; border: none; cursor: pointer;
  font: inherit; font-size: var(--text-sm);
  color: var(--text-primary); text-decoration: none;
  border-radius: var(--radius-sm);
}
.rowmenu__item:hover { background: var(--bg-hover); }
.rowmenu__item--danger { color: var(--danger-fg); }

/* Narrow screens: the usage bars are the first thing to give up their width. */
@media (max-width: 60rem) {
  .usage__bar { display: none; }
  td.usage { min-width: 0; }
}

/* ---------------------------------------------------------------------------
   New-workspace form — DigitalOcean droplet-create shape.

   Prerequisites lead (they disable the whole form), then one numbered section
   per decision. Every choice is a styled radio, so the picker grids need no
   JavaScript and keep native keyboard and screen-reader behaviour.
   --------------------------------------------------------------------------- */

.prereq { border-left: 3px solid var(--border-strong); }
.prereq--err { border-left-color: var(--danger-border); }
.prereq strong { color: var(--text-heading); }

.wsnew__section { margin-bottom: var(--space-4); }
.wsnew__step {
  display: flex; align-items: center; gap: var(--space-3);
  margin: 0 0 var(--space-3);
  font-size: var(--text-lg);
}
.wsnew__num {
  display: inline-flex; align-items: center; justify-content: center;
  width: 1.6rem; height: 1.6rem; flex: none;
  border-radius: var(--radius-pill);
  background: var(--bg-muted); color: var(--text-secondary);
  font-size: var(--text-sm); font-weight: 700;
}
.wsnew__name { max-width: 26rem; }

/* Card pickers. The radio itself is visually hidden but still focusable, so
   :focus-visible on the input styles the label — keyboard users get a real
   focus ring rather than an invisible one. */
.pickgrid {
  display: grid; gap: var(--space-3);
  grid-template-columns: repeat(auto-fit, minmax(14rem, 1fr));
}
.pickgrid--plans { grid-template-columns: repeat(auto-fit, minmax(11rem, 1fr)); }

.pick { position: relative; display: block; cursor: pointer; }
.pick > input {
  position: absolute; opacity: 0;
  width: 1px; height: 1px; margin: 0;
}
.pick__body {
  display: flex; flex-direction: column; gap: var(--space-1);
  height: 100%;
  padding: var(--space-3) var(--space-4);
  background: var(--bg-surface);
  border: 1px solid var(--border);
  border-radius: var(--radius-md);
  transition: border-color .1s, box-shadow .1s, background .1s;
}
.pick:hover > input:not(:disabled) + .pick__body { border-color: var(--border-strong); }
.pick > input:checked + .pick__body {
  border-color: var(--brand-primary);
  box-shadow: 0 0 0 1px var(--brand-primary);
  background: var(--bg-hover);
}
.pick > input:focus-visible + .pick__body {
  outline: 2px solid var(--brand-primary-ring);
  outline-offset: 2px;
}
.pick__title { font-weight: 600; color: var(--text-heading); }
.pick__meta, .pick__desc {
  font-size: var(--text-sm); color: var(--text-secondary);
}
.pick__specs {
  display: flex; gap: var(--space-4);
  font-size: var(--text-sm); color: var(--text-secondary);
}
.pick__specs strong { color: var(--text-heading); }

/* Base-image cards must all render at one size (#367). The description is the
   only variable-length field, and the grid stretches cards within a row — so a
   long one doesn't grow its own card, it grows its whole ROW, and two rows of
   images stop lining up. Fixing the description to exactly three lines settles
   it from both sides: clamped so a long description can't push the row taller,
   min-height so a short one can't leave it shorter. The shipped descriptions
   are written to fit (test_base_image_cards.py); the clamp is what keeps an
   operator's own manifest from breaking the grid. Scoped to this grid because
   the plan and egress pickers reuse .pick__desc with short, uniform copy that
   a three-line floor would only pad out. */
.pickgrid--images .pick__desc {
  display: -webkit-box;
  -webkit-line-clamp: 3;
  -webkit-box-orient: vertical;
  overflow: hidden;
  line-height: 1.4;
  min-height: calc(3 * 1.4em);
}

/* Unavailable plan: dimmed, not-allowed, and it says why. Never silently grey. */
.pick--disabled { cursor: not-allowed; }
.pick--disabled .pick__body {
  background: var(--bg-sunken);
  border-style: dashed;
  opacity: .65;
}
.pick__blocked {
  font-size: var(--text-xs); font-weight: 600;
  text-transform: uppercase; letter-spacing: 0.03em;
  color: var(--danger-fg);
}

/* Summary sits with the action so "what am I about to make" and "make it" are
   the same glance. */
.wsnew__summary {
  display: flex; flex-wrap: wrap; gap: var(--space-4);
  align-items: center; justify-content: space-between;
}
.wsnew__summary-text { flex: 1 1 20rem; }
.wsnew__summary-text p { margin: 0 0 var(--space-1); }
.wsnew__summary .form-actions { margin: 0; display: flex; gap: var(--space-2); }

/* ---------------------------------------------------------------------------
   Show and Tell + the workspaces activity ticker (plan 2026-08-08-002).
   One list style for both surfaces so a share reads the same wherever it is
   seen; the ticker adds the one-row constraint on top.
   --------------------------------------------------------------------------- */

.activity-announce { margin-bottom: var(--space-4); }

.activity-list { list-style: none; margin: 0; padding: 0; }

.activity__item {
  display: flex; align-items: center; justify-content: space-between;
  gap: var(--space-3);
  padding: var(--space-3) var(--space-4);
  border-top: 1px solid var(--border);
}
.activity__item:first-child { border-top: 0; }

.activity__main { min-width: 0; }
.activity__name { font-weight: 600; }
.activity__meta {
  display: flex; flex-wrap: wrap; gap: var(--space-3);
  color: var(--text-muted); font-size: var(--text-sm);
}

/* Published shares as grid widgets (feedback report 8). As full-width rows a
   share left its middle empty — a share is four short fields, and one share on
   a wide panel stretched the whole width to hold them. auto-fill + minmax is
   what makes it a widget rather than a fixed layout: the column count comes
   from the available width, so it is one column on a phone and several on a
   desktop without a breakpoint.

   A separate pair from .smarty-grid/.smarty-card on purpose. The shape is the
   same and deliberately so, but those names belong to the smarty catalog;
   sharing them would couple two unrelated surfaces to one rule. */
.published-grid {
  list-style: none; margin: 0; padding: 0;
  display: grid; gap: var(--space-3);
  /* min(240px, 100%), not a bare 240px: inside the page and card padding a
     328px phone leaves 232px, and a bare minimum would hold the track at
     240px and push the card 8px past the edge. The min() clamps the single
     column to the container instead. */
  grid-template-columns: repeat(auto-fill, minmax(min(240px, 100%), 1fr));
}
.published-card {
  display: flex; flex-direction: column; gap: var(--space-1);
  border: 1px solid var(--border); border-radius: var(--radius-md);
  padding: var(--space-3) var(--space-4); background: var(--bg-muted);
}
/* Name left, audience pill right — the pill must not push the name's line
   down, hence flex-start rather than center. */
.published-card__head {
  display: flex; justify-content: space-between; align-items: flex-start;
  gap: var(--space-2);
}
.published-card__tags { display: flex; gap: var(--space-2); flex-wrap: wrap; }

/* A share whose workspace is stopped is de-emphasized, not hidden (R8): the
   entry still says the work exists and names why the link won't answer yet. */
.published-card--down { opacity: .6; }

/* What the resolver said, leading a Recently-fixed entry (feedback #18);
   the reporter/date meta line drops to a supporting role beneath it. */
.activity__message { margin-bottom: var(--space-1); }

/* The ticker. One row, always: `overflow: hidden` on the item strip is what
   implements "as many as fit" (KTD4) — the server sends every item because it
   cannot see the viewport, and the clip decides how many are seen. A taller
   strip would push the workspace table below the fold on a laptop, which is
   the thing this must not do. */
.activity-ticker {
  display: flex; align-items: center; gap: var(--space-3);
  margin-bottom: var(--space-4);
  padding: var(--space-2) var(--space-3);
  background: var(--bg-sunken);
  border: 1px solid var(--border);
  border-radius: var(--radius-md);
}
.activity-ticker__items {
  display: flex; align-items: center; gap: var(--space-4);
  flex: 1 1 auto;
  min-width: 0;            /* let the flex item shrink so the clip applies */
  overflow: hidden;
  white-space: nowrap;
}
.activity-ticker__item {
  display: inline-flex; align-items: baseline; gap: var(--space-2);
  flex: 0 0 auto;
  font-size: var(--text-sm);
}
.activity-ticker__item--down { opacity: .6; }
/* Never shrinks and never clips: the way to the full list has to survive
   however many items are in front of it. */
.activity-ticker__more {
  flex: 0 0 auto;
  font-size: var(--text-sm); font-weight: 600; white-space: nowrap;
}
