:root {
  /* Visual identity refresh (2026-09-25) — see DESIGN.md's "Visual identity
     refresh" entry for the full reasoning and the 4-option comparison this
     came out of ("Soft Approachable"). This is a VALUE swap under the
     EXISTING token names, not a rename: --navy/--accent/--btn-primary etc.
     keep their established roles (structural chrome, interactive accent,
     action buttons) so every one of the ~40 rules referencing them below
     doesn't need touching individually — only what each token resolves to
     changes. --navy was a literal navy blue and is now a deep violet
     (still the "structural/authority" colour — panel headers, active tab
     state, brand text); --accent moves from teal to the same violet family
     as the new brand accent; --btn-primary is now the SAME hue as --accent
     (previously a third, distinct blue) — one consistent brand hue for
     "interactive," not three different blues/teals for three unrelated
     roles, which was part of what read as an unbranded default before. */
  --navy: #4a3e8c;
  --navy-dark: #332b66;
  --hub-phase: #0e7490; /* Gantt phase bars: teal, apart from the purple tasks */
  --hub-phase-bg: #e3f2f5; /* the whole phase row, in every Hub list */
  --accent: #7c5cfc;
  --accent-light: #efe9fe;
  --ink: #241f38;
  --muted: #7a7290;
  --border: #e7e1f5;
  --bg: #f6f4fb;
  --surface: #ffffff;
  --warn: #b07a12;
  --warn-bg: #fdefd9;
  --ok: #22935a;
  --ok-bg: #e1f5e7;
  --danger: #c24c58;
  --radius: 10px;
  /* A second, larger radius for the prominent hero/card components the
     refresh introduced (theme cards, the hero ring panel) — deliberately
     not a global --radius bump, which would also round every dense table
     cell and input to the same degree and hurt legibility there. */
  --radius-lg: 18px;

  /* Sidebar nav (2026-10-05 — see DESIGN.md's "Sidebar navigation replaces
     the top bar" entry). Widths match the confirmed mockup exactly:
     256px expanded, 68px collapsed (icon rail) — same numbers cms1's own
     SidebarNav uses (w-64/w-[68px]), reused here because they're sensible
     sizes, not because of where they came from; every colour below is
     this site's own token, never cms1's. --sidebar-mobile-h is the slim
     top-bar height the sidebar collapses into below the mobile breakpoint
     (styles.css's existing 640px convention — see the Mobile section). */
  --sidebar-w: 256px;
  --sidebar-w-collapsed: 68px;
  --sidebar-mobile-h: 56px;

  /* Display type, layered on top of headings and big numbers only — body
     copy stays on the system stack below. Loaded via a Google Fonts
     <link> in every page's <head> — see DESIGN.md's "Visual identity
     refresh" entry for the exact families/weights loaded. */
  --font-display: "Sora", -apple-system, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;

  /* Button colour — same token as --accent now (see the refresh comment
     above): still its own named token, not a direct reuse of --cat-1,
     even though it's the same hex: --cat-1 is reserved for policy-
     category identity (WHS) per this file's "never reuse one family for a
     different meaning" rule (see the Categorical palette comment below),
     and .btn is a generic action colour used everywhere, unrelated to any
     one category — reusing --cat-1 itself here would make a plain "Save"
     button visually claim "WHS category". --btn-primary-hover is a darker
     step of the same violet, matching this site's existing light/dark
     button-hover relationship. */
  --btn-primary: #7c5cfc;
  --btn-primary-hover: #5a3fe0;

  /* Status palette (fixed — represents state, never re-themed or reused for
     categorical series). good/warning/serious map to Complete / For review /
     Needs update throughout the dashboards. */
  /* Values updated as part of the 2026-09-25 visual identity refresh —
     the same good/warn/serious tones the --status-green/-amber/-red
     shared primitives above now use (kept as separate variables, not
     merged into one family: this one drives .badge.ok/warn/serious/muted
     and .kpi-tile.good/warning/serious specifically, a different call
     site set than the risk/burn/stakeholder families, per this file's
     established "each family keeps its own class names" rule). */
  --status-good: #22935a;
  --status-good-bg: #e1f5e7;
  --status-warning: #b07a12;
  --status-warning-bg: #fdefd9;
  --status-serious: #c24c58;
  --status-serious-bg: #fce3e5;
  --status-muted: #7a7290;
  --status-muted-bg: #efe9fe;

  /* Categorical palette (fixed order — never reordered/reassigned), one slot
     per policy section. Six of the eight validated default slots. */
  --cat-1: #2a78d6; /* blue    — Work Health and Safety */
  --cat-2: #eb6834; /* orange  — Human Resources */
  --cat-3: #1baf7a; /* aqua    — Time and Expense Management */
  --cat-4: #eda100; /* yellow  — Ethics */
  --cat-5: #e87ba4; /* magenta — IT, Privacy and Data Management */
  --cat-6: #008300; /* green   — Terms and Conditions */

  /* Spacing scale (2026-09-22, "make aha_tools feel as spacious as cms1"
     pass). cms1 (a Tailwind app, default spacing scale) reads noticeably
     more spacious than this site did — not a different visual language,
     just consistently bigger numbers at every step: its card padding is
     p-6 (24px all sides, HomePage.tsx's shortcut tiles/panels), its
     stat/shortcut tile grid gap is gap-5 (20px), its panel header padding
     is px-6 py-4 (24px/16px), its list-row padding is px-5 py-3.5
     (20px/14px), and its major section margins are mb-8 (32px) — extracted
     directly from cms1's own source (HomePage.tsx, AppointmentsListCard.tsx,
     ClientDirectory.tsx), not guessed. This site's equivalents were roughly
     half that at every comparable spot (.panel-body: 0.7rem 0.85rem ≈
     11px/14px; .kpi-row gap: 0.6rem ≈ 9.6px; table cells: 0.45rem 0.7rem ≈
     7px/11px) — which is what actually reads as "cramped" next to cms1,
     not a colour or component difference (those were already addressed in
     the earlier cms1-comparison pass above, for hover/text colour).

     One scale, referenced everywhere spacing is set on a shared component
     (.panel/.panel-header/.panel-body, .card, .kpi-row/.kpi-tile,
     table.dashboard-table cells, .policy-row, main's own top-level
     padding, .tabs/.filter-row/.search-bar/.category-filters/.btn-row) —
     extend this scale for new spacing rather than hardcoding a new rem
     value, the same "one token system, not scattered magic numbers"
     discipline the colour tokens above already follow. Not a 1:1 copy of
     Tailwind's own scale (that has far more steps than this site needs) —
     just enough steps to cover every existing spacing value in this file,
     each one now sized to actually close the gap with cms1's numbers
     above rather than sitting at roughly half of them. */
  --space-1: 0.25rem;  /* 4px  — tightest gaps (badge dot, icon-to-label) */
  --space-2: 0.5rem;   /* 8px  — small inline gaps */
  --space-3: 0.75rem;  /* 12px — compact padding (panel-header, table cells) */
  --space-4: 1rem;     /* 16px — standard padding/margin step */
  --space-5: 1.25rem;  /* 20px — cms1's list-row/tile-grid-gap step */
  --space-6: 1.5rem;   /* 24px — cms1's card padding (p-6) */
  --space-7: 2rem;     /* 32px — cms1's major section margin (mb-8) */
  --space-8: 3rem;     /* 48px — page bottom padding */

  /* Base type size/leading bumped alongside the spacing scale — cms1 (an
     unset Tailwind app) renders body text at Tailwind's 16px/1.5 default;
     this site's 15px/1.45 wasn't itself the main source of the "cramped"
     read (the spacing values above were), but a slightly larger base size
     and looser leading is part of the same "more breathing room" request
     and costs nothing else to move alongside the spacing bump. */
  font-size: 16px;
}

* { box-sizing: border-box; }

body {
  margin: 0;
  font-family: -apple-system, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
  color: var(--ink);
  background: var(--bg);
  line-height: 1.5;
}

a { color: var(--accent); }

/* Sidebar nav (2026-10-05 — replaces the 2026-09-25 horizontal top bar;
   see DESIGN.md's "Sidebar navigation replaces the top bar" entry for the
   full design/reasoning/verification). `#site-header` is still the exact
   same `<header id="site-header">` element every `docs/*.html` shell
   already had — no markup changed on any page, only this CSS (fixed-
   position, vertical) and nav.js's own innerHTML (a sidebar shell instead
   of a flat link row). `body`'s own margin-left is what makes `main`/
   `footer` reflow around it (see the body rule beneath main below) rather
   than overlapping it — `header` is removed from normal flow entirely
   (position:fixed), so it no longer needs flex alignment with siblings
   the way the old horizontal bar did.

   Three children, one skeleton (nav.js's sidebarShellHtml()), restyled by
   this file depending on viewport rather than rendered twice:
   `.sidebar-topbar` (mobile-only slim brand+hamburger strip, hidden on
   desktop), `.sidebar-panel` (the actual nav surface — IS the sidebar on
   desktop, becomes a slide-in drawer on mobile once opened), and
   `.sidebar-backdrop` (mobile-only, click-to-close dim layer, hidden on
   desktop). See the Mobile section further down for the breakpoint
   override. */
.site-header {
  position: fixed;
  top: 0;
  left: 0;
  bottom: 0;
  width: var(--sidebar-w);
  background: var(--surface);
  color: var(--ink);
  border-right: 1px solid var(--border);
  box-shadow: 2px 0 14px rgba(124, 92, 252, 0.08);
  display: flex;
  flex-direction: column;
  z-index: 40;
  transition: width 0.18s ease;
}
.site-header.collapsed { width: var(--sidebar-w-collapsed); }

.site-header .sidebar-panel {
  display: flex;
  flex-direction: column;
  height: 100%;
  width: 100%;
  overflow: hidden;
}
/* Mobile-only elements — hidden on desktop; see the Mobile section for
   where each one turns back on. */
.site-header .sidebar-topbar,
.site-header .sidebar-backdrop,
.site-header .sidebar-close-btn { display: none; }

.site-header .sidebar-head {
  padding: 0.9rem 1rem;
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 0.5rem;
  border-bottom: 1px solid var(--border);
  flex: none;
}
.site-header .sidebar-brand,
.site-header .sidebar-topbar .sidebar-brand {
  display: flex;
  align-items: center;
  gap: 0.55rem;
  font-family: var(--font-display);
  font-weight: 700;
  font-size: 0.95rem;
  color: var(--ink);
  text-decoration: none;
  min-width: 0;
}
.site-header .sidebar-logo {
  height: 28px;
  width: 28px;
  border-radius: 8px;
  display: block;
  flex: none;
  object-fit: contain;
}
.site-header .sidebar-brand-text {
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}
.site-header.collapsed .sidebar-brand-text { display: none; }

.site-header .sidebar-collapse-btn {
  background: none;
  border: none;
  color: var(--muted);
  cursor: pointer;
  padding: 0.3rem;
  border-radius: 8px;
  display: inline-flex;
  flex: none;
  transition: background .15s ease, color .15s ease, transform .18s ease;
}
.site-header .sidebar-collapse-btn:hover { background: var(--accent-light); color: var(--navy); }
.site-header.collapsed .sidebar-collapse-btn { transform: scaleX(-1); }
/* Collapsed: the brand row has only the toggle left, so centre it instead
   of space-between against an invisible logo+text pair. */
.site-header.collapsed .sidebar-head { justify-content: center; }
.site-header.collapsed .sidebar-head .sidebar-brand { display: none; }

.site-header .sidebar-nav {
  flex: 1;
  overflow-y: auto;
  padding: 0.5rem 0 0.75rem;
}
.site-header .sidebar-section-label {
  padding: 0.9rem 1rem 0.3rem;
  font-size: 0.66rem;
  font-weight: 700;
  letter-spacing: 0.07em;
  text-transform: uppercase;
  color: var(--muted);
  opacity: 0.85;
}
.site-header .sidebar-section-label:first-child { padding-top: 0.4rem; }
.site-header.collapsed .sidebar-section-label { display: none; }

