/* ============================================================================
   CANOPY v2 APP SHELL — redesign phase 1, slice S1
   ----------------------------------------------------------------------------
   Loaded ONLY by templates/shell/base.html, which is the only template that
   sets `data-shell="v2"`. With SHELL_V2_ENABLED off nothing links this file.

   HARD RULES, enforced by tests/test_shell_tokens_contract.py:
     * zero hex literals in this file;
     * zero Layer 1 primitives (--paper-*, --bark-*, --spruce-*, --marker-*,
       --ink-*, --state-*) — only Layer 2 and --shell-* names;
     * every --shell-* name used here is defined in tokens.css.
   Those three are what make palette C · Evening ledger a later token swap
   rather than a second design (spec §4.5).

   Structural tokens (--space-*, --radius-*, --text-*, --duration-*, …) are
   REUSED from tokens.css. The shell does not fork the spacing scale (§4.4).
   ========================================================================= */

/* --- Self-hosted type (spec §5.2) ------------------------------------------
   No CDN: base.html:9 pulls Inter + Fraunces from Google Fonts, and the shell
   deliberately does not inherit that. Both faces are SIL OFL 1.1 and vendored
   in static/fonts/ with their licences beside them.

   Both files are the latin subset of the VARIABLE face, so one binary covers
   the whole weight range and JetBrains Mono needs no separate 600 file (the
   spec's §5.2 file list assumed static instances; two identical binaries would
   be waste). `font-display: swap` means a director at 6:45am on a cold cache
   sees the board immediately in the fallback face, never a blank page. */
@font-face {
  font-family: 'InterVariable';
  src: url('../fonts/inter-var-latin.woff2') format('woff2-variations'),
       url('../fonts/inter-var-latin.woff2') format('woff2');
  font-weight: 400 700;
  font-style: normal;
  font-display: swap;
  unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6,
                 U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F,
                 U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215,
                 U+FEFF, U+FFFD;
}

/* The storybook voice's face (whimsy core, David 2026-08-21: the display
   serif returns to the shell as a VOICE, not as chrome — headings stay on the
   grotesk). Same vendoring rules as the two faces above: variable latin
   subset, OFL 1.1, licence beside it in static/fonts/. */
@font-face {
  font-family: 'Fraunces';
  src: url('../fonts/fraunces-var-latin.woff2') format('woff2-variations'),
       url('../fonts/fraunces-var-latin.woff2') format('woff2');
  font-weight: 400 700;
  font-style: normal;
  font-display: swap;
  unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6,
                 U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F,
                 U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215,
                 U+FEFF, U+FFFD;
}

@font-face {
  font-family: 'JetBrains Mono';
  src: url('../fonts/jetbrains-mono-var-latin.woff2') format('woff2-variations'),
       url('../fonts/jetbrains-mono-var-latin.woff2') format('woff2');
  font-weight: 400 700;
  font-style: normal;
  font-display: swap;
  unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6,
                 U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F,
                 U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215,
                 U+FEFF, U+FFFD;
}


/* --- Reset ----------------------------------------------------------------
   The shell loads tokens.css and this file and nothing else — no Bootstrap,
   no app.css — so it owns the two resets those used to provide. Scoped to the
   shell attribute so it can never reach a legacy page. Without the border-box
   rule the rail's `height: 100vh` plus its padding overflows the viewport by
   exactly the padding, and the user block at its foot is clipped. */
[data-shell="v2"] *,
[data-shell="v2"] *::before,
[data-shell="v2"] *::after {
  box-sizing: border-box;
}

[data-shell="v2"] body {
  margin: 0;
}


/* --- Document ------------------------------------------------------------ */

.shell {
  display: grid;
  grid-template-columns: var(--shell-rail-width) minmax(0, 1fr);
  min-height: 100vh;
  background: var(--color-bg);
  color: var(--color-text-primary);
  font-family: var(--font-body);
  font-size: var(--text-body);
  line-height: var(--leading-normal);
}

/* Skip link — first focusable element, hidden until focused (§2.4). */
.shell__skip {
  position: absolute;
  left: var(--space-3);
  top: calc(var(--space-3) * -10);
  z-index: 60;
  padding: var(--space-3) var(--space-5);
  background: var(--color-surface);
  color: var(--color-text-link);
  border: 1px solid var(--color-border-strong);
  border-radius: var(--radius-sm);
  font-weight: var(--weight-semibold);
  transition: top var(--duration-fast) var(--ease-standard);
}

.shell__skip:focus {
  top: var(--space-3);
}


/* Screen-reader-only text. base.html gets this from Bootstrap; the shell
   loads no Bootstrap, and the ratio pill's accessible verdict (S3) depends on
   it, so the shell owns its own copy. */
.visually-hidden {
  position: absolute !important;
  width: 1px;
  height: 1px;
  padding: 0;
  margin: -1px;
  overflow: hidden;
  clip: rect(0, 0, 0, 0);
  white-space: nowrap;
  border: 0;
}


/* --- Rail (§2.2) --------------------------------------------------------- */

.shell-rail {
  position: sticky;
  top: 0;
  align-self: start;
  height: 100vh;
  display: flex;
  flex-direction: column;
  background: var(--shell-rail-bg);
  color: var(--shell-rail-fg);
  padding: var(--space-4) 0 var(--space-4);
}

.shell-rail__wordmark {
  display: block;
  padding: 0 var(--space-4) var(--space-4);
  font-size: var(--text-h3);
  font-weight: var(--weight-bold);
  letter-spacing: var(--tracking-tight);
  color: var(--color-surface);
  text-decoration: none;
}

.shell-rail__list {
  list-style: none;
  margin: 0;
  padding: 0;
  flex: 1 1 auto;
}

/* Group labels (grouped rail, nav review 2026-09-24): a small uppercase
   caption before the first visible item of each group -- the sidebar
   section-label treatment CANOPY-DESIGN reserves --tracking-wider for. Not a
   link and not a list item (role="presentation" in the template). */
.shell-rail__group {
  padding: var(--space-5) var(--space-4) var(--space-2);
  font-size: var(--text-micro);
  font-weight: var(--weight-semibold);
  letter-spacing: var(--tracking-wider);
  text-transform: uppercase;
  line-height: var(--leading-snug);
  color: var(--shell-rail-fg-muted);
}

.shell-rail__group:first-child {
  padding-top: var(--space-2);
}

.shell-rail__link {
  display: flex;
  align-items: center;
  gap: var(--space-3);
  min-height: 44px;              /* CANOPY-DESIGN §8 minimum touch target */
  padding: var(--space-3) var(--space-4);
  border-left: 3px solid transparent;
  color: var(--shell-rail-fg);
  text-decoration: none;
  font-size: var(--text-body-sm);
  transition: background-color var(--duration-fast) var(--ease-standard);
}

.shell-rail__link:hover {
  background: var(--shell-rail-bg-active);
  color: var(--color-surface);
}

/* Active state is NEVER colour alone (CANOPY-DESIGN §7.3): a border AND a
   weight change AND aria-current="page" on the element itself. */
.shell-rail__link[aria-current="page"] {
  background: var(--shell-rail-bg-active);
  border-left-color: var(--shell-active-marker);
  color: var(--color-surface);
  font-weight: var(--weight-semibold);
}

.shell-rail__icon {
  flex: 0 0 auto;
  display: block;
  width: 16px;
  height: 16px;
}

/* The rail badge pill (Task 1, R7): the points-pending count, moved off the
   legacy People-link badge (base.html:48-49, dies with the People module,
   Task 4) onto the rail item it now actually follows -- gated on
   points_view, not role (the same coherence fix R7 documents). Reuses the
   shared danger token so it reads the same as the legacy `badge bg-danger`
   without forking a new colour. */
.shell-rail__badge {
  margin-left: auto;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  min-width: 1.4rem;
  height: 1.4rem;
  padding: 0 var(--space-2);
  border-radius: var(--radius-pill);
  background: var(--color-danger-solid);
  color: var(--color-text-on-brand);
  font-size: var(--text-micro);
  font-weight: var(--weight-semibold);
  font-variant-numeric: tabular-nums;
}

/* The phone account menu: shown only at the collapsed width (see the
   max-width: 1024px block), where the user block is hidden. */
.shell-rail__compact-account {
  display: none;
  position: relative;
}

.shell-rail__compact-account > summary {
  list-style: none;
  cursor: pointer;
}

.shell-rail__compact-account > summary::-webkit-details-marker {
  display: none;
}

.shell-rail__compact-account[open] > summary {
  background: var(--shell-rail-bg-active);
  color: var(--color-surface);
}

.shell-rail__compact-account-panel {
  position: absolute;
  left: 100%;
  bottom: 0;
  z-index: 40;
  display: flex;
  flex-direction: column;
  gap: var(--space-1);
  min-width: 11rem;
  padding: var(--space-4);
  background: var(--shell-rail-bg);
  border: 1px solid var(--shell-rail-bg-active);
  border-radius: 0 var(--radius-md) var(--radius-md) 0;
  box-shadow: var(--shadow-lg);
  color: var(--shell-rail-fg-muted);
  font-size: var(--text-body-sm);
}

.shell-rail__compact-account-panel strong {
  color: var(--shell-rail-fg);
  font-weight: var(--weight-semibold);
}

/* Phone tap targets: the plain underlined links become 44px rows. */
.shell-rail__compact-account-panel .shell-rail__user-actions a,
.shell-rail__compact-account-panel .shell-rail__user-actions button {
  display: flex;
  align-items: center;
  min-height: 44px;
  font-size: var(--text-body-sm);
}

.shell-rail__user {
  padding: var(--space-3) var(--space-4) 0;
  border-top: 1px solid var(--shell-rail-bg-active);
  margin-top: var(--space-4);
  font-size: var(--text-micro);
  color: var(--shell-rail-fg-muted);
  line-height: var(--leading-snug);
}

.shell-rail__user strong {
  display: block;
  color: var(--shell-rail-fg);
  font-weight: var(--weight-semibold);
}

/* Change Password / Log out (COBR R1 Batch 6c) -- base.html's deleted
   user-menu modal has no replacement, so these two plain actions sit
   under the username/role instead. */
.shell-rail__user-actions {
  display: flex;
  flex-direction: column;
  gap: var(--space-1);
  margin-top: var(--space-2);
}

.shell-rail__user-actions a,
.shell-rail__user-actions button {
  background: none;
  border: none;
  padding: 0;
  margin: 0;
  font: inherit;
  font-size: var(--text-micro);
  color: var(--shell-rail-fg-muted);
  text-align: left;
  text-decoration: underline;
  cursor: pointer;
}

.shell-rail__user-actions a:hover,
.shell-rail__user-actions button:hover {
  color: var(--shell-rail-fg);
}


/* --- Context bar (§2.3) -------------------------------------------------- */

.shell-context {
  position: sticky;
  top: 0;
  z-index: 20;
  display: flex;
  align-items: center;
  gap: var(--space-4);
  min-height: var(--shell-context-height);
  padding: var(--space-2) var(--space-5);
  background: var(--shell-surface);
  border-bottom: 1px solid var(--shell-border);
  font-size: var(--text-body-sm);
}

.shell-context__spacer {
  flex: 1 1 auto;
}

.shell-context__date {
  color: var(--color-text-secondary);
}