.site-header .sidebar-item {
  display: flex;
  align-items: center;
  gap: 0.7rem;
  margin: 0 0.5rem 0.1rem;
  padding: 0.6rem 0.75rem;
  border-radius: 9px;
  color: var(--muted);
  text-decoration: none;
  font-size: 0.86rem;
  font-weight: 600;
  transition: background .15s ease, color .15s ease;
}
.site-header .sidebar-item svg { flex: none; }
.site-header .sidebar-item:hover { background: var(--accent-light); color: var(--navy); }
.site-header .sidebar-item.active { background: var(--accent); color: #fff; }
.site-header .sidebar-item-label {
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}
.site-header .sidebar-item-tag {
  margin-left: auto;
  font-size: 0.62rem;
  font-weight: 700;
  background: var(--warn-bg);
  color: var(--warn);
  border-radius: 5px;
  padding: 0.05rem 0.3rem;
  flex: none;
}
.site-header.collapsed .sidebar-item { justify-content: center; padding: 0.6rem; }
.site-header.collapsed .sidebar-item-label,
.site-header.collapsed .sidebar-item-tag { display: none; }

.site-header .sidebar-foot {
  flex: none;
  padding: 0.85rem 1rem;
  border-top: 1px solid var(--border);
  display: flex;
  align-items: center;
  gap: 0.55rem;
}
.site-header .sidebar-avatar {
  width: 28px;
  height: 28px;
  border-radius: 999px;
  background: var(--accent-light);
  color: var(--navy);
  font-size: 0.68rem;
  font-weight: 700;
  display: flex;
  align-items: center;
  justify-content: center;
  flex: none;
}
.site-header .sidebar-username {
  flex: 1;
  min-width: 0;
  font-size: 0.82rem;
  font-weight: 600;
  color: var(--ink);
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}
.site-header button.sidebar-signout {
  background: none;
  border: none;
  color: var(--muted);
  cursor: pointer;
  font-size: 0.78rem;
  font-weight: 600;
  padding: 0.3rem;
  flex: none;
}
.site-header button.sidebar-signout:hover { color: var(--navy); }
.site-header.collapsed .sidebar-foot { justify-content: center; }
.site-header.collapsed .sidebar-username,
.site-header.collapsed button.sidebar-signout { display: none; }
/* Public (pre-auth) pages' foot has no avatar/name, just a "Sign in"
   sidebar-item — let it fill the row like any other nav item instead of
   the flex/gap rules above meant for the avatar+name+button layout. */
.site-header .sidebar-foot .sidebar-item { flex: 1; margin: 0; }

/* Every page's own content reflows around the fixed sidebar via a left
   margin on body — main/footer don't need their own offset since they're
   body's only remaining normal-flow children (header is position:fixed,
   out of flow). Kept on body rather than main/footer individually so a
   page that doesn't use the exact main/footer pair (there isn't one, but
   this is the single point of truth either way) still reflows correctly. */
body {
  margin-left: var(--sidebar-w);
  transition: margin-left 0.18s ease;
}
body.sidebar-collapsed { margin-left: var(--sidebar-w-collapsed); }

main {
  max-width: 1080px;
  margin: 0 auto;
  padding: var(--space-6) var(--space-5) var(--space-8);
}

main.wide { max-width: 1280px; }
.narrow { max-width: 520px; }

h1 { font-family: var(--font-display); font-weight: 700; font-size: 1.5rem; margin: 0 0 var(--space-2); }
h2 { font-family: var(--font-display); font-weight: 700; font-size: 1.05rem; margin: var(--space-7) 0 var(--space-3); }
.subtitle { color: var(--muted); margin-top: 0; font-size: 0.9rem; }

.back-link { display: inline-block; margin-bottom: var(--space-4); font-size: 0.85rem; }

/* A .cat-chip that's also a link (policy.html, pointing back to its theme
   on the landing page) — same look, just clickable, with its own small gap
   under the title so it doesn't read as part of it. */
/* transition is on text-decoration-color, the only property this hover
   state actually changes color-wise — text-decoration-line itself (the
   underline appearing) isn't a smoothly-interpolable property per the CSS
   spec, so this is a harmless, low-risk addition rather than a visibly
   animated fade; browsers that do support animating the line do so via a
   sudden flip, not this transition. */
a.cat-chip { margin: 0.1rem 0 0.5rem; text-decoration: none; cursor: pointer; transition: text-decoration-color .15s ease; }
a.cat-chip:hover { text-decoration: underline; }

.card {
  background: var(--surface);
  border: 1px solid var(--border);
  border-radius: var(--radius);
  padding: var(--space-5) var(--space-6);
  margin-bottom: var(--space-4);
}

.search-bar {
  display: flex;
  gap: 0.6rem;
  margin: var(--space-5) 0;
  flex-wrap: wrap;
}

input[type="search"] {
  padding: 0.45rem 0.65rem;
  border: 1px solid var(--border);
  border-radius: var(--radius);
  font-size: 0.9rem;
  font: inherit;
}

.search-bar input[type="search"] {
  flex: 1;
  min-width: 220px;
  padding: 0.5rem 0.75rem;
  font-size: 0.95rem;
}

.category-filters {
  display: flex;
  gap: 0.4rem;
  flex-wrap: wrap;
  margin-bottom: var(--space-5);
}

.chip {
  border: 1px solid var(--border);
  background: var(--surface);
  border-radius: 999px;
  padding: 0.3rem 0.8rem;
  font-size: 0.8rem;
  cursor: pointer;
  color: var(--ink);
}

.chip.active { background: var(--navy); color: #fff; border-color: var(--navy); }

.policy-list { list-style: none; margin: 0; padding: 0; display: grid; gap: var(--space-3); }
/* Grid items stretch to the row's tallest sibling by default, but that
   stretch lands on the <li> itself — its .policy-row/.module-row anchor
   child still sizes to its own content unless told to fill it. Matters
   only once a grid actually has multiple items per row (.module-grid-2col/
   -auto below); .policy-list's plain single-column use is unaffected since
   every row there only ever has one item anyway. */
.policy-list > li { display: flex; }

.policy-row {
  display: flex;
  flex: 1;
  justify-content: space-between;
  align-items: center;
  gap: 1rem;
  background: var(--surface);
  border: 1px solid var(--border);
  border-radius: var(--radius);
  padding: var(--space-4) var(--space-5);
  text-decoration: none;
  color: inherit;
}
.policy-row:hover { border-color: var(--accent); }
.policy-row .title { font-weight: 600; font-size: 0.93rem; }
.policy-row .summary { color: var(--muted); font-size: 0.85rem; margin-top: 0.1rem; }

/* Module-picker cards (index.html) + its tile grid — retired 2026-10-07
   along with modulePicker.js itself (see DESIGN.md's "Home page" entry).
   admin.js's Modules-management table still has a `.module-row` CLASS on
   its draggable <tr> (wireDragReorder()'s own selector target), but never
   depended on any of these rules for its own look — padding on a <tr> has
   no layout effect — so removing the CSS here changes nothing there.
   #module-picker-root no longer exists in any page either, so the old
   .module-grid-2col/-auto/.module-picker-wide tile-grid rules (here and in
   the mobile media query below) could never match anything any more. */

.badge {
  display: inline-flex;
  align-items: center;
  gap: 0.3rem;
  font-size: 0.72rem;
  font-weight: 600;
  padding: 0.15rem 0.55rem;
  border-radius: 999px;
  white-space: nowrap;
}
.badge.ok { background: var(--status-good-bg); color: var(--status-good); }
.badge.warn { background: var(--status-warning-bg); color: #8a6300; }
.badge.serious { background: var(--status-serious-bg); color: #b8451f; }
.badge.muted { background: var(--status-muted-bg); color: var(--status-muted); }
.badge.pending { background: var(--status-muted-bg); color: var(--status-muted); font-style: italic; }
.badge::before { content: ""; width: 6px; height: 6px; border-radius: 50%; background: currentColor; flex: none; }

/* Risk register Low/Medium/High — a traffic-light green/yellow/red,
   deliberately its own palette rather than reusing the ok/warn/serious
   status badges above, which mean something different (on track/due/
   overdue) and shouldn't visually collide with a risk score. Text colours
   are darkened well past their background for contrast (≈6:1 on medium,
   same approach as .badge.warn/.serious).

   Defined once as variables and reused by both .badge.risk-* (the
   read-only view) and select.risk-* (the admin-editable <select> in
   riskRegister.js) — the two were previously colour-coded independently,
   which is exactly how a colour fixed in one silently never reached the
   other: the badge got the right value, the <select> admins actually see
   and edit was never wired to any colour class at all. One set of values,
   both call sites reference it, so that gap can't reopen. */
/* Shared traffic-light primitives — --risk-*, --burn-*, --stakeholder-*
   below were three separately-named families with byte-identical hex
   values (general cleanup pass, 2026-09-23). DESIGN.md's own "different
   KIND of value gets its own tokens" rule (see the --burn-* / --stakeholder-*
   comments below) is about keeping those three families semantically
   separate — a risk score, a burn percentage, and a relations status are
   different things and each still gets its own class names and CSS
   variables, unchanged. What was actual duplication was the raw hex
   values themselves, repeated three times with no link between them, so a
   colour fixed in one family (or edited by a future pass) could silently
   drift from the other two. These --status-*-bg/fg primitives are the one
   place the literal colours live now; every family below aliases them via
   var() instead of repeating the hex, so the families can't drift apart
   again, but no class name, variable name or rendered colour changed. */
:root {
  /* Values updated as part of the 2026-09-25 visual identity refresh (see
     DESIGN.md's entry) — softened to match the new palette's warmer,
     less-saturated tone. Every family below (--risk-* /--burn-* /
     --stakeholder-*) aliases these, so this one edit is what carries the
     refresh into the WHS risk register, Timesheet burn badges and
     Stakeholder Tracker status badges automatically, per this block's own
     "one place the literal colours live" comment above. */
  --status-green-bg: #e1f5e7; --status-green-fg: #22935a;
  --status-amber-bg: #fdefd9; --status-amber-fg: #b07a12;
  --status-red-bg: #fce3e5; --status-red-fg: #c24c58;
}
:root {
  --risk-low-bg: var(--status-green-bg); --risk-low-fg: var(--status-green-fg);
  --risk-medium-bg: var(--status-amber-bg); --risk-medium-fg: var(--status-amber-fg);
  --risk-high-bg: var(--status-red-bg); --risk-high-fg: var(--status-red-fg);
}
.badge.risk-low { background: var(--risk-low-bg); color: var(--risk-low-fg); }
.badge.risk-medium { background: var(--risk-medium-bg); color: var(--risk-medium-fg); }
.badge.risk-high { background: var(--risk-high-bg); color: var(--risk-high-fg); }

/* select.risk-select is riskRegister.js's editable Initial/Resulting risk
   dropdown — .dashboard-table's generic select rule (above) sets a
   transparent background and forces it white on hover/focus, so the
   colour states are repeated at :hover/:focus too rather than relying on
   a specificity order that the next stylesheet edit could quietly break. */
.dashboard-table select.risk-select.risk-low,
.dashboard-table select.risk-select.risk-low:hover,
.dashboard-table select.risk-select.risk-low:focus { background: var(--risk-low-bg); color: var(--risk-low-fg); border-color: transparent; }
.dashboard-table select.risk-select.risk-medium,
.dashboard-table select.risk-select.risk-medium:hover,
.dashboard-table select.risk-select.risk-medium:focus { background: var(--risk-medium-bg); color: var(--risk-medium-fg); border-color: transparent; }
.dashboard-table select.risk-select.risk-high,
.dashboard-table select.risk-select.risk-high:hover,
.dashboard-table select.risk-select.risk-high:focus { background: var(--risk-high-bg); color: var(--risk-high-fg); border-color: transparent; }
.dashboard-table select.risk-select.risk-none { color: var(--muted); }
.dashboard-table select.risk-select option[value="Low"] { background: var(--risk-low-bg); color: var(--risk-low-fg); }
.dashboard-table select.risk-select option[value="Medium"] { background: var(--risk-medium-bg); color: var(--risk-medium-fg); }
.dashboard-table select.risk-select option[value="High"] { background: var(--risk-high-bg); color: var(--risk-high-fg); }

/* Timesheet Tier 2 (budgets & burn rate) — its own badge family, distinct
   from .badge.risk-* (a risk score) and .badge.ok/warn/serious/muted/pending
   (policy/staff status) above, same "different KIND of value, own tokens"
   reasoning as those two: a burn percentage isn't a risk score or an
   approval status, even though its colour direction (green ok, amber
   warning, red over) happens to read the same way risk-low/medium/high
   does. One shared set of --burn-* / badge / mini-bar-fill classes, referenced
   by BOTH timesheet.js's project-level summary AND its per-user breakdown
   (burnBadgeHtml()/burnBarHtml(), both keyed off the same burnClass()) — the
   same "one set of values, every rendering path references it" discipline
   the risk-badge fix established, applied here from the start rather than
   fixed after the fact. Thresholds (<80% ok / 80-99% warning / >=100% over)
   live in timesheet.js's own BURN_WARNING_THRESHOLD/BURN_OVER_THRESHOLD, not
   here — this file only owns how each named class looks. */
:root {
  --burn-ok-bg: var(--status-green-bg); --burn-ok-fg: var(--status-green-fg);
  --burn-warning-bg: var(--status-amber-bg); --burn-warning-fg: var(--status-amber-fg);
  --burn-over-bg: var(--status-red-bg); --burn-over-fg: var(--status-red-fg);
}
.badge.burn-ok { background: var(--burn-ok-bg); color: var(--burn-ok-fg); }
.badge.burn-warning { background: var(--burn-warning-bg); color: var(--burn-warning-fg); }
.badge.burn-over { background: var(--burn-over-bg); color: var(--burn-over-fg); }
/* .mini-bar-fill's own default background (--status-good, see below) is
   overridden per burn class so the same bar component reads ok/warning/over
   at a glance, not just the percentage badge next to it. */
.mini-bar-fill.burn-ok { background: var(--burn-ok-fg); }
.mini-bar-fill.burn-warning { background: var(--burn-warning-fg); }
.mini-bar-fill.burn-over { background: var(--burn-over-fg); }

/* Stakeholder Tracker's traffic-light family — its own tokens, distinct from
   --risk-* / --burn-* above and --smp-color-* below, per this file's own
   "different KIND of value, own tokens" rule (checked both existing
   families first: a stakeholder-relations status isn't a risk score or a
   burn percentage). Amber = open/in progress, Green = resolved/met, Red =
   overdue or high-severity-open — the spreadsheet's own stated colour key,
   reproduced exactly. One shared set of --stakeholder-* / badge classes,
   referenced by every rendering path in stakeholders.js that shows a
   commitment/complaint status (the read-only badge AND the editable
   <select> in the Register/Commitments/Complaints tabs) via one shared
   `stakeholderTone()` function, so a colour fixed once can't fail to reach
   a second rendering path the way the original risk-badge bug did. */
:root {
  --stakeholder-amber-bg: var(--status-amber-bg); --stakeholder-amber-fg: var(--status-amber-fg);
  --stakeholder-green-bg: var(--status-green-bg); --stakeholder-green-fg: var(--status-green-fg);
  --stakeholder-red-bg: var(--status-red-bg); --stakeholder-red-fg: var(--status-red-fg);
}
.badge.stakeholder-amber { background: var(--stakeholder-amber-bg); color: var(--stakeholder-amber-fg); }
.badge.stakeholder-green { background: var(--stakeholder-green-bg); color: var(--stakeholder-green-fg); }
.badge.stakeholder-red { background: var(--stakeholder-red-bg); color: var(--stakeholder-red-fg); }

.dashboard-table select.stakeholder-status-select.stakeholder-amber,
.dashboard-table select.stakeholder-status-select.stakeholder-amber:hover,
.dashboard-table select.stakeholder-status-select.stakeholder-amber:focus { background: var(--stakeholder-amber-bg); color: var(--stakeholder-amber-fg); border-color: transparent; }
.dashboard-table select.stakeholder-status-select.stakeholder-green,
.dashboard-table select.stakeholder-status-select.stakeholder-green:hover,
.dashboard-table select.stakeholder-status-select.stakeholder-green:focus { background: var(--stakeholder-green-bg); color: var(--stakeholder-green-fg); border-color: transparent; }
.dashboard-table select.stakeholder-status-select.stakeholder-red,
.dashboard-table select.stakeholder-status-select.stakeholder-red:hover,
.dashboard-table select.stakeholder-status-select.stakeholder-red:focus { background: var(--stakeholder-red-bg); color: var(--stakeholder-red-fg); border-color: transparent; }
.dashboard-table select.stakeholder-status-select.stakeholder-none { color: var(--muted); }

/* Commitments tab — overdue Due-date styling (table AND board view) plus the
   Board view itself (List <-> Board toggle in stakeholders.js). Reuses the
   same --stakeholder-red-fg token as the badge/select family right above —
   one red for "overdue" across every rendering path for this value, keyed
   off the SAME commitmentProgressFlag() computation both commitmentRowHtml()
   and commitmentCardHtml() already read for the Progress badge, not a
   second overdue check. */
.due-overdue { color: var(--stakeholder-red-fg); font-weight: 700; }

/* Board columns/cards deliberately reuse .panel (background/border/radius/
   shadow) rather than a new bordered-box convention — see DESIGN.md's "One
   rendering mechanism per output" rule and this file's own retirement of
   .card in favour of .panel project-wide. */
.commitment-board {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(240px, 1fr));
  gap: var(--space-5);
  align-items: start;
  margin: 0;
}
.commitment-board-column { margin: 0; }
/* Drop-target feedback — same visual language as .cal-day.drag-over
   (timesheet/SMP calendar drag-reschedule) for the same kind of interaction:
   a card/chip dropped into a different container, not a same-list reorder
   (that's .draggable-row.drag-over's own shape, unrelated here). */
.commitment-board-column.drag-over { box-shadow: inset 0 0 0 2px var(--accent); background: var(--accent-light); }
.commitment-board-cards { display: flex; flex-direction: column; gap: var(--space-3); padding: var(--space-3); min-height: 60px; }
.commitment-card { padding: var(--space-3) var(--space-4); cursor: grab; display: flex; flex-direction: column; gap: 0.25rem; }
.commitment-card.dragging { opacity: 0.4; }
.commitment-card-stakeholder { font-weight: 700; font-size: 0.85rem; color: var(--navy); }
/* line-clamp truncates by CSS at render time, not by cutting the string in
   JS before it ever reaches the DOM — the full commitment text still exists
   in the markup (selectable, readable by a screen reader); only the
   on-screen box is capped at three lines. */
.commitment-card-text {
  margin: 0;
  font-size: 0.85rem;
  color: var(--ink);
  display: -webkit-box;
  -webkit-line-clamp: 3;
  line-clamp: 3;
  -webkit-box-orient: vertical;
  overflow: hidden;
}
.commitment-card-owner { display: flex; align-items: center; gap: 0.35rem; font-size: 0.78rem; color: var(--muted); }
.commitment-card-due { font-size: 0.78rem; color: var(--muted); }
/* Compound selector, not just relying on `.due-overdue` alone winning by
   source order — `.commitment-card-due` and `.due-overdue` are both single-
   class selectors (equal specificity), so whichever is declared LATER in
   this file wins the `color` property on an element carrying both classes.
   Found by actually checking getComputedStyle on a rendered card (a screen-
   shot alone made the bold-but-not-red text easy to miss), not by reading
   the two rules and assuming source order didn't matter — see this file's
   own risk-select-family comment above ("a specificity order that the next
   stylesheet edit could quietly break") for the same trap. This compound
   rule's specificity (0,2,0) beats either single-class rule outright, so it
   can't silently lose again if `.commitment-card-due`'s own rule ever moves. */
.commitment-card-due.due-overdue { color: var(--stakeholder-red-fg); font-weight: 700; }
/* Same compound-selector trap, same fix, for the Task register's own due-date
   cell (stakeholders.js's taskCompactRowHtml(), 2026-10-01) — found the SAME
   way: a rendered mock's getComputedStyle showed the muted grey still
   winning over plain `.due-overdue`, because `.task-compact-row
   .task-compact-field` (styles.css, above) is a compound descendant
   selector that outranks a single `.due-overdue` class regardless of source
   order. Not caught by reading the two rules — see the comment above this
   one for why that check isn't enough on its own. */
.task-compact-row .task-compact-field.due-overdue { color: var(--stakeholder-red-fg); font-weight: 700; }

/* Owner avatar-initials chip — board cards only (a table row already fits a
   full name comfortably). One flat accent colour rather than a per-person
   hash: this codebase has no existing "colour per person" convention to
   extend (an owner is either a staff profile or a typed name — see
   stakeholderShared.js's ownerDisplay()/ownerInitials() in stakeholders.js),
   so a single consistent colour for the chip family stays in line with that
   rather than inventing a new one-off palette. */
.owner-chip {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 20px;
  height: 20px;
  border-radius: 50%;
  background: var(--accent);
  color: #fff;
  font-size: 0.64rem;
  font-weight: 700;
  flex: none;
  line-height: 1;
}

/* Social Media Planner's status pipeline — its own badge family, distinct
   from .badge.ok/warn/serious/muted/pending (policy/staff status) and
   .badge.risk-* (a risk score) above, same reasoning as the risk-* family's
   own comment: this is a different KIND of status, not a value that fits
   either existing meaning, so it gets its own tokens rather than borrowing
   one of theirs.

   Group A refinement: status itself became admin-manageable (public.
   smp_lookups, category='status') instead of a fixed 7-value code constant,
   so these classes are now keyed by COLOUR (an admin-picked value from a
   small fixed palette, smp_lookups.color) rather than by a specific status
   NAME — "status-idea"/"status-published"/etc. would have broken the moment
   an admin renamed or reordered a status. Same "one set of values, every
   rendering path references it" discipline as risk-low/medium/high and the
   original SMP_STATUS_CLASS map: smpStatusBadge() (read-only — Campaigns,
   My Tasks) and smpStatusSelectHtml() (the editable <select> in the Tasks
   tab) in assets/js/smpConstants.js both resolve a status NAME to its
   smp_lookups row's `color`, then to one of these `smp-color-*` classes, so
   a colour fixed in one rendering path reaches every other one (badges, the
   editable Tasks/Campaigns status selects) the same way. Group C: the
   Calendar tab's task chips no longer key off this family — they colour by
   content type instead (--smp-cat-*, below) with status demoted to a small
   corner-dot overlay; see that section's own comment for why. */
:root {
  --smp-color-grey-bg: #eceef0; --smp-color-grey-fg: #52606d;
  --smp-color-blue-bg: #e3eefc; --smp-color-blue-fg: #1d5fbf;
  --smp-color-purple-bg: #f1e9fb; --smp-color-purple-fg: #6b3fa0;
  --smp-color-teal-bg: #e0f7f7; --smp-color-teal-fg: #0f7a7a;
  --smp-color-indigo-bg: #e8eafd; --smp-color-indigo-fg: #4338ca;
  --smp-color-green-bg: #e3f6e8; --smp-color-green-fg: #1a7f37;
  --smp-color-tan-bg: #ece7df; --smp-color-tan-fg: #7a6a4f;
  --smp-color-pink-bg: #fbe7ef; --smp-color-pink-fg: #9d174d;
  --smp-color-red-bg: #fbe1e1; --smp-color-red-fg: #b3261e;
  --smp-color-amber-bg: #fdecc8; --smp-color-amber-fg: #8a5a00;
}
.badge.smp-color-grey { background: var(--smp-color-grey-bg); color: var(--smp-color-grey-fg); }
.badge.smp-color-blue { background: var(--smp-color-blue-bg); color: var(--smp-color-blue-fg); }
.badge.smp-color-purple { background: var(--smp-color-purple-bg); color: var(--smp-color-purple-fg); }
.badge.smp-color-teal { background: var(--smp-color-teal-bg); color: var(--smp-color-teal-fg); }
.badge.smp-color-indigo { background: var(--smp-color-indigo-bg); color: var(--smp-color-indigo-fg); }
.badge.smp-color-green { background: var(--smp-color-green-bg); color: var(--smp-color-green-fg); }
.badge.smp-color-tan { background: var(--smp-color-tan-bg); color: var(--smp-color-tan-fg); }
.badge.smp-color-pink { background: var(--smp-color-pink-bg); color: var(--smp-color-pink-fg); }
.badge.smp-color-red { background: var(--smp-color-red-bg); color: var(--smp-color-red-fg); }
.badge.smp-color-amber { background: var(--smp-color-amber-bg); color: var(--smp-color-amber-fg); }

.dashboard-table select.smp-status-select.smp-color-grey,
.dashboard-table select.smp-status-select.smp-color-grey:hover,
.dashboard-table select.smp-status-select.smp-color-grey:focus { background: var(--smp-color-grey-bg); color: var(--smp-color-grey-fg); border-color: transparent; }
.dashboard-table select.smp-status-select.smp-color-blue,
.dashboard-table select.smp-status-select.smp-color-blue:hover,
.dashboard-table select.smp-status-select.smp-color-blue:focus { background: var(--smp-color-blue-bg); color: var(--smp-color-blue-fg); border-color: transparent; }
.dashboard-table select.smp-status-select.smp-color-purple,
.dashboard-table select.smp-status-select.smp-color-purple:hover,
.dashboard-table select.smp-status-select.smp-color-purple:focus { background: var(--smp-color-purple-bg); color: var(--smp-color-purple-fg); border-color: transparent; }
.dashboard-table select.smp-status-select.smp-color-teal,
.dashboard-table select.smp-status-select.smp-color-teal:hover,
.dashboard-table select.smp-status-select.smp-color-teal:focus { background: var(--smp-color-teal-bg); color: var(--smp-color-teal-fg); border-color: transparent; }
.dashboard-table select.smp-status-select.smp-color-indigo,
.dashboard-table select.smp-status-select.smp-color-indigo:hover,
.dashboard-table select.smp-status-select.smp-color-indigo:focus { background: var(--smp-color-indigo-bg); color: var(--smp-color-indigo-fg); border-color: transparent; }
.dashboard-table select.smp-status-select.smp-color-green,
.dashboard-table select.smp-status-select.smp-color-green:hover,
.dashboard-table select.smp-status-select.smp-color-green:focus { background: var(--smp-color-green-bg); color: var(--smp-color-green-fg); border-color: transparent; }
.dashboard-table select.smp-status-select.smp-color-tan,
.dashboard-table select.smp-status-select.smp-color-tan:hover,
.dashboard-table select.smp-status-select.smp-color-tan:focus { background: var(--smp-color-tan-bg); color: var(--smp-color-tan-fg); border-color: transparent; }
.dashboard-table select.smp-status-select.smp-color-pink,
.dashboard-table select.smp-status-select.smp-color-pink:hover,
.dashboard-table select.smp-status-select.smp-color-pink:focus { background: var(--smp-color-pink-bg); color: var(--smp-color-pink-fg); border-color: transparent; }
.dashboard-table select.smp-status-select.smp-color-red,
.dashboard-table select.smp-status-select.smp-color-red:hover,
.dashboard-table select.smp-status-select.smp-color-red:focus { background: var(--smp-color-red-bg); color: var(--smp-color-red-fg); border-color: transparent; }
.dashboard-table select.smp-status-select.smp-color-amber,
.dashboard-table select.smp-status-select.smp-color-amber:hover,
.dashboard-table select.smp-status-select.smp-color-amber:focus { background: var(--smp-color-amber-bg); color: var(--smp-color-amber-fg); border-color: transparent; }

/* Social Media Planner — Option A compact task row + <details> expand
   (2026-09-30, confirmed design — see DESIGN.md's own entry). taskRowHtml()
   now renders ONE full-width <td> per <tr data-task> (no <thead> column
   labels any more — a column-aligned table stopped matching what the row
   shows once most fields moved off the always-visible line) holding a
   one-line compact summary plus a <details class="task-detail"> for
   everything else, reusing the exact same disclosure
   .commitment-history/.history-list below already established rather than
   a new toggle mechanism. */
/* .task-compact-cell is the flex container for BOTH the always-visible
   row and the <details> expand toggle that follows it — the two used to be
   plain stacked block siblings (a <div> then a <details>), which put the
   expand toggle on its own full-width line below the row no matter how the
   row's own children behaved. Making the <td> itself the flex row and
   letting .task-compact-row grow while .task-detail's closed <summary>
   sizes to its own content is what puts everything on one line; .task-detail
   only claims the full row (and wraps onto its own line) once it's [open],
   so the expanded detail-grid still gets full width below. */
/* .compact-cell/.compact-row/.row-detail (2026-10-01, Stakeholder Tracker
   Register row-redesign) are generic aliases on these same three
   structural rules — reused verbatim (no second copy of the layout logic)
   for any OTHER page's own compact-row-plus-expand, by a plain, non-"task"-
   prefixed name, since the mechanism itself (a flex cell holding an
   always-visible row plus a <details> that claims the full row width only
   once [open]) has nothing SMP- or task-specific about it. See
   stakeholders.js's stakeholderRowHtml()/stakeholderDetailHtml() for the
   first other caller, mirroring taskRowHtml()/taskDetailHtml()'s approach
   (commit 5cef804) exactly: nothing above/around the row shifts when it
   expands, and the Edit/Delete icon toolbar sits on the detail-grid's own
   first row rather than floating separately. */
.task-compact-cell, .compact-cell { padding: 0 !important; display: flex; align-items: center; flex-wrap: wrap; }
.task-compact-row, .compact-row { display: flex; align-items: center; gap: 0.75rem; padding: 0.6rem 0.85rem; flex-wrap: wrap; flex: 1 1 auto; min-width: 0; }
.task-compact-row .task-title-block { flex: 1 1 220px; min-width: 160px; }
.task-compact-row .task-title { font-weight: 600; font-size: 0.87rem; }
.task-compact-row .task-sub { font-size: 0.74rem; color: var(--muted); margin-top: 0.1rem; }
.task-compact-row .task-compact-field { flex: 0 0 auto; font-size: 0.82rem; color: var(--muted); white-space: nowrap; }
/* Overrides .dashboard-table select's generic width:100%/min-width:100px
   (above) — that rule is what forced this select onto its own full-width
   line in the first place; a status dropdown in a dense compact row needs
   to size to its own content instead. */
.task-compact-row select.smp-status-select { flex: 0 0 auto; width: auto; min-width: 0; }
/* Same override, same reason, for the Task register's own status <select>
   (stakeholders.js's taskStatusSelectHtml(), 2026-10-01) — without it,
   .dashboard-table select's generic width:100%/min-width:100px (below)
   stretches it to fill the row instead of sizing to its own text, exactly
   the bug this file already fixed once for SMP's own status select. Found
   the same way: rendering the compact row and measuring it, not assumed
   fixed by this rule's mere existence for a *different* class. */
.task-compact-row select.task-status-select { flex: 0 0 auto; width: auto; min-width: 0; }
/* Reserved-width column for the Review badge (taskCompactRowHtml() wraps
   smpReviewStatusBadge() in this div) — 140px fits the longest label
   ("Changes requested", ~135px rendered) with the shorter labels ("—",
   "Pending", "Approved") left-aligned inside the same space instead of
   sizing to their own text, so the Due date/avatar/expand-toggle that follow
   land at the same x-position on every row regardless of which label is
   showing. See taskCompactRowHtml()'s own comment for why this exists. */
.task-compact-row .task-review-slot { flex: 0 0 140px; }
.task-compact-table tbody tr:last-child .task-detail:not([open]),
.compact-table tbody tr:last-child .row-detail:not([open]) { border-bottom: none; }

/* Assignee avatar chip (task-compact-row) — same small initials-circle
   shape as other per-person chips on this site (e.g. Stakeholder Tracker's
   commitment-card owner initials), introduced here since the compact row
   is this module's first use of one. */
.avatar {
  width: 26px; height: 26px; border-radius: 50%;
  background: var(--accent-light); color: var(--navy);
  display: inline-flex; align-items: center; justify-content: center;
  font-size: 0.68rem; font-weight: 700; flex: none;
}
.avatar.avatar-none { background: var(--status-muted-bg); color: var(--muted); font-weight: 400; }

/* Always a full-width flex item (flex: 1 1 100%), open or closed — so the
   "Details & actions" toggle always wraps onto its own line directly below
   the compact row's fields, at the same x/y either way (bug fix,
   2026-10-01, Stakeholder Tracker's Register rows AND Social Media
   Planner's Task rows, both of which render this exact same
   `.task-detail`/`.row-detail` — raised earlier the same day for SMP and
   paused mid-session, then confirmed in scope for both). Before: closed,
   this was `flex: 0 0 auto` — a compact item sizing to its own summary
   text and sitting wherever flex-wrap happened to put it at the END of the
   compact row's own fields (often far right, after Status/Due date/Owner);
   open, it switched to `flex: 1 1 100%` and wrapped onto its own new line
   at the row's LEFT margin — the toggle visually jumped from the right
   end of one line to the start of a different line the moment it was
   clicked, the "button moves" complaint. Making it unconditionally
   `flex: 1 1 100%` means the compact row's own fields wrap among
   themselves first, and the toggle always starts its own line right below
   them, open or closed — only the detail-grid content under it (and the
   border-top separating it, kept open-only below) appears/disappears.
   Verified with getBoundingClientRect() on the summary element in both
   states: identical x/y before vs after clicking, for both modules.
   `border-top` is always 1px, transparent when closed and `--border`
   when open, rather than only added on [open] — an open-only border
   would itself add 1px of box height the moment it appeared, shifting the
   summary inside it down by exactly that 1px on every open/close (caught
   by this same getBoundingClientRect() check, not assumed fine from the
   rule alone). */
details.task-detail, details.row-detail { flex: 1 1 100%; border-top: 1px solid transparent; }
details.task-detail[open], details.row-detail[open] { border-top-color: var(--border); }
details.task-detail summary, details.row-detail summary { list-style: none; cursor: pointer; padding: 0.5rem 0.85rem; font-size: 0.75rem; color: var(--accent); font-weight: 600; display: flex; align-items: center; gap: 0.3rem; white-space: nowrap; }
details.task-detail summary::-webkit-details-marker, details.row-detail summary::-webkit-details-marker { display: none; }
details.task-detail summary::before, details.row-detail summary::before { content: "▸"; display: inline-block; transition: transform 0.15s ease; }
details.task-detail[open] summary::before, details.row-detail[open] summary::before { transform: rotate(90deg); }
/* Fourth column ("auto", not a 1fr share like the three text fields) holds
   .detail-toolbar's icon buttons (below) — sized to their own content so
   Content type/Calendar/Asset link keep sharing the row's width evenly
   between them, same as before this column existed. Description's own
   `grid-column: 1 / -1` (inline style, taskDetailHtml()) still spans every
   column including this one. */
details.task-detail .detail-grid, details.row-detail .detail-grid { padding: 0 0.85rem 0.85rem; display: grid; grid-template-columns: repeat(3, 1fr) auto; gap: 0.6rem 1rem; font-size: 0.8rem; }
details.task-detail .detail-grid .k, details.row-detail .detail-grid .k { color: var(--muted); font-size: 0.68rem; text-transform: uppercase; letter-spacing: 0.03em; }
details.task-detail .review-comments { padding: 0 0.85rem 0.85rem; }

/* Option B (confirmed design, 2026-09-30), moved onto the detail-grid's own
   first row the same day (Edit/Delete — see taskDetailHtml()'s
   .icon-btn markup, .icon-btn itself already established by quizBuilder.js's
   row actions) and the self-contained Review card wrapping .panel/
   .panel-header/.panel-body (DESIGN.md's "the only bordered container").
   .detail-toolbar is now one of .detail-grid's own cells (the fourth,
   auto-width column above) rather than a separate bar above it — no label
   (unlike its Content type/Calendar/Asset link siblings), so align-self: end
   sits it level with their value text instead of their "k" labels one line
   up. .review-panel is only a margin wrapper — .panel itself already
   supplies the border/background/shadow. Nested one level deeper than
   pendingReviewRowHtml()'s own bare reviewCommentsThreadHtml() call (which
   still gets the .review-comments padding rule above, directly inside
   .task-detail), this override resets that padding to 0 since .panel-body
   already pads its own content — without it, a review card thread would get
   panel-body's padding AND the 0.85rem rule above, stacked. */
details.task-detail .detail-toolbar, details.row-detail .detail-toolbar { display: flex; align-items: center; justify-content: flex-end; align-self: end; gap: 0.4rem; }
details.task-detail .review-panel, details.row-detail .review-panel { margin: 0 0.85rem 0.85rem; }
details.task-detail .review-panel .review-comments { padding: 0; }
.review-header-actions { display: flex; align-items: center; gap: 0.4rem; flex-wrap: wrap; }
.review-requested-meta { color: var(--muted); font-size: 0.78rem; margin: 0 0 0.6rem; }

/* Pending-my-review row's comment-then-decide panel (Problem 2, same day)
   — the textarea that feeds whichever of Approve/Send back gets clicked
   (see pendingReviewRowHtml()/wirePendingReviewPanel() in
   socialMediaPlanner.js), sitting directly alongside those two actions. */
details.task-detail .review-decision { padding: 0 0.85rem 0.85rem; }
.review-decision label { display: block; font-size: 0.72rem; color: var(--muted); text-transform: uppercase; letter-spacing: 0.03em; margin-bottom: 0.3rem; }
.review-decision textarea { width: 100%; margin-bottom: 0.5rem; }

/* Toolbar above the Tasks tab's compact list — "Select all" (bulk edit)
   and the campaign-sort toggle both moved here off the old <thead>, which
   Option A no longer renders. */
.task-list-toolbar { display: flex; align-items: center; gap: 0.75rem; padding: 0 0.85rem 0.5rem; flex-wrap: wrap; }
.task-list-toolbar label { display: inline-flex; align-items: center; gap: 0.35rem; font-size: 0.82rem; color: var(--muted); }

/* Social Media Planner — phase B additions (Dashboard/Calendar/Timeline).
   See DESIGN.md's "Social Media Planner" section for the full reasoning. */

/* Period-over-period growth comparison (Dashboard tab's Growth view) — green
   up / muted-red down, per the spec. --stat-down is a deliberately SOFTER red
   than --status-serious-bg's foreground (#b8451f, an orange-leaning "serious"
   warning) or risk-high-fg (#b3261e, a saturated alert red) — this is a
   normal, expected direction for a metric to move in some months, not a
   warning state, so it reads as muted rather than alarming. */
:root {
  --stat-up: var(--status-good);
  --stat-down: #a8554c;
}
.stat-delta { font-variant-numeric: tabular-nums; font-weight: 600; }
.stat-delta.up { color: var(--stat-up); }
.stat-delta.down { color: var(--stat-down); }
.stat-delta.flat { color: var(--muted); font-weight: 400; }

/* Timeline heatmap — one sequential teal ramp (matching --accent, this site's
   own brand hue, rather than an unrelated one), light to dark, one step per
   task-count bucket (1, 2, 3, 4, 5+). Verified with the dataviz skill's
   palette validator: lightness is monotone light->dark, single hue (5°
   spread), and each step's paired text colour below clears WCAG 4.5:1 against
   that step's own background (ink #1f2933 on steps 1-3: 12.88/9.50/6.61:1;
   white on steps 4-5: 4.87/11.04:1). 0 tasks gets NO fill (a bordered blank
   cell) rather than the lightest ramp step — see smpConstants.js's
   smpHeatLevel() comment for why step 1 is reserved for "1 task". */
:root {
  --smp-heat-1: #e1f3f4; --smp-heat-1-fg: var(--ink);
  --smp-heat-2: #9fdadb; --smp-heat-2-fg: var(--ink);
  --smp-heat-3: #55bdbe; --smp-heat-3-fg: var(--ink);
  --smp-heat-4: #307c7e; --smp-heat-4-fg: #fff;
  --smp-heat-5: #194243; --smp-heat-5-fg: #fff;
}
.smp-heatmap td.smp-heat-0 { background: var(--surface); color: var(--muted); }
.smp-heatmap td.smp-heat-1 { background: var(--smp-heat-1); color: var(--smp-heat-1-fg); }
.smp-heatmap td.smp-heat-2 { background: var(--smp-heat-2); color: var(--smp-heat-2-fg); }
.smp-heatmap td.smp-heat-3 { background: var(--smp-heat-3); color: var(--smp-heat-3-fg); }
.smp-heatmap td.smp-heat-4 { background: var(--smp-heat-4); color: var(--smp-heat-4-fg); }
.smp-heatmap td.smp-heat-5 { background: var(--smp-heat-5); color: var(--smp-heat-5-fg); }
.smp-heatmap td { text-align: center; font-variant-numeric: tabular-nums; font-size: 0.8rem; min-width: 2.1rem; }
.smp-heatmap th.smp-heat-row-label { text-align: left; white-space: nowrap; }
.smp-heat-legend { display: flex; align-items: center; gap: 0.35rem; font-size: 0.78rem; color: var(--muted); margin: 0.6rem 0; flex-wrap: wrap; }
.smp-heat-legend .swatch { display: inline-block; width: 14px; height: 14px; border-radius: 3px; border: 1px solid var(--border); background: var(--surface); }
.smp-heat-legend .swatch.smp-heat-1 { background: var(--smp-heat-1); border-color: var(--smp-heat-1); }
.smp-heat-legend .swatch.smp-heat-2 { background: var(--smp-heat-2); border-color: var(--smp-heat-2); }
.smp-heat-legend .swatch.smp-heat-3 { background: var(--smp-heat-3); border-color: var(--smp-heat-3); }
.smp-heat-legend .swatch.smp-heat-4 { background: var(--smp-heat-4); border-color: var(--smp-heat-4); }
.smp-heat-legend .swatch.smp-heat-5 { background: var(--smp-heat-5); border-color: var(--smp-heat-5); }

/* Group C — a second, independent categorical colour family: --smp-cat-1..8,
   the dataviz-skill-validated 8-hue reference order (blue/orange/aqua/
   yellow/magenta/green/violet/red — `node scripts/validate_palette.js
   "#2a78d6,#eb6834,#1baf7a,#eda100,#e87ba4,#008300,#4a3aa7,#e34948" --mode
   light` passes lightness band/chroma floor/CVD adjacent-pair separation/
   normal-vision floor; three light-mode slots sit below 3:1 contrast on the
   surface, mitigated the same way every chip here already is — a visible
   text label always rides alongside the colour, never colour alone). This
   is the SAME reference order this site's own --cat-1..6 (policy-category
   chips, top of this file) already uses — but under its own token names,
   not --cat-* itself, since --cat-* is reserved for policy categories
   specifically (this file's "never reuse one family for a different
   meaning" rule) and this is two different fields (content type, channel)
   on a different page. Two tiers per slot, like --smp-color-*-bg/-fg above:
   *-bg/*-fg (a pale tint + a readable dark text colour) for badges/chips/
   legend swatches, and *-fg alone reused as a solid chart-mark fill (see
   .smp-cat-fill below) — see smpConstants.js's smpCategoricalClass() for
   how a value resolves to a slot. */
:root {
  --smp-cat-1-bg: #dceafb; --smp-cat-1-fg: #1a5fa8; /* blue */
  --smp-cat-2-bg: #fce4d9; --smp-cat-2-fg: #b34a1d; /* orange */
  --smp-cat-3-bg: #d7f3e7; --smp-cat-3-fg: #12805a; /* aqua */
  --smp-cat-4-bg: #fdecc7; --smp-cat-4-fg: #8a6100; /* yellow */
  --smp-cat-5-bg: #fbe3ee; --smp-cat-5-fg: #a33a68; /* magenta */
  --smp-cat-6-bg: #d7f0d7; --smp-cat-6-fg: #0a6b0a; /* green */
  --smp-cat-7-bg: #e6e2f7; --smp-cat-7-fg: #392c85; /* violet */
  --smp-cat-8-bg: #fbdede; --smp-cat-8-fg: #a52322; /* red */
  --smp-cat-other-bg: #eceef0; --smp-cat-other-fg: #52606d; /* grey — 9th+/none */

  /* Status corner-dot overlay (Calendar tab) — a NEW small state indicator,
     not a reuse of --status-good/-warning/-serious (this file's existing
     policy on-track/due/overdue family): those are tuned for a badge's text
     contrast, not a 7px dot, and the spec names true red/orange/green. Kept
     as its own tiny token set for that reason, distinct from every other
     colour family here per DESIGN.md's own "never reuse one family for a
     different meaning" rule — this is a new meaning (a task's own due-date
     urgency), just visually similar in spirit to the policy overdue-tracking
     idea elsewhere in this app. */
  --smp-dot-overdue: #d03b3b;
  --smp-dot-soon: #eda100;
  --smp-dot-done: #0ca30c;
}
.smp-cat-1 { background: var(--smp-cat-1-bg); color: var(--smp-cat-1-fg); }
.smp-cat-2 { background: var(--smp-cat-2-bg); color: var(--smp-cat-2-fg); }
.smp-cat-3 { background: var(--smp-cat-3-bg); color: var(--smp-cat-3-fg); }
.smp-cat-4 { background: var(--smp-cat-4-bg); color: var(--smp-cat-4-fg); }
.smp-cat-5 { background: var(--smp-cat-5-bg); color: var(--smp-cat-5-fg); }
.smp-cat-6 { background: var(--smp-cat-6-bg); color: var(--smp-cat-6-fg); }
.smp-cat-7 { background: var(--smp-cat-7-bg); color: var(--smp-cat-7-fg); }
.smp-cat-8 { background: var(--smp-cat-8-bg); color: var(--smp-cat-8-fg); }
.smp-cat-other, .smp-cat-none { background: var(--smp-cat-other-bg); color: var(--smp-cat-other-fg); }
/* Chart bars (Dashboard's channel-volume-over-time chart) use the solid *-fg
   hue as a fill rather than the pale *-bg tint above — a thin SVG bar reads
   as identity-carrying colour, not text-on-a-tint, so the stronger step is
   the right one (see socialMediaPlanner.js's stackedBarChartSvg()). */
.smp-cat-fill.smp-cat-1 { fill: var(--smp-cat-1-fg); }
.smp-cat-fill.smp-cat-2 { fill: var(--smp-cat-2-fg); }
.smp-cat-fill.smp-cat-3 { fill: var(--smp-cat-3-fg); }
.smp-cat-fill.smp-cat-4 { fill: var(--smp-cat-4-fg); }
.smp-cat-fill.smp-cat-5 { fill: var(--smp-cat-5-fg); }
.smp-cat-fill.smp-cat-6 { fill: var(--smp-cat-6-fg); }
.smp-cat-fill.smp-cat-7 { fill: var(--smp-cat-7-fg); }
.smp-cat-fill.smp-cat-8 { fill: var(--smp-cat-8-fg); }
.smp-cat-fill.smp-cat-other, .smp-cat-fill.smp-cat-none { fill: var(--smp-cat-other-fg); }

/* Calendar tab (month view) — day cells reuse .panel/.dashboard-table's own
   border/radius language rather than a new bordered-box style.

   Group C: task chips are now coloured by CONTENT TYPE (--smp-cat-* above),
   a deliberate departure from every other view in this module (which colours
   by status, --smp-color-*) — see smpConstants.js's smpCategoricalClass()
   comment. Status becomes a small corner-dot overlay instead (.cal-chip-dot-*
   below), on TOP of the content-type colour, not instead of it. */
.cal-header { display: flex; flex-direction: column; gap: 0.5rem; }
.cal-header-top { display: flex; align-items: center; justify-content: space-between; flex-wrap: wrap; gap: 0.5rem; }
.cal-header-top select#cal-month-select { padding: 0.3rem 0.5rem; border: 1px solid var(--border); border-radius: var(--radius); font-size: 0.82rem; }
.cal-legend { display: flex; align-items: center; flex-wrap: wrap; gap: 0.6rem; font-size: 0.76rem; color: var(--muted); }
.cal-legend-label { font-weight: 600; color: var(--ink); }
.cal-legend-item { display: inline-flex; align-items: center; gap: 0.3rem; }
.cal-legend-swatch { display: inline-block; width: 12px; height: 12px; border-radius: 3px; }
.cal-month-block-title { margin: 0 0 0.5rem; font-size: 0.95rem; color: var(--ink); }
.cal-grid { display: grid; grid-template-columns: repeat(7, 1fr); gap: 1px; background: var(--border); border: 1px solid var(--border); border-radius: var(--radius); overflow: hidden; }
/* --navy, matching table.dashboard-table th's own reassignment above — this
   is the exact same role (a structural column-header row, SUN/MON/…) on the
   exact same #f0f3f6 background, so it gets the same colour for the same
   reason rather than being left as the one grey outlier among this file's
   column headers. */
.cal-grid-header { background: #f0f3f6; font-size: 0.72rem; text-transform: uppercase; letter-spacing: 0.03em; color: var(--navy); text-align: center; padding: 0.35rem 0; }
.cal-day { background: var(--surface); min-height: 96px; padding: 0.35rem; display: flex; flex-direction: column; gap: 0.2rem; }
.cal-day.out-of-month { background: #fafbfc; }
.cal-day.today { box-shadow: inset 0 0 0 2px var(--accent); }
/* Drag-and-drop reschedule drop-target feedback — the equivalent of
   .draggable-row.drag-over above, for this genuinely different drag
   interaction (a day-cell drop target/due_date update, not a sort_order
   reorder — see socialMediaPlanner.js's wireCalendarDragDrop()). */
.cal-day.drag-over { box-shadow: inset 0 0 0 2px var(--accent); background: var(--accent-light); }
.cal-day-num { font-size: 0.78rem; color: var(--muted); font-variant-numeric: tabular-nums; }
.cal-day.out-of-month .cal-day-num { color: #c3cbd2; }
.cal-task-chip { position: relative; display: block; width: 100%; text-align: left; border: none; border-radius: 4px; padding: 0.1rem 1rem 0.1rem 0.35rem; font-size: 0.72rem; line-height: 1.3; cursor: grab; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.cal-task-chip:hover { filter: brightness(0.95); }
/* Status corner-dot overlay — small, top-right corner of the chip, on top of
   the content-type background. See taskCalendarDotClass() in
   socialMediaPlanner.js for red/orange/green's exact meaning. */
.cal-chip-dot { position: absolute; top: 3px; right: 3px; width: 7px; height: 7px; border-radius: 50%; box-shadow: 0 0 0 1px rgba(255,255,255,0.7); }
.cal-chip-dot-overdue { background: var(--smp-dot-overdue); }
.cal-chip-dot-soon { background: var(--smp-dot-soon); }
.cal-chip-dot-done { background: var(--smp-dot-done); }
.cal-day summary.cal-more-toggle { font-size: 0.72rem; color: var(--accent); cursor: pointer; list-style: none; }
.cal-day summary.cal-more-toggle::-webkit-details-marker { display: none; }
@media (max-width: 640px) {
  .cal-grid { grid-template-columns: repeat(7, 1fr); }
  .cal-day { min-height: 64px; font-size: 0.85rem; }
  .cal-grid-header { font-size: 0.62rem; }
}

/* Timesheet calendar view (2026-09-29) — Option A drag-to-extend, from the
   approved mockup (https://claude.ai/artifact/FdfgirUQ99XAh9okHN9k8y). Month
   zoom reuses .cal-grid/.cal-day/.cal-day.drag-over/.cal-task-chip/
   .cal-more-toggle ABOVE verbatim (the same family built for Social Media
   Planner's own calendar tab) — .ts-task-chip below is only a colour
   modifier on .cal-task-chip (that base class has no background of its own
   outside SMP's content-type classes), not a replacement of it. Week/
   Fortnight zoom need a taller, richer day panel (a real chip card with a
   drag handle) that .cal-day's compact month-density layout was never built
   to hold, so those get their own small class family here instead of
   forcing month-density chips into a week view. See timesheet.js's own
   calendarBodyHtml()/wireCalendarDrag() for the drag mechanic itself. */
.seg { display: inline-flex; border: 1px solid var(--accent); border-radius: var(--radius); overflow: hidden; }
.seg-opt { padding: 0.35rem 0.75rem; font-size: 0.72rem; font-weight: 700; color: var(--accent); background: var(--surface); border: none; font-family: inherit; cursor: pointer; }
.seg-opt.active { background: var(--accent); color: #fff; }
.combo { border: 1px solid var(--accent); border-radius: var(--radius); padding: 0.45rem 0.6rem; display: flex; flex-direction: column; gap: 0.05rem; background: var(--surface); }
.combo b { font-size: 0.82rem; }
.combo span { font-size: 0.72rem; color: var(--muted); }
.ts-cal-grid { display: grid; grid-template-columns: repeat(7, minmax(0, 1fr)); gap: 0.4rem; }
.ts-day-head { text-align: center; font-size: 0.7rem; font-weight: 700; color: var(--accent); text-transform: uppercase; }
.ts-day-num { display: block; font-weight: 400; color: var(--muted); text-transform: none; font-size: 0.7rem; }
.ts-day-panel { background: var(--surface); border: 1px solid var(--border); border-radius: var(--radius); min-height: 128px; padding: 0.4rem; display: flex; flex-direction: column; gap: 0.35rem; position: relative; }
.ts-day-panel.ts-drag-over { box-shadow: inset 0 0 0 2px var(--accent); background: var(--accent-light); }
/* cursor: pointer (2026-09-29, click-to-detail, item 2) — the chip's own body
   (distinct from its .ts-chip-handle child, which keeps its own ew-resize
   cursor below) is now a click target that opens that entry's detail view. */
.ts-chip { background: var(--accent-light); border: 1px solid var(--border); border-radius: 7px; padding: 0.3rem 0.9rem 0.3rem 0.4rem; font-size: 0.72rem; line-height: 1.3; position: relative; cursor: pointer; }
.ts-chip b { display: block; color: var(--ink); font-size: 0.76rem; }
.ts-chip-sub { color: var(--accent); font-weight: 600; }
.ts-chip-handle { position: absolute; right: 3px; top: 50%; transform: translateY(-50%); width: 11px; height: 11px; border-radius: 50%; background: var(--accent); border: 2px solid var(--surface); box-shadow: 0 0 0 1px var(--accent); cursor: ew-resize; }
.ts-drag-count-badge { position: absolute; left: -2px; right: -2px; top: -1.6rem; text-align: center; z-index: 2; pointer-events: none; }
.ts-drag-count-badge span { background: var(--ink); color: #fff; font-size: 0.65rem; font-weight: 600; padding: 0.15rem 0.55rem; border-radius: 10px; white-space: nowrap; }
.ts-week-label { font-size: 0.78rem; font-weight: 700; color: var(--muted); margin: 0.25rem 0; }
.ts-task-chip { background: var(--accent-light); color: var(--ink); }
@media (max-width: 640px) {
  .ts-cal-grid { grid-template-columns: repeat(7, 1fr); }
  .ts-day-panel { min-height: 80px; font-size: 0.75rem; }
}

/* Dashboard tab (Group C) — the custom date-range period-filter mode's two
   date inputs, alongside the existing preset <select> (only one of the two
   renders at a time — see socialMediaPlanner.js's periodFilterRowHtml()). */
.dash-custom-range { display: inline-flex; align-items: center; gap: 0.4rem; }
.dash-custom-range input[type="date"] { padding: 0.3rem 0.5rem; border: 1px solid var(--border); border-radius: var(--radius); font-size: 0.82rem; }

table.dashboard-table { width: 100%; border-collapse: collapse; background: var(--surface); border: 1px solid var(--border); border-radius: var(--radius); overflow: hidden; font-size: 0.87rem; }
/* Soft row banding for easier reading on long tables (risk registers). */
table.striped-table tbody tr:nth-child(even) { background: var(--bg, #f7f8fa); }
/* Generic row hover for every .dashboard-table, not just the named row
   types that already had one (.policy-row, .pack-policy-row — see
   DESIGN.md). Deliberately var(--accent-light), not var(--bg): var(--bg)
   is the exact colour .striped-table's even rows already use above, so a
   hover using the same value would look like nothing happened when
   hovering an even row on a striped table. var(--accent-light) is already
   this site's established "hover/highlight" background elsewhere (.btn.
   secondary:hover, .add-q-btn:hover) and reads clearly against both the
   surface (odd rows) and --bg (even/striped rows) tones. Placed after the
   striped-table rule above so equal-specificity source order resolves in
   its favour when both would otherwise match. */
table.dashboard-table tbody tr:hover { background: var(--accent-light); }
table.dashboard-table th, table.dashboard-table td { text-align: left; padding: var(--space-3) var(--space-4); border-bottom: 1px solid var(--border); }
/* Colour reassigned --muted -> --navy (2026-09-16, "too much grey across the
   site" pass): this is a structural column-header row, the same level as
   .panel-header/.acc-title, which already use --navy — not a caption for an
   adjacent value the way .kpi-label/.donut-hole .lbl are, so it gets the same
   treatment rather than staying grey. --btn-primary/--cat-1 was tried first
   (the more saturated blue) but measured only 3.96:1 against this header's own
   #f0f3f6 background — below the 4.5:1 AA floor this text needs at 0.72rem
   bold (not "large text" under WCAG). --navy clears 11.76:1 here, comfortably
   legible, and matches .panel-header exactly so the two things in the report
   that prompted this (see DESIGN.md) now read as one consistent colour. */
table.dashboard-table th { background: #f0f3f6; font-size: 0.72rem; text-transform: uppercase; letter-spacing: 0.03em; color: var(--navy); }
table.dashboard-table tr:last-child td { border-bottom: none; }
table.dashboard-table td.num { font-variant-numeric: tabular-nums; }
/* Sortable column headers (Policies tab list) — see DESIGN.md's Sortable
   table columns entry. Not every .dashboard-table header is clickable, so
   this is opt-in per <th> via the .sortable-col class rather than a blanket
   `table.dashboard-table th` rule. */
table.dashboard-table th.sortable-col { cursor: pointer; user-select: none; white-space: nowrap; transition: color .15s ease; }
table.dashboard-table th.sortable-col:hover,
table.dashboard-table th.sortable-col:focus-visible { color: var(--ink); }

.btn {
  display: inline-block;
  background: var(--btn-primary);
  color: #fff;
  border: none;
  padding: 0.5rem 1.1rem;
  border-radius: var(--radius);
  font-size: 0.88rem;
  cursor: pointer;
  text-decoration: none;
  transition: background-color .15s ease;
  box-shadow: 0 1px 2px rgba(15, 23, 32, 0.05);
}
.btn:hover { background: var(--btn-primary-hover); }
.btn.secondary { background: var(--surface); color: var(--navy); border: 1px solid var(--navy); }
.btn.secondary:hover { background: var(--accent-light); }
.btn.small { padding: 0.3rem 0.7rem; font-size: 0.8rem; }
.btn[disabled] { opacity: 0.5; cursor: not-allowed; }
/* Danger: solid red for a confirm button ("Delete", "Permanently delete");
   with .secondary, an outlined red button. (Fixed 2026-10-09: a plain
   .btn.danger kept the purple fill, so its red text was barely readable.) */
.btn.danger { background: var(--status-serious, #b3261e); border-color: var(--status-serious, #b3261e); color: #fff; }
.btn.danger:hover { background: #a63d48; }
.btn.secondary.danger { background: var(--surface); border-color: var(--status-serious, #b3261e); color: var(--status-serious, #b3261e); }
.btn.secondary.danger:hover { background: #fbeceb; }
/* Same trap as .btn-row below: .btn's own display:inline-block is an author
   rule and beats the browser's built-in "[hidden] { display: none }"
   default outright. Hit for real by #policy-form-save-progress (admin.js,
   Policies tab) — toggled via el.hidden depending on whether an existing
   policy is being edited, and without this rule it rendered regardless of
   the hidden attribute. Confirmed by rendering it before and after. */
.btn[hidden] { display: none; }

/* Two or more actions in one table cell — the one way to group them.
   .btn is display: inline-block with no wrapper of its own, so two
   buttons/links placed directly in a <td> wrap onto separate lines the
   moment the column is narrower than both combined, each stacking full-
   width with the table cell's own padding around it — which is what
   made a plain "Edit" and "Report" pair read as two oversized stacked
   pills instead of a compact button group. flex-wrap: nowrap is
   deliberate, not the more typical "wrap": these tables already scroll
   horizontally in their .panel-body (overflow-x: auto) rather than
   reflowing columns at narrow widths, and letting this one cell wrap
   independently while every other cell in the row stays a fixed single
   line is what actually produced the tall stacked-pill look in the
   first place — confirmed by reproducing it at the table's real column
   count before concluding flex-wrap: wrap alone (tried first) wasn't
   enough. Nowrap keeps it consistent with the rest of the row: this
   cell also scrolls now instead of wrapping on its own. */
.row-actions { display: flex; gap: 0.4rem; flex-wrap: nowrap; align-items: center; }
/* Without this, squeezing two buttons onto one flex line just moves the
   wrapping problem inside the button: "Edit / replace" itself started
   breaking onto two lines ("Edit /" / "replace") instead of the row
   scrolling as a whole. white-space: nowrap keeps every button's own
   label on one line, so the row-actions group overflows (and the
   table's existing horizontal scroll takes over) instead of any single
   button's text wrapping internally. */
.row-actions .btn { white-space: nowrap; }

/* Action buttons read as too heavy at the density of a table row — same
   .btn.small, tightened specifically inside .dashboard-table rather than
   changing .btn.small's own site-wide size (toolbar-level small buttons
   outside a table stay as they are). */
.dashboard-table .btn.small { padding: 0.25rem 0.55rem; font-size: 0.78rem; }

.dashboard-table input[type="text"],
.dashboard-table input[type="date"],
.dashboard-table input[type="number"],
.dashboard-table select,
.dashboard-table textarea {
  width: 100%;
  min-width: 100px;
  border: 1px solid transparent;
  background: transparent;
  padding: 0.25rem 0.35rem;
  font: inherit;
  border-radius: 4px;
}
/* Timesheet weekly grid's own hour cells (.ts-cell) are narrower than a
   typical dashboard-table cell — this table's usual min-width:100px above
   would force 7 day-columns plus a totals/status/remove column wider than
   the panel, so this specific input narrows itself rather than changing
   the shared min-width every other .dashboard-table number/text input
   still wants. */
.dashboard-table input.ts-cell { min-width: 0; text-align: right; }
.dashboard-table input[type="text"]:not([disabled]):hover,
.dashboard-table input[type="date"]:not([disabled]):hover,
.dashboard-table input[type="number"]:not([disabled]):hover,
.dashboard-table select:not([disabled]):hover,
.dashboard-table textarea:not([disabled]):hover,
.dashboard-table input:focus,
.dashboard-table select:focus,
.dashboard-table textarea:focus {
  border-color: var(--border);
  background: #fff;
}
.dashboard-table input[disabled],
.dashboard-table select[disabled],
.dashboard-table textarea[disabled] { color: var(--ink); }
.dashboard-table tr.add-row { background: #f7faf8; }

/* Bug fix (2026-09-25, found while reviewing the Stakeholder Tracker
   Settings tab after the visual identity refresh — pre-existing, not
   caused by that refresh: reproduced against the unmodified colorPicker.js
   markup via Playwright before touching this rule). The generic
   `input[type="text"]`/`select` width:100% rule above incidentally also
   matches the colour picker's own internal hex input and mode <select>
   whenever colorPickerHtml() is embedded inside a .dashboard-table cell
   (Stakeholder Tracker/Social Media Planner status Settings tables) — the
   hex input forcing to 100% width breaks .color-picker-controls' intended
   compact inline-with-the-swatch flex row, wrapping the swatch/hex/preview
   onto three separate lines instead of one, which in turn made those rows
   much taller and squeezed the Save/Deactivate buttons in the same row.
   The mode <select> is deliberately left alone: 100% width there is
   correct (it's meant to span its own row above .color-picker-controls,
   same as everywhere else this component appears) — only the hex input
   needs its own fixed width back. .color-picker-hex-input alone has LOWER
   specificity than .dashboard-table input[type="text"] — (0,2,0) vs
   (0,2,1), the generic rule's own element-type part outweighs this
   rule's second class — so a first attempt at this fix
   (`.dashboard-table .color-picker-hex-input`, no element type) silently
   lost to the generic rule despite being declared later; confirmed via
   the browser's own matched-CSSRules list on the actual element (a
   Playwright getComputedStyle/offsetWidth check), not by re-reading the
   two rules and assuming specificity — which is exactly what got the
   first attempt's specificity math wrong. Repeating `input[type="text"]`
   here (0,3,1) beats the generic rule outright rather than relying on
   source order to break a tie. */
.dashboard-table input[type="text"].color-picker-hex-input { width: 84px; min-width: 84px; }
.dashboard-table tr.inactive { opacity: 0.6; }

.btn-row { display: flex; gap: 0.6rem; flex-wrap: wrap; margin: var(--space-4) 0; align-items: center; }
/* .btn-row's own display:flex is an author rule, so it beats the browser's
   built-in "[hidden] { display: none }" default outright — an element
   carrying both `hidden` and class="btn-row" (the form action bar's two
   swapped button rows, admin.js) would otherwise show through regardless of
   the hidden attribute. Confirmed by rendering it: both rows appeared at
   once until this was added. */
.btn-row[hidden] { display: none; }

form.stack { display: grid; gap: 0.65rem; }
form.stack label { font-weight: 600; font-size: 0.85rem; display: block; margin-bottom: 0.2rem; }
form.stack input, form.stack select, form.stack textarea {
  width: 100%;
  padding: 0.45rem 0.65rem;
  border: 1px solid var(--border);
  border-radius: var(--radius);
  font-size: 0.92rem;
  font-family: inherit;
}

.error-message { color: var(--danger); font-size: 0.85rem; margin-top: 0.3rem; }
.success-message { color: var(--ok); font-size: 0.85rem; margin-top: 0.3rem; }
.centered-message { text-align: center; padding: 3rem 1rem; color: var(--muted); }

/* The "review before sending" splash screen (assets/js/emailModal.js) —
   a native <dialog>, so the backdrop, centring and Esc-to-close come free
   rather than being hand-rolled. */
.email-modal {
  width: min(560px, calc(100vw - 2rem));
  max-height: calc(100vh - 4rem);
  padding: 1.25rem 1.4rem;
  border: 1px solid var(--border);
  border-radius: var(--radius);
  background: var(--surface);
  box-shadow: 0 12px 40px rgba(0, 0, 0, 0.18);
}
.email-modal::backdrop { background: rgba(15, 23, 32, 0.5); }
/* Wider/taller variant for a form with several fields (Stakeholder
   Tracker's Register/Engagement Log/Commitments/Complaints add/edit
   dialogs — assets/js/emailModal.js's showFormDialog(), DESIGN.md's
   "Stakeholder Tracker forms move into a dialog (Option C)" entry) — a
   modifier on the same .email-modal class/component, not a second dialog
   implementation. overflow-y:auto lets a tall form (e.g. Commitments' own
   nested "+ Add new stakeholder" panel opening inside it) scroll within the
   dialog instead of overflowing a short viewport. */
.email-modal.form-modal {
  width: min(720px, calc(100vw - 2rem));
  max-height: calc(100vh - 3rem);
  overflow-y: auto;
}

.policy-content { background: var(--surface); border: 1px solid var(--border); border-radius: var(--radius); padding: 1.2rem 1.4rem; font-size: 0.93rem; line-height: 1.6; }
/* Soft tint band behind each section heading — same #f0f3f6 used for
   panel headers and group headers elsewhere, so a long policy reads as a
   sequence of clearly-separated sections rather than one grey wall of text. */
.policy-content h1,
.policy-content h2 {
  background: #f0f3f6;
  color: var(--navy);
  margin: 1.4rem -0.5rem 0.6rem;
  padding: 0.5rem 0.75rem;
  border-radius: var(--radius);
  font-size: 1.02rem;
}
.policy-content h3 {
  background: #f0f3f6;
  color: var(--navy);
  margin: 1.1rem -0.3rem 0.5rem;
  padding: 0.4rem 0.6rem;
  border-radius: var(--radius);
  font-size: 0.95rem;
}
.policy-content h1:first-child,
.policy-content h2:first-child,
.policy-content h3:first-child { margin-top: 0; }
.policy-content ul, .policy-content ol { padding-left: 1.2rem; margin: 0.4rem 0; }
.policy-content p { margin: 0.5rem 0; }
.policy-content table { border-collapse: collapse; margin: 0.6rem 0; }
.policy-content table td { border: 1px solid var(--border); padding: 0.3rem 0.5rem; }

.quiz-question { border-top: 1px solid var(--border); padding: 0.85rem 0; }
.quiz-question:first-child { border-top: none; padding-top: 0; }
.quiz-question p.q-text { font-weight: 600; margin: 0 0 0.4rem; font-size: 0.93rem; }
.quiz-question label {
  display: flex;
  align-items: center;
  gap: 0.5rem;
  padding: 0.4rem 0.6rem;
  border: 1px solid var(--border);
  border-radius: var(--radius);
  margin-bottom: 0.3rem;
  cursor: pointer;
  font-weight: 400;
  font-size: 0.9rem;
}
.quiz-question label:hover { border-color: var(--accent); }
.quiz-question label.correct { border-color: var(--status-good); background: var(--status-good-bg); }
.quiz-question label.incorrect { border-color: var(--danger); background: #fdecea; }
.quiz-question label.disabled-option { opacity: 0.45; cursor: not-allowed; }
.quiz-question .correction-note { font-size: 0.82rem; color: #8a6300; background: var(--status-warning-bg); border-radius: var(--radius); padding: 0.4rem 0.6rem; margin-top: 0.3rem; }

.attempt-history { font-size: 0.82rem; color: var(--muted); margin-bottom: 0.75rem; }

.attestation-box { background: var(--accent-light); border: 1px solid var(--accent); border-radius: var(--radius); padding: 0.85rem 1.05rem; margin-top: 1rem; }
.attestation-box label { display: flex; gap: 0.5rem; align-items: flex-start; font-weight: 500; cursor: pointer; font-size: 0.9rem; }

footer.site-footer { text-align: center; color: var(--muted); font-size: 0.8rem; padding: 1.5rem 1rem; }

/* ---------------------------------------------------------------------- */
/* Dashboard: KPI tiles, panel grid, status donut                          */
/* ---------------------------------------------------------------------- */

.kpi-row {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(130px, 1fr));
  gap: var(--space-5);
  margin: var(--space-6) 0;
}

.kpi-tile {
  background: var(--surface);
  border: 1px solid var(--border);
  border-radius: var(--radius);
  padding: var(--space-4) var(--space-5);
  border-top: 3px solid var(--status-muted);
}
.kpi-tile.good { border-top-color: var(--status-good); }
.kpi-tile.warning { border-top-color: var(--status-warning); }
.kpi-tile.serious { border-top-color: var(--status-serious); }
.kpi-tile .kpi-value { font-size: 1.7rem; font-weight: 700; line-height: 1.1; font-variant-numeric: tabular-nums; }
.kpi-tile .kpi-label { font-size: 0.76rem; color: var(--muted); text-transform: uppercase; letter-spacing: 0.03em; margin-top: 0.15rem; }

.panel-grid {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(280px, 1fr));
  gap: var(--space-5);
  align-items: start;
}
.panel-grid.cols-3 { grid-template-columns: repeat(auto-fit, minmax(230px, 1fr)); }

.panel {
  background: var(--surface);
  border: 1px solid var(--border);
  border-radius: var(--radius);
  overflow: hidden;
  box-shadow: 0 1px 2px rgba(15, 23, 32, 0.05);
}
/* #policy-form-panel is the one panel with a position:sticky descendant
   (.action-bar, in the policy editor form) — overflow:hidden on an
   ancestor establishes it as the sticky element's scrolling container,
   which here has no independent scroll region of its own (the page
   scrolls, not the panel), so the sticky bar never left its normal
   document position: it just sat at the bottom of the (very long) form,
   scrolling with the page like any static element. Confirmed with
   getBoundingClientRect() at several scroll positions before this fix —
   the bar's top moved exactly with scroll offset instead of staying
   pinned. Overriding to visible for this one panel only, since every
   other .panel relies on overflow:hidden to clip square-cornered children
   (.panel-header, .no-pad tables) to its own rounded corners and none of
   them contain a sticky element. */
#policy-form-panel { overflow: visible; }
/* .panel-body's own overflow-x:auto (below) forces its overflow-y to
   compute as auto too, per the CSS overflow spec ("if overflow-x or
   overflow-y is neither visible nor clip, the other computes to auto if
   it is visible") — that silently makes .panel-body a second scroll
   container ancestor blocking the same sticky .action-bar, even after
   the fix above. Confirmed via getComputedStyle before adding this rule:
   overflow was still reported "auto", not "visible", despite no CSS rule
   naming overflow-y directly. This panel-body holds a form, not a wide
   table, so it never needed the horizontal scroll in the first place. */
#policy-form-panel-body { overflow: visible; }
.panel-header {
  background: #f0f3f6;
  border-bottom: 1px solid var(--border);
  padding: var(--space-3) var(--space-5);
  font-size: 0.8rem;
  font-weight: 700;
  text-transform: uppercase;
  letter-spacing: 0.03em;
  color: var(--navy);
  display: flex;
  justify-content: space-between;
  align-items: center;
  gap: 0.5rem;
  border-radius: var(--radius) var(--radius) 0 0;
}
.panel-body { padding: var(--space-5) var(--space-6); overflow-x: auto; }

/* Appears only once at least one row is checked — a bulk-select action bar
   sitting between the panel header and the table it acts on. */
.bulk-action-bar {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 0.6rem;
  background: var(--accent-light);
  border-bottom: 1px solid var(--border);
  padding: var(--space-3) var(--space-5);
  font-size: 0.85rem;
  font-weight: 600;
  color: var(--navy);
}
.panel-body.no-pad { padding: 0; }
.panel-body.scroll { max-height: 320px; overflow-y: auto; }

.panel-list { list-style: none; margin: 0; padding: 0; }
.panel-list li {
  display: flex;
  justify-content: space-between;
  align-items: center;
  gap: 0.5rem;
  padding: 0.45rem 0.85rem;
  border-top: 1px solid var(--border);
  font-size: 0.86rem;
}
.panel-list li:first-child { border-top: none; }
.panel-list .item-title { font-weight: 600; }
.panel-list .item-meta { color: var(--muted); font-size: 0.78rem; }

.donut-wrap { display: flex; align-items: center; gap: 1rem; flex-wrap: wrap; }
.donut {
  width: 120px;
  height: 120px;
  border-radius: 50%;
  flex: none;
  display: flex;
  align-items: center;
  justify-content: center;
}
.donut .donut-hole {
  width: 68px;
  height: 68px;
  border-radius: 50%;
  background: var(--surface);
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
}
.donut .donut-hole .n { font-family: var(--font-display); font-size: 1.15rem; font-weight: 700; line-height: 1; }
.donut .donut-hole .lbl { font-size: 0.62rem; color: var(--muted); text-transform: uppercase; }

.legend { list-style: none; margin: 0; padding: 0; display: grid; gap: 0.35rem; font-size: 0.84rem; }
.legend li { display: flex; align-items: center; gap: 0.5rem; }
.legend .swatch { width: 10px; height: 10px; border-radius: 3px; flex: none; }
.legend .count { margin-left: auto; font-weight: 600; font-variant-numeric: tabular-nums; }

/* ---------------------------------------------------------------------- */
/* Hero ring + theme cards (2026-09-25 refresh) — see DESIGN.md's          */
/* "Dashboard hero ring + theme cards" component entry                     */
/* ---------------------------------------------------------------------- */

/* .hero-ring-panel is .donut-wrap's larger, vertically-stacked sibling:
   the same conic-gradient .donut technique, just bigger and centred, used
   as the single prominent "how am I doing overall" moment at the top of a
   dashboard, with .legend underneath instead of beside it (donut-wrap's
   own side-by-side layout is still used everywhere it already was —
   inside .panel-grid two-up layouts — this is additive, not a
   replacement). */
.hero-ring-panel { display: flex; flex-direction: column; align-items: center; text-align: center; }
.hero-ring-panel .donut { width: 160px; height: 160px; margin: var(--space-3) 0; }
.hero-ring-panel .donut .donut-hole { width: 92px; height: 92px; }
.hero-ring-panel .donut .donut-hole .n { font-size: 1.7rem; }
.hero-ring-panel .legend { width: 100%; margin-top: var(--space-3); }

/* .progress-ring is a SMALLER, single-value version of the same
   conic-gradient technique (no multi-segment breakdown, no centre number —
   just a percentage ring around an icon), for the per-theme cards below.
   A new component rather than reusing .donut at a smaller size: .donut's
   centre content is a number+label pair sized for 120px+; a card icon
   needs the hole itself to host an icon glyph instead, and needs a
   `background` set per-instance (the conic-gradient value), which
   `progressRing()` in charts.js generates inline the same way
   `renderDonut()` already does for `.donut`. */
.progress-ring {
  display: flex;
  align-items: center;
  justify-content: center;
  border-radius: 50%;
  flex: none;
  width: var(--ring-size, 46px);
  height: var(--ring-size, 46px);
}
.progress-ring .progress-ring-hole {
  width: 76%;
  height: 76%;
  border-radius: 50%;
  background: var(--surface);
  display: flex;
  align-items: center;
  justify-content: center;
  color: var(--ring-icon-color, var(--muted));
}
.progress-ring .progress-ring-hole svg { width: 55%; height: 55%; }

/* .theme-card is the per-category/per-project/per-channel "at a glance"
   unit a progress-ring pairs with: icon+ring on the left, name + a short
   meta line (count/percentage) on the right — used by dashboard.js today
   and, per DESIGN.md's rollout note, the same shape the other four
   module dashboards (Timesheet Budgets, Social Media Planner, Stakeholder
   Tracker, Financial Dashboard) adopt as they're each built out. Reuses
   .panel's own surface/border/shadow language (background, border,
   box-shadow) rather than inventing a second "bordered box" convention —
   only the radius (--radius-lg, softer) and padding differ from a plain
   .panel. */
.theme-card-grid {
  display: grid;
  grid-template-columns: repeat(2, minmax(0, 1fr));
  gap: var(--space-4);
}
.theme-card {
  display: flex;
  align-items: center;
  gap: var(--space-4);
  background: var(--surface);
  border: 1px solid var(--border);
  border-radius: var(--radius-lg);
  padding: var(--space-4) var(--space-5);
  box-shadow: 0 1px 2px rgba(15, 23, 32, 0.05);
}
.theme-card-name { font-weight: 700; font-size: 0.85rem; }
.theme-card-meta { font-size: 0.76rem; color: var(--muted); margin-top: 0.1rem; }

/* .needs-attention-panel — the compact "what's overdue/not started" list
   that sits alongside the hero ring on the dashboards using this pattern;
   reuses .panel-list's own row shape (below) rather than a third list
   convention, just without the badge-on-the-right layout .panel-list
   elsewhere uses (a status icon precedes the text here instead). */
.needs-attention-list { list-style: none; margin: 0; padding: 0; display: flex; flex-direction: column; gap: var(--space-3); }
.needs-attention-list li { display: flex; align-items: flex-start; gap: 0.55rem; }
.needs-attention-list svg { flex: none; margin-top: 0.1rem; }
.needs-attention-list .item-title { font-weight: 600; font-size: 0.82rem; line-height: 1.4; }
.needs-attention-list .item-meta { font-size: 0.74rem; font-weight: 400; }
/* Clickable needs-attention item (Home page, 2026-10-08 — needsAttentionItem()'s
   optional `href`) — the <a> carries the same flex layout/gap the bare <li>
   already had above, so a linked item looks identical to a non-linked one,
   plus a hover affordance (nothing else in this list is interactive). */
.needs-attention-list li a { display: flex; align-items: flex-start; gap: 0.55rem; text-decoration: none; color: inherit; width: 100%; border-radius: var(--radius); }
.needs-attention-list li a:hover .item-title { text-decoration: underline; }

/* Confetti burst (2026-09-25) — a one-off celebratory flourish (a quiz
   pass, per fireConfetti()'s doc comment in confetti.js) rather than a
   persistent UI element: fireConfetti() builds and removes this container
   itself, so the only job of this CSS is the fall animation each piece
   runs once. Fixed + pointer-events:none so it always sits on top without
   ever blocking a click on whatever's underneath it (e.g. the "Continue to
   acknowledgement" button that can appear the same moment). */
.confetti-burst { position: fixed; inset: 0; pointer-events: none; z-index: 999; overflow: hidden; }
.confetti-piece {
  position: absolute;
  top: -12px;
  border-radius: 2px;
  opacity: 0.95;
  animation-name: confetti-fall;
  animation-timing-function: cubic-bezier(0.35, 0, 0.65, 1);
  animation-fill-mode: forwards;
}
@keyframes confetti-fall {
  0% { transform: translate(0, 0) rotate(0deg); opacity: 1; }
  100% { transform: translate(var(--confetti-drift), 105vh) rotate(var(--confetti-rotate)); opacity: 0; }
}
@media (prefers-reduced-motion: reduce) {
  .confetti-burst { display: none; }
}

.filter-row { display: flex; gap: 0.6rem; align-items: center; flex-wrap: wrap; margin-bottom: var(--space-4); }
.filter-row select { padding: 0.4rem 0.6rem; border: 1px solid var(--border); border-radius: var(--radius); font-size: 0.87rem; }

/* Pill tab row (2026-09-25 refresh) — was an underlined-tab-strip
   (border-bottom on the row, a matching border-bottom on the active
   .tab-btn); now a row of pill buttons, the active one filled with
   --accent. No visual container line needed once the buttons carry their
   own shape, so the row's own border-bottom is dropped. */
.tabs { display: flex; align-items: center; gap: 0.35rem; flex-wrap: wrap; margin-bottom: var(--space-6); }
/* admin.html only — groups the tab bar into "Platform" (Users, Settings)
   vs. "Policy & Procedure" (Dashboard, Policies, Activity, Reports)
   sections, per DESIGN.md's "Admin console architecture" section. Same
   uppercase/muted/small-caps treatment already used for table headers
   (table.dashboard-table th) and kpi-tile labels — reused, not a new
   label style. Purely visual: doesn't affect .tab-btn's own click/hash
   wiring, which is unaware of grouping. */
.tab-group-label {
  font-size: 0.7rem;
  font-weight: 700;
  text-transform: uppercase;
  letter-spacing: 0.04em;
  /* --navy, not --muted (2026-09-16) — this label's own comment already ties
     it to table.dashboard-table th's small-caps treatment, which moved to
     --navy in the same pass; a section label grouping the tab bar is the
     same structural-header level as those, not a caption, so it follows. */
  color: var(--navy);
  white-space: nowrap;
  margin-right: 0.1rem;
}
.tab-group-divider { width: 1px; align-self: stretch; background: var(--border); margin: 0 0.5rem; }

/* admin.html only — the app-group selector (Platform/Projects/Policy &
   Procedure/Social Media Planner/Timesheet), the top level of the
   two-level admin nav (2026-09-22 restructure — see DESIGN.md's "Admin
   console architecture: two-level nav" section). New, narrowly-scoped
   classes rather than reusing/editing .tabs/.tab-btn's own rules above —
   this pass doesn't touch existing shared component spacing (a concurrent
   task owns a platform-wide spacing pass). .tab-group-label/
   .tab-group-divider above are left in place, unused by admin.js as of
   this change but not deleted, for the same reason. */
.admin-group-tabs { flex-wrap: wrap; gap: 0.45rem; margin-bottom: 1.1rem; }
.group-tab-btn {
  background: var(--surface);
  border: 1px solid var(--border);
  border-radius: 999px;
  padding: 0.5rem 1rem;
  font-size: 0.87rem;
  font-weight: 600;
  color: var(--ink);
  cursor: pointer;
  display: flex;
  align-items: center;
  gap: 0.4rem;
  transition: background .15s ease, border-color .15s ease, color .15s ease;
}
.group-tab-btn svg { flex: none; }
.group-tab-btn:hover { border-color: var(--navy); }
.group-tab-btn.active { background: var(--navy); border-color: var(--navy); color: #fff; }
/* Each non-active group's own tab row (`<div class="tabs" data-group-tabs
   hidden>`) carries both the `hidden` attribute and `.tabs`'s own
   `display: flex` — an author rule always beats the UA `[hidden] {
   display: none }` default regardless of specificity (see .btn-row[hidden]/
   .tab-panel[hidden] above, the same trap for the same reason). Without
   this, every group's tab row rendered stacked at once — reproduced via
   Playwright screenshot, not just read as "should be hidden" from the
   markup. */
[data-group-tabs][hidden] { display: none; }

.tab-btn {
  background: none;
  border: none;
  border-radius: 999px;
  padding: 0.55rem 1rem;
  font-size: 0.87rem;
  font-weight: 600;
  /* --ink, not --muted: this is the tab's entire visible content (its own
     name — "Dashboard", "Policies", …), not a caption paired with some
     other already-ink value elsewhere in the same element, so it's the
     "main point" case the muted/ink rebalance note in DESIGN.md calls out
     — reassigned from --muted as part of that pass. */
  color: var(--ink);
  cursor: pointer;
  display: flex;
  align-items: center;
  gap: 0.4rem;
  transition: background .15s ease, color .15s ease;
  /* .tab-btn started life as a <button> only (register.html/admin.html's
     JS-switched tabs); the Policies module's in-page tab row (nav.js's
     moduleTabsHtml()) reuses this same class on real <a> elements instead,
     so the browser default underline needs explicitly turning off here —
     buttons never had one, so this changes nothing for the existing
     button-based tab rows. */
  text-decoration: none;
}
.tab-btn:hover { background: var(--accent-light); }
.tab-btn svg { flex: none; }
/* Filled pill on active (2026-09-25 refresh) — was an underline-only
   active state (transparent background, --navy text + border-bottom);
   the pill shape needs a filled background to read as "current" the same
   way .chip.active already fills solid, not just a colour/underline
   change on an otherwise-invisible button shape. */
.tab-btn.active { background: var(--accent); color: #fff; }
.tab-btn.active:hover { background: var(--accent); }
.tab-panel[hidden] { display: none; }
/* Same author-rule-beats-[hidden] trap for the button itself (display:
   flex above): Project Hub's tab buttons ship hidden and are switched on
   per viewer, and without this they showed for everyone. Caught by the
   Hub's Playwright run as a staff user, 2026-10-08. */
.tab-btn[hidden] { display: none; }

/* ---------------------------------------------------------------------- */
/* Category colour-coding (fixed slots, --cat-1..--cat-6)                  */
/* ---------------------------------------------------------------------- */
.cat-1 { --cat: var(--cat-1); }
.cat-2 { --cat: var(--cat-2); }
.cat-3 { --cat: var(--cat-3); }
.cat-4 { --cat: var(--cat-4); }
.cat-5 { --cat: var(--cat-5); }
.cat-6 { --cat: var(--cat-6); }

.cat-chip {
  display: inline-flex;
  align-items: center;
  gap: 0.4rem;
  font-size: 0.72rem;
  font-weight: 700;
  /* Deliberately NOT var(--cat) here — yellow/magenta/aqua fail text contrast
     on a white surface at this weight/size (see the dataviz palette notes).
     Identity comes from the swatch dot below, not the text colour. */
  color: var(--ink);
}
.cat-chip::before { content: ""; width: 10px; height: 10px; border-radius: 3px; background: var(--cat, var(--navy)); flex: none; }
/* Per-category stroke icon (sections.js's categoryIconHtml()) — same 16px
   stroke language as admin.html's tab icons, sized down to 14px to sit
   inside a chip's small type without dwarfing it. flex: none so it doesn't
   get squeezed at narrow widths, same treatment as the .cat-chip swatch dot.
   Also used directly (not inside a chip) as an accordion-header icon in the
   admin Policies tab's "Public display order" panel — coloured navy there
   to match every other .acc-head icon (.icon class), same visual language. */
.cat-icon { flex: none; }
.acc-head .cat-icon { color: var(--navy); }

/* Registers page: one group per policy theme, each carrying its own tab
   set of individual registers — the same left-accent-border language as
   .section-card, so the page reads as an extension of the same colour
   coding used everywhere else, and scales cleanly as more themes' worth
   of registers get added alongside WHS (e.g. a Privacy register group). */
.register-group {
  border: 1px solid var(--border);
  border-left: 5px solid var(--cat, var(--border));
  border-top-left-radius: 0;
  border-bottom-left-radius: 0;
  border-top-right-radius: var(--radius);
  border-bottom-right-radius: var(--radius);
  background: var(--surface);
  padding: var(--space-5) var(--space-5) var(--space-6);
  margin-bottom: var(--space-6);
}
.register-group-header { margin-bottom: var(--space-4); }
.register-tabs { margin-top: 0; }

/* Colour-coded section cards — one shared component used on the landing
   page, both dashboards, and the admin "by policy" view. A flat white body
   keeps categories visually distinct via the border/chip/progress-bar
   colour alone, rather than a tinted fill (which washed out to near-
   identical pale backgrounds for hues of similar lightness, e.g. aqua vs
   green). The left accent's corners are squared off deliberately: a
   border-radius on all four corners fights a thick border-left, producing
   a slightly "skewed" notch where the curve meets the straight edge. */
/* Six fixed categories laid out 2 columns x 3 rows on the landing page and
   individual dashboard, so all themes are visible at a glance rather than
   one long scrolling stack. Collapses to a single column on narrow/mobile
   screens (see the max-width: 640px media query near the end of this file). */
#policy-groups,
.card-stack {
  display: grid;
  grid-template-columns: repeat(2, 1fr);
  gap: var(--space-5);
  align-items: start;
}

.section-card {
  border: 1px solid var(--border);
  border-left: 5px solid var(--cat, var(--border));
  border-top-left-radius: 0;
  border-bottom-left-radius: 0;
  border-top-right-radius: var(--radius);
  border-bottom-right-radius: var(--radius);
  background: var(--surface);
  margin-bottom: 0;
  overflow: hidden;
}
.section-card > summary,
.section-card > .section-card-head {
  list-style: none;
  cursor: pointer;
  padding: var(--space-4) var(--space-5);
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 0.6rem;
}
.section-card-head { cursor: default; }
.section-card > summary::-webkit-details-marker { display: none; }
/* Ring + icon (progressRing(), same component themeCard() uses) grouped
   with the name/count text as one flex item, so `justify-content:
   space-between` on the row above still puts the chevron alone at the far
   right (2026-09-25 — replaced a flat swatch chip + separate progress bar,
   see DESIGN.md's "My Dashboard: category cards get the ring treatment"
   entry). Reuses .theme-card-name/.theme-card-meta rather than inventing a
   third name/count text pairing. */
.section-card-head-main { display: flex; align-items: center; gap: var(--space-3); min-width: 0; }
.section-card > summary::after {
  content: "▸";
  color: var(--muted);
  transition: transform 0.15s ease;
  flex: none;
}
.section-card[open] > summary::after { transform: rotate(90deg); }
.section-card .section-card-body { padding: 0 var(--space-5) var(--space-5); }
.section-card .policy-list, .section-card .panel-list { margin: 0; }

.section-progress {
  height: 5px;
  background: var(--status-muted-bg);
  margin: 0 0.9rem 0.6rem;
  border-radius: 3px;
  overflow: hidden;
}
.section-progress-fill { height: 100%; background: var(--cat, var(--status-good)); border-radius: 3px; }

.mini-bar { width: 90px; height: 6px; background: var(--status-muted-bg); border-radius: 3px; overflow: hidden; display: inline-block; vertical-align: middle; }
/* display: block is load-bearing (2026-10-08): the fill is a <span>, and
   width/height do nothing on an inline element, so every mini bar on the
   site had been drawing an empty track. Same trap as .switch-track. */
.mini-bar-fill { display: block; height: 100%; background: var(--status-good); border-radius: 3px; }

/* ---------------------------------------------------------------------- */
/* Policy metadata box (version / owner / last updated)                    */
/* ---------------------------------------------------------------------- */
.policy-meta-box {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(140px, 1fr));
  gap: 0.5rem;
  background: var(--navy);
  color: #fff;
  border-radius: var(--radius);
  padding: 0.75rem 1rem;
  margin: 0.6rem 0 1rem;
}
.policy-meta-box .meta-item .label { font-size: 0.68rem; text-transform: uppercase; letter-spacing: 0.04em; color: #b8c6d4; }
.policy-meta-box .meta-item .value { font-size: 0.95rem; font-weight: 600; }
.policy-meta-box .meta-item .value.missing { font-weight: 400; font-style: italic; color: #b8c6d4; }

.overdue-tag { font-size: 0.72rem; color: var(--status-serious); font-weight: 700; }

/* ---------------------------------------------------------------------- */
/* Fillable forms                                                          */
/* ---------------------------------------------------------------------- */
.form-print-area label { font-weight: 600; font-size: 0.85rem; display: block; margin-bottom: 0.2rem; }
.form-print-area .form-input {
  width: 100%;
  padding: 0.45rem 0.65rem;
  border: 1px solid var(--border);
  border-radius: var(--radius);
  font: inherit;
  background: #fff;
}
.form-print-area textarea.form-input { resize: vertical; }
.form-print-area h3 { border-bottom: 1px solid var(--border); padding-bottom: 0.3rem; }
.print-only { display: none; }
/* html2pdf (form.js's emailed-PDF path) screenshots the live screen DOM —
   it has no concept of @media print — so .print-only content is invisible
   to it by default. form.js adds this class to the capture target just
   for the capture, to force the letterhead into the render. */
.pdf-capturing .print-only { display: block !important; }

.form-letterhead {
  text-align: center;
  padding-bottom: 0.85rem;
  margin-bottom: 1.25rem;
  border-bottom: 2px solid var(--navy);
}
.form-letterhead-logo { height: 44px; margin: 0 auto 0.6rem; display: block; }
.form-letterhead-title { margin: 0; font-size: 1.3rem; font-weight: 700; color: var(--navy); }
.form-letterhead-date { margin-top: 0.25rem; font-size: 0.85rem; color: var(--muted); }

/* Printed/PDF output only: keep each section and table row intact rather
   than letting html2pdf or the browser's own paginator slice one in half
   across a page boundary. */
.form-print-area section,
.form-print-area table tr,
.form-print-area .card {
  page-break-inside: avoid;
  break-inside: avoid;
}

.certificate-card {
  max-width: 640px;
  margin: 1.5rem auto;
  padding: 2.5rem 2rem;
  text-align: center;
  border: 2px solid var(--navy);
  outline: 1px solid var(--gold, var(--navy));
  outline-offset: -8px;
}
.certificate-logo { height: 48px; margin: 0 auto 1rem; display: block; }
.certificate-kicker { font-size: 0.78rem; letter-spacing: 0.12em; text-transform: uppercase; color: var(--muted); }
.certificate-heading { color: var(--navy); font-size: 1.6rem; margin: 0.3rem 0 1.25rem; }
.certificate-body { color: var(--muted); margin: 0.2rem 0; }
.certificate-name { font-size: 1.4rem; font-weight: 700; color: var(--ink); margin: 0.3rem 0; }
.certificate-subject { font-size: 1.15rem; font-weight: 600; color: var(--navy); margin: 0.3rem 0 1rem; }
.certificate-date { color: var(--muted); font-size: 0.9rem; margin: 0 0 1.75rem; }
.certificate-sign-line { display: inline-block; text-align: center; font-size: 0.85rem; color: var(--muted); }
.certificate-sign-rule { width: 220px; border-top: 1px solid var(--border); margin-bottom: 0.35rem; }

@media print {
  .no-print { display: none !important; }
  .print-only { display: block !important; }
  /* The certificate's border is decorative and deliberate — only strip the
     plain .card box border everything else gets from being a .card. */
  .form-print-area:not(.certificate-card) { border: none; padding: 0; }
  main { max-width: none; }
  body { background: #fff; }
  /* The sidebar (2026-10-05) is position:fixed, which a browser's native
     print either tiles across every page or renders with unpredictable
     positioning — hide it outright and drop body's own sidebar offset,
     rather than relying on individual pages' own `.no-print` class on
     `#site-header` (several pages don't carry it, since the one actual
     "get a PDF out of this" mechanism is pdfExport.js's html2canvas
     capture of a specific content selector, never the full page — see
     DESIGN.md's "Print/PDF: one mechanism" — so this is a generic safety
     net for a manual browser Ctrl+P, not a path anything here relies on). */
  .site-header { display: none !important; }
  body, body.sidebar-collapsed { margin-left: 0 !important; padding-top: 0 !important; }
  @page { margin: 16mm 14mm; }
  /* Applies to every printed page (user/policy reports, certificates), not
     just fillable forms — a browser's own paginator will still slice a
     table row in half at a page boundary if not told not to. */
  table tr, .panel, .form-letterhead, .certificate-card {
    page-break-inside: avoid;
    break-inside: avoid;
  }
  table thead { display: table-header-group; }
}
.tab-panel[hidden] { display: none; }

/* ---------------------------------------------------------------------- */
/* Mobile                                                                   */
/* ---------------------------------------------------------------------- */
@media (max-width: 640px) {
  main { padding: 1rem 0.75rem 2.5rem; }

  /* Sidebar collapses to a slim top bar + hamburger-opened drawer below
     this breakpoint (2026-10-05 — see DESIGN.md's "Sidebar navigation
     replaces the top bar" entry). `body`'s desktop sidebar offset no
     longer applies — the sidebar isn't holding horizontal space any
     more, just the top-bar's own height, via padding-top. */
  body, body.sidebar-collapsed { margin-left: 0; padding-top: var(--sidebar-mobile-h); }

  .site-header,
  .site-header.collapsed {
    width: 100%;
    height: var(--sidebar-mobile-h);
    flex-direction: row;
    align-items: center;
    border-right: none;
    border-bottom: 1px solid var(--border);
  }
  .site-header .sidebar-topbar {
    display: flex;
    align-items: center;
    justify-content: space-between;
    width: 100%;
    padding: 0 0.85rem;
  }
  /* The panel's own head only needs a close button on mobile (it's a
     drawer, opened from the topbar above, which has its own brand) — the
     desktop collapse toggle has nothing to collapse into here. */
  .site-header .sidebar-head .sidebar-collapse-btn { display: none; }
  .site-header .sidebar-head .sidebar-brand { display: none; }
  .site-header .sidebar-close-btn {
    display: inline-flex;
    background: none;
    border: none;
    color: var(--muted);
    cursor: pointer;
    padding: 0.3rem;
    border-radius: 8px;
    margin-left: auto;
  }
  .site-header .sidebar-close-btn:hover { background: var(--accent-light); color: var(--navy); }
  .site-header .sidebar-head { justify-content: flex-end; }

  .site-header .sidebar-panel {
    display: none;
    position: fixed;
    top: 0;
    left: 0;
    bottom: 0;
    width: 270px;
    max-width: 85vw;
    box-shadow: 2px 0 24px rgba(36, 31, 56, 0.18);
    z-index: 50;
    background: var(--surface);
  }
  .site-header.menu-open .sidebar-panel { display: flex; }
  .site-header.menu-open .sidebar-backdrop {
    display: block;
    position: fixed;
    inset: 0;
    background: rgba(36, 31, 56, 0.45);
    z-index: 45;
  }

  /* The 2x3 theme grid only works with room to breathe; one column below
     tablet width keeps each card full-width and readable. .whs-record-grid
     is already one column at every width, nothing to override here. */
  #policy-groups,
  .card-stack {
    grid-template-columns: 1fr;
  }

  .kpi-row { grid-template-columns: repeat(2, 1fr); }
  .panel-grid, .panel-grid.cols-3 { grid-template-columns: 1fr; }
  .theme-card-grid { grid-template-columns: 1fr; }
  .ps-task-line-grid { grid-template-columns: 1fr; }
  .ps-col-totals { align-items: flex-start; text-align: left; }
  .ps-line-cost-pair { align-items: flex-start; }

  .policy-meta-box { grid-template-columns: 1fr 1fr; }

  .btn-row { gap: 0.4rem; }
  .btn.small { padding: 0.35rem 0.6rem; }

  table.dashboard-table { font-size: 0.8rem; }
}

/* ---------------------------------------------------------------------- */
/* WHS training register — one full-width row per record (not a card       */
/* grid, so a long list of a person's training reads top-to-bottom like a  */
/* register, not left-right-left), with the notes field free to wrap       */
/* instead of truncating like a dense table cell would.                    */
/* ---------------------------------------------------------------------- */
.whs-record-grid {
  display: grid;
  grid-template-columns: 1fr;
  gap: 0.6rem;
  align-items: start;
}
.whs-record {
  display: flex;
  flex-wrap: wrap;
  gap: 0.85rem;
  justify-content: space-between;
  align-items: flex-start;
  border: 1px solid var(--border);
  border-radius: var(--radius);
  padding: 0.75rem 0.85rem;
  background: var(--surface);
}
.whs-record.inactive { background: var(--bg); opacity: 0.75; }
.whs-record.add-row { border-style: dashed; background: #f7faf8; }

.whs-record-main { flex: 1 1 320px; min-width: 260px; display: grid; gap: 0.5rem; }
.whs-record-main .badge { justify-self: start; }
/* Soft highlight so the course title reads as the row's heading at a
   glance, scanning down a full list of rows — a permanent light tint
   rather than only appearing on hover/focus like a plain editable field. */
.whs-record-title { font-weight: 600; border: 1px solid transparent; background: var(--bg, #f7f8fa); padding: 0.3rem 0.5rem; font-size: 0.95rem; border-radius: 4px; }
.whs-record-title:not([disabled]):hover, .whs-record-title:focus { border-color: var(--border); background: #fff; }
.whs-record-title[disabled] { color: var(--ink); }

.whs-record-fields { display: flex; flex-wrap: wrap; gap: 0.85rem; }
.whs-record-fields label,
.whs-record-main > label {
  display: flex;
  flex-direction: column;
  gap: 0.2rem;
  font-size: 0.72rem;
  font-weight: 600;
  text-transform: uppercase;
  letter-spacing: 0.03em;
  color: var(--muted);
}
.whs-record-fields input,
.whs-record-main textarea {
  font: inherit;
  text-transform: none;
  letter-spacing: normal;
  font-weight: 400;
  color: var(--ink);
  border: 1px solid var(--border);
  border-radius: 4px;
  padding: 0.3rem 0.5rem;
  background: #fff;
  min-width: 160px;
}
.whs-record-main textarea { width: 100%; min-width: 220px; resize: vertical; }
.whs-record-fields input[disabled],
.whs-record-main textarea[disabled] { background: transparent; border-color: transparent; padding-left: 0; }

.whs-record-actions { display: flex; flex-direction: column; gap: 0.4rem; min-width: 110px; }

/* ---------------------------------------------------------------------- */
/* Induction pack policy picker — one box, grouped by category            */
/* ---------------------------------------------------------------------- */
.pack-policy-group-header {
  padding: 0.4rem 0.6rem;
  background: #f0f3f6;
  border-bottom: 1px solid var(--border);
  border-top: 1px solid var(--border);
}
.pack-policy-group:first-child .pack-policy-group-header { border-top: none; }
form.stack label.pack-policy-row {
  display: flex;
  align-items: flex-start;
  gap: 0.5rem;
  padding: 0.5rem 0.6rem 0.5rem 1rem;
  font-weight: 400;
  cursor: pointer;
  border-bottom: 1px solid var(--border);
  margin-bottom: 0;
}
.pack-policy-group .pack-policy-row:last-child { border-bottom: none; }
.pack-policy-row:hover { background: var(--bg); }
tr.table-group-header td {
  background: #f0f3f6;
  padding: 0.4rem 0.6rem;
  font-weight: 700;
}

form.stack .pack-policy-row input[type="checkbox"] {
  flex: none;
  width: 16px;
  height: 16px;
  padding: 0;
  border: 1px solid var(--border);
  margin-top: 0.2rem;
}
.pack-policy-row-text { display: flex; flex-direction: column; gap: 0.1rem; }
.pack-policy-row-title { font-weight: 600; }
.pack-policy-row-desc { color: var(--muted); font-size: 0.82rem; }

/* "Trigger staff review?" radio rows (admin.js's review-trigger-prompt) —
   plain labels inside form.stack, which otherwise stretches every <input>
   to width:100% with input-field padding/border, blowing a radio up into a
   full-width box disconnected from its own label text. Same override this
   codebase already needed for .pack-policy-row's checkbox and .qb-option's
   radio above/below. margin-top optically centres the circle on the first
   line of a label that wraps to two or three lines, the same trick used for
   .pack-policy-row's checkbox. */
.review-trigger-option { display: flex; align-items: flex-start; gap: 0.5rem; font-weight: 400; }
.review-trigger-option input[type="radio"] { flex: none; width: auto; padding: 0; margin: 0.2rem 0 0; }

/* Invite form's module checkboxes (admin.js's Users tab) — the third
   occurrence of the exact same form.stack-stretches-every-input problem
   .pack-policy-row and .review-trigger-option above already had to work
   around, so this is the shared fix DESIGN.md's "one way to do it" calls
   for once the same pattern shows up a third time, not a fourth one-off. */
.module-grant-option { display: flex; align-items: center; gap: 0.5rem; font-weight: 400; }
.module-grant-option input[type="checkbox"],
.module-grant-option input[type="radio"] { flex: none; width: auto; padding: 0; margin: 0; }

/* ---------------------------------------------------------------------- */
/* Admin: policy form                                                       */
/* ---------------------------------------------------------------------- */
/* Quill runs the bubble theme here, not snow: the toolbar only pops up next
   to selected text rather than sitting as a permanent bar, which sidesteps
   needing it to track scroll position at all through a long policy body.
   Bubble strips its own border, so give the editor area one back. */
#p-content-editor .ql-container.ql-bubble {
  border: 1px solid var(--border);
  border-radius: var(--radius);
}

/* A short one-line label and a long two-line one sat in the same grid row
   otherwise start their inputs at different heights — reserve two lines
   for every label so the row stays level regardless of wording length.
   Applies to every .panel-grid.cols-3 (the policy form and the bulk-edit
   panel both use it), not just the policy form. */
.panel-grid.cols-3 > div > label {
  display: block;
  min-height: 2.4em;
}

/* Content editor + live preview, side by side — the preview pane is a
   plain .policy-content div (the exact class policyView.js renders a
   published policy into), so what shows here while editing is the same
   markup a staff member will actually see, not a second approximation of
   it that could drift out of sync. */
.editor-split { display: grid; grid-template-columns: 1fr 1fr; gap: 1rem; align-items: start; }
.editor-preview-pane { max-height: 480px; overflow-y: auto; }
@media (max-width: 900px) {
  .editor-split { grid-template-columns: 1fr; }
}

/* Generic small square icon button — quiz builder row actions today,
   available for any future icon-only action button. */
.icon-btn {
  width: 30px;
  height: 30px;
  border-radius: 6px;
  border: 1px solid var(--border);
  background: #fff;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  color: var(--muted);
  cursor: pointer;
  flex: none;
  transition: border-color .15s ease, color .15s ease;
}
.icon-btn:hover { border-color: var(--navy); color: var(--navy); }
/* Destructive variant (SMP task-detail's Delete icon) — same hover pattern
   as .btn.danger above, applied to the icon-only button shape. */
.icon-btn.danger:hover { border-color: var(--danger); color: var(--danger); }

/* Dashed "add a new item" button — quiz builder's "+ Add a question"
   today, the same shape any future add-to-a-list action should reuse. */
.add-q-btn {
  width: 100%;
  border: 1.5px dashed var(--border);
  border-radius: var(--radius);
  background: transparent;
  padding: 0.6rem;
  display: flex;
  align-items: center;
  justify-content: center;
  gap: 0.5rem;
  color: var(--accent);
  font-weight: 600;
  font-size: 0.87rem;
  cursor: pointer;
}
.add-q-btn:hover { border-color: var(--accent); background: var(--accent-light); }

/* Visual quiz builder — one card per topic ("slot"), each holding one or
   more variant phrasings. Replaces a raw JSON textarea; see quizBuilder.js. */
.qb-list { display: flex; flex-direction: column; gap: 0.6rem; margin-bottom: 0.6rem; }
.qb-topic { border: 1px solid var(--border); border-radius: var(--radius); background: #fbfcfd; padding: 0.7rem 0.85rem; }
.qb-topic-head { display: flex; align-items: center; gap: 0.5rem; margin-bottom: 0.6rem; }
.qb-topic-head .grip { color: var(--muted); cursor: grab; flex: none; }
.qb-topic-name { flex: 1; font-weight: 600; }
.qb-variants { display: flex; flex-direction: column; gap: 0.6rem; }
.qb-variant { border-top: 1px solid var(--border); padding-top: 0.6rem; }
.qb-variant:first-child { border-top: none; padding-top: 0; }
.qb-label { font-size: 0.8rem; font-weight: 600; display: block; margin-bottom: 0.2rem; }
.qb-question { width: 100%; margin-bottom: 0.5rem; }
.qb-options { display: flex; flex-direction: column; gap: 0.4rem; }
.qb-option { display: flex; align-items: center; gap: 0.5rem; }
.qb-option .qb-option-text { flex: 1; width: auto; }
.qb-option input[type="radio"] { flex: none; width: auto; }
.qb-variant-actions { display: flex; gap: 1rem; margin-top: 0.5rem; }
.qb-add-option, .qb-remove-variant, .qb-add-variant-topic {
  background: none;
  border: none;
  color: var(--accent);
  font-size: 0.82rem;
  font-weight: 600;
  cursor: pointer;
  padding: 0;
}
.qb-remove-variant { color: var(--danger); }
.qb-add-variant-topic { margin-top: 0.6rem; }

/* Drag-and-drop reordering — quiz-builder topics and induction-pack items
   share this exact visual language (dim the dragged row, highlight the
   drop target) so the interaction reads as one pattern across the admin. */
.draggable-row { cursor: grab; }
.draggable-row.dragging { opacity: 0.4; }
.draggable-row.drag-over { border-color: var(--accent) !important; box-shadow: 0 0 0 2px var(--accent-light); }

/* Deep-link scroll target (social-media-planner.html's ?task=<uuid>
   support — see socialMediaPlanner.js's highlightDeepLinkedTask()) — a
   brief colour flash that fades out on its own so the row reads as "you
   were just sent here" without permanently marking it. var(--accent-light),
   same "temporary highlight" language .draggable-row.drag-over/.cal-day.
   drag-over above already use for a live interaction state, reused here for
   a one-shot arrival flash instead. */
@keyframes deep-link-flash {
  0%, 35% { background-color: var(--accent-light); }
  100% { background-color: transparent; }
}
tr.deep-link-highlight { animation: deep-link-flash 2.4s ease-out; }

/* ---------------------------------------------------------------------- */
/* Admin: policy editor — always-visible dashboard strip + collapsible     */
/* sections, replacing one long flat form.                                 */
/* ---------------------------------------------------------------------- */

.field-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(240px, 1fr)); gap: 1rem; }
/* Same fix as .panel-grid.cols-3 above, same reason: "Title" and "Policy
   owner (person or role accountable for it)" sit in the same row and wrap
   to different heights otherwise. */
.field-grid > div > label { display: block; min-height: 2.4em; }

/* Searchable select (combobox) — assets/js/searchSelect.js. Originally
   relied on form.stack input's own rule for its box model (width/padding/
   border/font) — fine for the dialog forms that happen to wrap a combobox
   in a form.stack, but every project-picker call site added 2026-09-28/29
   (Timesheet's entry bar, Stakeholder Tracker's picker panel, Project
   Setup's picker row) sits in a plain .btn-row/.panel-body/flex row
   instead, so the input fell back to the browser's native ~20-char default
   width with no padding/border/radius at all — visibly tiny, its
   placeholder text truncated mid-word, and (Project Setup) crushed up
   against the "+ New project" button beside it. Reported directly against
   a live screenshot of exactly this ("don't cramb it in") — root cause
   confirmed with a standalone Playwright repro (real searchSelect.js/
   styles.css, no Supabase involved) before touching anything: the search-
   filter LOGIC was never broken (typing already narrowed the list
   correctly), only this box-model gap made the field look broken. Fixed by
   giving .combobox-input its own base style, unconditional on whatever
   wraps it — same values form.stack input already used, so nothing
   already working (the dialog-form combobox instances) visibly changes.
   .combobox-field (the outer label+input wrapper searchSelectHtml() itself
   renders) gets a flex-grow rule so it actually claims the width it's now
   entitled to in a flex row instead of shrink-wrapping to content — a
   no-op in a grid context (field-grid/.panel-grid), since flex properties
   only apply to flex children. No z-index elsewhere in this file (every
   other floating surface is a native <dialog>, which stacks above the page
   for free) — a small explicit value here is the one legitimate use: this
   menu must sit above sibling form fields/table rows it's positioned over. */
.combobox-field { flex: 1 1 220px; min-width: 0; }
.combobox { position: relative; width: 100%; }
.combobox-input {
  width: 100%;
  padding: 0.45rem 0.65rem;
  border: 1px solid var(--border);
  border-radius: var(--radius);
  font-size: 0.92rem;
  font-family: inherit;
}
/* Timesheet entry bar's Task <select> (2026-09-29) — reported directly
   against a live screenshot as a plain boxy native dropdown next to the
   pill-treatment Project combobox right beside it. Same root cause as
   .combobox-input's own box-model-gap fix just above (2026-09-28/29): this
   <select> is rendered by fields.js's selectField(), which emits a bare
   <select> with no class of its own and relies on an ancestor selector
   (form.stack select / .filter-row select) — neither of which wraps this
   entry bar's plain .btn-row#add-row-form, so it fell back to full
   browser-default styling. Same values as .combobox-input (reused, not
   reinvented), scoped to #add-row-form's own <select> rather than a
   page-wide `select` rule, so nothing else on the page changes appearance. */
#add-row-form select {
  padding: 0.45rem 0.65rem;
  border: 1px solid var(--border);
  border-radius: var(--radius);
  font-size: 0.92rem;
  font-family: inherit;
  background: var(--surface);
}
/* A combobox whose floating menu sits directly inside a .panel gets
   clipped/scrolled by that panel's own overflow — the exact bug class
   #policy-form-panel/#policy-form-panel-body above already documents
   (.panel's own overflow:hidden, plus .panel-body's overflow-x:auto
   forcing overflow-y to compute as auto per the CSS overflow spec).
   Reused here as a class, not more one-off ids, since this now applies to
   the Stakeholder Tracker and Project Setup project pickers alike, both
   sitting directly in a .panel-body with no wide table/sticky element of
   their own that would need real clipping. */
.panel-overflow-visible, .panel-overflow-visible > .panel-body { overflow: visible; }
.combobox-menu {
  position: absolute;
  top: calc(100% + var(--space-1));
  left: 0;
  right: 0;
  z-index: 20;
  max-height: 260px;
  overflow-y: auto;
  background: var(--surface);
  border: 1px solid var(--border);
  border-radius: var(--radius);
  box-shadow: 0 8px 24px rgba(0, 0, 0, 0.12);
  padding: var(--space-1) 0;
}
.combobox-option {
  padding: var(--space-2) var(--space-3);
  font-size: 0.88rem;
  cursor: pointer;
  display: flex;
  justify-content: space-between;
  gap: var(--space-3);
}
.combobox-option:hover, .combobox-option:focus, .combobox-option.combobox-active { background: var(--accent-light); }
.combobox-option.combobox-create { color: var(--accent); font-weight: 600; border-bottom: 1px solid var(--border); }
.combobox-sub { color: var(--muted); font-size: 0.8rem; }
/* Bold title / muted sub-line stacked vertically, for a combobox option
   passing searchSelect.js's own `html` field instead of plain label/sub
   (2026-09-28, Timesheet's project picker — project name bold, client name
   on its own line below, not a flat concatenated string). The wrapping div
   in that `html` markup is .combobox-option's only flex child, so it isn't
   affected by that rule's own justify-content: space-between. */
.combobox-option-title { font-weight: 600; }
.combobox-empty { padding: var(--space-2) var(--space-3); font-size: 0.85rem; color: var(--muted); }

/* Multi-select tag-picker (assets/js/tagPicker.js, 2026-10-05 — Stakeholder
   Tracker cohorts' Register-form assignment UI). Sits on top of the SAME
   .combobox/.combobox-menu/.combobox-option rules above — only the chips
   row (.tag-picker-chips/.tag-chip/.tag-chip-remove) is new, so this
   picker's dropdown already matches every other combobox on the site. */
.tag-picker-field { display: flex; flex-direction: column; gap: 0.3rem; }
.tag-picker-chips {
  display: flex;
  flex-wrap: wrap;
  gap: 0.35rem;
  min-height: 1.6rem;
}
.tag-chip {
  display: inline-flex;
  align-items: center;
  gap: 0.3rem;
  background: var(--accent-light);
  color: var(--ink);
  border: 1px solid var(--border);
  border-radius: 999px;
  padding: 0.2rem 0.3rem 0.2rem 0.7rem;
  font-size: 0.82rem;
  line-height: 1.4;
}
.tag-chip-remove {
  all: unset;
  cursor: pointer;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 1.1rem;
  height: 1.1rem;
  border-radius: 50%;
  font-size: 0.95rem;
  line-height: 1;
  opacity: 0.75;
}
.tag-chip-remove:hover, .tag-chip-remove:focus { opacity: 1; background: rgba(0, 0, 0, 0.12); }
.tag-chip-empty { color: var(--muted); font-size: 0.85rem; padding: 0.2rem 0; }

/* List-view filter bar (assets/js/stakeholders.js's Register/Engagement
   Log/Commitments/Complaints tabs — see DESIGN.md's "List-view filters"
   entry). A label+control pair per filter, outside any form.stack (that
   selector's own bold/block label styling doesn't reach here), so its own
   compact label style is given explicitly rather than duplicating
   form.stack's rule verbatim. */
.filter-bar { display: flex; gap: var(--space-4); flex-wrap: wrap; align-items: flex-end; margin: var(--space-3) 0 var(--space-4); }
/* `> div` (not a class) so a plain `<div><label>X</label><select>…</select></div>`
   filter item stacks its label above its control with no markup change
   needed anywhere this bar is used. Root cause of a real bug (2026-10-01,
   reported as "the Tasks tab is completely blank"/"the kanban toggle does
   nothing"): stakeholders.js's own Engagement Log/Commitments/Complaints/
   Tasks filter bars used to wrap their `searchSelectHtml()` stakeholder
   picker in an EXTRA plain `<div>` (sometimes with an inline
   `style="min-width:220px;"` duplicating what `.combobox-field` already
   sets below) — that wrapper is ALSO a direct `.filter-bar > div` child, so
   it ALSO became a column-direction flex box. `.combobox-field`'s own
   `flex: 1 1 220px` (below) is written for sitting directly in a flex ROW
   (this bar's own `display:flex`, default row direction) — nested one level
   deeper inside a column-direction single-child box instead, that same
   220px flex-basis applied along the COLUMN axis, making the picker render
   ~220px TALL instead of ~220px wide. Confirmed with a real rendered
   `getBoundingClientRect()` (not just read as plausible): `.combobox-field`
   measured exactly 220×220, and the filter-bar's later siblings (Status/
   Assignee/etc.) were shoved ~200px down a blank-looking gap — on a normal
   (non-full-page) screenshot or viewport, that gap alone fills the visible
   area, pushing the actual table below the fold and reading as "the tab is
   empty"/"nothing responds to clicks" even though every row (and the List/
   Board toggle) was still there, just scrolled out of view. Fixed by
   dropping the extra wrapper — `searchSelectHtml()`'s own `.combobox-field`
   is now a DIRECT `.filter-bar` child everywhere it's used in a filter bar,
   exactly what its own flex-row-item styling already assumes. See
   stakeholders.js's engagementFilterBarHtml()/commitmentFilterBarHtml()/
   complaintFilterBarHtml()/taskFilterBarHtml(). */
.filter-bar > div { display: flex; flex-direction: column; gap: 0.2rem; }
.filter-bar label { font-weight: 600; font-size: 0.78rem; color: var(--muted); text-transform: uppercase; letter-spacing: 0.02em; }
.filter-bar select, .filter-bar input[type="date"], .filter-bar .combobox-input {
  padding: 0.4rem 0.6rem;
  border: 1px solid var(--border);
  border-radius: var(--radius);
  font-size: 0.85rem;
  font-family: inherit;
}

/* Hybrid assignee field (owner/owner_name) — assets/js/stakeholderShared.js.
   Compact variant used in admin.js's cross-project Settings table cells. */
.owner-mode-row { flex-wrap: nowrap; }
.owner-mode-row label { font-weight: 400; white-space: nowrap; }
td .owner-mode-row { margin: 0 0 var(--space-1); }
td .owner-staff-select, td .owner-custom-input { font-size: 0.85rem; padding: 0.3rem 0.5rem; }

/* The per-policy dashboard — completion ring, KPI counts, currency at a
   glance. Never collapses (it's not an <details>): it's the one thing an
   admin should see before anything else on this page, editing or not. */
.dash-panel {
  background: var(--surface);
  border: 1px solid var(--border);
  border-radius: var(--radius);
  padding: 1.1rem 1.15rem;
  display: flex;
  flex-direction: column;
  gap: 1rem;
  margin-bottom: 0.85rem;
}
.dash-top-row { display: flex; gap: 1.6rem; align-items: center; flex-wrap: wrap; }
.currency-strip { display: flex; gap: 1.6rem; flex-wrap: wrap; border-top: 1px solid var(--border); padding-top: 0.85rem; font-size: 0.83rem; color: var(--muted); }
.currency-strip b { color: var(--ink); }

/* One collapsible section per concern (details/owner, currency/schedule,
   quiz/pass settings, staff status, content) — condensed by default so the
   page opens short; the exact <details>/<summary> idiom sections.js's
   categoryCard() already uses elsewhere, so this isn't a second accordion
   mechanism, just this page's use of the one the site already has. */
.acc { background: var(--surface); border: 1px solid var(--border); border-radius: var(--radius); overflow: hidden; margin-bottom: 0.6rem; }
.acc-head { list-style: none; cursor: pointer; display: flex; align-items: center; gap: 0.55rem; background: #f0f3f6; padding: 0.65rem 1rem; }
.acc-head::-webkit-details-marker { display: none; }
.acc-head .icon { flex: none; color: var(--navy); }
.acc-title { font-size: 0.82rem; font-weight: 700; text-transform: uppercase; letter-spacing: 0.03em; color: var(--navy); }
.acc-meta { margin-left: auto; font-size: 0.8rem; color: var(--muted); padding-right: 0.6rem; }
.acc-head::after { content: "▸"; color: var(--muted); flex: none; transition: transform 0.15s ease; }
.acc[open] > .acc-head::after { transform: rotate(90deg); }
.acc > .card-body { padding: 1.1rem 1.15rem; }

/* Accessible toggle switch — a visually-hidden native checkbox plus a
   styled track/knob sibling, so it stays keyboard/screen-reader operable
   while looking like a switch instead of a checkbox for "Requires quiz" /
   "Requires acknowledgement", which read as settings, not list selections. */
/* display: inline-block on .switch-track is load-bearing — it's a <span>,
   inline by default, and width/height are silently no-ops on a non-
   replaced inline element per the CSS spec. Its explicit 38x21 never
   actually applied: getComputedStyle reported 38px correctly (the
   stylesheet rule matched) while the real laid-out box was zero-size,
   collapsing the track to nothing and leaving only its absolutely-
   positioned .switch-knob child floating with no visible track under it —
   plus the also-absolute .switch-input (meant to be invisible) rendering
   at native size across the label text instead of being safely out of the
   way inside a properly-sized parent. Caught by comparing
   getComputedStyle (said 38px) against getBoundingClientRect (said 0),
   not by trusting the stylesheet rule matched meant it took effect. */
.switch-label { position: relative; display: inline-flex; align-items: center; gap: 0.55rem; font-weight: 400; cursor: pointer; }
/* form.stack label (display: block; weight 600) otherwise outranks
   .switch-label inside a form, dropping the label text below the track. */
form.stack label.switch-label { display: inline-flex; font-weight: 400; margin-bottom: 0; }
.switch-input { position: absolute; top: 0; left: 0; opacity: 0; width: 1px; height: 1px; }
.switch-track { display: inline-block; width: 38px; height: 21px; border-radius: 20px; background: var(--border); position: relative; flex: none; transition: background 0.15s ease; }
.switch-track .switch-knob { position: absolute; top: 2px; left: 2px; width: 17px; height: 17px; border-radius: 50%; background: #fff; box-shadow: 0 1px 2px rgba(0,0,0,0.25); transition: left 0.15s ease; }
.switch-input:checked + .switch-track { background: var(--accent); }
.switch-input:checked + .switch-track .switch-knob { left: 19px; }
.switch-input:focus-visible + .switch-track { outline: 2px solid var(--accent); outline-offset: 2px; }

/* Shared colour-picker component (docs/assets/js/colorPicker.js) — the one
   colour-configuration control used everywhere an admin picks a colour
   (Stakeholder Tracker and Social Media Planner status colours, Odoo
   Dashboard chart colours), replacing the old fixed-palette selects.
   Simplified 2026-09-25 to match CMS1's own colour settings pattern: a
   native colour swatch plus a plain hex text field, nothing else — see
   colorPicker.js's own top comment. The compact modifier
   (.color-picker-compact) fits a dense table cell, the default spacing
   suits a roomier settings dialog. */
.color-picker { display: flex; flex-direction: column; gap: 0.3rem; min-width: 150px; }
.color-picker label { font-weight: 400; }
.color-picker-mode { width: 100%; }
.color-picker-controls { display: flex; align-items: center; gap: 0.5rem; flex-wrap: wrap; }
/* Same specificity as the rule above, so this one (declared after, and more
   specific once the attribute is added) reliably wins over the browser's
   own `[hidden] { display: none }` UA rule, which ties with `.color-picker-
   controls` on specificity alone and can lose the cascade to it — verified
   during the 2026-09-25 simplification pass that this was actually
   happening: switching a Stakeholder Tracker colour picker to "Automatic"/
   "Grey" left the swatch+hex controls visibly showing instead of hiding. */
.color-picker-controls[hidden] { display: none; }
.color-picker-swatch-input { width: 34px; height: 30px; padding: 0; border: 1px solid var(--border); border-radius: 6px; background: none; cursor: pointer; flex: none; }
.color-picker-hex-input { width: 84px; font-family: monospace; font-size: 0.8rem; padding: 0.3rem 0.4rem; }
.color-picker-preview-swatch { width: 26px; height: 26px; border-radius: 6px; border: 1px solid var(--border); flex: none; }
.color-picker-preview-badge { flex: none; }
.color-picker-compact { min-width: 130px; }

/* Staff status section's search + status-filter row — same shape as the
   registers page's filter-row, scoped here since it also needs a search
   input the filter-row pattern doesn't normally carry. */
.staff-filter-row { display: flex; gap: 0.6rem; align-items: center; flex-wrap: wrap; margin-bottom: 0.75rem; }
.staff-filter-row input[type="search"] { flex: 1; min-width: 180px; padding: 0.45rem 0.7rem; border: 1px solid var(--border); border-radius: var(--radius); font-size: 0.87rem; font-family: inherit; }

/* One action bar at the bottom of the policy form, replacing the previous
   two same-form submit buttons ("Save changes" above the fold, "Publish"
   below it) that always did exactly the same thing — collapsible sections
   already solve "I don't want to scroll past everything to save one field". */
.action-bar {
  position: sticky;
  bottom: 0;
  background: rgba(255, 255, 255, 0.97);
  border-top: 1px solid var(--border);
  box-shadow: 0 -4px 14px rgba(15, 23, 32, 0.06);
  padding: 0.8rem 1.1rem;
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 0.75rem;
  margin: 0.75rem -0.85rem -0.7rem;
  /* Flush against #policy-form-panel's bottom/left/right inner edges (the
     negative margins above cancel .panel-body's own padding exactly) —
     now that the panel no longer clips via overflow:hidden, this bar's
     own corners need to match the panel's rounded bottom corners itself
     for the panel to still look like one shape once the form's end is
     reached and the bar rests statically instead of stuck to the viewport. */
  border-radius: 0 0 var(--radius) var(--radius);
}
.action-bar-note { font-size: 0.8rem; color: var(--muted); }

/* Stakeholder Tracker's inline <details> history disclosure (Commitments/
   Complaints tabs) — small, table-cell-scale text so a row's history reads
   as a secondary detail, not a second table. The summary toggle itself is
   deliberately NOT muted, though: 2026-10-05 triage found it blending into
   the surrounding dense text next to the much more prominent "Details &
   actions" toggle (details.row-detail/task-detail summary below). Given
   its own weight/colour/chevron here — --navy rather than --accent so the
   two toggles stay visually distinct from each other, not identical.
   Verified per rendering path, not just per class name: Commitments'/
   Complaints' <details class="commitment-history"|"complaint-history"> sit
   directly in a plain <td> (commitmentHistoryHtml()/complaintHistoryHtml()
   in stakeholders.js), so this rule is what actually paints them — that's
   the genuinely-fixed case. Register's/Task register's <details
   class="stakeholder-history"|"task-history"> are instead NESTED inside
   <details class="row-detail"|"task-detail"> (stakeholderHistoryHtml()/
   taskDetailHtml()), so `details.row-detail summary`/`details.task-detail
   summary` above — a higher-specificity descendant selector, one more
   type selector than this rule — already wins there regardless of this
   rule's own colour; those two were already rendering bold/--accent/
   chevron before this change (not muted to begin with) and are visually
   unchanged by it, not regressed. Confirmed with real-markup Playwright
   renders of all four contexts, not assumed from the selector list. */
.commitment-history,
.complaint-history,
.task-history,
.stakeholder-history { margin-top: 0.4rem; font-size: 0.8rem; }
.commitment-history summary,
.complaint-history summary,
.task-history summary,
.stakeholder-history summary {
  cursor: pointer;
  color: var(--navy);
  font-weight: 600;
  display: inline-flex;
  align-items: center;
  gap: 0.3rem;
}
.commitment-history summary::-webkit-details-marker,
.complaint-history summary::-webkit-details-marker,
.task-history summary::-webkit-details-marker,
.stakeholder-history summary::-webkit-details-marker { display: none; }
.commitment-history summary::before,
.complaint-history summary::before,
.task-history summary::before,
.stakeholder-history summary::before { content: "▸"; display: inline-block; transition: transform 0.15s ease; }
.commitment-history[open] summary::before,
.complaint-history[open] summary::before,
.task-history[open] summary::before,
.stakeholder-history[open] summary::before { transform: rotate(90deg); }
.history-list { list-style: none; margin: 0.4rem 0; padding: 0; display: grid; gap: 0.25rem; }
.history-list li { border-left: 2px solid var(--border); padding-left: 0.5rem; }

/* Social Media Planner's reviewer comment thread (smp_review_comments) —
   reuses .history-list above as-is; unlike .commitment-history/
   .complaint-history it's not a collapsed <details> (this feature is
   explicitly meant to be visible without an extra click), so it gets its
   own top margin/label spacing instead of inheriting theirs. */
.review-comments { margin-top: 0.6rem; font-size: 0.8rem; }
.review-comments > label { display: block; margin-bottom: 0.3rem; }

/* Project Setup: Team & budget's task-line row.
   2026-09-29 layout fix: the original shape here (first assigned person
   inline on a wrapping flex head-row, additional people as indented
   .ps-task-subrow lines below) went ragged at real content widths — task
   name + $ wrapped to a full line, then hours/+/-/assignee/total scattered
   with inconsistent spacing on the line(s) below (see this repo's own
   report on this fix). Replaced with a tight, even 3-column CSS grid, one
   third each: task (name + quoted $), team (every assigned person, each
   their own row with an inline hours input — no more first-inline/
   rest-indented split), and totals (hours, wholesale $, retail $, Remove).
   No new colour tokens: reuses --muted/.badge/.btn exactly as every other
   panel in this file already does. */
.ps-task-line { margin-top: 0.75rem; }
.ps-task-line-grid { display: grid; grid-template-columns: 1fr 1fr 1fr; gap: 0.5rem 1.25rem; padding: 0.85rem 1.1rem; align-items: start; }
.ps-col-task { display: flex; flex-direction: column; gap: 0.3rem; min-width: 0; }
.ps-col-team { display: flex; flex-direction: column; gap: 0.4rem; min-width: 0; }
.ps-col-totals { display: flex; flex-direction: column; align-items: flex-end; gap: 0.4rem; text-align: right; }
.ps-task-person-row { display: flex; align-items: center; gap: 0.5rem; flex-wrap: wrap; }
.ps-line-name { font-weight: 600; overflow-wrap: break-word; }
.ps-line-quoted { font-size: 0.8rem; color: var(--muted); white-space: nowrap; }
.ps-person { min-width: 6rem; }
.ps-hours-input { width: 4.5rem; }
.ps-cost { font-variant-numeric: tabular-nums; min-width: 4rem; white-space: nowrap; }
.ps-add-person-select { max-width: 12rem; }
.ps-line-hours { font-weight: 600; font-variant-numeric: tabular-nums; white-space: nowrap; }
.ps-line-cost-pair { display: flex; flex-direction: column; align-items: flex-end; gap: 0.1rem; font-variant-numeric: tabular-nums; white-space: nowrap; }
.ps-line-cost-wholesale { color: var(--muted); font-size: 0.85rem; }
.ps-line-cost-retail { font-weight: 600; }
.ps-line-total { font-weight: 600; font-variant-numeric: tabular-nums; white-space: nowrap; }
.ps-tasks-total {
  display: flex;
  align-items: center;
  justify-content: flex-end;
  gap: 0.6rem;
  padding-top: 0.6rem;
  border-top: 1px solid var(--border);
  margin-top: 0.6rem;
  font-weight: 600;
}

/* Project Setup's Guided Sequence (2026-10-05, Option A mockup): Steps 1-3
   (Team/Budget/Rates) are plain .panel panels, each panel-header carrying
   one of these numbered circles — same --accent/--border/--muted tokens
   every other badge/pill in this file already uses, no new colour. .locked
   is purely presentational (grey instead of accent); the real gate is
   psStepsGate() in admin.js, this class never decides anything on its own.
   .ps-locked dims a gated panel's whole body — same opacity convention as
   .btn[disabled]/.disabled-option above, just applied to a .panel instead
   of a control, since a locked step has nothing interactive in it to also
   need pointer-events:none. */
.ps-step-badge {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 1.4rem;
  height: 1.4rem;
  border-radius: 50%;
  background: var(--accent);
  color: #fff;
  font-weight: 700;
  font-size: 0.75rem;
  flex: none;
}
.ps-step-badge.locked { background: var(--border); color: var(--muted); }
.ps-locked { opacity: 0.6; }

/* ---------------------------------------------------------------------------
   Stakeholder Tracker: Register row redesign — accessibility-focused
   (2026-10-05). A dyslexia/cognitive-load complaint named three colour
   sources stacking on this one table, all at once, specifically at the
   moment a row is opened: .striped-table's nth-child row banding, the
   shared .dashboard-table hover-background swap (hover and "being
   opened" are the same pointer event on click, so the row visibly
   "darkens twice"), and cohortBadgesHtml()'s old full-hex-fill badges.
   See DESIGN.md's matching entry for the three design options considered
   and the full accessibility rationale (WCAG 2.1 SC 1.4.1 — colour is
   never the only signal; British Dyslexia Association style guide — no
   striped backgrounds behind text, a limited consistent palette; W3C
   COGA — calm, predictable state changes, not a background swap that
   reads as a state the user didn't choose; Gestalt chunking/proximity —
   related fields get a visible group label instead of one flat grid).
   This implements the confirmed "Option 2 — Grouped & Labelled", plus
   one addition: the stakeholder's name becomes the single, deliberate
   colour signal for "this row is open" (replacing the background swap),
   while the open-row affordance itself is a soft shadow + accent ring,
   never a background colour.

   Everything below is scoped to table.compact-table — the Register
   table's own class, not shared with any other .dashboard-table (Tasks/
   My Tasks/Campaigns keep their existing striping and hover exactly as
   before; .task-title/.task-compact-row on those tables sit under a
   different table class — task-compact-table — and never match these
   selectors). .striped-table is simply left off the Register <table>'s
   class list in stakeholders.js — removes the nth-child banding for
   this table only, no CSS override needed for that half. */
table.dashboard-table.compact-table tbody tr:hover { background: transparent; }

/* Open-row indicator — a soft shadow plus an inset accent ring, never a
   background-colour change. Keyed off the row's own <details
   class="row-detail">[open] via :has(), so no extra class or JS
   bookkeeping is needed just to know "is this row open". Box-shadow on
   a <tr> doesn't reliably paint in a collapsed-border table, so it's set
   on the row's own single <td class="compact-cell"> instead; inset keeps
   it from being clipped by the table's own border-radius/overflow:hidden
   at the first/last row. */
table.compact-table tbody tr:has(details.row-detail[open]) td.compact-cell {
  box-shadow: inset 0 0 0 1.5px var(--accent), inset 0 0 16px rgba(124, 92, 252, 0.14);
}

/* Name colour — the PRIMARY "this row is open" signal; the shadow above
   is a secondary reinforcement, not a second competing signal. One
   colour, applied once, never stacked with a fill behind it (WCAG
   1.4.1's "not colour alone" is satisfied by the shadow/ring doing the
   same job redundantly, not by this rule alone). */
table.compact-table tbody tr:has(details.row-detail[open]) .task-title {
  color: var(--accent);
}

/* Grouped & Labelled detail layout — Contact / Classification / Notes,
   replacing the old single flat 7-cell .detail-grid for this row only.
   Each group gets its own .detail-grid, reusing the existing grid/.k
   styling, with a tighter 2-column template than the generic 3-col+auto
   one task-detail/row-detail share (the specificity of the extra
   .stakeholder-groups ancestor class wins over that shared rule without
   needing !important) — no group here needs the generic grid's 4th
   "auto" column, since Edit/Delete moved out of the grid entirely (see
   stakeholders.js's stakeholderDetailHtml()). */
.stakeholder-groups { display: flex; flex-direction: column; gap: 1rem; padding: 0.6rem 0 0.85rem; }
/* Stakeholder card (2026-10-08): "+ Note / + Task / + Commitment" on the
   top line, pushed right; they wrap under the name on a narrow screen.
   History entries carry an edit pencil (stakeholderHistoryHtml()). */
.compact-row .sh-quick-log { display: inline-flex; gap: 0.35rem; margin-left: auto; flex-wrap: wrap; }
.compact-row .sh-quick-log .btn { padding: 0.2rem 0.55rem; font-size: 0.75rem; }
.stakeholder-history .sh-history-item { display: flex; align-items: flex-start; justify-content: space-between; gap: 0.5rem; }
.stakeholder-history .sh-history-edit { width: 24px; height: 24px; flex: none; }
@media (max-width: 640px) {
  .compact-row .sh-quick-log { margin-left: 0; width: 100%; }
}
.stakeholder-group { display: flex; flex-direction: column; gap: 0.4rem; }
.stakeholder-group h4 { margin: 0; padding: 0 0.85rem; font-size: 0.68rem; font-weight: 700; text-transform: uppercase; letter-spacing: 0.04em; color: var(--accent); }
.row-detail .stakeholder-groups .detail-grid { grid-template-columns: 1fr 1fr; gap: 0.5rem 1.5rem; }
.stakeholder-cohorts { padding: 0 0.85rem; display: flex; align-items: center; flex-wrap: wrap; gap: 0.3rem; }
.stakeholder-cohorts .k { color: var(--muted); font-size: 0.68rem; text-transform: uppercase; letter-spacing: 0.03em; margin-right: 0.3rem; }
.stakeholder-notes { margin: 0 0.85rem; padding: 0.6rem 0.75rem; font-size: 0.82rem; background: var(--bg); border-radius: var(--radius); }
.stakeholder-groups .detail-toolbar { padding: 0 0.85rem; }

/* Cohort chip — neutral pill + small colour dot, replacing the old
   per-cohort full-hex-fill badge (the third colour source the complaint
   named). Reuses .chip, the same neutral pill every other tag on this
   row already uses; only the cohort's own admin-set colour survives, and
   only as a small dot, never a fill behind text. */
.chip.cohort-chip { display: inline-flex; align-items: center; gap: 0.35rem; cursor: default; margin: 0 0.3rem 0.3rem 0; }
.chip.cohort-chip .dot { width: 7px; height: 7px; border-radius: 50%; flex: none; }

/* Commitments tab row redesign (2026-10-05, "squashed layout" fix —
   reported via screenshot: Edit button + History disclosure + add-note
   form all crammed into the narrow Progress-adjacent column, making that
   one row taller/busier than every other). Same STRUCTURAL discipline as
   the Register row-detail redesign above — dedicated full-width space for
   expanded content, never squeezed into one data column — but Commitments
   keeps its own real Stakeholder/Commitment/Owner/Due/Status/Progress
   columns for the collapsed row (unlike Register's single flex column),
   so the expand can't be a <details> nested inside one <td> the way
   Register's is: a <tr> can't be a child of <details>. See
   commitmentRowHtml()/commitmentDetailRowHtml() in stakeholders.js — the
   old trailing column now holds only a small chevron toggle; what it
   toggles is a plain sibling <tr class="commitment-detail-row"> whose one
   <td colspan="7"> spans the table's full width, using the table's own
   default th/td padding (table.dashboard-table th, td above) rather than
   a bespoke padding rule. */
.commitment-toggle-cell { text-align: center; white-space: nowrap; }
.commitment-toggle-btn .commitment-toggle-icon { display: inline-block; color: var(--accent); transition: transform 0.15s ease; }
.commitment-toggle-btn[aria-expanded="true"] .commitment-toggle-icon { transform: rotate(90deg); }
/* The detail row is a plain sibling <tr>, always present right after its
   own data row (just `hidden` when closed) — pinned to the table's own
   surface colour at rest and on hover so it never reads as a banded or
   hoverable data row of its own. */
table.dashboard-table tbody tr.commitment-detail-row,
table.dashboard-table tbody tr.commitment-detail-row:hover { background: var(--surface); }
/* Explicit alternating-row colour, replacing `.striped-table`'s raw
   nth-child(even) for this table only (commitmentsTableHtml() in
   stakeholders.js no longer carries that class). Every commitment is now
   TWO sibling <tr>s (its data row + its own detail row), so nth-child
   parity always lands on a detail row, never a data row — this table's
   striping would otherwise go silently inert. `.commitment-row-alt` is
   applied by the commitment's position in the rendered list instead
   (commitmentRowHtml()'s `index` param), so it keeps alternating
   correctly no matter which rows are expanded. */
table.dashboard-table tbody tr.commitment-row.commitment-row-alt { background: var(--bg); }

/* Complaints tab row redesign (2026-10-05) — same squashed-layout fix as
   Commitments immediately above, found by the follow-up sweep across
   Engagement Log/Complaints/Tasks (Register and Commitments already
   covered). Complaints has the identical real-table-columns shape, one
   column wider than Commitments (Stakeholder/Received/Category/Severity/
   Owner/Status/Days-open, then one trailing column), so the same chevron +
   colspan="8" sibling-row structure applies — see complaintRowHtml()/
   complaintDetailRowHtml() in stakeholders.js. */
.complaint-toggle-cell { text-align: center; white-space: nowrap; }
.complaint-toggle-btn .complaint-toggle-icon { display: inline-block; color: var(--accent); transition: transform 0.15s ease; }
.complaint-toggle-btn[aria-expanded="true"] .complaint-toggle-icon { transform: rotate(90deg); }
table.dashboard-table tbody tr.complaint-detail-row,
table.dashboard-table tbody tr.complaint-detail-row:hover { background: var(--surface); }
/* Same nth-child-goes-inert issue and fix as `.commitment-row-alt` above —
   Complaints is also two sibling <tr>s per row now, so explicit striping
   by rendered-list position (complaintRowHtml()'s `index` param) replaces
   `.striped-table` (dropped from this table in renderComplaintsTab()). */
table.dashboard-table tbody tr.complaint-row.complaint-row-alt { background: var(--bg); }

/* Project Hub (phase 1 build, 2026-10-08) — Tasks tab and task panel.
   Status and burn reuse the shared .badge tones (ok/warn/serious/muted)
   and .badge.burn-* classes; overdue reuses .due-overdue. Only what's
   specific to the Hub's indented task list lives here. */
.hub-task-table .hub-name { display: flex; align-items: center; gap: 0.35rem; }
.hub-task-table .hub-open { background: none; border: 0; padding: 0; font: inherit; color: var(--ink); text-align: left; cursor: pointer; }
.hub-task-table .hub-open:hover, .hub-task-table .hub-open:focus-visible { color: var(--accent); text-decoration: underline; }
.hub-task-table tr.hub-phase-row td { background: var(--hub-phase-bg); font-weight: 700; }
.hub-task-table tr.hub-phase-row td:first-child { box-shadow: inset 4px 0 0 var(--hub-phase); }
.hub-task-table tr.hub-closed-row .hub-name { color: var(--muted); }
.hub-task-table .nowrap { white-space: nowrap; }
.hub-task-table .hub-actions { display: flex; gap: 0.3rem; justify-content: flex-end; }
/* Dates may wrap at the dash so three row actions fit without scrolling. */
.hub-task-table td.hub-dates { min-width: 6.5rem; }
.hub-task-table td:first-child { min-width: 12rem; }
.hub-task-table .hub-actions .icon-btn { width: 28px; height: 28px; }
.hub-kind { color: var(--accent); font-size: 0.75rem; }
.hub-due-soon { color: var(--status-warning); font-weight: 600; }
.hub-check { display: flex; align-items: center; gap: 0.35rem; align-self: flex-end; font-weight: 400; font-size: 0.85rem; padding-bottom: 0.45rem; }
.hub-link-list { list-style: none; margin: 0 0 0.6rem; padding: 0; display: flex; flex-direction: column; gap: 0.35rem; }
.hub-link-list li { display: flex; align-items: center; gap: 0.5rem; flex-wrap: wrap; font-size: 0.85rem; }

/* Project Hub — Plan tab (DHTMLX Gantt). The library's own look is
   overridden through its --dhx-gantt-* variables so bars, links and
   fonts follow the site tokens; day shading reuses the shared status
   tones. Bar colours: open = accent, done = green, waiting (soft) =
   amber, blocked (hard) / overdue = red outline. */
.hub-gantt-body { overflow: hidden; }
.hub-gantt { width: 100%; min-height: 240px; }
.hub-gantt .gantt_container, .hub-gantt.gantt_container {
  --dhx-gantt-font-family: inherit;
  --dhx-gantt-font-size: 0.82rem;
  --dhx-gantt-task-background: var(--accent);
  --dhx-gantt-task-color: #fff;
  --dhx-gantt-project-background: var(--navy);
  --dhx-gantt-milestone-background: var(--accent);
  --dhx-gantt-link-background: var(--muted);
  --dhx-gantt-base-colors-border: var(--border);
  font-family: inherit;
}
.hub-gantt .gantt_task_cell.hub-weekend, .hub-gantt .gantt_scale_cell.hub-weekend { background: var(--bg); }
.hub-gantt .gantt_task_cell.hub-holiday, .hub-gantt .gantt_task_cell.hub-closure, .hub-gantt .gantt_scale_cell.hub-holiday { background: var(--status-red-bg); }
.hub-gantt .hub-off-fill { position: absolute; inset: 0; }
/* Connect mode (hubPlan.js): 🔗 on a row, then click the task that waits. */
.hub-gantt .gantt_grid_data .gantt_cell[data-column-name="connect"] { padding: 0; }
.hub-gantt .gantt_grid_data .gantt_cell[data-column-name="connect"] .gantt_tree_content { display: flex; align-items: center; justify-content: center; height: 100%; }
.hub-gantt .hub-connect-btn { border: 1px solid var(--border); background: var(--surface); color: var(--accent); border-radius: 6px; width: 26px; height: 24px; padding: 0; display: inline-flex; align-items: center; justify-content: center; cursor: pointer; }
.hub-gantt .hub-connect-btn:hover, .hub-gantt .hub-connect-btn:focus-visible { background: var(--accent); color: #fff; border-color: var(--accent); }
.hub-connect-banner { display: flex; align-items: center; gap: 0.75rem; justify-content: space-between; padding: 0.55rem 1rem; background: var(--accent-light); border-bottom: 1px solid var(--border); font-size: 0.88rem; }
.hub-connect-banner[hidden] { display: none; }
.hub-gantt .gantt_row.hub-connect-source, .hub-gantt .gantt_task_line.hub-connect-source { box-shadow: inset 0 0 0 2px var(--accent); }
.hub-gantt .gantt_row.hub-connect-ok { background: var(--accent-light); cursor: pointer; }
.hub-gantt .gantt_task_line.hub-connect-ok { box-shadow: 0 0 0 2px var(--accent); cursor: pointer; }
.hub-gantt .gantt_row.hub-connect-no, .hub-gantt .gantt_task_line.hub-connect-no { opacity: 0.4; }
/* DHTMLX's dashed guide line while drawing a link snaps to the target's
   link point and was seen under the pointer when a drop failed to
   register; it's visual only, so it shouldn't take mouse events. (The main
   cause of missed drops was DHTMLX reading drag moves at most 60 times a
   second — see release() in tests/hub/planTab.test.cjs.) */
.hub-gantt .gantt_link_direction { pointer-events: none; }
.hub-gantt .gantt_task_cell.hub-school { background-image: repeating-linear-gradient(45deg, transparent 0 4px, var(--accent-light) 4px 8px); }
.hub-gantt .gantt_task_cell.hub-today { background-color: rgba(194, 76, 88, 0.14); box-shadow: inset 2px 0 0 var(--status-red-fg); }
.hub-gantt .gantt_task_line.hub-bar-done { --dhx-gantt-task-background: var(--status-green-fg); }
.hub-gantt .gantt_task_line.hub-bar-waiting { --dhx-gantt-task-background: var(--status-amber-bg); --dhx-gantt-task-color: var(--status-amber-fg); }
.hub-gantt .gantt_task_line.hub-bar-blocked { --dhx-gantt-task-background: var(--status-red-bg); --dhx-gantt-task-color: var(--status-red-fg); }
/* Phases in their own colour (teal), whatever their status, until done. */
.hub-gantt .gantt_task_line.hub-bar-phase { --dhx-gantt-task-background: var(--hub-phase); --dhx-gantt-project-background: var(--hub-phase); --dhx-gantt-task-color: #fff; background: var(--hub-phase); border-color: var(--hub-phase); }
.hub-gantt .gantt_task_line.hub-bar-phase.hub-bar-done { --dhx-gantt-task-background: var(--status-green-fg); --dhx-gantt-project-background: var(--status-green-fg); background: var(--status-green-fg); border-color: var(--status-green-fg); }
.hub-key-phase { background: var(--hub-phase); border-color: var(--hub-phase); }
.hub-gantt .gantt_task_line.hub-bar-overdue { box-shadow: 0 0 0 2px var(--status-red-fg); }
/* Staff leave (phase 2, step 5): days an assignee is on leave are hatched
   amber on that task's row, and a bar whose dates overlap their leave gets
   a dashed amber outline (an outline, so it shows alongside the overdue ring). */
.hub-gantt .gantt_task_cell.hub-leave { background-image: repeating-linear-gradient(-45deg, transparent 0 4px, rgba(201, 138, 0, 0.32) 4px 7px); }
.hub-gantt .gantt_task_line.hub-bar-leave { outline: 2px dashed var(--status-amber-fg); outline-offset: 2px; }
.hub-key-leave { background-image: repeating-linear-gradient(-45deg, transparent 0 3px, rgba(201, 138, 0, 0.45) 3px 5px); }
.hub-tip-warn { color: var(--status-amber-fg); font-weight: 600; }
.hub-leave-note { display: block; margin-top: 0.15rem; color: var(--status-amber-fg); font-size: 0.8rem; }
/* Baselines (phase 3, step 2): the saved plan as a thin grey bar along the
   bottom of each row, under the live bar (drawn per timeline cell). */
.hub-gantt .hub-base-seg { position: absolute; bottom: 2px; height: 4px; background: #8a8fa3; border-radius: 1px; pointer-events: none; }
.hub-key-base { height: 4px; border: 0; background: #8a8fa3; }
.hub-base-note { display: block; font-size: 0.78rem; color: var(--muted); }
.hub-base-note.hub-base-late { color: var(--status-red-fg); }
/* Running timer bar (phase 3, step 3, timer.js): bottom right of every
   signed-in page while a timer runs. */
.hub-timer { position: fixed; right: 16px; bottom: 16px; z-index: 900; display: flex; align-items: center; gap: 0.6rem; max-width: min(420px, calc(100vw - 32px)); padding: 0.5rem 0.6rem 0.5rem 0.8rem; background: var(--navy); color: #fff; border-radius: 999px; box-shadow: 0 6px 20px rgba(20, 20, 50, 0.25); font-size: 0.85rem; }
.hub-timer[hidden] { display: none; }
.hub-timer-dot { width: 9px; height: 9px; border-radius: 50%; background: #ff6b6b; flex: none; animation: hub-timer-pulse 1.6s ease-in-out infinite; }
@keyframes hub-timer-pulse { 50% { opacity: 0.35; } }
.hub-timer-clock { font-variant-numeric: tabular-nums; font-weight: 700; flex: none; }
.hub-timer-task { min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.hub-timer-project { display: block; font-size: 0.72rem; opacity: 0.75; overflow: hidden; text-overflow: ellipsis; }
.hub-timer .btn { flex: none; }
@media print { .hub-timer { display: none !important; } }
/* Team workload (phase 3, step 4, hubWorkload.js): one cell per person
   per week, tinted by how loaded they are. */
.hw-table td.hw-name { white-space: nowrap; font-weight: 600; }
.hw-table .hw-cell { text-align: center; font-variant-numeric: tabular-nums; white-space: nowrap; cursor: default; }
.hw-table .hw-cap { color: var(--muted); font-size: 0.75rem; margin-left: 0.15rem; }
.hw-over { background: var(--status-red-bg, #fbe3e6); }
.hw-full { background: var(--status-amber-bg); }
.hw-ok { background: #e3f4ea; }
.hw-under { background: #f1f2f6; }
.hw-table .hw-none { color: var(--muted); }
.hw-key { border: 0; }
.hw-table tr.hw-unassigned td { font-style: italic; }
.hub-stakeholder-tag { display: block; margin-top: 0.1rem; color: var(--muted); font-size: 0.78rem; }
.hub-gantt .gantt_task_line.hub-bar-cancelled { opacity: 0.45; }
.hub-gantt .gantt_task_link.hub-link-hard { --dhx-gantt-link-background: var(--status-red-fg); }
.hub-gantt .gantt_task_link.hub-link-soft { --dhx-gantt-link-background: var(--muted); }
/* A phase's whole row is tinted, in the list and across the chart. */
.hub-gantt .gantt_row.hub-grid-phase { font-weight: 700; background: var(--hub-phase-bg); box-shadow: inset 4px 0 0 var(--hub-phase); }
.hub-gantt .gantt_task_row.hub-grid-phase { background: var(--hub-phase-bg); }
/* Not scoped to the chart: the dashboard and the banked-hours tables use
   these too (they were unstyled outside the Gantt until 2026-10-09). */
.hub-due-overdue { color: var(--stakeholder-red-fg); font-weight: 700; }
.hub-due-today, .hub-due-soon { color: var(--status-warning); font-weight: 600; }
.hub-plan-toolbar { align-items: flex-end; }
.hub-plan-help { margin: 0 0 0.6rem; }
.hub-legend { display: flex; flex-wrap: wrap; gap: 1rem; font-size: 0.78rem; color: var(--muted); margin: 0.5rem 0 0; }
.hub-legend span { display: inline-flex; align-items: center; gap: 0.35rem; }
.hub-key { display: inline-block; width: 14px; height: 10px; border-radius: 2px; border: 1px solid var(--border); }
.hub-key-today { background: rgba(194, 76, 88, 0.14); box-shadow: inset 2px 0 0 var(--status-red-fg); }
.hub-key-holiday { background: var(--status-red-bg); }
.hub-key-weekend { background: var(--bg); }
.hub-key-school { background-image: repeating-linear-gradient(45deg, transparent 0 3px, var(--accent-light) 3px 6px); }
.hub-key-hard { height: 0; border: 0; border-top: 2px solid var(--status-red-fg); }
.hub-key-soft { height: 0; border: 0; border-top: 2px solid var(--muted); }
/* Days off panel */
.hub-dialog-h { margin: 1rem 0 0.4rem; font-size: 0.95rem; }
/* Task panel buttons: pinned to the bottom of the (scrolling) dialog so
   Save is always in reach, also while scrolled down to Dependencies or
   Notes. Delete on the left, Save last on the right. The negative margins
   take the bar to the dialog's edges (.email-modal padding). */
.hub-dialog-actions {
  position: sticky; bottom: -1.25rem; z-index: 3;
  display: flex; align-items: center; gap: 0.5rem;
  margin: 1rem -1.4rem -1.25rem; padding: 0.7rem 1.4rem;
  background: var(--surface); border-top: 1px solid var(--border);
}
.hub-dialog-actions-right { display: flex; gap: 0.5rem; margin-left: auto; }
.hub-daysoff-list { list-style: none; margin: 0 0 0.4rem; padding: 0; display: flex; flex-direction: column; gap: 0.25rem; max-height: 14rem; overflow-y: auto; font-size: 0.85rem; }
.hub-daysoff-list li { display: grid; grid-template-columns: 12rem 1fr auto; gap: 0.6rem; align-items: center; }
.hub-daysoff-list li.hd-editing { display: block; }
form.stack.hd-edit-form { display: flex; flex-wrap: wrap; gap: 0.4rem; align-items: center; }
form.stack.hd-edit-form input { width: auto; padding: 0.3rem 0.5rem; font-size: 0.85rem; }
form.stack.hd-edit-form input[type="text"] { flex: 1 1 12rem; min-width: 0; }
/* My work (myWork.js), a Timesheet tab: one card per planned task. */
.mw-head { display: flex; justify-content: space-between; align-items: flex-end; gap: 0.75rem; flex-wrap: wrap; margin-bottom: 0.75rem; }
.mw-head label { font-size: 0.8rem; font-weight: 600; color: var(--muted); }
.mw-list { display: grid; gap: 0.6rem; }
.mw-row { border: 1px solid var(--border); border-radius: var(--radius); padding: 0.6rem 0.8rem; display: grid; gap: 0.35rem; background: var(--surface); }
.mw-top { display: flex; flex-wrap: wrap; align-items: center; gap: 0.4rem 0.6rem; }
.mw-meta { display: flex; flex-wrap: wrap; gap: 0.25rem 1rem; font-size: 0.8rem; color: var(--muted); }
.mw-acts { display: flex; gap: 0.4rem; flex-wrap: wrap; }

/* Project dashboard (hubDashboard.js), top of the Dashboard tab. */
.hub-dash { margin-bottom: 1.25rem; }
.hub-dash .kpi-row { margin-bottom: 0.75rem; }
.hub-dash-alert { margin: 0 0 0.5rem; }
.hub-dash-grid { display: grid; grid-template-columns: minmax(0, 1fr) minmax(0, 1fr); gap: 0.5rem 2rem; }
@media (max-width: 900px) { .hub-dash-grid { grid-template-columns: 1fr; } }
.hub-dash-row { display: grid; grid-template-columns: minmax(6rem, 1fr) minmax(5rem, 1.4fr) auto; gap: 0.6rem; align-items: center; font-size: 0.85rem; padding: 0.2rem 0; }
.hub-dash-row .mini-bar { width: 100%; }
.hub-dash-name { min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.hub-dash-fig { color: var(--muted); white-space: nowrap; font-variant-numeric: tabular-nums; }
.hub-attn { list-style: none; margin: 0; padding: 0; display: grid; gap: 0.5rem; font-size: 0.85rem; }
.hub-attn li { display: flex; gap: 0.5rem; align-items: flex-start; }
.hub-attn-dot { width: 8px; height: 8px; border-radius: 50%; margin-top: 0.4rem; flex: none; }
.hub-attn-bad { background: var(--status-serious); }
.hub-attn-warn { background: var(--status-warning); }
.hub-attn-open { border: none; background: none; padding: 0; color: var(--accent); font: inherit; font-weight: 600; cursor: pointer; text-align: left; }
.hub-attn-open:hover { text-decoration: underline; }
.hub-warn { background: var(--status-amber-bg); color: var(--status-amber-fg); padding: 0.5rem 0.75rem; border-radius: var(--radius); font-size: 0.85rem; margin: 0 0 0.6rem; }
/* New project dialog: projects with a similar name (projectPicker.js). */
.pp-similar-list { list-style: none; margin: 0.4rem 0 0; padding: 0; display: grid; gap: 0.3rem; }
.pp-similar-list li { display: flex; align-items: center; justify-content: space-between; gap: 0.6rem; }

/* Project Hub — Team & budget tab. Tables reuse .dashboard-table; these
   only lay the panels out and size the inline inputs. */
.hub-team-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(360px, 1fr)); gap: 1rem; margin-bottom: 1rem; }
#ht-bank { margin-bottom: 1rem; }
.hub-team-grid > .panel, #team-root > .panel { margin-bottom: 1rem; }
.hub-team-grid > .panel { margin-bottom: 0; }
/* In-table hours and rate inputs follow the shared .dashboard-table input
   convention (border shows on hover/focus); only their width is set here.
   A budget line's "+ Add a person" picker sits outside any form.stack, so
   it gets the form-field box itself. */
.hub-team-table input[type="number"] { width: 7.5rem; min-width: 0; text-align: right; }
.hub-select { width: 100%; font: inherit; font-size: 0.9rem; padding: 0.35rem 0.55rem; border: 1px solid var(--border); border-radius: var(--radius); background: var(--surface); color: var(--ink); }
.hub-team-table tfoot .hub-total-row td { font-weight: 700; border-top: 2px solid var(--border); }
.hub-team-table tr.hub-muted-row td { color: var(--muted); }
.hub-inline-form { padding: 0.75rem 1rem; }
.hub-inline-form select, .hub-inline-form input[type="text"] { flex: 1 1 auto; min-width: 0; }
.hub-panel-note { margin: 0.5rem 1rem 0.75rem; font-size: 0.8rem; }
/* Plan & budget tree (hubTeam.js): one compact row per plan item, indented
   by depth (--depth), budget lines expand to their people. Drop marks show
   where a dragged row will land: a line above or below, or inside. */
.hb-tree td { padding-top: 0.4rem; padding-bottom: 0.4rem; vertical-align: middle; }
/* No ellipsis on the name (2026-10-09): it replaced the whole name box with
   "…" when the cell overflowed by a pixel. A long name wraps instead
   (.pf-namecell below). */
.hb-tree .hb-name { padding-left: calc(0.5rem + var(--depth, 0) * 1.4rem); max-width: 24rem; }
.hb-tree .hb-grip { cursor: grab; color: var(--muted); letter-spacing: -0.15em; user-select: none; }
.hb-tree .hb-toggle { border: none; background: none; color: var(--accent); cursor: pointer; width: 1.2rem; padding: 0; font-size: 0.8rem; }
.hb-tree .hb-toggle-gap { display: inline-block; width: 1.2rem; }
.hb-tree .hb-has-line .hb-title { cursor: pointer; }
.hb-tree .hb-tag { display: inline-block; margin-left: 0.4rem; padding: 0 0.4rem; border-radius: 999px; font-size: 0.68rem; font-weight: 600; background: var(--bg); color: var(--muted); border: 1px solid var(--border); vertical-align: 1px; }
.hb-tree .hb-tag-line { background: var(--accent-light); color: var(--accent); border-color: transparent; }
.hb-tree tr.hb-phase td { background: var(--hub-phase-bg); }
.hb-tree tr.hb-phase td:first-child { box-shadow: inset 4px 0 0 var(--hub-phase); }
.hb-tree tr.hb-phase .hb-title { font-weight: 700; }
.hb-tree .hb-people { color: var(--muted); font-size: 0.8rem; max-width: 12rem; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.hb-tree .hb-actions { white-space: nowrap; text-align: right; }
.hb-tree .hb-actions .icon-btn { width: 26px; height: 26px; }
.hb-tree tr.hb-detail > td { background: var(--surface); padding: 0.5rem 1rem 0.75rem 2.5rem; }
.hb-tree tr.hb-dragging { opacity: 0.45; }
.hb-tree tr.hb-drop-before td { box-shadow: inset 0 2px 0 var(--accent); }
.hb-tree tr.hb-drop-after td { box-shadow: inset 0 -2px 0 var(--accent); }
.hb-tree tr.hb-drop-inside td { background: var(--accent-light); box-shadow: inset 0 0 0 1px var(--accent); }
.hb-offplan { display: flex; align-items: center; justify-content: space-between; gap: 0.75rem; flex-wrap: wrap; margin: 0.75rem 1rem 0; padding: 0.6rem 0.85rem; border-radius: var(--radius); background: var(--accent-light); font-size: 0.85rem; }

/* Project Hub exports. Inside a Hub print area being captured for a PDF,
   on-screen-only controls (.no-print) are left out of the image too —
   scoped to .hub-print-area so existing PDFs elsewhere are unchanged. */
.hub-export-row { justify-content: flex-end; margin: 0 0 0.5rem; }
.hub-print-area.pdf-capturing .no-print { display: none !important; }
.hub-print-area.pdf-capturing .form-letterhead { padding: 1rem 1rem 0.85rem; }
/* Plan tab's landscape Gantt PDF (hubPlanExport.js): drawn into a hidden
   #hub-gantt-print only while capturing, at a fixed 1040px width — the
   landscape page's printable width (277mm), which html2pdf crops to.
   Rows avoid splitting across pages. */
.hub-gpdf { width: 1040px; padding: 0 0 12px; background: #fff; color: var(--navy); }
.hub-gpdf[hidden] { display: none; }
.hub-gpdf-figures { margin: 0.5rem 0 1rem; }
.hub-gpdf-figures .kpi-tile { padding: 0.6rem 0.8rem; }
.hub-gpdf-figures .kpi-tile .kpi-value { font-size: 1.3rem; }
.hub-gpdf-figures .kpi-tile .kpi-label { font-size: 0.68rem; }
.hub-gpdf-chart { border: 1px solid var(--border); border-radius: 6px; overflow: hidden; }
.hub-gpdf-row { display: grid; grid-template-columns: var(--name-w) var(--dates-w) var(--time-w); align-items: center; border-top: 1px solid var(--border); font-size: 12px; min-height: 26px; break-inside: avoid; page-break-inside: avoid; }
.hub-gpdf-head { border-top: 0; background: #f4f5f9; font-weight: 700; font-size: 11px; text-transform: uppercase; letter-spacing: 0.03em; min-height: 30px; }
.hub-gpdf-row-phase { background: var(--hub-phase-bg); font-weight: 700; }
.hub-gpdf-name { padding: 4px 8px 4px 6px; min-width: 0; overflow-wrap: anywhere; }
.hub-gpdf-who { display: block; font-weight: 400; font-size: 10.5px; color: var(--muted); }
.hub-gpdf-dates { padding: 4px 8px; color: var(--muted); font-size: 10.5px; white-space: nowrap; }
.hub-gpdf-time { position: relative; align-self: stretch; min-height: 26px; border-left: 1px solid var(--border); }
.hub-gpdf-tick { position: absolute; top: 8px; padding-left: 3px; border-left: 1px solid var(--border); font-size: 10px; white-space: nowrap; height: 100%; }
.hub-gpdf-today { position: absolute; top: 0; bottom: 0; width: 0; border-left: 2px solid var(--status-red-fg); opacity: 0.55; }
.hub-gpdf-bar { position: absolute; top: 50%; height: 12px; margin-top: -6px; border-radius: 3px; background: var(--accent); }
.hub-gpdf-bar.hub-gpdf-phase { height: 8px; margin-top: -4px; background: var(--hub-phase); }
.hub-gpdf-done { background: var(--status-good); }
.hub-gpdf-overdue { background: var(--status-serious); }
.hub-gpdf-open { background: var(--accent); }
.hub-gpdf-base { position: absolute; bottom: 2px; height: 4px; background: #8a8fa3; border-radius: 1px; }
.hub-gpdf-ms { position: absolute; top: 50%; width: 11px; height: 11px; margin-top: -6px; background: var(--navy); transform: rotate(45deg); }
.hub-gpdf-key { display: inline-block; width: 14px; height: 10px; border-radius: 2px; }
.hub-gpdf-ms-key { width: 9px; height: 9px; background: var(--navy); transform: rotate(45deg); border-radius: 0; }
.hub-gpdf-legend { margin-top: 0.6rem; }
/* Budget line dialog (Team & budget) */
.bl-people { display: flex; flex-direction: column; gap: 0.45rem; }
.bl-person { display: grid; grid-template-columns: minmax(0, 1fr) 7rem 6rem auto; gap: 0.5rem; align-items: center; }
.bl-name { font-weight: 600; font-size: 0.92rem; padding: 0 0.25rem; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.bl-cost { text-align: right; font-size: 0.85rem; color: var(--muted); }
.bl-total { margin: 0.25rem 0 0; font-size: 0.9rem; }

/* Plan & fees tab (2026-10-09): the plan is the budget. One tree with each
   item's fee, people and hours; a fee strip above, a Team panel below. */
.pf-strip { margin-bottom: 0.75rem; }
.pf-strip .kpi-label .pf-sub { display: block; text-transform: none; letter-spacing: 0; font-size: 0.72rem; margin-top: 0.1rem; }
.pf-strip .pf-under .kpi-value { color: var(--ok); }
.pf-strip .pf-over .kpi-value { color: var(--danger); }
/* Name cell: grip and name side by side; a long name wraps. */
.pf-namecell { display: flex; align-items: center; gap: 0.35rem; min-width: 12rem; }
.pf-tree .pf-chip, .pf-tree .pf-addp { white-space: nowrap; }
.pf-namewrap { display: flex; flex-direction: column; min-width: 0; }
.pf-name { border: none; background: none; padding: 0; font: inherit; color: inherit; text-align: left; cursor: text; border-bottom: 1px dashed transparent; }
.pf-name:hover, .pf-name:focus-visible { border-bottom-color: var(--muted); }
.hb-phase .pf-name { font-weight: 600; }
.pf-rename { font: inherit; padding: 0.15rem 0.4rem; border: 1.5px solid var(--accent); border-radius: var(--radius); min-width: 12rem; background: var(--surface); color: var(--ink); }
.pf-cellsub { display: block; font-size: 0.72rem; color: var(--muted); font-weight: 400; }
.pf-tree .chips { display: flex; flex-wrap: wrap; gap: 0.3rem; align-items: center; }
.pf-chip { cursor: pointer; border: 1px solid transparent; font: inherit; font-size: 0.74rem; }
.pf-chip.pf-zero { background: transparent; border: 1px dashed var(--muted); color: var(--muted); }
.pf-addp { border: 1px dashed var(--border); background: transparent; color: var(--muted); border-radius: 999px; font: inherit; font-size: 0.72rem; padding: 0.05rem 0.5rem; cursor: pointer; }
.pf-addp:hover, .pf-addp:focus-visible { border-color: var(--accent); color: var(--accent); }
.pf-fee { border: none; background: none; padding: 0; font: inherit; color: inherit; cursor: pointer; text-align: right; border-bottom: 1px dashed transparent; }
.pf-fee:hover, .pf-fee:focus-visible { border-bottom-color: var(--muted); }
.pf-kind .seg-opt { padding: 0.2rem 0.5rem; }
.pf-under-text { color: var(--ok); }
.pf-over-text { color: var(--danger); }
.pf-tree tr.pf-archived td { opacity: 0.55; }
.pf-tree tr.pf-hi td { background: var(--accent-light); }
.pf-person { cursor: pointer; }
.pf-person:hover td, .pf-person.pf-sel td { background: var(--accent-light); }
.pf-foot { display: flex; flex-wrap: wrap; gap: 0.5rem 1rem; align-items: center; justify-content: space-between; padding: 0.6rem 1rem; }
.linkbtn { border: none; background: none; padding: 0; color: var(--accent); font: inherit; font-size: 0.85rem; text-decoration: underline; text-underline-offset: 2px; cursor: pointer; }
.hub-status { margin: 0.6rem 1rem 0; padding: 0.45rem 0.7rem; border-radius: var(--radius); background: var(--accent-light); font-size: 0.86rem; }
.hub-status[hidden] { display: none; }
.pf-log { list-style: none; margin: 0; padding: 0; max-height: 60vh; overflow-y: auto; display: flex; flex-direction: column; gap: 0.55rem; }
.pf-log li { border-left: 3px solid var(--accent-light); padding-left: 0.6rem; font-size: 0.86rem; }
.pf-log-when { display: block; font-size: 0.74rem; color: var(--muted); }

/* How-to guides (guide.js; first one: Projects, 2026-10-09). Numbered
   steps, text beside a screenshot on wide screens, stacked on narrow ones.
   --guide-mark is the orange of the numbered markers drawn on the
   screenshots (tests/guide/shots.cjs uses the same value), so the list in
   the text and the marks on the picture read as one. */
:root { --guide-mark: #e8590c; }
.guide-intro .panel-body > p:first-child { margin-top: 0; }
.guide-toc { margin: var(--space-3) 0 0; padding-left: 1.2rem; display: grid; gap: var(--space-2); }
.guide-toc > li { font-weight: 600; }
.guide-toc-steps { font-weight: 400; font-size: 0.86rem; margin: 0.2rem 0 0; padding-left: 1.1rem; color: var(--muted); }
.guide-who { font-size: 0.74rem; font-weight: 600; color: var(--muted); margin-left: 0.35rem; }
.guide-section { margin-bottom: var(--space-5); scroll-margin-top: var(--space-4); }
.guide-section .panel-header { display: flex; justify-content: space-between; align-items: center; gap: var(--space-2); }
.guide-section-intro { margin-bottom: var(--space-4); }
.guide-section-intro p:first-child { margin-top: 0; }
.guide-steps { list-style: none; margin: 0; padding: 0; display: flex; flex-direction: column; gap: var(--space-6); }
.guide-step { display: grid; grid-template-columns: minmax(16rem, 2fr) minmax(0, 3fr); gap: var(--space-5); align-items: start; scroll-margin-top: var(--space-4); padding-top: var(--space-5); border-top: 1px solid var(--border); }
.guide-step:first-child { border-top: none; padding-top: 0; }
.guide-step.guide-step-text-only { grid-template-columns: minmax(0, 1fr); }
/* A wide screenshot (a whole tab, not a small window) sits full width under
   its text, so it stays readable. */
.guide-step.guide-step-wide { grid-template-columns: minmax(0, 1fr); }
.guide-step-wide .guide-shot { max-width: 60rem; }
.guide-step h3 { display: flex; align-items: center; gap: var(--space-2); margin: 0 0 var(--space-2); font-size: 1.02rem; }
.guide-num { flex: none; display: inline-grid; place-items: center; width: 1.7rem; height: 1.7rem; border-radius: 50%; background: var(--accent); color: #fff; font-size: 0.85rem; font-weight: 700; }
.guide-step-text p { margin: 0 0 var(--space-2); line-height: 1.55; }
.guide-marks { list-style: none; counter-reset: gm; margin: var(--space-2) 0; padding: 0; display: flex; flex-direction: column; gap: var(--space-2); }
.guide-marks > li { counter-increment: gm; position: relative; padding-left: 2rem; line-height: 1.5; }
.guide-marks > li::before { content: counter(gm); position: absolute; left: 0; top: 0.05rem; width: 1.35rem; height: 1.35rem; border-radius: 50%; background: var(--guide-mark); color: #fff; font-size: 0.78rem; font-weight: 700; display: grid; place-items: center; }
.guide-tip { background: var(--accent-light); border-radius: var(--radius); padding: var(--space-2) var(--space-3); font-size: 0.9rem; }
.guide-shot { margin: 0; }
.guide-zoom { display: block; width: fit-content; max-width: 100%; padding: 0; border: 1px solid var(--border); border-radius: var(--radius); background: var(--surface); cursor: zoom-in; overflow: hidden; box-shadow: 0 2px 10px rgba(0, 0, 0, 0.06); }
.guide-zoom:hover, .guide-zoom:focus-visible { border-color: var(--accent); }
/* Shown at its own size (the width/height attributes from guideShots.js),
   shrunk to fit but never blown up and blurred; the attributes also hold
   its space before it loads, so the page doesn't jump. */
.guide-zoom img { display: block; max-width: 100%; height: auto; }
.guide-shot figcaption { font-size: 0.76rem; color: var(--muted); margin-top: 0.3rem; text-align: right; }
.guide-top { margin: var(--space-5) 0 0; font-size: 0.85rem; }
.email-modal.form-modal.guide-dialog { width: min(1200px, calc(100vw - 2rem)); }
.guide-full { display: block; max-width: 100%; height: auto; border: 1px solid var(--border); border-radius: var(--radius); margin-bottom: var(--space-3); }
@media (max-width: 900px) {
  .guide-step { grid-template-columns: minmax(0, 1fr); }
}
/* With no project picked, the Projects tab row shows only the guide. */
#sh-tabs.sh-no-project .tab-btn:not([data-tab="guide"]) { display: none; }
/* The Financial Dashboard's guide sits after <main> (it has no tab row). */
.odoo-guide { max-width: 1280px; margin: 0 auto; padding: 0 var(--space-5) var(--space-8); }
.odoo-guide[hidden] { display: none; }

/* Quotes module (2026-10-09, quotes.js). Reuses .panel, .dashboard-table,
   .kpi-row, .chip / .pf-addp and Plan & fees' People and hours rows
   (.bl-person); only what a quote adds is here. */
#q-kpis .kpi-label .pf-sub { display: block; text-transform: none; letter-spacing: 0; font-size: 0.72rem; margin-top: 0.1rem; }
.q-phase .panel-header { gap: 0.75rem; }
/* Phase line in the teal the Gantt and Hub lists use for phases (Joel
   2026-10-10: "make the phase line of quote a different colour like the
   gantt chart so its clear"); same tokens, so the two can't drift. */
.q-phase > .panel-header { background: var(--hub-phase-bg); border-left: 4px solid var(--hub-phase); }
.q-phase > .panel-header .q-phase-name { font-weight: 700; color: var(--navy); }
.q-phase .chips, .q-lines .chips { display: flex; flex-wrap: wrap; gap: 0.3rem; align-items: center; }
.q-phase .chip, .q-lines .chip { cursor: pointer; font: inherit; font-size: 0.74rem; border: 1px solid transparent; }
.q-inline { font: inherit; width: 100%; min-width: 10rem; padding: 0.2rem 0.4rem; border: 1px solid transparent; border-radius: var(--radius); background: transparent; color: inherit; }
.q-inline:hover { border-color: var(--border); }
.q-inline:focus { border-color: var(--accent); background: var(--surface); outline: none; }
.panel-header .q-phase-name { font-weight: 700; max-width: 32rem; }
/* Table inputs follow the site's inline-edit look (.dashboard-table input:
   border on hover/focus); numbers just sit right-aligned and narrower. */
.dashboard-table input.q-num { min-width: 0; width: 6.5rem; text-align: right; }
.q-pad { padding: 0.5rem 0.85rem; margin: 0; }
.q-disb { border-top: 1px solid var(--border); }
.q-optional td { color: var(--muted); }
.q-check { display: inline-flex; align-items: center; gap: 0.4rem; font-weight: 400; cursor: pointer; }
.q-traveller-list { display: flex; flex-direction: column; gap: 0.4rem; max-height: 50vh; overflow: auto; }
.q-hint { display: block; font-size: 0.75rem; margin: 0.15rem 0 0; }
.q-out td { vertical-align: top; }
/* Quote costing grid (quotes.js, 2026-10-10): one column per consultant
   (hours in the cell, $ under it, a note button), task column pinned on
   the left when the grid scrolls sideways. Same column widths in every
   phase table and the totals table, so the columns line up down the page. */
.q-grid-wrap { overflow-x: auto; }
.q-grid { table-layout: fixed; width: max-content; min-width: 100%; }
.q-grid .q-gt { position: sticky; left: 0; z-index: 1; background: var(--surface, #fff); width: 13rem; min-width: 13rem; max-width: 13rem; }
.q-grid thead .q-gt { background: inherit; }
/* Room for six consultants across a laptop screen (Joel 2026-10-10). */
.q-grid th.q-ch, .q-grid td.q-cell, .q-grid td.q-cell-total, #q-consultant-totals td.num { width: 6.25rem; min-width: 6.25rem; padding-left: 0.35rem; padding-right: 0.35rem; }
.q-grid .q-task-pick { min-width: 0; }
.q-grid td.hb-actions { white-space: nowrap; }
.q-task-line { display: flex; align-items: center; gap: 0.3rem; }
.q-grid .q-task-line .q-task-pick { flex: 1 1 auto; width: auto; min-width: 0; }
.q-task-line .q-note-toggle { flex: none; padding-left: 0.45rem; padding-right: 0.45rem; }
.q-ch { text-align: center; position: relative; line-height: 1.2; }
.q-ch-name { display: block; text-transform: none; letter-spacing: 0; font-size: 0.8rem; }
.q-ch-rate { display: block; font-weight: 400; text-transform: none; letter-spacing: 0; font-size: 0.72rem; color: var(--muted); }
.q-del-consultant { position: absolute; top: 2px; right: 2px; border: 0; background: none; color: var(--muted); cursor: pointer; font-size: 0.9rem; line-height: 1; padding: 2px 4px; }
.q-del-consultant:hover { color: var(--status-red-fg); }
.q-cell { text-align: center; vertical-align: top; }
.q-cell .q-hours { width: 100%; text-align: center; }
.q-cell-on { background: var(--accent-light); }
.q-cell-foot { display: flex; align-items: center; gap: 0.2rem; margin-top: 0.25rem; font-size: 0.7rem; white-space: nowrap; }
.q-consultant-bar #q-add-consultant { width: auto; min-width: 16rem; }
.q-cell-cost { color: var(--muted); }
.q-pnote { border: 1px dashed var(--border); background: none; border-radius: 999px; font: inherit; font-size: 0.7rem; padding: 0 0.4rem; cursor: pointer; color: var(--muted); white-space: nowrap; flex: none; margin-left: auto; }
.q-pnote.q-has-pnote { border-style: solid; border-color: var(--hub-phase); color: var(--hub-phase); font-weight: 600; }
.q-subtotal td { font-weight: 600; background: var(--surface-2, #f4f5f9); }
.q-cell-total { text-align: center; font-size: 0.82rem; }
.q-pnotes { display: flex; flex-direction: column; gap: 0.4rem; }
.q-pnote-list { margin: 0; padding-left: 1.2rem; display: flex; flex-direction: column; gap: 0.35rem; }
.q-pnote-list li { line-height: 1.35; }
.q-pnote-meta { display: block; font-size: 0.72rem; color: var(--muted); }
.q-pnote-body { white-space: pre-wrap; }
.q-pnote-del { border: 0; background: none; color: var(--muted); cursor: pointer; margin-left: 0.3rem; }
.q-pnote-del:hover { color: var(--status-red-fg); }
.q-brief-task { border-top: 1px solid var(--border); padding-top: 0.5rem; }
.q-brief-task-name { font-weight: 600; margin-bottom: 0.3rem; }

/* Quote timeline (quotes.js timelineChartHtml, 2026-10-10): name column,
   (Timeline tab only) start-week and weeks inputs, then a track split into
   one column per week. Bars are placed by percentage of the track. Phases
   teal and tasks purple, as on the project Gantt. */
.q-tl-wrap { overflow-x: auto; }
.q-tl { --q-tl-name: 15rem; min-width: 36rem; font-size: 0.85rem; }
.q-tl-row { display: grid; grid-template-columns: var(--q-tl-name) 1fr; align-items: stretch; border-top: 1px solid var(--border); min-height: 2.1rem; break-inside: avoid; page-break-inside: avoid; }
.q-tl-row.q-tl-edit { grid-template-columns: var(--q-tl-name) 4.5rem 4.5rem 1fr; }
.q-tl-head { border-top: 0; background: var(--surface-2, #f4f5f9); font-weight: 700; font-size: 0.72rem; text-transform: uppercase; letter-spacing: 0.03em; }
.q-tl-name { padding: 0.35rem 0.6rem; min-width: 0; overflow-wrap: anywhere; align-self: center; }
.q-tl-h { align-self: center; padding: 0 0.3rem; }
.q-tl-row .q-num { align-self: center; width: 4rem; margin: 0 0.25rem; }
.q-tl-dates { display: block; font-weight: 400; font-size: 0.72rem; color: var(--muted); text-transform: none; letter-spacing: 0; }
.q-tl-phase { background: var(--hub-phase-bg); font-weight: 700; }
.q-tl-track { position: relative; display: grid; grid-template-columns: repeat(var(--q-tl-weeks), 1fr); border-left: 1px solid var(--border); }
.q-tl-col { border-left: 1px dashed var(--border); }
.q-tl-col:first-child, .q-tl-wk:first-child { border-left: 0; }
.q-tl-school { background: rgba(14, 116, 144, 0.08); }
.q-tl-phline { position: absolute; top: 0; bottom: 0; width: 0; border-left: 2px solid var(--status-red-fg); opacity: 0.6; }
.q-tl-wk { display: flex; flex-direction: column; justify-content: center; padding: 0.25rem 0.3rem; border-left: 1px dashed var(--border); line-height: 1.15; }
.q-tl-wk .q-tl-date { font-weight: 400; text-transform: none; letter-spacing: 0; color: var(--muted); }
.q-tl-ph { color: var(--status-red-fg, #b91c1c); font-size: 0.7rem; }
.q-tl-bar { position: absolute; top: 50%; height: 0.8rem; margin-top: -0.4rem; border-radius: 3px; background: var(--accent); }
.q-tl-bar-phase { height: 0.55rem; margin-top: -0.275rem; background: var(--hub-phase); }
.q-tl-hols { padding: 0.5rem 0.75rem; margin: 0; }
.q-print .q-tl { --q-tl-name: 12rem; min-width: 0; font-size: 0.75rem; }
.q-print .q-tl-wrap { overflow: visible; }

/* Quote task notes (2026-10-10): rich text under the task, a short plain
   preview when closed. */
.q-tasks .q-task-pick, .q-disb .q-disb-pick { width: 100%; min-width: 12rem; }
.q-note-preview { margin-top: 0.3rem; font-size: 0.78rem; color: var(--muted); white-space: pre-wrap; }
.q-note-row td { background: var(--surface); padding-top: 0.25rem; }
.q-note-label { display: block; font-size: 0.8rem; margin: 0 0 0.35rem; }
.q-note-editor { min-height: 6rem; background: #fff; }
.q-note-editor .ql-editor { min-height: 6rem; font-size: 0.9rem; }
.q-has-note { border-color: var(--hub-phase); color: var(--hub-phase); }
/* Quote PDF and consultant brief (quotes.js, 2026-10-10): drawn into the
   hidden #q-print only while capturing, at 718px, the portrait A4 page's
   printable width (190mm), which html2pdf crops to. Rows avoid splitting
   across pages. */
.q-print { width: 718px; padding: 0 0 12px; background: #fff; color: var(--navy); }
.q-print[hidden] { display: none; }
.q-print .form-letterhead { padding: 1rem 1rem 0.85rem; }
.q-print .panel { box-shadow: none; }
.q-print tr { break-inside: avoid; page-break-inside: avoid; }
.q-print table.dashboard-table tbody tr:hover { background: none; }
.q-print > p { margin: 0.75rem 0; }
.q-brief td { vertical-align: top; }
.q-brief-phase td { background: var(--hub-phase-bg); font-weight: 700; }
.q-brief-off td { color: var(--muted); }
.q-brief-note { margin-top: 0.2rem; font-size: 0.8rem; color: var(--muted); white-space: pre-wrap; }

/* "Log a bug" (2026-10-09): the sidebar's one button among links, styled
   as a .sidebar-item (reset the button chrome), and its window. */
.site-header button.sidebar-item { width: calc(100% - 1rem); border: none; background: none; font: inherit; font-size: 0.86rem; font-weight: 600; text-align: left; cursor: pointer; }
.bug-modal { max-width: 34rem; width: calc(100% - 2rem); }
.bug-check { display: inline-flex; align-items: center; gap: 0.45rem; font-weight: 400 !important; cursor: pointer; }
.bug-check input { width: auto !important; }
.bug-shot { display: block; max-width: 100%; border: 1px solid var(--border); border-radius: var(--radius); }
.bug-list { margin: 0.2rem 0 0 1.1rem; padding: 0; font-size: 0.85rem; }
.bug-list li { margin: 0 0 0.35rem; }
.bug-pre { white-space: pre-wrap; word-break: break-word; font-size: 0.75rem; margin: 0.15rem 0; color: var(--muted); max-height: 14rem; overflow: auto; }
.bug-detail { max-width: 46rem; }