.shell-context__school {
  font-weight: var(--weight-semibold);
  color: var(--color-text-primary);
  background: transparent;
  border: 1px solid var(--color-border);
  border-radius: var(--radius-sm);
  padding: var(--space-2) var(--space-3);
  min-height: 32px;
  font-family: inherit;
  font-size: var(--text-body-sm);
}

.shell-context__school-static {
  font-weight: var(--weight-semibold);
}

.shell-search {
  display: inline-flex;
  align-items: center;
  gap: var(--space-3);
  min-height: 32px;
  padding: var(--space-2) var(--space-3);
  background: var(--color-surface-2);
  border: 1px solid var(--color-border);
  border-radius: var(--radius-sm);
  color: var(--color-text-muted);
  font: inherit;
  font-size: var(--text-body-sm);
  cursor: pointer;
}

.shell-search:hover {
  border-color: var(--color-border-strong);
}

.shell-kbd {
  font-family: var(--font-mono);
  font-size: var(--text-micro);
  padding: 0 var(--space-2);
  border: 1px solid var(--color-border-strong);
  border-radius: var(--radius-xs);
  color: var(--color-text-secondary);
}


/* --- Main ---------------------------------------------------------------- */

.shell-main {
  min-width: 0;
  display: flex;
  flex-direction: column;
}

.shell-content {
  padding: var(--space-6) var(--space-5) var(--space-10);
}

/* S8 (2026-09-01 review): opt-in for the SPA-shaped pages (dashboard.index /
   .rooms) that need to render edge-to-edge rather than sit inside this
   container's own horizontal padding -- their own inner container (e.g.
   Bootstrap's `.container`) already manages width, so this container's
   horizontal padding would only double it up. Vertical padding is left
   alone: it is what keeps the page's own header off the context bar above
   it, and nothing about "wide" asks for that too. Tokens only -- 0 is not a
   new value, it removes a value. */
.shell-content--wide {
  padding-left: 0;
  padding-right: 0;
}

/* The shell loads no Bootstrap, so link colour is the browser's blue unless
   the token says otherwise. */
.shell-content a {
  color: var(--color-text-link);
}

.shell-content code {
  font-family: var(--font-mono);
  font-size: var(--text-body-sm);
  padding: 0 var(--space-2);
  background: var(--color-surface-2);
  border-radius: var(--radius-xs);
}

.shell-page-title {
  margin: 0 0 var(--space-2);
  font-size: var(--text-h2);
  font-weight: var(--weight-bold);
  letter-spacing: var(--tracking-tight);
  line-height: var(--leading-snug);
}

/* The shared page header (templates/shell/_page_header.html, nav review F7).
   One vertical rhythm for every page top: back link, title row, meta,
   toolbar, each separated by the same gap, so the title never sits at a
   different height from one page to the next. The title's own bottom margin
   is zeroed here because the header's gap owns the spacing now.

   The weight is restated at (0,2,0) on purpose: on the pages that also load
   bootstrap-canopy-theme.css, its `:where(...) :is(h1, .h1, ...)` heading
   rule scores (0,1,0) through `.h1` and loads after this file, so it beat a
   bare `.shell-page-title` and rendered those titles semibold while every
   shell hub rendered bold. */
.page-header {
  display: flex;
  flex-direction: column;
  gap: var(--space-2);
  margin-bottom: var(--space-5);
}

.page-header .shell-page-title {
  margin: 0;
  font-weight: var(--weight-bold);
}

.page-header__back {
  align-self: flex-start;
  display: inline-flex;
  align-items: center;
  gap: var(--space-2);
  min-height: var(--space-8);
  font-size: var(--text-body-sm);
  font-weight: var(--weight-medium);
  color: var(--color-text-link);
  text-decoration: none;
}

.page-header__back:hover {
  text-decoration: underline;
}

/* Title left, actions right; flex-wrap drops the actions onto their own line
   below the title once they no longer fit beside it (a phone), instead of
   squeezing the title into a narrow column. */
.page-header__row {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  justify-content: space-between;
  gap: var(--space-3);
}

.page-header__row > .shell-page-title {
  flex: 1 1 auto;
  min-width: 0;
  overflow-wrap: anywhere;
}

.page-header__actions {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: var(--space-2);
}

.page-header__meta > * {
  margin-top: 0;
  margin-bottom: 0;
}

.page-header__meta > * + * {
  margin-top: var(--space-1);
}

.page-header__toolbar {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: var(--space-3);
  margin-top: var(--space-2);
}

/* The S1 placeholder every unbuilt destination lands on. Named as unbuilt on
   the page: a shell that renders an empty panel reads as a broken board. */
.shell-placeholder {
  max-width: 46rem;
  padding: var(--space-6);
  background: var(--color-surface);
  border: 1px solid var(--color-border);
  border-radius: var(--radius-md);
  box-shadow: var(--shadow-sm);
}

.shell-placeholder h2 {
  margin: 0 0 var(--space-3);
  font-size: var(--text-h3);
  font-weight: var(--weight-semibold);
}

.shell-placeholder p {
  margin: 0 0 var(--space-3);
  color: var(--color-text-secondary);
}

.shell-placeholder p:last-child {
  margin-bottom: 0;
}

.shell-note {
  color: var(--color-text-muted);
  font-size: var(--text-caption);
}

/* Flash-category variants (opus review item 2). shell/base.html and
   shell/public.html both render every flash as `.shell-note
   .shell-note--{{ category }}`; only the three below get a distinct
   treatment (a category outside this set, e.g. "info", keeps the plain
   muted .shell-note look above). Tokens only -- --color-danger-fg/-bg etc.
   are Layer 2 semantic tokens, already used the same way elsewhere in this
   file (e.g. .env-banner--dev). */
.shell-note--danger,
.shell-note--error {
  color: var(--color-danger-fg);
  background: var(--color-danger-bg);
  padding: var(--space-2) var(--space-3);
  border-radius: var(--radius-sm);
}

.shell-note--warning {
  color: var(--color-warning-fg);
  background: var(--color-warning-bg);
  padding: var(--space-2) var(--space-3);
  border-radius: var(--radius-sm);
}

.shell-note--success {
  color: var(--color-success-fg);
  background: var(--color-success-bg);
  padding: var(--space-2) var(--space-3);
  border-radius: var(--radius-sm);
}

/* shell/public.html's compact top-right user bar (opus review item 1) --
   the same actions shell/_rail.html's .shell-rail__user-actions renders,
   but public.html has no rail (dark sidebar) to sit inside, so this reuses
   the page's own light surface tokens instead of the rail's dark ones. */
.shell-user-bar {
  position: absolute;
  top: var(--space-4);
  right: var(--space-5);
  display: flex;
  align-items: center;
  gap: var(--space-4);
  font-size: var(--text-caption);
  z-index: 1;
}

.shell-user-bar a,
.shell-user-bar button {
  background: none;
  border: none;
  padding: 0;
  margin: 0;
  font: inherit;
  font-size: var(--text-caption);
  color: var(--color-text-secondary);
  text-decoration: underline;
  cursor: pointer;
}

.shell-user-bar a:hover,
.shell-user-bar button:hover {
  color: var(--color-text-primary);
}

.shell-user-bar form {
  display: contents;
}


/* --- The Today board (§3) -------------------------------------------------
   Tokens only, and only Layer 2 / --shell-* ones: no hex and no Layer 1
   primitive, so palette C · Evening ledger stays a single token block later
   rather than a hex hunt under deadline (tests/test_shell_tokens_contract.py
   is the gate).
   -------------------------------------------------------------------------- */

/* Section A — the two freshness facts, side by side, always both. */
.board-freshness {
  margin: 0;
  font-size: var(--text-caption);
  color: var(--color-text-secondary);
  display: flex;
  flex-wrap: wrap;
  gap: var(--space-2);
  font-variant-numeric: tabular-nums;
}

.board-freshness__sep {
  color: var(--color-text-muted);
}

.board-freshness__fact--warn {
  color: var(--color-warning-fg);
  font-weight: var(--weight-medium);
}

.board-banner {
  margin: 0 0 var(--space-5);
  padding: var(--space-3) var(--space-4);
  border-radius: var(--radius-sm);
  font-size: var(--text-body-sm);
  font-weight: var(--weight-medium);
}

.board-banner--crit {
  background: var(--color-danger-bg);
  border: 1px solid var(--color-danger-border);
  color: var(--color-danger-fg);
}

/* Section B — KPIs. `—` is a first-class value here, not an empty cell. */
.board-kpis {
  list-style: none;
  margin: 0 0 var(--space-6);
  padding: 0;
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(9rem, 1fr));
  gap: var(--space-3);
}

/* The card is the FRAME; `.board-kpi__body` is the content, and when the card
   deep-links (S4) that body is the <a>. The padding therefore lives on the
   body, not the card: an <a> inset inside a padded <li> would leave a dead
   border of card that looks clickable and is not. */
.board-kpi {
  padding: 0;
  background: var(--color-surface);
  border: 1px solid var(--color-border);
  border-radius: var(--radius-md);
}

.board-kpi__body {
  display: flex;
  flex-direction: column;
  gap: var(--space-1);
  padding: var(--space-4);
  min-height: 44px;              /* CANOPY-DESIGN §8 minimum touch target */
  border-radius: inherit;
}

/* `.shell-content a` paints every link green; a whole card in link green reads
   as one enormous button, so the card keeps its own text colours and the
   affordance is the hover/focus state instead. */
a.board-kpi__body--link {
  color: inherit;
  text-decoration: none;
  transition: background-color var(--duration-fast) var(--ease-standard);
}

a.board-kpi__body--link:hover {
  background: var(--color-surface-2);
}

/* State is never colour alone: the hover tint is joined by an underline on the
   label, so a clickable card is identifiable without relying on hue — and on a
   touch device, where there is no hover at all, the arrangement is unchanged
   because nothing here is hover-dependent for MEANING. */
a.board-kpi__body--link:hover .board-kpi__label,
a.board-kpi__body--link:focus-visible .board-kpi__label {
  text-decoration: underline;
}

/* The ring is drawn on the card's own edge rather than inside it: `.shell
   :focus-visible` sets a 2px outline at 2px offset, and on a body that fills
   its parent exactly that ring lands ON the border, which is where a reader
   looks for it. */
a.board-kpi__body--link:focus-visible {
  outline-offset: 0;
}

.board-kpi__value {
  font-size: var(--text-display);
  font-weight: var(--weight-semibold);
  line-height: var(--leading-tight);
}

.board-kpi__label {
  font-size: var(--text-caption);
  color: var(--color-text-secondary);
}

.board-kpi__detail {
  font-size: var(--text-micro);
  color: var(--color-text-muted);
}

.board-kpi__detail:empty {
  display: none;
}

.board-columns {
  display: grid;
  grid-template-columns: minmax(0, 2fr) minmax(0, 1fr);
  gap: var(--space-6);
  align-items: start;
}

.board-section-title {
  margin: 0 0 var(--space-4);
  font-size: var(--text-h3);
  font-weight: var(--weight-semibold);
  display: flex;
  align-items: baseline;
  gap: var(--space-3);
}

.board-count {
  font-size: var(--text-caption);
  color: var(--color-text-muted);
}

/* Section C — rooms. */
.board-room-list {
  list-style: none;
  margin: 0;
  padding: 0;
  background: var(--color-surface);
  border: 1px solid var(--color-border);
  border-radius: var(--radius-md);
}

.board-room {
  display: flex;
  align-items: center;
  flex-wrap: wrap;
  gap: var(--space-3);
  /* ≥ 44px without a fixed height, so a wrapped row still grows (§2.6). */
  min-height: 44px;
  padding: var(--space-3) var(--space-4);
  border-bottom: 1px solid var(--color-border);
}

.board-room:last-child {
  border-bottom: none;
}

/* ⟨S3 verify 8⟩ Equal-width pills are not enough on their own: `.board-room`
   is a flex row, so the pill's left edge is wherever the room NAME and the
   staff chips happen to leave it. Measured across seven LSNC-shaped rooms
   before this rule: a 35px spread, wider than a digit.

   The LIST owns the tracks and each row adopts them, which is the only way
   one row's pill can know where another row's pill starts. Behind @supports
   because the flex layout above is right on an engine without subgrid — it
   is ragged, not broken, and ragged is the correct thing to degrade to.
   Held above the phone tier: at 640px the row is meant to wrap, and three
   tracks that cannot wrap would overflow instead. */
@supports (grid-template-columns: subgrid) {
  @media (min-width: 641px) {
    .board-room-list {
      display: grid;
      grid-template-columns: minmax(9rem, 1fr) max-content max-content;
    }

    .board-room {
      display: grid;
      grid-column: 1 / -1;
      grid-template-columns: subgrid;
      align-items: center;
    }
  }
}

.board-room__name {
  flex: 1 1 12rem;
  font-weight: var(--weight-medium);
}

/* The botanical mark (whimsy core): a line drawing of the room's namesake,
   sized under the row's own line box so it can never move a row. Decoration
   in the brand ink, and quiet about it — compliance stays the loudest thing
   on the row. */
.board-room__mark {
  display: inline-block;
  vertical-align: -3px;
  margin-right: var(--space-1);
  color: var(--color-brand);
}

.board-room__age {
  margin-left: var(--space-2);
  font-size: var(--text-caption);
  font-weight: var(--weight-regular);
  color: var(--color-text-muted);
}

.board-room__merged {
  font-size: var(--text-micro);
  letter-spacing: var(--tracking-wide);
  text-transform: uppercase;
  color: var(--color-text-muted);
}

.board-room__staff {
  display: flex;
  gap: var(--space-2);
}

.board-room__initials {
  position: relative;
  padding: var(--space-1) var(--space-2);
  font-size: var(--text-micro);
  border-radius: var(--radius-xs);
  background: var(--color-surface-2);
  color: var(--color-text-secondary);
  cursor: default;
}

/* Chip sources (2026-08-28, staffing from actual attendance). Counted chips
   stay solid; a LIVE chip (Playground placed the person in this room) gets the
   stronger text colour so the eye can tell "seen here" from "assumed here"
   (source scheduled: signed in but unplaced, the schedule's room stands).
   EXPECTED chips are hollow: scheduled now, not clocked in, counted nowhere.
   Tokens only, no new hex. */
.board-room__initials--live {
  color: var(--color-text-primary);
}

.board-room__initials--expected {
  background: transparent;
  border: 1px dashed var(--color-border-strong);
  color: var(--color-text-tertiary, var(--color-text-secondary));
}

.board-unplaced {
  margin: var(--space-2) 0 0;
  font-size: var(--text-micro);
  color: var(--color-text-secondary);
}

.board-unplaced__label {
  font-weight: 600;
}

/* The chip carries a hover (full name · shift window). The native `title`
   attribute stays as the accessibility carrier, but its own tooltip needs a
   1-2s dwell and never fires on touch, so on hover/focus we also paint an
   instant tooltip from `data-hover` (same text, set by the template and by
   shell.js's chip rebuild). Tokens only, no new hex/sizes; no transition or
   animation here, matching the reduced-motion posture elsewhere in this file. */
.board-room__initials:hover::after,
.board-room__initials:focus-visible::after {
  content: attr(data-hover);
  position: absolute;
  bottom: calc(100% + var(--space-1));
  left: 50%;
  transform: translateX(-50%);
  white-space: nowrap;
  background: var(--color-surface);
  color: var(--color-text-primary);
  border: 1px solid var(--color-border-strong);
  border-radius: var(--radius-xs);
  padding: var(--space-1) var(--space-2);
  font-size: var(--text-micro);
  box-shadow: var(--shadow-sm);
  z-index: 20;
}

/* Past the 2-teacher cap the whole cluster goes amber (David's standing
   ruling: never more than two teachers in a room — the third body is placed
   and FLAGGED). Colour is never the only carrier: the same fact lands as a
   warn attention item. The tooltip above declares its own background/colour/
   border, none of which are inherited properties, so this override cannot
   leak into the ::after and does not need to repeat it. */
.board-room__staff--overcap .board-room__initials {
  background: var(--color-warning-bg);
  border: 1px solid var(--color-warning-fg);
  color: var(--color-warning-fg);
}

.board-empty {
  margin: 0;
  padding: var(--space-5);
  background: var(--color-surface);
  border: 1px dashed var(--color-border-strong);
  border-radius: var(--radius-md);
  color: var(--color-text-secondary);
  /* The storybook voice: empty states speak in the display face. Glyph
     widths change and nothing else — size and line-height are untouched. */
  font-family: var(--font-voice);
}

/* The voice line under the title (whimsy core): a greeting that claims
   nothing, plus the EARNED calm sentence when today_board composed one.
   Both in the voice face; the calm half in the quiet AA-safe ink. */
.board-voice {
  margin: var(--space-1) 0 0;
  font-family: var(--font-voice);
  font-size: var(--text-body);
  color: var(--color-text-secondary);
}

.board-voice__calm {
  color: var(--color-text-quiet);
}

/* The earned moment (Morning Light's first-light bloom, adopted at David's
   extended duration — 3000ms over the gallery's 1800). Applied by the poller
   to the ROOM ROW, only when its pill leaves over/crit for ok/warning: light
   gathers on the row, blooms once, and leaves. Nothing is left behind to
   dismiss. The row, not the pill, because the pill's state background must
   never be animated over — a moment may sit beside a state, never on top of
   it. NOT a cny-pill-- class on purpose: this is a moment, never a state,
   and the state vocabulary is pinned by test. */
.board-cleared {
  animation: shell-first-light 3000ms var(--ease-standard) 1 forwards;
}

@keyframes shell-first-light {
  0% {
    background-color: var(--shell-moment-light);
    box-shadow: 0 0 0 0 var(--shell-moment-light);
  }
  35% {
    background-color: var(--shell-moment-light);
    box-shadow: 0 0 22px 8px var(--shell-moment-light);
  }
  100% {
    background-color: transparent;
    box-shadow: 0 0 26px 10px transparent;
  }
}

/* Reduced motion keeps the meaning and drops the movement: the pill holds
   the light statically for the moment's lifetime (the poller removes the
   class), so the moment is still marked for a reader who cannot use
   animation. */
@media (prefers-reduced-motion: reduce) {
  .board-cleared {
    animation: none;
    background-color: var(--shell-moment-light);
  }
}

.board-note {
  margin: var(--space-3) 0 0;
  font-size: var(--text-caption);
  color: var(--color-text-muted);
}

/* Section D — attention. */
.board-attention-list {
  list-style: none;
  margin: 0;
  padding: 0;
  display: flex;
  flex-direction: column;
  gap: var(--space-3);
}

.board-attention-item {
  display: flex;
  flex-direction: column;
  gap: var(--space-1);
  padding: var(--space-3) var(--space-4);
  background: var(--color-surface);
  border: 1px solid var(--color-border);
  /* State is never colour alone: the left rule is a shape as well as a hue,
     and the item's own words carry the severity. */
  border-left: 3px solid var(--color-border-strong);
  border-radius: var(--radius-sm);
}

.board-attention-item--crit {
  border-left-color: var(--color-danger-solid);
}

.board-attention-item--warn {
  border-left-color: var(--color-warning-solid);
}

.board-attention__title {
  font-size: var(--text-body-sm);
  font-weight: var(--weight-medium);
}

.board-attention__detail {
  font-size: var(--text-caption);
  color: var(--color-text-secondary);
}

.board-attention__detail:empty {
  display: none;
}

/* --- Section D's two tiers (S4) -------------------------------------------
   The QUIET tier is data-quality plumbing: real, worth surfacing, and not what
   a director's 6:45am is about. It is quieter in three independent ways — it is
   collapsed, its rule is the neutral border rather than a state colour, and its
   type is one step down — so the difference survives a greyscale print and a
   colour-blind reader. It is never REMOVED: the heading count above still
   includes it, and the disclosure says how many. */
.board-attention-quiet {
  margin-top: var(--space-3);
}

.board-attention-quiet__summary {
  min-height: 44px;
  display: flex;
  align-items: center;
  gap: var(--space-2);
  padding: var(--space-2) var(--space-3);
  border: 1px solid var(--color-border);
  border-radius: var(--radius-sm);
  background: var(--color-surface-2);
  color: var(--color-text-secondary);
  font-size: var(--text-caption);
  font-variant-numeric: tabular-nums;
  cursor: pointer;
}

.board-attention-quiet[open] .board-attention-quiet__summary {
  margin-bottom: var(--space-3);
}

.board-attention-list--quiet .board-attention-item {
  padding: var(--space-2) var(--space-3);
  /* The neutral rule, deliberately overriding the per-level colour: severity
     inside the quiet tier is not the reader's decision to make this morning. */
  border-left-color: var(--color-border-strong);
}

.board-attention-list--quiet .board-attention__title {
  font-size: var(--text-caption);
  font-weight: var(--weight-regular);
}

.board-attention-list--quiet .board-attention__detail,
.board-attention-list--quiet .board-attention__verb {
  font-size: var(--text-micro);
}

/* A 44px verb is right in the loud tier, where it is a primary action. Inside a
   collapsed group of plumbing notes it would make each row taller than the item
   it belongs to, so the target comes from padding instead of a min-height. */
.board-attention-list--quiet .board-attention__verb {
  min-height: 0;
  padding: var(--space-2) 0;
}

/* --- Section D scrolls on its own (S4) ------------------------------------
   Desktop only. See the 641px media query at the foot of this file: at phone
   widths the board is one column and an inner scroll region inside the page
   scroll is the thing David's fourth item asks us NOT to do. This bare rule
   exists so the region is a real element at every width, with no layout of its
   own until the breakpoint grants it one. */
.board-attention__scroll {
  min-width: 0;
}

.board-attention__verb {
  align-self: flex-start;
  min-height: 44px;
  display: inline-flex;
  align-items: center;
  font-size: var(--text-caption);
  font-weight: var(--weight-medium);
}

.board-quiet {
  margin: var(--space-4) 0 0;
  font-size: var(--text-body-sm);
  color: var(--color-text-secondary);
}

.board-quiet .board-note {
  display: block;
}

/* The ratio pill (§3.4). `.cny-pill` is the shared, token-driven class; the
   inline .cny-ratio-pill block in timeline.html is left alone in phase 1 —
   converging the live scheduler is phase-4 work. */
.cny-pill {
  display: inline-flex;
  align-items: center;
  gap: var(--space-2);
  padding: var(--space-2) var(--space-3);
  border-radius: var(--radius-pill);
  border: 1px solid var(--color-border);
  font-size: var(--text-body-sm);
  font-variant-numeric: tabular-nums;
  white-space: nowrap;
}

.cny-pill--ok {
  background: var(--color-success-bg);
  border-color: var(--color-success-border);
  color: var(--color-success-fg);
}

.cny-pill--warning {
  background: var(--color-warning-bg);
  border-color: var(--color-warning-border);
  color: var(--color-warning-fg);
}

/* `over` = over the room's OPERATING target but within the KY legal max
   (2026-08-21, David's live review): a warn colour, not red. The heavier
   border is what separates it from `warning` (at target) at a glance; the
   glyph and verdict text carry the difference for everyone else. */
.cny-pill--over {
  background: var(--color-warning-bg);
  border-color: var(--color-warning-fg);
  color: var(--color-warning-fg);
}

/* `crit` = over the KY legal maximum for the room's age band — the one red
   on the board, reserved for a reg-verified legal breach. */
.cny-pill--crit {
  background: var(--color-danger-bg);
  border-color: var(--color-danger-border);
  color: var(--color-danger-fg);
}

/* ⟨S3 verify 8⟩ Tabular figures make every DIGIT the same width; they do not
   make every FIELD the same width, so `7 · 2 · 1:3.5` and `23 · 2 · 1:11.5`
   put their numbers in different places and a reader scanning the column for
   the room that is over target reads a ragged edge.

   The slots are sized in `ch` deliberately: with tabular figures one `ch` is
   exactly one digit, so 2ch is "room for a two-digit count" and 6ch is "room
   for 1:11.5" — a measure of the content, not a spacing value borrowed from
   the scale. Counts right-align into their slot so the units column lines up
   whatever the magnitude. */
.cny-pill [data-pill-field="children"],
.cny-pill [data-pill-field="staff"] {
  min-width: 2ch;
  text-align: right;
}

.cny-pill [data-pill-field="display_label"] {
  min-width: 6ch;
  text-align: right;
}

/* The target chip is the last thing before the GLYPH, so an unslotted `1:4`
   beside a `1:12` moves the ✓/⚠/✕ — the one mark a director scans a column
   for. 4ch is `1:12`, in the chip's own smaller face. */
.cny-pill [data-pill-field="target"] {
  display: inline-block;
  min-width: 4ch;
  text-align: right;
}

.cny-pill__target {
  font-size: var(--text-micro);
  opacity: 0.85;
}

.cny-pill__glyph {
  font-weight: var(--weight-bold);
}


/* --- Command palette (§2.5) ---------------------------------------------- */

.shell-palette[hidden] {
  display: none;
}

.shell-palette {
  position: fixed;
  inset: 0;
  z-index: 50;
  display: flex;
  justify-content: center;
  align-items: flex-start;
  padding-top: 12vh;
}

.shell-palette__backdrop {
  position: absolute;
  inset: 0;
  background: var(--color-surface-inv);
  opacity: 0.45;
}

.shell-palette__dialog {
  position: relative;
  width: min(34rem, calc(100% - var(--space-8)));
  background: var(--color-surface);
  border: 1px solid var(--color-border-strong);
  border-radius: var(--radius-md);
  box-shadow: var(--shadow-lg);
  overflow: hidden;
}

.shell-palette__input {
  width: 100%;
  padding: var(--space-4) var(--space-5);
  border: 0;
  border-bottom: 1px solid var(--color-border);
  font: inherit;
  font-size: var(--text-body-lg);
  color: var(--color-text-primary);
  background: var(--color-surface);
}

.shell-palette__list {
  list-style: none;
  margin: 0;
  padding: var(--space-2);
  max-height: 50vh;
  overflow-y: auto;
}

.shell-palette__option {
  display: flex;
  align-items: center;
  gap: var(--space-3);
  min-height: 44px;
  padding: var(--space-3) var(--space-4);
  border-radius: var(--radius-sm);
  color: var(--color-text-primary);
  text-decoration: none;
  cursor: pointer;
}

.shell-palette__option[aria-selected="true"] {
  background: var(--color-surface-2);
  box-shadow: inset 3px 0 0 0 var(--shell-active-marker);
}

.shell-palette__kind {
  margin-left: auto;
  font-size: var(--text-micro);
  text-transform: uppercase;
  letter-spacing: var(--tracking-wide);
  color: var(--color-text-muted);
}

.shell-palette__empty {
  padding: var(--space-5);
  color: var(--color-text-muted);
}


/* --- Focus (§2.4) -------------------------------------------------------- */

.shell :focus-visible,
.shell-palette :focus-visible {
  outline: 2px solid var(--shell-focus);
  outline-offset: 2px;
}

/* On the dark rail the accent green has too little contrast to read as a ring,
   so the marker amber does the job there. Same 2px/2px geometry. */
.shell-rail :focus-visible {
  outline-color: var(--shell-active-marker);
}


/* --- Responsive (§2.6) ---------------------------------------------------- */

/* Above the phone tier, section D gets its own scroll (S4, David's third item):
   the rooms grid is the page's subject and a long attention feed must not push
   it off the screen. 641px is the complement of the documented 640px phone
   breakpoint — not a new breakpoint, the other side of an existing one, exactly
   as the subgrid rule above uses it.

   `overscroll-behavior: contain` stops a flick at the bottom of the list from
   scrolling the page behind it, which on a trackpad is how an inner scroll
   region starts feeling broken. */
@media (min-width: 641px) {
  .board-attention__scroll {
    max-height: var(--shell-attention-max-height);
    overflow-y: auto;
    overscroll-behavior: contain;
    /* The scrollbar overlays the items' right edge on Windows/Linux, where it
       takes real width. A hair of padding keeps it off the cards. */
    padding-right: var(--space-1);
  }
}

/* 1024px is the DOCUMENTED sidebar-collapse breakpoint (tokens.css). The
   mockup's 860px is deliberately not used: a second undocumented breakpoint
   164px from an existing one produces two rails behaving differently on a
   tablet. */
@media (max-width: 1024px) {
  .shell {
    grid-template-columns: var(--shell-rail-width-collapsed) minmax(0, 1fr);
  }

  .shell-rail__label,
  .shell-rail__user {
    display: none;
  }

  /* The user block (Help, Change Password, Log out) is hidden at this
     width, so an account button at the foot of the rail opens the same
     actions. */
  .shell-rail__compact-account {
    display: block;
  }

  /* Collapsed to icons, a group label would clip: the text goes and the
     <li> becomes a thin divider between groups. The first group needs no
     divider above it. */
  .shell-rail__group-label {
    display: none;
  }

  .shell-rail__group {
    padding: 0;
    margin: var(--space-2) var(--space-3);
    border-top: 1px solid var(--shell-rail-bg-active);
  }

  .shell-rail__group:first-child {
    display: none;
  }

  /* "Canopy" at --text-body is wider than the 52px collapsed rail, so it
     clips. Drop to the micro step rather than to an initial: the wordmark is
     the only thing telling a phone user which app this is. */
  .shell-rail__wordmark {
    padding: 0 var(--space-1) var(--space-4);
    text-align: center;
    font-size: var(--text-micro);
    letter-spacing: var(--tracking-normal);
  }

  .shell-rail__link {
    justify-content: center;
    padding: var(--space-3) 0;
    border-left: 0;
    border-right: 3px solid transparent;
  }

  /* The marker moves to the RIGHT edge so it still touches the content. */
  .shell-rail__link[aria-current="page"] {
    border-right-color: var(--shell-active-marker);
  }
}

/* 640px — the phone tier documented alongside sidebar-collapse in tokens.css
   (David's O5 ruling: plan for phone, desktop stays the design centre). */
@media (max-width: 640px) {
  .shell-content {
    padding: var(--space-5) var(--space-4) var(--space-9);
  }

  .shell-context {
    flex-wrap: wrap;
    row-gap: var(--space-2);
    padding: var(--space-3) var(--space-4);
  }

  .shell-context__spacer {
    display: none;
  }

  /* Nothing may depend on hover at this width, and every interactive target
     stays at the 44px minimum — not just the rail's. */
  .shell-context__school,
  .shell-search {
    min-height: 44px;
  }

  .shell-palette {
    padding-top: var(--space-5);
  }

  /* The board stacks; nothing here depends on hover. */
  .board-columns {
    grid-template-columns: minmax(0, 1fr);
  }

  .board-kpis {
    grid-template-columns: repeat(auto-fit, minmax(7.5rem, 1fr));
  }
}

@media (prefers-reduced-motion: reduce) {
  .shell *,
  .shell-palette * {
    transition-duration: 0ms !important;
    animation-duration: 0ms !important;
  }
}

/* ============================================================================
   FACILITIES WORKLIST (Task 5): /app/facilities.
   Tokens only, same three rules as the Today board block above (no hex, no
   Layer 1 primitive, every --shell-* name defined in tokens.css):
   tests/test_shell_tokens_contract.py is the gate for all three here too.

   The one reduced-motion-safe visual touch David asked for lives in
   `.fac-row__panel` below: a flat `--shell-moment-light` background with no
   transition and no animation keyframe, so the blanket rule immediately
   above has nothing to switch off.
   ========================================================================= */

.fac-head {
  margin-bottom: var(--space-5);
}

.fac-kpis {
  list-style: none;
  margin: 0 0 var(--space-6);
  padding: 0;
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(9rem, 1fr));
  gap: var(--space-3);
}

.fac-kpi {
  display: flex;
  flex-direction: column;
  gap: var(--space-1);
  padding: var(--space-4);
  background: var(--color-surface);
  border: 1px solid var(--color-border);
  border-radius: var(--radius-md);
}

.fac-kpi__value {
  font-size: var(--text-display);
  font-weight: var(--weight-semibold);
  line-height: var(--leading-tight);
}

.fac-kpi__label {
  font-size: var(--text-caption);
  color: var(--color-text-secondary);
}

.fac-toolbar {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  justify-content: space-between;
  gap: var(--space-3);
  margin-bottom: var(--space-4);
}

.fac-tabs {
  display: flex;
  flex-wrap: wrap;
  row-gap: var(--space-2);
  gap: var(--space-2);
}

/* `.shell-content a` (specificity 0,1,1) paints every link
   `--color-text-link`, which beats a bare `.fac-tabs__link` (0,1,0), and in
   the v2 shell `--color-text-link` IS `--color-brand`, the same value the
   active tab uses as its own background, so the active tab rendered brand
   text on a brand background (invisible) and the inactive tab rendered link
   green instead of secondary ink. Scoped under `.shell-content` (0,2,0) so
   these two rules win without an !important. The next `.fac-` author: any
   new `.fac-tabs__link`-shaped rule needs the same `.shell-content` prefix or
   it re-hits this trap. */
.shell-content .fac-tabs__link {
  padding: var(--space-2) var(--space-4);
  border: 1px solid var(--color-border);
  border-radius: var(--radius-pill);
  color: var(--color-text-secondary);
  font-size: var(--text-body-sm);
  text-decoration: none;
}

/* The toolbar's one link that LEAVES the page (Approved vendors, David
   2026-09-11): same pill as the tabs so the row reads as one control group,
   a stronger border so it does not pass for a third status tab. */
.shell-content .fac-tabs__link--away {
  border-color: var(--color-border-strong);
  color: var(--color-text-primary);
  margin-left: var(--space-2);
}

.shell-content .fac-tabs__link--active {
  background: var(--color-brand);
  border-color: var(--color-brand);
  color: var(--color-text-on-brand);
}

/* The single "All schools" / "Only <school>" toggle (David): replaces the
   fac-school-filter <select> -- Facilities now follows the ambient shell
   school switcher, this is the one link that widens or narrows it. Styled
   like `.fac-btn` (a bordered pill), not a bare link, since it is the
   toolbar's other interactive control. */
.fac-school-toggle {
  padding: var(--space-2) var(--space-4);
  background: var(--color-surface);
  color: var(--color-text-primary);
  border: 1px solid var(--color-border-strong);
  border-radius: var(--radius-sm);
  font-size: var(--text-body-sm);
  text-decoration: none;
}

.fac-table {
  border: 1px solid var(--color-border);
  border-radius: var(--radius-md);
  overflow: hidden;
}

.fac-table__head,
.fac-row__summary {
  display: grid;
  grid-template-columns: 8rem 10rem 12rem minmax(0, 1fr) 8rem;
  gap: var(--space-3);
  align-items: center;
  padding: var(--space-3) var(--space-4);
}

.fac-table__head {
  background: var(--color-surface-2);
  color: var(--color-text-secondary);
  font-size: var(--text-body-sm);
  font-weight: var(--weight-semibold);
}

.fac-row {
  border-top: 1px solid var(--color-border);
}

.fac-row__summary {
  cursor: pointer;
  list-style: none;
}

/* Chrome/Safari draw a default disclosure triangle on <summary> even with
   `list-style: none` (a ::marker, not a list bullet), suppressed here so
   the row reads as a table row, not a collapsed list item. */
.fac-row__summary::-webkit-details-marker {
  display: none;
}

.fac-row__summary::marker {
  content: '';
}

.fac-row__cell {
  font-size: var(--text-body-sm);
  color: var(--color-text-primary);
  overflow-wrap: anywhere;
}

.fac-chip {
  display: inline-block;
  margin-right: var(--space-2);
  padding: 0 var(--space-2);
  background: var(--color-brand-subtle);
  color: var(--color-brand);
  border-radius: var(--radius-xs);
  font-size: var(--text-micro);
  font-weight: var(--weight-semibold);
}

.fac-row__panel {
  padding: var(--space-4);
  /* The one reduced-motion-safe touch (David's ruling): the earned moment's
     light, consumed only through this shell-scoped alias (see tokens.css's
     own comment on --shell-moment-light). Flat colour, no transition, no
     keyframe. */
  background: var(--shell-moment-light);
  border-top: 1px solid var(--color-border);
}

.fac-row__note-label {
  display: block;
  margin-bottom: var(--space-1);
  font-size: var(--text-body-sm);
  color: var(--color-text-secondary);
}

.fac-row__note {
  width: 100%;
  padding: var(--space-2) var(--space-3);
  background: var(--color-surface);
  color: var(--color-text-primary);
  border: 1px solid var(--color-border-strong);
  border-radius: var(--radius-sm);
  font-family: var(--font-body);
  resize: vertical;
}

.fac-row__actions {
  display: flex;
  flex-wrap: wrap;
  gap: var(--space-2);
  margin-top: var(--space-3);
}

.fac-btn {
  padding: var(--space-2) var(--space-4);
  background: var(--color-surface);
  color: var(--color-text-primary);
  border: 1px solid var(--color-border-strong);
  border-radius: var(--radius-sm);
  font-size: var(--text-body-sm);
  cursor: pointer;
}

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

.fac-btn--danger {
  background: var(--color-surface);
  color: var(--color-danger-fg);
  border-color: var(--color-danger-border);
}

.fac-row__stamp {
  margin: var(--space-3) 0 0;
  font-size: var(--text-body-sm);
  color: var(--color-text-quiet);
}

/* `done` = quiet: `.cny-pill` carries no neutral variant of its own (only
   ok/warning/over/crit, all live states), so this is a --fac- modifier
   layered on the shared base class rather than a new `.cny-pill--*` this
   page has no authority to add. */
.fac-pill--quiet {
  background: var(--color-surface-2);
  border-color: var(--color-border);
  color: var(--color-text-quiet);
}

/* FF&E section at the foot of the Facilities page (grouped rail,
   2026-09-24): set apart from the worklist above, links in a wrapping row
   so four stacked full-width buttons do not read as more ticket rows. */
.fac-ffe {
  margin-top: var(--space-8);
}

/* Compound selector: `.hub-link-list` is declared further down this file and
   would otherwise win the flex-direction tie. */
.hub-link-list.fac-ffe__links {
  flex-direction: row;
  flex-wrap: wrap;
}

.fac-empty {
  padding: var(--space-5);
  color: var(--color-text-secondary);
  border: 1px solid var(--color-border);
  border-radius: var(--radius-md);
}

/* Punch lists (2026-09-01 spec). The page reuses the facilities worklist's
   own table/row/panel blocks wholesale; only the two things that genuinely
   differ are declared here -- a SIXTH column (the synced Canopy note, which
   the ticket worklist has no equivalent of) and the quieted completed row.
   Tokens only, same rule as every block above. */
.shell-content .fac-punch-meta {
  color: var(--color-text-secondary);
  font-size: var(--text-body-sm);
  margin: var(--space-2) 0 0;
}

.shell-content .fac-table--punch .fac-table__head,
.shell-content .fac-table--punch .fac-row__summary {
  grid-template-columns: 7rem 8rem 10rem minmax(0, 1fr) 8rem 12rem;
}

/* Completed rows stay in the list (the sheet is the record, and an operator
   checks what the contractor closed) but recede: the service already sorts
   them last. */
.shell-content .fac-row--done .fac-row__summary {
  color: var(--color-text-quiet);
}

@media (max-width: 640px) {
  .fac-table__head {
    display: none;
  }

  .fac-table__head,
  .fac-row__summary {
    grid-template-columns: 1fr;
  }

  .shell-content .fac-table--punch .fac-table__head,
  .shell-content .fac-table--punch .fac-row__summary {
    grid-template-columns: 1fr;
  }

  .fac-kpis {
    grid-template-columns: repeat(auto-fit, minmax(7.5rem, 1fr));
  }
}

/* ============================================================================
   HUB SECTIONS (Task 1): Setup / Scheduling Admin / Points.
   Tokens only, same three rules as the blocks above (no hex, no Layer 1
   primitive, every --shell-* name defined in tokens.css):
   tests/test_shell_tokens_contract.py is the gate for all three here too.

   A hub page is a directory, not a feature -- one heading, N sections, each
   section a plain list of links out to an EXISTING route. Reuses
   `.board-section-title` for section headings rather than a new class.
   ========================================================================= */

.hub-head {
  margin-bottom: var(--space-5);
}

.hub-section {
  margin-bottom: var(--space-6);
}

.hub-section:last-child {
  margin-bottom: 0;
}

.hub-link-list {
  list-style: none;
  margin: 0;
  padding: 0;
  display: flex;
  flex-direction: column;
  gap: var(--space-2);
}

/* `.shell-content a` (specificity 0,1,1) paints every link
   `--color-text-link`, which beats a bare `.hub-link` (0,1,0) -- same trap
   `.fac-tabs__link` documents above. Scoped under `.shell-content` (0,2,0)
   so these two rules win without an !important. */
.shell-content .hub-link {
  display: flex;
  align-items: center;
  gap: var(--space-2);
  min-height: 44px;
  padding: var(--space-3) var(--space-4);
  background: var(--color-surface);
  border: 1px solid var(--color-border);
  border-radius: var(--radius-sm);
  color: var(--color-text-primary);
  text-decoration: none;
  font-size: var(--text-body-sm);
}

.shell-content .hub-link:hover {
  border-color: var(--color-border-strong);
  background: var(--color-surface-2);
}

.hub-link__badge {
  margin-left: auto;
  padding: 0 var(--space-2);
  border-radius: var(--radius-pill);
  background: var(--color-danger-solid);
  color: var(--color-text-on-brand);
  font-size: var(--text-micro);
  font-weight: var(--weight-semibold);
  font-variant-numeric: tabular-nums;
}

.hub-note {
  margin: var(--space-2) 0 0;
  font-size: var(--text-caption);
  color: var(--color-text-secondary);
}

.hub-empty {
  padding: var(--space-5);
  color: var(--color-text-secondary);
  border: 1px solid var(--color-border);
  border-radius: var(--radius-md);
}

/* Hub destination cards + stat strip (Points hub, David 2026-09-26). A
   destination and a count must never look alike: cards are raised white
   surfaces with a title, a one-line purpose and "Open ->"; stats are flat,
   tinted, non-navigable cells with a coloured top rule. `.hub-narrow` caps
   the width so three cards do not stretch across a wide monitor. */
.hub-narrow {
  max-width: 64rem;
}

.hub-cards {
  list-style: none;
  margin: 0;
  padding: 0;
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(15rem, 1fr));
  gap: var(--space-4);
}

.hub-card {
  min-width: 0;
  background: var(--color-surface);
  border: 1px solid var(--color-border);
  border-radius: var(--radius-md);
  box-shadow: var(--shadow-sm);
  transition: box-shadow var(--duration-fast) var(--ease-standard),
              transform var(--duration-fast) var(--ease-standard);
}

.hub-card:hover {
  box-shadow: var(--shadow-md);
  transform: translateY(-1px);
}

/* Scoped under `.shell-content` to beat `.shell-content a` (0,1,1), the
   same trap `.hub-link` documents above. */
.shell-content .hub-card__link {
  display: flex;
  flex-direction: column;
  gap: var(--space-2);
  height: 100%;
  min-height: 44px;
  padding: var(--space-5);
  border-radius: inherit;
  color: var(--color-text-primary);
  text-decoration: none;
}

.shell-content .hub-card__link:focus-visible {
  outline: 2px solid var(--color-brand);
  outline-offset: 2px;
}

.hub-card__top {
  display: flex;
  align-items: center;
  justify-content: space-between;
  flex-wrap: wrap;
  gap: var(--space-2) var(--space-3);
}

.hub-card__title {
  font-family: var(--font-display);
  font-size: var(--text-h3);
  font-weight: var(--weight-semibold);
}

.hub-card__pill {
  padding: 2px 8px;
  border-radius: var(--radius-pill);
  background: var(--color-warning-bg);
  color: var(--color-warning-fg);
  font-size: var(--text-caption);
  font-weight: var(--weight-semibold);
  font-variant-numeric: tabular-nums;
  white-space: nowrap;
}

.hub-card__desc {
  font-size: var(--text-body-sm);
  color: var(--color-text-secondary);
}

.hub-card__open {
  margin-top: auto;
  padding-top: var(--space-2);
  font-size: var(--text-caption);
  font-weight: var(--weight-medium);
  color: var(--color-brand);
}

.hub-stats__head {
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  flex-wrap: wrap;
  gap: var(--space-1) var(--space-3);
  margin-bottom: var(--space-3);
}

.hub-stats__title {
  margin: 0 0 var(--space-3);
  font-size: var(--text-micro);
  font-weight: var(--weight-semibold);
  text-transform: uppercase;
  letter-spacing: var(--tracking-wide);
  color: var(--color-text-label);
}

.hub-stats__head .hub-stats__title {
  margin-bottom: 0;
}

.hub-stats__fresh,
.hub-stats__none {
  font-size: var(--text-caption);
  color: var(--color-text-secondary);
}

.hub-stats__none {
  margin: 0;
  font-size: var(--text-body-sm);
}

.hub-stats {
  list-style: none;
  margin: 0;
  padding: 0;
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(9rem, 1fr));
  gap: var(--space-3);
}

.hub-stat {
  display: flex;
  flex-direction: column;
  padding: var(--space-3) var(--space-4);
  border-radius: var(--radius-sm);
  border-top: 3px solid var(--color-border-strong);
  background: var(--color-surface-2);
}

.hub-stat--brand { border-top-color: var(--color-brand); }
.hub-stat--warn { border-top-color: var(--color-warning-solid); }
.hub-stat--danger { border-top-color: var(--color-danger-solid); }

.hub-stat__value {
  font-family: var(--font-display);
  font-size: var(--text-h1);
  font-weight: var(--weight-bold);
  line-height: var(--leading-tight);
  font-variant-numeric: tabular-nums;
}

.hub-stat__label {
  font-size: var(--text-body-sm);
  color: var(--color-text-primary);
}

.hub-stat__sub {
  font-size: var(--text-caption);
  color: var(--color-text-secondary);
  font-variant-numeric: tabular-nums;
}

/* ============================================================================
   HUB REDESIGN (Insights / Setup, 2026-09-02 design approval -- static mock
   reviewed by David): the child-page button row both hubs now render under
   their title (`.hub-tabs`), the Setup hub's per-school readiness checklist
   grid (`.hub-checklist` / `.hub-check-card` / `.hub-chip`), and a shared
   2-column board layout (`.hub-board`) for a main grid + a slim side list.
   Tokens only, same rule as every block above -- no hex, no Layer 1
   primitive reached directly (tests/test_shell_tokens_contract.py).
   ========================================================================= */

.hub-tabs {
  display: flex;
  flex-wrap: wrap;
  gap: var(--space-3);
  margin: 0 0 var(--space-6);
}

.hub-tab {
  display: flex;
  flex-direction: column;
  gap: 2px;
  min-width: 9.5rem;
  min-height: 44px;
  padding: var(--space-3) var(--space-4);
  background: var(--color-surface);
  border: 1px solid var(--color-border);
  border-radius: var(--radius-sm);
  text-decoration: none;
  color: var(--color-text-primary);
  transition: border-color var(--duration-fast) var(--ease-standard),
              background-color var(--duration-fast) var(--ease-standard);
}

.hub-tab:hover {
  border-color: var(--color-border-strong);
  background: var(--color-surface-2);
}

.hub-tab:focus-visible {
  outline: 2px solid var(--color-brand);
  outline-offset: 2px;
}

.hub-tab__label {
  font-size: var(--text-body-sm);
  font-weight: var(--weight-semibold);
}

.hub-tab__caption {
  font-size: var(--text-caption);
  color: var(--color-text-secondary);
}

/* Status chips (readiness checklist rows + the KPI row's "unavailable"
   fallback). Same four-state vocabulary CANOPY-DESIGN §2 already defines
   for status families -- `unknown` has no dedicated family, so it borrows
   the neutral surface/muted-text pair every other quiet chrome state uses. */
.hub-chip {
  display: inline-block;
  padding: 2px 8px;
  border-radius: var(--radius-pill);
  font-size: var(--text-micro);
  font-weight: var(--weight-semibold);
  white-space: nowrap;
}

.hub-chip--done {
  background: var(--color-success-bg);
  color: var(--color-success-fg);
}

.hub-chip--pending {
  background: var(--color-warning-bg);
  color: var(--color-warning-fg);
}

/* F11, 2026-09-03 review: "not started" is an ordinary, expected early-setup
   state (never coexists with `pending` on the same row -- a readiness row is
   done/none/unknown only; `pending` belongs to the ratio-policy row alone),
   not a danger/alarm condition -- warning, not the red family. */
.hub-chip--none {
  background: var(--color-warning-bg);
  color: var(--color-warning-fg);
}

.hub-chip--unknown {
  background: var(--color-surface-2);
  color: var(--color-text-muted);
  border: 1px solid var(--color-border);
}

.hub-checklist {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(15rem, 1fr));
  gap: var(--space-5);
  margin-bottom: var(--space-6);
}

.hub-check-card {
  background: var(--color-surface);
  border: 1px solid var(--color-border);
  border-radius: var(--radius-md);
  box-shadow: var(--shadow-sm);
  padding: var(--space-5);
}

.hub-check-card h3 {
  font-size: var(--text-h3);
  font-weight: var(--weight-semibold);
  margin: 0;
}

.hub-check-card__ratio {
  font-size: var(--text-caption);
  color: var(--color-text-muted);
  margin: 0 0 var(--space-3);
}

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

.hub-check-row {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--space-3);
  padding: 8px 4px;
  min-height: 44px;
  border-bottom: 1px solid var(--color-border);
  text-decoration: none;
  color: var(--color-text-primary);
  font-size: var(--text-body-sm);
}

.hub-check-list li:last-child .hub-check-row {
  border-bottom: none;
}

a.hub-check-row:hover {
  background: var(--color-surface-2);
}

/* CAN-064 fix list item 5: the weekly-hours-budget form (setup_hub.html's
   per-school hub-check-row) -- input + Save button laid out inline, not
   left to each element's own display default. */
.hub-inline-form {
  display: flex;
  align-items: center;
  gap: var(--space-2);
}

.hub-inline-form input[type="number"] {
  width: 6rem;
}

.hub-board {
  display: grid;
  grid-template-columns: minmax(0, 2fr) minmax(0, 1fr);
  gap: var(--space-6);
  align-items: start;
}

@media (max-width: 900px) {
  .hub-board {
    grid-template-columns: 1fr;
  }
}

/* F10, 2026-09-03 review: a grid item's default min-width is `auto` (its
   content's intrinsic width), which lets the checklist grid's own content
   push `.hub-board` wider than its column track rather than wrap -- the
   same fix `.hub-side` already has below. */
.hub-board-main {
  min-width: 0;
}

.hub-side {
  min-width: 0;
}

.hub-side .hub-section:first-child {
  margin-top: 0;
}

/* --- Attendance: absence form, badges, dialog, out-today strip ----------
   David 2026-09-11 (Option D). One form partial rendered on the Attendance
   page and inside the Today board's <dialog>; tokens only. ---------------- */
.absence-form { display: flex; flex-direction: column; gap: var(--space-4); }
.absence-form__field { display: flex; flex-direction: column; gap: var(--space-2); margin: 0; padding: 0; border: 0; min-width: 0; }
/* callouts.js shows and hides the type-driven blocks with the `hidden`
   attribute, but the display rules on __field / __dates / __help outrank the
   browser's own [hidden] rule, so every block rendered at once (Edge, NCNS,
   Leave kind and Pay under every type). Scoped to this form only. */
.absence-form [hidden] { display: none !important; }
.absence-form__label { font-size: var(--text-body-sm); font-weight: var(--weight-medium); color: var(--color-text-primary); }
.absence-form__optional { color: var(--color-text-muted); font-weight: var(--weight-regular); }
.absence-form__help { margin: 0; font-size: var(--text-caption); color: var(--color-text-secondary); }
.absence-form__select,
.absence-form__input {
  min-height: 44px;
  padding: var(--space-2) var(--space-3);
  font: inherit;
  font-size: var(--text-body);
  color: var(--color-text-primary);
  background: var(--color-surface);
  border: 1px solid var(--color-border-strong);
  border-radius: var(--radius-sm);
}
.absence-form__select:focus-visible,
.absence-form__input:focus-visible { outline: none; box-shadow: var(--shadow-focus); border-color: var(--color-border-focus); }
.absence-form__type-row { display: flex; flex-wrap: wrap; gap: var(--space-2); }
.absence-form__type { position: relative; }
.absence-form__type input { position: absolute; opacity: 0; inset: 0; margin: 0; cursor: pointer; }
/* Child combinator, not descendant: the --card variant below nests name,
   meaning and points spans inside this one, and a descendant match turned
   each of them into its own bordered pill. */
.absence-form__type > span {
  display: inline-flex; align-items: center; min-height: 44px;
  padding: var(--space-2) var(--space-4);
  border: 1px solid var(--color-border-strong); border-radius: var(--radius-pill);
  background: var(--color-surface); color: var(--color-text-primary); font-size: var(--text-body-sm);
}
.absence-form__type input:checked + span { background: var(--color-brand); border-color: var(--color-brand); color: var(--color-text-on-brand); }
.absence-form__type input:focus-visible + span { box-shadow: var(--shadow-focus); }

/* Points words PR A: the outcome-grouped Absence-type picker (routes/
   callouts.py::late_or_early_form_context builds absence_type_groups) and
   the Pay-for-this-day radio pair both use the --card row/type modifiers
   below -- a full-width row with a meaning line and a points chip, unlike
   the compact inline pills .absence-form__type-row/.absence-form__type
   alone still render for the Edge (Arrival/Departure) picker above. Muted
   colour cue per group (amber-leaning warning tokens for "draws points",
   success tokens for "no points"), tokens only, no new hex. */
.absence-form__type-group { display: flex; flex-direction: column; gap: var(--space-2); padding: var(--space-3); border-radius: var(--radius-sm); border: 1px solid var(--color-border); }
.absence-form__type-group--draws { background: var(--color-warning-bg); border-color: var(--color-warning-border); }
.absence-form__type-group--none { background: var(--color-success-bg); border-color: var(--color-success-border); }
.absence-form__type-group + .absence-form__type-group { margin-top: var(--space-2); }
.absence-form__type-group-label {
  margin: 0; font-size: var(--text-caption); font-weight: var(--weight-medium);
  text-transform: uppercase; letter-spacing: .04em; color: var(--color-text-secondary);
}
.absence-form__type-group--draws .absence-form__type-group-label { color: var(--color-warning-fg); }
.absence-form__type-group--none .absence-form__type-group-label { color: var(--color-success-fg); }
.absence-form__type-row--card { flex-direction: column; }
.absence-form__type--card { display: block; }
.absence-form__type--card > span {
  display: flex; align-items: center; justify-content: space-between; gap: var(--space-3);
  width: 100%; min-height: 44px; border-radius: var(--radius-sm); text-align: left;
}
/* A light fill, not the pill's solid brand fill: the meaning and points
   lines are secondary-coloured text that would vanish on dark green. */
.absence-form__type--card input:checked + span { background: var(--color-brand-subtle); color: var(--color-text-primary); box-shadow: 0 0 0 2px var(--color-brand) inset; border-color: var(--color-brand); }
.absence-form__type-text { display: flex; flex-direction: column; gap: 2px; min-width: 0; }
.absence-form__type-name { font-weight: var(--weight-medium); }
.absence-form__type-meaning { font-weight: var(--weight-regular); color: var(--color-text-secondary); font-size: var(--text-caption); }
.absence-form__type-points { flex: none; font-size: var(--text-caption); font-weight: var(--weight-medium); white-space: nowrap; color: var(--color-text-secondary); }
.absence-form__check { display: flex; gap: var(--space-3); align-items: flex-start; min-height: 44px; font-size: var(--text-body-sm); }
.absence-form__check input { margin-top: var(--space-2); }
.absence-form__dates { display: grid; grid-template-columns: repeat(auto-fit, minmax(10rem, 1fr)); gap: var(--space-4); }
.absence-form__count {
  margin: 0; padding: var(--space-3) var(--space-4); border-radius: var(--radius-sm);
  font-size: var(--text-body-sm); background: var(--color-success-bg);
  border: 1px solid var(--color-success-border); color: var(--color-success-fg);
}
.absence-form__count--over { background: var(--color-warning-bg); border-color: var(--color-warning-border); color: var(--color-warning-fg); }
.absence-form__actions { display: flex; justify-content: flex-end; gap: var(--space-3); padding-top: var(--space-3); border-top: 1px solid var(--color-border); }
.absence-form__actions .fac-btn { min-height: 44px; }
.absence-form__spacer { flex: 1 1 auto; }
.absence-form__reminder { color: var(--color-warning-fg); }
.absence-form__locked { color: var(--color-text-secondary); }

/* Badges: NCNS = danger; bereavement + other protected leave = neutral strong
   border (the old inline purple hex is gone). Never colour alone: each
   badge carries its word. */
.absence-badge {
  display: inline-flex; align-items: center; gap: var(--space-1);
  padding: var(--space-1) var(--space-2); border-radius: var(--radius-xs);
  font-size: var(--text-micro); font-weight: var(--weight-medium); letter-spacing: var(--tracking-wide);
  text-transform: uppercase; border: 1px solid var(--color-border-strong);
  background: var(--color-surface-2); color: var(--color-text-secondary); white-space: nowrap;
}
.absence-badge--ncns { background: var(--color-danger-bg); border-color: var(--color-danger-border); color: var(--color-danger-fg); }
.absence-badge--bereavement, .absence-badge--protected { background: var(--color-surface); }
.absence-badge--pto { background: var(--color-info-bg); border-color: var(--color-info-border); color: var(--color-info-fg); }
.absence-badge--open { background: var(--color-danger-bg); border-color: var(--color-danger-border); color: var(--color-danger-fg); }
.absence-badge--payroll { background: var(--color-warning-bg); border-color: var(--color-warning-border); color: var(--color-warning-fg); }

/* The board's dialog */
.absence-dialog { border: 0; padding: 0; border-radius: var(--radius-lg); box-shadow: var(--shadow-lg); width: min(36rem, calc(100vw - 2 * var(--space-5))); background: var(--color-surface); color: var(--color-text-primary); }
.absence-dialog::backdrop { background: var(--color-scrim); }
.absence-dialog__head { display: flex; justify-content: space-between; align-items: center; padding: var(--space-4) var(--space-5); border-bottom: 1px solid var(--color-border); }
.absence-dialog__title { margin: 0; font-size: var(--text-h3); font-weight: var(--weight-semibold); }
.absence-dialog__body { padding: var(--space-5); }
.absence-dialog__close { min-height: 44px; min-width: 44px; border: 0; background: transparent; color: var(--color-text-secondary); font-size: var(--text-body-lg); cursor: pointer; }

/* The Today board's "Attendance today" strip (section A2) */
.board-out { margin: 0 0 var(--space-5); }
.board-out__head { display: flex; align-items: baseline; gap: var(--space-3); margin: 0 0 var(--space-3); }
.board-out__title { margin: 0; font-size: var(--text-h3); font-weight: var(--weight-semibold); }
.board-out__row { display: flex; flex-wrap: wrap; gap: var(--space-3); align-items: stretch; }
.board-out__cta { display: inline-flex; align-items: center; justify-content: center; min-height: 44px; padding: var(--button-padding-y) var(--button-padding-x); background: var(--color-brand); color: var(--color-text-on-brand); border: 0; border-radius: var(--radius-sm); font: inherit; font-size: var(--text-body-sm); font-weight: var(--weight-medium); cursor: pointer; flex-shrink: 0; }
.board-out__cta:hover { background: var(--color-brand-hover); }
.board-out__cta:focus-visible { box-shadow: var(--shadow-focus); outline: none; }
.board-out__chip { display: inline-flex; align-items: stretch; gap: 0; min-height: 44px; background: var(--color-surface); border: 1px solid var(--color-border); border-radius: var(--radius-sm); color: inherit; }
.board-out__link { display: inline-flex; flex-direction: column; gap: var(--space-1); padding: var(--space-2) var(--space-3); color: inherit; text-decoration: none; }
.board-out__link:hover { background: var(--color-surface-2); text-decoration: none; }
.board-out__edit { min-width: 44px; border: 0; border-left: 1px solid var(--color-border); background: transparent; color: var(--color-text-secondary); font-size: var(--text-body); cursor: pointer; border-radius: 0 var(--radius-sm) var(--radius-sm) 0; }
.board-out__edit:hover { background: var(--color-surface-2); color: var(--color-text-primary); }
.board-out__edit:focus-visible { box-shadow: var(--shadow-focus); outline: none; }
.board-out__name { font-size: var(--text-body-sm); font-weight: var(--weight-medium); }
.board-out__meta { display: flex; gap: var(--space-2); flex-wrap: wrap; }
.board-out__empty { margin: 0; font-family: var(--font-voice); font-size: var(--text-body); color: var(--color-text-secondary); align-self: center; }
@media (max-width: 640px) {
  .board-out__row { flex-wrap: nowrap; overflow-x: auto; padding-bottom: var(--space-2); scrollbar-width: thin; }
  .board-out__cta { position: sticky; left: 0; z-index: 2; box-shadow: var(--shadow-sm); }
}

/* "Coming up" (next 14 days), nested under the Today board's "Attendance
   today" section -- a compact glance, not a second report. */
.board-coming-up { margin-top: var(--space-3); padding-top: var(--space-3); border-top: 1px solid var(--color-border); }
.board-coming-up__title { margin: 0 0 var(--space-2); font-size: var(--text-body-sm); font-weight: var(--weight-semibold); color: var(--color-text-secondary); text-transform: uppercase; letter-spacing: var(--tracking-wide); }
.board-coming-up__list { list-style: none; margin: 0; padding: 0; display: flex; flex-direction: column; }
.board-coming-up__item + .board-coming-up__item { border-top: 1px solid var(--color-border); }
.board-coming-up__link { display: flex; flex-direction: column; gap: var(--space-1); padding: var(--space-2) 0; color: inherit; text-decoration: none; }
.board-coming-up__link:hover { text-decoration: underline; }
/* Name and date range share ONE row (space-between); badges sit on their
   own row below. Kept separate from `__meta` deliberately -- wrapping the
   date in with the badges left it stranded, left-aligned, on its own line
   at phone width (390px, "Coming up" browser check, 2026-09-24). */
.board-coming-up__row { display: flex; justify-content: space-between; align-items: baseline; gap: var(--space-2); }
.board-coming-up__name { font-weight: var(--weight-medium); }
.board-coming-up__dates { color: var(--color-text-secondary); font-size: var(--text-body-sm); white-space: nowrap; }
.board-coming-up__meta { display: flex; align-items: baseline; gap: var(--space-2); flex-wrap: wrap; }
.board-coming-up__empty { margin: 0; font-family: var(--font-voice); font-size: var(--text-body-sm); color: var(--color-text-secondary); }

/* ============================================================================
   EMPLOYEE PAGE (/people/<email>, version 1, read-only). Reuses .fac-tabs,
   .fac-btn, .fac-kpis, .fac-chip, .hub-empty and .hub-note; these rules only
   lay out the header, the fact list and the plain tables. Tokens only, same
   contract as every block above (tests/test_shell_tokens_contract.py).
   ========================================================================= */

.emp-head {
  display: flex;
  flex-wrap: wrap;
  align-items: flex-start;
  justify-content: space-between;
  gap: var(--space-4);
  margin-bottom: var(--space-5);
}

.emp-head__main {
  min-width: 0;
}

.emp-head__sub,
.emp-head__meta {
  margin: 0 0 var(--space-1);
  font-size: var(--text-body-sm);
  color: var(--color-text-secondary);
}

.emp-head__meta {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: var(--space-3);
  margin-top: var(--space-2);
}

.emp-head__today {
  margin: var(--space-3) 0 0;
  font-weight: var(--weight-semibold);
}

.emp-head__actions {
  display: flex;
  flex-wrap: wrap;
  gap: var(--space-2);
}

/* `.shell-content a` paints links in link colour (0,1,1); these are buttons,
   so the same `.shell-content` prefix the .fac-tabs__link rule documents. */
.shell-content .emp-head__actions .fac-btn,
.shell-content .emp-panel__head .fac-btn {
  color: var(--color-text-primary);
  text-decoration: none;
}

.emp-status {
  display: inline-block;
  padding: 2px 8px;
  border-radius: var(--radius-pill);
  font-size: var(--text-micro);
  font-weight: var(--weight-semibold);
  border: 1px solid var(--color-border);
  background: var(--color-surface-2);
  color: var(--color-text-secondary);
}

.emp-status--active {
  background: var(--color-success-bg);
  color: var(--color-success-fg);
  border-color: var(--color-success-border);
}

.emp-status--out {
  background: var(--color-warning-bg);
  color: var(--color-warning-fg);
  border-color: var(--color-warning-border);
}

.emp-status--separated {
  background: var(--color-danger-bg);
  color: var(--color-danger-fg);
  border-color: var(--color-danger-border);
}

.emp-tabs {
  flex-wrap: wrap;
  margin-bottom: var(--space-5);
}

.emp-panel {
  margin-bottom: var(--space-6);
  padding: var(--space-5);
  background: var(--color-surface);
  border: 1px solid var(--color-border);
  border-radius: var(--radius-md);
}

.emp-panel__head {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  justify-content: space-between;
  gap: var(--space-3);
  margin-bottom: var(--space-3);
}

.emp-panel__title {
  margin: 0 0 var(--space-3);
  font-size: var(--text-h3);
  font-weight: var(--weight-semibold);
}

.emp-panel__head .emp-panel__title {
  margin-bottom: 0;
}

.emp-sub {
  margin: var(--space-5) 0 var(--space-2);
  font-size: var(--text-body);
  font-weight: var(--weight-semibold);
}

.emp-facts {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(14rem, 1fr));
  gap: var(--space-3) var(--space-5);
  margin: 0;
}

.emp-fact dt {
  font-size: var(--text-caption);
  color: var(--color-text-secondary);
}

.emp-fact dd {
  margin: 2px 0 0;
  font-size: var(--text-body-sm);
  overflow-wrap: anywhere;
}

.emp-src,
.emp-note {
  margin-left: var(--space-1);
  font-size: var(--text-micro);
  font-weight: var(--weight-regular);
  color: var(--color-text-quiet);
}

.emp-list {
  margin: 0;
  padding-left: var(--space-5);
  font-size: var(--text-body-sm);
}

.emp-scroll {
  overflow-x: auto;
}

.emp-table {
  width: 100%;
  /* Fixed layout so the two absence tables (and the two week tables) line
     their columns up instead of each sizing to its own content. */
  table-layout: fixed;
  border-collapse: collapse;
  font-size: var(--text-body-sm);
}

.emp-table th,
.emp-table td {
  padding: var(--space-2) var(--space-3);
  border-bottom: 1px solid var(--color-border);
  text-align: left;
  vertical-align: top;
}

.emp-table thead th {
  background: var(--color-surface-2);
  color: var(--color-text-secondary);
  font-weight: var(--weight-semibold);
}

.emp-table tbody th {
  font-weight: var(--weight-semibold);
}

.emp-muted {
  color: var(--color-text-quiet);
}

.emp-kpis {
  margin-bottom: var(--space-4);
}

.emp-kpi__text {
  font-size: var(--text-body);
}

/* Locations tab: days at each school per month (David, 2026-09-24). One
   stacked bar per month, one colour per school from the chart aliases
   (tokens.css --cat-*), identical for every employee. */
.emp-legend {
  display: flex;
  flex-wrap: wrap;
  gap: var(--space-2) var(--space-4);
  margin: 0 0 var(--space-3);
  padding: 0;
  list-style: none;
  font-size: var(--text-caption);
  color: var(--color-text-secondary);
}

.emp-swatch {
  display: inline-block;
  width: 10px;
  height: 10px;
  margin-right: var(--space-2);
  border-radius: var(--radius-xs);
  vertical-align: -1px;
}

.emp-school--0 { background: var(--cat-staffing); }
.emp-school--1 { background: var(--cat-financial); }
.emp-school--2 { background: var(--cat-compliance); }
.emp-school--3 { background: var(--cat-enrollment); }

.emp-days {
  width: 100%;
  border-collapse: collapse;
  font-size: var(--text-body-sm);
  font-variant-numeric: tabular-nums;
}

.emp-days th,
.emp-days td {
  padding: var(--space-1) 0;
  vertical-align: middle;
}

/* `.emp-days` prefix: the `.emp-days th/td` padding rule above is 0,1,1 and
   would beat a bare class. `width: 1%` + nowrap shrinks the label columns to
   their text so the bar takes the rest, at any width. */
.emp-days .emp-days__month {
  width: 1%;
  padding-right: var(--space-3);
  text-align: left;
  font-weight: var(--weight-regular);
  color: var(--color-text-secondary);
  white-space: nowrap;
}

.emp-days__bar {
  display: flex;
  height: 12px;
  overflow: hidden;
  border-radius: var(--radius-xs);
  background: var(--color-surface-2);
}

.emp-days__bar span {
  display: block;
  height: 100%;
}

.emp-days .emp-days__count {
  width: 1%;
  padding-left: var(--space-3);
  text-align: right;
  white-space: nowrap;
  color: var(--color-text-secondary);
}

/* Doors into the Employee page from the points screens (David, 2026-09-24:
   the name links were styled `color: inherit` and nobody could see them).
   `.shell-content` prefix for the same 0,1,1 reason the .fac-tabs__link rule
   documents; the points pages also load Bootstrap, whose `a` rule this
   must outrank. */
.shell-content .emp-name-link {
  color: var(--color-text-link);
  text-decoration: none;
}

.shell-content .emp-name-link:hover,
.shell-content .emp-name-link:focus-visible {
  color: var(--color-text-link-hover);
  text-decoration: underline;
}

/* The points profile breadcrumb: same look as its "Points roster" crumb. */
.shell-content .emp-crumb-link {
  color: var(--color-brand-accent);
  text-decoration: underline;
}

/* Same look as the row's existing "View profile" link beside it (brand
   accent, semibold, underlined), so the two read as a pair of actions. */
.shell-content .emp-row-link {
  display: inline-block;
  margin-left: var(--space-3);
  color: var(--color-brand-accent);
  font-size: var(--text-body-sm);
  font-weight: var(--weight-semibold);
  text-decoration: underline;
  white-space: nowrap;
}

/* The profile's header button: a .fac-btn drawn as a link, sized to sit in
   the Bootstrap btn-sm row beside "+ Record event". */
.shell-content .emp-page-btn {
  display: inline-block;
  color: var(--color-text-primary);
  font-weight: var(--weight-semibold);
  text-decoration: none;
  line-height: 1.5;
}

.shell-content .emp-page-btn:hover {
  background: var(--color-surface-2);
  color: var(--color-text-primary);
}

/* ============================================================================
   INSIGHTS TILE HUB (David 2026-09-25, option D): the Insights hub is the
   summary landing page. A subtitle under the title, an attention strip
   (reuses `.board-attention-list` / `.board-attention-item--crit|--warn`),
   "This week" primary tiles -- one whole-tile link per Executive Summary
   page, filled by static/js/insights_hub.js -- then the existing
   `.hub-tabs` row as smaller "More insights" tiles and the org-wide
   `.board-kpis` row. Tile shape follows CANOPY-DESIGN 4.1 (KPI card as a
   drill-in): surface, hairline border, --radius-md, --shadow-sm, lifting to
   --shadow-md on hover. Tokens only.
   ========================================================================= */

.hub-subtitle {
  margin: 0;
  font-size: var(--text-body-sm);
  color: var(--color-text-secondary);
}

/* The tiles section is the wrapper's last child, so `.hub-section:last-child`
   zeroes its margin; the wrapper restores the gap before "More insights". */
.insights-hub {
  margin-bottom: var(--space-6);
}

.hub-attention {
  margin: 0 0 var(--space-6);
}

.hub-attention[hidden] {
  display: none;
}

/* CANOPY-DESIGN 4.2: the strip reads across, critical first, rather than
   stacking three full-width cards above the tiles. */
.hub-attention .board-attention-list {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(15rem, 1fr));
}

.hub-attention .board-attention__verb {
  min-height: 32px;
}

.hub-tiles {
  list-style: none;
  margin: 0;
  padding: 0;
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(11rem, 1fr));
  gap: var(--space-4);
}

.hub-tile {
  min-width: 0;
  background: var(--color-surface);
  border: 1px solid var(--color-border);
  border-radius: var(--radius-md);
  box-shadow: var(--shadow-sm);
  transition: box-shadow var(--duration-fast) var(--ease-standard),
              transform var(--duration-fast) var(--ease-standard);
}

.hub-tile:hover {
  box-shadow: var(--shadow-md);
  transform: translateY(-1px);
}

/* `.shell-content a` paints every link green; the whole tile is the link, so
   it keeps its own text colours and the affordance is hover + "Open". */
.shell-content .hub-tile__link {
  display: flex;
  flex-direction: column;
  gap: var(--space-2);
  height: 100%;
  min-height: 44px;
  padding: var(--space-5);
  border-radius: inherit;
  color: var(--color-text-primary);
  text-decoration: none;
}

.shell-content .hub-tile__link:focus-visible {
  outline: 2px solid var(--color-brand);
  outline-offset: 2px;
}

.hub-tile__title {
  font-size: var(--text-micro);
  font-weight: var(--weight-semibold);
  text-transform: uppercase;
  letter-spacing: var(--tracking-wide);
  color: var(--color-text-label);
}

.hub-tile__value {
  font-family: var(--font-display);
  font-size: var(--text-display);
  font-weight: var(--weight-bold);
  letter-spacing: var(--tracking-tight);
  line-height: var(--leading-tight);
  font-variant-numeric: tabular-nums;
}

/* "Not available" is a statement, not a numeral: body type, muted. */
.hub-tile__value--na {
  font-family: var(--font-body);
  font-size: var(--text-body);
  font-weight: var(--weight-medium);
  letter-spacing: var(--tracking-normal);
  color: var(--color-text-muted);
}

.hub-tile__compare {
  font-size: var(--text-caption);
  color: var(--color-text-secondary);
}

.hub-tile__compare:empty {
  display: none;
}

.hub-tile__foot {
  display: flex;
  align-items: center;
  flex-wrap: wrap;
  gap: var(--space-3);
  margin-top: auto;
  padding-top: var(--space-2);
}

.hub-tile__pill {
  display: inline-block;
  padding: 2px 8px;
  border-radius: var(--radius-pill);
  font-size: var(--text-caption);
  font-weight: var(--weight-medium);
  white-space: nowrap;
  background: var(--color-surface-2);
  color: var(--color-text-secondary);
}

.hub-tile__pill[hidden] {
  display: none;
}

.hub-tile__pill--good {
  background: var(--color-success-bg);
  color: var(--color-success-fg);
}

.hub-tile__pill--warn {
  background: var(--color-warning-bg);
  color: var(--color-warning-fg);
}

.hub-tile__pill--bad {
  background: var(--color-danger-bg);
  color: var(--color-danger-fg);
}

.hub-tile__open {
  margin-left: auto;
  font-size: var(--text-body-sm);
  font-weight: var(--weight-semibold);
  color: var(--color-text-link);
}

.shell-content .hub-tile__link:hover .hub-tile__open {
  text-decoration: underline;
}

/* "More insights": the same `.hub-tab` buttons, one step quieter than the
   primary tiles above them. */
.hub-tabs--secondary {
  margin-bottom: 0;
}

@media (max-width: 640px) {
  .hub-tiles {
    grid-template-columns: minmax(0, 1fr);
    gap: var(--space-3);
  }

  .shell-content .hub-tile__link {
    padding: var(--space-4);
  }

  .hub-tabs--secondary .hub-tab {
    flex: 1 1 9.5rem;
  }

  .hub-attention .board-attention__verb {
    min-height: 44px;
  }
}

@media (prefers-reduced-motion: reduce) {
  .hub-tile:hover {
    transform: none;
  }
}
/* Employee page v2: training-hours entry on the Training hours tab (moved
   from the points profile). Field and input sizes follow .absence-form so
   the two forms on this page read alike; 44px targets for a phone. */
.emp-train-form {
  display: flex;
  flex-wrap: wrap;
  align-items: flex-end;
  gap: var(--space-3);
  margin: 0 0 var(--space-3);
}
.emp-train-field {
  display: flex;
  flex-direction: column;
  gap: var(--space-1);
  margin: 0;
  font-size: var(--text-body-sm);
  font-weight: var(--weight-medium);
  color: var(--color-text-primary);
}
.emp-train-field--hours { width: 7rem; }
.emp-train-field--grow { flex: 1 1 14rem; min-width: 0; }
.emp-input {
  min-height: 44px;
  padding: var(--space-2) var(--space-3);
  font: inherit;
  font-size: var(--text-body);
  color: var(--color-text-primary);
  background: var(--color-surface);
  border: 1px solid var(--color-border-strong);
  border-radius: var(--radius-sm);
}
.emp-input:focus-visible { outline: none; box-shadow: var(--shadow-focus); border-color: var(--color-border-focus); }
.emp-train-form .fac-btn { min-height: 44px; }
.emp-inline-form { display: inline; margin: 0; }
.emp-table .emp-table__action { text-align: right; white-space: nowrap; }
/* Five columns with a button do not fit a phone at fixed layout; the table
   keeps a readable width and scrolls inside its .emp-scroll wrapper. */
.emp-table--training { min-width: 34rem; }

/* The Attendance points tab holds the points profile partial, which draws
   its own card; inside the tab panel it sits flush. */
.emp-panel .emp-points-embed .card { border: 0; box-shadow: none; }
.emp-panel .emp-points-embed .card-body { padding: 0; }
