/* Scryos entry app — THE INSTRUMENT, operator side (WS1, 2026-08-11).
   ------------------------------------------------------------------
   The dashboard got the instrument pass on 2026-08-11 and this app did
   not, so for one day the product had two design systems: reticled
   hairline panels and mono-first density over there, rounded cards and
   sans over here. This sheet closes that fork. It is the SAME move the
   dashboard made — the class names are untouched, so not one renderer
   changes; only the values under them do.

   ONE TOKEN AUTHORITY. /dashboard/tokens.css (loaded first by
   index.html, and already on the served allowlist) owns every :root in
   the product. This sheet declares NONE — the old alias block
   (--paper/--wash/--navy/--teal/--danger/--focus…) is gone, and the
   rules below name the shared tokens directly. That is the "honest
   renaming" the 2026-08-09 collapse promised: two names for one colour
   is how a second design system starts. Guards test_p4 (no :root here)
   and test_p4b (every var() names a real token) hold it.

   DARK MODE ARRIVES WITH THE TOKENS. Nothing below is theme-aware on
   its own; every surface, rule and state colour is a token, so both
   themes fall out of tokens.css. The explicit `data-theme` stamp beats
   the OS default in both directions — theme.js does the stamping.

   TWO DELIBERATE DIFFERENCES FROM THE DASHBOARD, kept on purpose:
   1. This app stays TOP-NAV. Staff read it on a phone, mid-morning,
      standing up; a 210px left rail is furniture they cannot afford.
   2. FORM INPUTS STAY BIG. 44px targets, roomy padding, one column at
      375px. Density is for reading, not for typing — the dashboard's
      12.5px rows would be a data-entry accident here.

   TYPE (D1.1, the recommendation applied): mono is the voice for
   VALUES and metadata — inputs, tables, badges, buttons, chrome — and
   sans carries QUESTION TEXT and prose, because legibility while
   typing beats aesthetic purity. Clash Display survives in the
   wordmark only.

   Unchanged commitments: 44px touch targets, visible focus, no
   horizontal scroll at 375px, one reduced-motion standdown. */

/* Brand typography (unification packet decision 3, 2026-09-11): Geist Sans
   is the dashboard/settings header face. Vendored at the one shared brand
   door (frontend/public/dashboard-brand/, served org-independently by
   server/kpi/next_settings.py for every app on this origin - see that
   module's BRAND_FILES/is_brand handling - so no api.py allowlist row is
   needed here). Scoped to the header/nav chrome only, matching the packet's
   "do not restyle page bodies" boundary; the mono-first body voice this
   file's own header documents (WS1, D1.1) is untouched. */
@font-face {
  font-family: "ScryOS Geist";
  src: url("/dashboard-brand/Geist-Sans.woff2") format("woff2");
  font-style: normal;
  font-weight: 400;
  font-display: swap;
}

* { box-sizing: border-box; }

html, body {
  margin: 0;
  padding: 0;
  background: var(--bg);
  color: var(--ink);
  /* mono first — this one line is most of the character change */
  font: 13px/1.5 var(--mono);
  font-variant-numeric: tabular-nums;
}
/* prose and question text are the exception, not the rule */
p, legend, .field label, .note, .muted, .who-role, .lead-why,
.setup-conn-steps li, .login-legal, .warn-list li,
.dialog-body p, .conn-sub, .conn-meta { font-family: var(--sans); }

/* ---- type: the display face leads a page, the mono bands a panel. */
h1, h2, h3 {
  font-family: var(--disp);
  font-weight: 500;
  letter-spacing: -0.01em;
  color: var(--ink);
}
h1 { font-size: 1.45rem; margin: 0 0 4px; }
h2 { font-size: 1.02rem; margin: 0; }
h3 { font-size: 0.95rem; margin: 14px 0 6px; }
/* the eyebrow names the TAB a page lives under (dashboard's .lg-eyebrow,
   ported as a local rule — same tokens, no new authority: --mono/--blue).
   Every page's h1 gets one, sourced from navRouteTable via ui.js's
   eyebrow() helper rather than a second hardcoded copy of the nav label. */
.eyebrow {
  font-family: var(--mono);
  font-size: 11px;
  font-weight: 600;
  color: var(--blue);
  letter-spacing: 0.12em;
  text-transform: uppercase;
  margin: 0 0 4px;
}
#brand {
  font-family: "ScryOS Geist", var(--disp);
  font-weight: 500;
  letter-spacing: -0.01em;
}

/* ---- chrome: the dev strip is a terminal, dark in BOTH themes ---- */
#dev-banner {
  /* --term-* rather than --ai: the terminal accents are the ones that
     hold their value across BOTH themes, and this strip is dark in
     both. --ai is tuned for readability on a white card, which is the
     opposite surface. */
  background: var(--topbar);
  color: var(--term-warn);
  text-align: center;
  font-family: var(--mono);
  font-size: 10.5px;
  letter-spacing: 0.06em;
  padding: 6px 12px;
  position: sticky;
  top: 0;
  z-index: 30;
}

#topbar {
  position: relative;
  /* flat opaque bar (no glass sheen) — same value tokens.css gives
     var(--glass-bg)/var(--glass-border), named honestly here. */
  background: var(--bg);
  color: var(--ink);
  border-bottom: 1px solid var(--line);
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 2px 8px;
  padding: 8px 16px;
}
.tb-row { display: flex; align-items: center; justify-content: space-between;
  width: 100%; gap: 8px; }
#brand { display: inline-flex; align-items: center; gap: 8px;
  color: var(--ink); font-size: 1.02rem; margin-right: auto; }
/* Scryos brand (kit v1.1): the header artwork IS the brand text; never
   below the kit's 120px wordmark floor. wm-light/wm-dark mirror the
   tokens.css theme rules - stamp wins, OS preference is the default. */
.brand-wordmark { width: 120px; height: auto; display: block; }
.wm-dark { display: none; }
html[data-theme="dark"] .wm-light { display: none; }
html[data-theme="dark"] .wm-dark { display: block; }
@media (prefers-color-scheme: dark) {
  html:not([data-theme="light"]) .wm-light { display: none; }
  html:not([data-theme="light"]) .wm-dark { display: block; }
}

/* the theme toggle sits in the top bar, the same seat the dashboard
   gives it (D1.2) — one product, one place to change the light */
/* a verb button - takes the capsule radius every other action control
   in the app uses, not the sharp page-plane one (Control Kit grammar). */
#themeBtn {
  background: transparent; cursor: pointer;
  border: 1px solid var(--line); border-radius: var(--r-cmd);
  color: var(--muted); font: 600 9.5px var(--mono); letter-spacing: 0.1em;
  min-height: 30px; padding: 2px 9px;
}
#themeBtn:hover { border-color: var(--ink); color: var(--ink); }

/* the nav is a control strip, not a website menu.
   VISUAL-QA-2026-08-19 finding #3: at mobile widths this bar is 1204px
   of buttons in a ~343px container (861px overhang, 7 of 12
   destinations off-screen) with the scrollbar explicitly hidden below
   and zero other cue that it scrolls — it reads as "the app only has
   four destinations." .scroll-x (tokens.css) supplies the shared fix:
   a visible thin scrollbar plus an edge-fade. It re-points
   --scroll-x-fade at the topbar's own background (not the flat card
   color .scroll-x defaults to) so the fade matches the bar it's
   actually sitting in — see tokens.css's SCROLL-X AFFORDANCE block for
   why that's the one override call sites make. */
#mainnav { display: flex; gap: 4px; width: 100%; flex-wrap: nowrap;
  padding-bottom: 8px; --scroll-x-fade: var(--bg); }
/* Control Kit grammar: "a capsule does something, an underline takes
   you somewhere" - navigation is an underline row, not a filled chip.
   Every .nav-btn shares the same height/padding/line-height so the
   underlines all land on one baseline regardless of which is active. */
.nav-btn {
  background: transparent;
  color: var(--muted);
  border: none;
  border-bottom: 2px solid transparent;
  border-radius: 0;
  min-height: 40px;
  padding: 0 11px;
  font: 600 10.5px var(--mono);
  letter-spacing: 0.08em;
  text-transform: uppercase;
  white-space: nowrap;
  flex: none;
  cursor: pointer;
  text-decoration: none;
  display: inline-flex;
  align-items: center;
}
.nav-btn:hover { color: var(--ink); }
/* the active page is the underline, not a filled block */
.nav-btn[aria-current="page"] {
  color: var(--ink);
  border-bottom-color: var(--blue);
}
.nav-dash { color: var(--blue); }
.nav-support { margin-left: auto; color: var(--muted); }

/* ---- subtab row (owner-approved nav rebuild, 2026-08-27) ----
   The second, lighter header row: the active tab's own subtabs (My Day
   > Today/My Performance, Team > People/HR/Hiring, etc.), or hidden
   entirely when the active tab has fewer than two visible subtabs -
   app.js's renderSubnav() decides that, this file only styles what it
   draws. Same horizontal-scroll affordance as #mainnav (VISUAL-QA-
   2026-08-19 finding #3, see that rule's own comment above) so a role
   with many visible subtabs scrolls rather than wraps into chaos at
   mobile widths - .scroll-x is shared, not reinvented. Deliberately
   QUIETER than the primary tabs: smaller type, no letter-spacing, no
   uppercase, so the two rows read as parent/child rather than two
   competing strips of equal weight. */
#subnav { display: flex; gap: 4px; width: 100%; flex-wrap: nowrap;
  padding-bottom: 6px; margin-top: -2px; --scroll-x-fade: var(--bg); }
#subnav[hidden] { display: none; }
/* the child row: same underline grammar, one size down, no colour on
   the underline itself so parent/child stay visually distinct. */
.nav-subbtn {
  background: transparent;
  color: var(--muted);
  border: none;
  border-bottom: 2px solid transparent;
  border-radius: 0;
  min-height: 32px;
  padding: 0 10px;
  font: 500 9.5px var(--mono);
  letter-spacing: 0.02em;
  white-space: nowrap;
  flex: none;
  cursor: pointer;
  text-decoration: none;
  display: inline-flex;
  align-items: center;
}
.nav-subbtn:hover { color: var(--ink); }
.nav-subbtn[aria-current="page"] {
  color: var(--ink);
  border-bottom-color: var(--line);
}

#menuBtn { width: 34px; height: 34px; border-radius: 50%;
  border: 1px solid var(--line); background: var(--card);
  color: var(--ink); font: 600 13px var(--mono); cursor: pointer;
  flex: none; }
#menuBtn:hover { border-color: var(--ink); }
/* a floating menu keeps its edge and takes the sheet radius, same as
   .dialog (round-5 doctrine). */
#menu { position: absolute; right: 10px; top: 54px; z-index: 40;
  background: var(--card); color: var(--ink); border-radius: var(--r-sheet);
  border: 1px solid var(--line);
  min-width: 220px; padding: 8px; }
#whoami { display: block; font-size: 0.95rem; }
.who-name { margin: 6px 10px 0; font-weight: 600; color: var(--ink); }
.who-role { margin: 0 10px 6px; color: var(--muted); font-size: 0.8rem;
  letter-spacing: 0.08em; text-transform: uppercase; }
.menu-item { display: block; width: 100%; box-sizing: border-box;
  text-align: left; background: none; border: none; border-top: 1px solid
  var(--line); color: var(--ink); font: 12.5px var(--mono); padding: 12px 10px;
  min-height: 44px; cursor: pointer; text-decoration: none; }
.menu-item:hover { background: var(--bg); }

/* UI round 4 patch batch, slice 2 (2026-09-03, owner ruling 08:00 ET,
   "layout law for EVERY page"): the centered ~760px column is gone.
   #main now spans the viewport at the dashboard's own grid gutters
   (dashboard-app/ledger.css's `.content` rule: max-width 1380px, 36px
   gutters, 14px under 767px - read verbatim, not re-invented) so every
   entry-app page stops wasting the right side of a wide monitor even
   before it adopts shared-app/frame.js. A page that DOES adopt the
   frame (Connections, this slice) renders its own .sf-frame inside
   #main and gets the identical width from .sf-content instead - the
   two numbers below and shared-app/frame.css's are the same on
   purpose, so nesting one inside the other never doubles the gutter. */
#main {
  max-width: 1380px;
  margin: 0 auto;
  padding: 14px 36px 60px;
}
@media (max-width: 767px) {
  #main { padding-left: 14px; padding-right: 14px; }
}
/* Page/route transitions (N4, 2026-08-18, motion foundation). Every
   render function clears #main and appends fresh elements (see
   entry-app/ui.js's clear() and the render* functions in myday.js,
   training.js, etc.) - each route is therefore genuinely NEW DOM, so a
   plain CSS animation on freshly-inserted direct children runs exactly
   once per navigation without any JS restart trick, the same mechanism
   dashboard-app/port.css's `.panel { animation: rt-fade-up ... }`
   already relies on for its own page transitions. Capped stagger, sub-
   200ms base duration (--dur is 220ms - close enough that "base" reads
   as the same family as the dashboard's own page-arrival feel; the two
   surfaces are meant to feel like one product, not two motion systems). */
#main > * { animation: rt-note-enter var(--dur) var(--ease) backwards; }
#main > *:nth-child(2) { animation-delay: 40ms; }
#main > *:nth-child(n+3) { animation-delay: 70ms; }

/* ---- shared text + layout helpers ---- */
.page-head {
  display: flex;
  flex-wrap: wrap;
  align-items: flex-start;
  justify-content: space-between;
  gap: 10px;
  margin: 8px 0 12px;
}
.sched-controls { display: flex; gap: 10px; flex-wrap: wrap; }
.muted { color: var(--muted); }
.small { font-size: 0.85rem; }
.muted.small { margin: 4px 0; }
.note { color: var(--muted); font-size: 0.88rem; margin: 6px 0; }
.warn { color: var(--warn); font-size: 0.88rem; margin: 6px 0; }
.stack { display: flex; flex-direction: column; gap: 16px; }
.sr-only {
  position: absolute;
  width: 1px;
  height: 1px;
  margin: -1px;
  padding: 0;
  overflow: hidden;
  clip: rect(0 0 0 0);
  white-space: nowrap;
  border: 0;
}

/* ---- cards: flat hairline panels with corner reticles (WS1) ----
   The 12px rounded card was the single most "web app" thing on this
   screen; the bracket marks replace it and read as targeting — the
   same swap port.css made for .panel, on the class the entry
   renderers already build.

   Frames stay OPT-IN carriers: a border is information, and
   information that is everywhere is decoration. .bz-act asks for a
   ruling, .bz-ai is model-written, .bz-warn attention, .bz-bad
   critical. The carrier values are TOKENS now, matching the
   dashboard's own .panel.bz-* overrides. */
.bz-act{--bz:var(--blue)}
.bz-ai{--bz:var(--ai)}
.bz-warn{--bz:var(--warn)}
.bz-bad{--bz:var(--bad)}

.cards { display: flex; flex-direction: column; gap: 14px; }
.card {
  /* UNBOXING (round 5): content on the page plane carries no
     border/radius/shadow. The card fill is a quiet surface tone, not a
     frame - .cards' own gap is what separates one card from the next. */
  background: var(--card);
  border: none;
  border-radius: var(--r);
  padding: 14px;
  position: relative;
}
/* carriers AFTER the quiet defaults - at equal specificity the frame
   must win the cascade over .card/.group. A full border reads as a box;
   semantic colour lives on the left edge instead (round-5 unboxing law),
   the same move .ai-draft already made below. */
.bz-frame { border-left: 3px solid var(--bz); padding-left: 10px; }
/* a row-level functional marker (e.g. an AI-drafted action awaiting a
   human ruling): the frame color as a left rule, not a full border */
.ai-draft { border-left: 3px solid var(--bz); padding-left: 10px; }
.card-head {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  justify-content: space-between;
  gap: 8px;
  margin-bottom: 8px;
}
/* a disclosure summary standing in for a card's h2 (myperf.js's
   collapsed-by-default methodology card) reads at the same weight */
.card > summary {
  font-family: var(--disp);
  font-weight: 500;
  font-size: 1.02rem;
  color: var(--ink);
  cursor: pointer;
}
.card > summary + * { margin-top: 8px; }

/* ---- badges: a state readout, not a badge. Squared off, mono,
   uppercase — and every colour is a semantic token, so red/amber/
   green mean the same thing here as they do on the dashboard. */
.badges {
  display: flex;
  flex-wrap: wrap;
  gap: 6px;
  align-items: center;
}
.badge {
  display: inline-block;
  font-family: var(--mono);
  font-size: 9.5px;
  font-weight: 600;
  letter-spacing: 0.1em;
  text-transform: uppercase;
  border-radius: var(--r-cmd);
  padding: 3px 8px;
  border: 1px solid var(--line);
  background: var(--chip);
  color: var(--muted);
  white-space: nowrap;
}
/* Draft is NOT a colour any more: an unfinished thing has no state to
   report yet, and giving it the accent put "not started" and "handed
   in" at the same visual weight. Progression now reads
   quiet -> accent -> good. */
.badge-draft     { background: var(--chip); color: var(--muted);
                   border-color: var(--line); }
.badge-submitted { background: var(--blue-bg); color: var(--blue);
                   border-color: var(--blue); }
.badge-reviewed  { background: var(--good-bg); color: var(--good);
                   border-color: var(--good); }
.badge-voided    { background: var(--bad-bg); color: var(--bad);
                   border-color: var(--bad); }
.badge-late      { background: var(--warn-bg); color: var(--warn);
                   border-color: var(--warn); }
/* Proxy is metadata about AUTHORSHIP, not a state, so it takes no
   semantic hue - an ink outline, which reads as "noted" rather than
   as good/bad. (The old violet was borrowed from nothing.) */
.badge-proxy     { background: var(--chip); color: var(--ink);
                   border-color: var(--ink); }
.badge-hyg       { background: var(--blue-bg); color: var(--blue);
                   border-color: var(--blue); }
.badge-closed, .badge-pto, .badge-off
                 { background: var(--chip); color: var(--faint);
                   border-color: var(--line); }
.badge-todo      { background: var(--card); color: var(--ink);
                   border-color: var(--ink); }
.badge-open      { background: var(--blue-bg); color: var(--blue);
                   border-color: var(--blue); }
/* Bug reports (099): status is declared vocabulary (open/acked/closed)
   but nothing writes acked/closed yet - the colour still needs to
   exist for the read screen's chip, between open (blue) and closed
   (already the faint/chip look above) rather than sharing either. */
.badge-acked     { background: var(--warn-bg); color: var(--warn);
                   border-color: var(--warn); }
.badge-loc       { background: var(--ink); color: var(--bg);
                   border-color: var(--ink); }
.badge-connected { background: var(--good-bg); color: var(--good);
                   border-color: var(--good); }
.badge-ok        { background: var(--good-bg); color: var(--good);
                   border-color: var(--good); }
.badge-warn      { background: var(--warn-bg); color: var(--warn);
                   border-color: var(--warn); }
.badge-pipe-off  { background: var(--chip); color: var(--faint); }
.badge-pipe-bad  { background: var(--bad-bg); color: var(--bad);
                   border-color: var(--bad); }

/* Competency provenance (N9, 092's owner-attestation ruling made
   visible): `clinical.PROVENANCE`'s `class` field, one badge colour per
   class, so "earned" and "attested" never share a look. This is the
   ruling's other half — a report reader must always be able to tell an
   attested level from an earned one — landing as CSS. */
.badge-earned    { background: var(--good-bg); color: var(--good);
                   border-color: var(--good); }
.badge-attested  { background: var(--blue-bg); color: var(--blue);
                   border-color: var(--blue); }
.badge-corrected { background: var(--bad-bg); color: var(--bad);
                   border-color: var(--bad); }
.badge-legacy, .badge-carried, .badge-unknown, .badge-untracked
                 { background: var(--chip); color: var(--muted);
                   border-color: var(--line); }
/* Costing lane chips (123): an estimate must never render
   indistinguishable from an actual, so the three lanes carry three
   distinct chips - typed (neutral), P&L/QBO (blue, a pipe's figure),
   estimated (warn tone: a derived number, not a recorded one). */
.badge-lane-manual    { background: var(--chip); color: var(--ink);
                        border-color: var(--line); }
.badge-lane-pnl       { background: var(--blue-bg); color: var(--blue);
                        border-color: var(--blue); }
.badge-lane-estimated { background: var(--warn-bg); color: var(--warn);
                        border-color: var(--warn); }

/* ---- rows (My Day, review queue) ----
   Round 5: the rule between rows is gone - spacing carries the
   separation now, not a border. */
.form-row {
  padding: 14px 0;
}
.form-row:last-child { padding-bottom: 0; }
.form-row-main {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  justify-content: space-between;
  gap: 8px;
}
.form-name { font-weight: 600; }
.queue-row .form-name { font-size: 0.95rem; }
.row-actions {
  display: flex;
  flex-wrap: wrap;
  gap: 8px;
  margin-top: 8px;
}

/* ---- buttons: controls, not web furniture. Touch targets are
   UNCHANGED at 40/44px - this is a phone app and the instrument pass
   is a font/colour/corner change here, never a shrink. */
.btn {
  /* the default/secondary voice: transparent pill, hairline border */
  min-height: 40px;
  padding: 0 14px;
  border-radius: var(--r-cmd);
  border: 1px solid var(--line);
  background: transparent;
  color: var(--ink);
  font: 600 10.5px var(--mono);
  letter-spacing: 0.06em;
  text-transform: uppercase;
  cursor: pointer;
}
/* Control Kit grammar (round 5): a verb capsule's primary voice is
   iris, not ink - matches the dashboard's own filled .hbtn. */
.btn-primary { background: var(--blue); border-color: var(--blue);
               color: var(--card); }
.btn-danger  { background: transparent; border-color: var(--bad);
               color: var(--bad); }
.btn-quiet   { border-color: var(--line); color: var(--muted); }
.btn-identity {
  display: block;
  width: 100%;
  text-align: left;
  margin-top: 8px;
  /* an identity is a NAME, and a name is not a control label */
  font: 13px var(--mono);
  letter-spacing: 0;
  text-transform: none;
  padding: 10px 12px;
  min-height: 44px;
}
.btn:hover { filter: brightness(0.96); }
#whoami .btn { min-height: 36px; }
/* bare link-actions: tertiary and destructive-but-deliberate controls */
.btn-link { background: none; border: none; padding: 10px 4px;
  min-height: 44px; color: var(--ink); font: 12.5px var(--sans);
  text-decoration: underline; text-underline-offset: 3px; cursor: pointer; }
.btn-link-danger { color: var(--bad); }

/* Focus is the accent, at 3px: the dashboard's 2px ring is read at a
   desk, this one is found with a thumb in daylight. */
:focus-visible {
  outline: 3px solid var(--blue);
  outline-offset: 2px;
}

/* ---- fields + inputs: THE PLACE DENSITY DOES NOT GO ----
   Labels are question text and stay sans; the value being typed is
   mono and tabular, which is what a number wants. Sizes are the
   pre-instrument sizes on purpose (D1.1). */
.field { margin: 0 0 14px; }
.field.slim { margin: 0; }
.field label {
  display: block;
  font-weight: 600;
  font-size: 0.92rem;
  margin-bottom: 5px;
  color: var(--ink);
}
.req { color: var(--bad); font-weight: 700; }
.input {
  width: 100%;
  max-width: 340px;
  min-height: 44px;
  padding: 8px 10px;
  font: 15px var(--mono);
  font-variant-numeric: tabular-nums;
  border: 1px solid var(--line);
  border-radius: var(--r-touch);
  background: var(--card);
  color: var(--ink);
}
.input:focus { border-color: var(--blue); }
.input:disabled { background: var(--bg); color: var(--muted); }
/* UI-02: the sign-in email field was the one input in this app with no
   `.input` class, so it fell to the browser's own placeholder grey - an
   unlabeled field's ONLY text, measured ~2.4:1. Every `.input` now
   states its placeholder colour explicitly, at the same --muted token
   every other secondary line in the app reads at. */
.input::placeholder { color: var(--muted); opacity: 1; }

.form-groups { display: flex; flex-direction: column; gap: 14px; }
.group {
  /* UNBOXING (round 5): a group is page-plane content, not a card - the
     surface tint alone marks it, no frame around it. */
  background: var(--card);
  border: none;
  border-radius: var(--r);
  padding: 0 0 12px;
  position: relative;
}
/* a group title is metadata about the questions below it, so it takes
   the panel band the dashboard gives .panel > h3. The band's own colour
   change against the group fill IS the separator - round 5 drops the
   border-bottom rule and lets the extra bottom margin carry the gap
   instead (rows separate by spacing, not rules). */
.group > h2 {
  margin: 0 0 14px; padding: 8px 12px;
  background: var(--surface2);
  font: 600 10px/1.5 var(--mono); letter-spacing: 0.14em;
  text-transform: uppercase; color: var(--ink);
}
.group > *:not(h2) { margin-left: 12px; margin-right: 12px; }

/* ---- notes: errors speak, successes confirm, empties stay silent ----
   Motion (N4, 2026-08-18): a refusal that slides in gets seen - one
   shot, no bounce, no loop, in SERVICE of the honesty law (an error
   must be noticed, never softened). This covers both mechanisms the
   codebase uses for these notes: a freshly-inserted node (errorNote()
   in ui.js, connections.js's ok/error swap) animates on insertion; a
   static placeholder toggled via the `hidden` attribute (formview.js's
   per-field .field-error) animates on the display:none -> visible flip,
   which restarts a CSS animation the same way insertion does. */
/* Round 5: semantic colour lives on a 3px left edge, never a full
   border - the same move .ai-note makes on the dashboard. */
.field-error, .error-note {
  color: var(--bad);
  background: var(--bad-bg);
  border-left: 3px solid var(--bad);
  padding: 6px 10px;
  font-size: 0.88rem;
  margin: 6px 0 0;
  animation: rt-refusal-cue var(--dur-fast) var(--ease) backwards;
}
.field-error[hidden] { display: none; }
/* An alert with nothing to say must not look like an alert (2026-08-07). */
.field-error:empty, .error-note:empty { display: none; }
/* no-orphan-access (migration 114): the same refusal palette as
   .error-note, but INLINE - the Accounts owner rollup and each
   affected row name the person in running text, not a boxed block. */
.error-note-inline {
  color: var(--bad);
}
.error-note-inline a { color: inherit; text-decoration: underline; }
p.error-note-inline.small {
  color: var(--bad);
  background: var(--bad-bg);
  border-left: 3px solid var(--bad);
  padding: 4px 8px;
  margin: 4px 0 0;
}
.ok-note {
  color: var(--good);
  background: var(--good-bg);
  border-left: 3px solid var(--good);
  padding: 6px 10px;
  font-size: 0.88rem;
  margin: 6px 0 0;
  /* enters softly rather than teleporting in */
  animation: rt-note-enter var(--dur-fast) var(--ease) backwards;
}

/* ---- form head + sticky footer ---- */
.form-head { margin: 8px 0 12px; }
.form-head-top { display: flex; align-items: center;
  justify-content: space-between; gap: 8px; }
.form-head h1 { margin: 2px 0 2px; }
.form-head p { margin: 0; }

/* Round 5 unboxing doctrine names an exception for floating objects -
   "the savebar action bar" keeps its edge. This sticky footer IS that
   savebar for the entry app (persistent Save/Cancel over scrolling
   content), so its top rule stays; it is not a page-plane row rule. */
.form-footer {
  position: sticky;
  bottom: 0;
  background: var(--bg);
  border-top: 1px solid var(--ink);
  margin-top: 16px;
  padding: 10px 0;
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 10px;
}
.footer-row { display: flex; align-items: center; gap: 10px; width: 100%; }
.footer-row .save-status { margin-right: auto; }
.footer-row .btn-link { flex: none; }
.footer-row .btn { white-space: nowrap; }
.footer-secondary { display: flex; gap: 14px; padding-bottom: 6px; }
.footer-secondary[hidden] { display: none; }

.save-status { font: 600 9.5px var(--mono); letter-spacing: 0.1em;
  text-transform: uppercase; color: var(--muted); min-width: 90px; }
.save-unsaved, .save-saving, .save-saving_dirty { color: var(--warn); }
/* Save-confirmation (N4, 2026-08-18): the class swap in formview.js
   (`saveStatus.className = "save-status save-" + autosaveState`) makes
   this a fresh application of the class each autosave tick, so the
   entrance keyframe plays exactly when the state actually becomes
   "saved" rather than looping. */
.save-saved { color: var(--good);
  animation: rt-note-enter var(--dur-fast) var(--ease); }
.save-error { color: var(--bad); }

/* ---- timeline (record history) ---- */
.timeline { list-style: none; margin: 0; padding: 0; }
.timeline li {
  display: flex;
  gap: 10px;
  padding: 8px 0;
  font-size: 0.85rem;
}
.tl-when { color: var(--muted); white-space: nowrap; }

/* ---- dialogs ---- */
.overlay {
  position: fixed;
  inset: 0;
  /* the same scrim the inspector uses on the dashboard */
  background: rgba(5, 10, 8, 0.45);
  display: flex;
  align-items: center;
  justify-content: center;
  padding: 16px;
  z-index: 40;
}
.dialog {
  /* a floating sheet keeps its edge (round-5 doctrine) - and takes the
     sheet radius, not the sharp page-plane one. */
  background: var(--card);
  border: 1px solid var(--line);
  border-radius: var(--r-sheet);
  max-width: 560px;
  width: 100%;
  max-height: 85vh;
  overflow-y: auto;
  padding: 18px;
}
.dialog h2 { margin-bottom: 10px; }
.dialog-body { margin-bottom: 14px; }
.dialog-buttons {
  display: flex;
  flex-wrap: wrap;
  gap: 10px;
  justify-content: flex-end;
}
.dialog-quick { display: flex; gap: 8px; margin: 10px 0; flex-wrap: wrap; }
.warn-list { margin: 10px 0; padding-left: 20px; }
.warn-list li { margin: 6px 0; }

/* ---- 409 conflict reconciliation ---- */
.diff-row {
  padding: 14px 0;
}
.diff-label { font-weight: 600; margin: 0 0 6px; }
.diff-choice {
  display: flex;
  align-items: center;
  gap: 8px;
  min-height: 44px;
  cursor: pointer;
}
.diff-choice input[type="radio"] { width: 20px; height: 20px; }

/* ---- dev identity selector ---- */
.selector { max-width: 480px; margin: 30px auto; }
.identity-list { margin-top: 12px; }

/* ---- key-value tables (huddle and kin) ---- */
.table { width: 100%; border-collapse: collapse; font-size: 12.5px; }
.table td { padding: 8px 0; vertical-align: top; }
.table td:first-child { color: var(--muted); padding-right: 12px; }
.table td:last-child { text-align: right; color: var(--ink);
  font-weight: 600; font-variant-numeric: tabular-nums; }
.table th { font: 600 9.5px var(--mono); letter-spacing: 0.12em;
  text-transform: uppercase; color: var(--faint); text-align: left;
  padding: 4px 0; }
/* A records table has five columns of things that cannot be abbreviated -
   a person's name, what the credential is, which office, a date - and at
   390px their combined minimum is wider than the phone. Measured
   2026-08-16: the page itself scrolled sideways, which moves every other
   screen element off-centre to show one more date column. So the TABLE
   scrolls inside its own card and the page does not. */
.table-scroll { overflow-x: auto; }
/* Scoped to the scrolling tables ONLY. The base .table pads its first
   column and nothing else, which is right for the two-column tables the
   rest of the app uses and unreadable at five, where "CREDENTIAL",
   "OFFICE" and "EXPIRES" ran into one word on a phone. */
.table-scroll .table td, .table-scroll .table th { padding-right: 14px; }
.table-scroll .table td:last-child,
.table-scroll .table th:last-child { padding-right: 0; }

/* ---- 091: the unlinked-accounts strip -------------------------------
   Three rules, and each one exists because a call site would otherwise
   be styling a ghost - the defect PR #176 closed on `.strip`, where the
   class name read like a design and resolved to nothing. */

/* One open question inside the card. `.panel` is the DASHBOARD's class
   and has no rule in this stylesheet, so this strip gets its own rather
   than inheriting a name that happens to be spelled the same. */
.subcard {
  /* the surface-tint shift against .card's own fill is the separator -
     no frame around it (round-5 unboxing). */
  border: none;
  border-radius: var(--r);
  padding: 12px;
  margin-top: 14px;
  background: var(--surface2);
}
.card h4 {
  margin: 16px 0 6px;
  font: 600 11px var(--mono);
  letter-spacing: 0.06em;
  text-transform: uppercase;
  color: var(--muted);
}
.subcard > h4 { margin-top: 0; color: var(--ink); text-transform: none;
  font: 600 13px var(--sans); letter-spacing: 0; }
/* A control that is visibly present and visibly unavailable. It stays
   rendered on purpose: an admin who never sees the button never learns
   that linking an account to a person is the owner's decision, and the
   `title` plus the prose line beneath carry the reason for the pointer
   and for everyone who has no pointer. `not-allowed` rather than
   `default`, so the cursor agrees with the sentence. */
.btn-off {
  border-color: var(--line);
  color: var(--muted);
  background: var(--bg);
  cursor: not-allowed;
}
.btn-off:hover { filter: none; }

/* ---- training: the knowledge check ---- */
/* border:none overrides the UA's default fieldset groove - without it,
   the browser's own inset border would reappear once ours is gone. */
.qform fieldset { border: none; border-radius: var(--r);
  margin: 0 0 12px; padding: 10px 12px 12px; background: var(--surface2); }
.qform legend { font-weight: 600; font-size: 0.95rem; padding: 0 4px;
  color: var(--ink); }
.qopt { display: flex; align-items: center; gap: 8px; min-height: 44px; }
.qopt input[type="radio"] { width: 20px; height: 20px; flex: none; }
.qopt label { font-family: var(--sans); font-size: 0.95rem;
  cursor: pointer; }

/* ---- sign-in ---- */
.login-card { max-width: 430px; margin: 44px auto 0; text-align: center; }
.login-brand { display: flex; flex-direction: column; align-items: center;
  margin-bottom: 4px; }
.login-lockup { display: flex; align-items: center; justify-content: center;
  gap: 11px; }
.login-wordmark-img { width: 220px; height: auto; }
.login-card .login-row { display: flex; gap: 10px; justify-content: center;
  flex-wrap: wrap; margin-top: 14px; }
.login-card input { text-align: center; min-width: 260px;
  border-radius: var(--r-touch); padding: 10px 12px; }
.login-card .btn-primary:hover { background: var(--navy); border-color: var(--navy); }
/* UI-02: 11px over --faint measured ~3.9:1 live - under the 4.5:1 small-
   text floor, and worse than it looks since --faint moves further from
   --bg's own value between light and dark. --muted is the token every other
   secondary line in this app already reads at (7.7:1 on --bg in light,
   comparably high in dark; see this file's own header on the one-token-
   authority rule) and 12.5px matches the smallest body text used
   elsewhere (.menu-item). Legal copy earns quiet, never illegible. */
.login-legal { font-size: 12.5px; color: var(--muted); margin: 24px 0 4px; }

/* ---- Team (accounts) ---- */
.check-label { display: inline-flex; align-items: center; gap: 6px;
  margin-right: 14px; min-height: 44px; }
.field-row { display: flex; align-items: center; gap: 8px; margin: 8px 0; }
.field-row .input { max-width: 200px; }
.manage-panel { margin-top: 18px; display: flex; flex-direction: column;
  gap: 8px; align-items: flex-start; }

/* ---- Connections: provider cards ----
   The provider mark is vendored and rendered EXACTLY as its owner
   supplies it - Intuit's brand rules forbid redrawn or restyled marks. */
.conn-card { padding: 0; overflow: hidden; }
/* the band's own tint against .conn-body's fill marks the boundary -
   the border-bottom rule is gone (row rules -> spacing, round 5). */
.conn-head {
  display: flex;
  align-items: center;
  gap: 12px;
  padding: 12px 14px;
  background: var(--surface2);
}
.conn-logo {
  width: 34px;
  height: 34px;
  flex: 0 0 34px;
  display: flex;
  align-items: center;
  justify-content: center;
  border-radius: var(--r);
  background: var(--card);
  border: none;
  overflow: hidden;
}
.conn-logo img { width: 100%; height: 100%; object-fit: contain; }
.conn-logo-fallback {
  font-weight: 700;
  font-size: 0.95rem;
  color: var(--muted);
}
.conn-title { flex: 1 1 auto; min-width: 0; }
.conn-title h2 {
  margin: 0;
  font-size: 1.02rem;
  line-height: 1.25;
}
.conn-title .conn-sub {
  margin: 2px 0 0;
  font-size: 0.82rem;
  color: var(--muted);
}
.conn-body { padding: 12px 16px 14px; }
.conn-body .conn-meta {
  margin: 0 0 10px;
  font-size: 0.85rem;
  color: var(--muted);
}
.conn-actions {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 10px;
}
/* the provider's own connect button: supplied image, every pixel theirs */
.conn-connect {
  display: inline-flex;
  align-items: center;
  padding: 0;
  border: 0;
  background: none;
  cursor: pointer;
  border-radius: 4px;
}
.conn-connect img { height: 36px; width: auto; display: block; }
.conn-connect:focus-visible {
  outline: 3px solid var(--blue);
  outline-offset: 3px;
}
@media (max-width: 560px) {
  .conn-actions { width: 100%; }
  .conn-actions .btn { flex: 1 1 auto; }
}
/* spending classes (040): the costing model's per-account vocabulary -
   a quiet hairline section on the card, no frame (classification is
   routine bookkeeping, not a signal) */
.conn-classes { margin-top: 22px; }
.conn-class-row { display: flex; align-items: center;
  justify-content: space-between; gap: 10px; padding: 4px 0; }
.conn-class-name { font-size: 0.85rem; overflow: hidden;
  text-overflow: ellipsis; white-space: nowrap; }
.conn-class-pick { flex: 0 0 auto; max-width: 55%; min-height: 44px; }

/* 2026-09-03 patch batch, slice 2: the mailbox table's own .mail-*
   rules (owner ruling 2026-09-02) are RETIRED here - the table moved
   onto shared-app/table.js (entry-app/connections.js's mailAccountsTable
   became buildMailTable(), built on shared-app/table.js's
   createTable()), and shared-app/table.css now carries the equivalent
   .sf-table/.sf-th/.sf-td/.sf-row-menu rules, loaded by every app that
   adopts the shared table rather than re-declared per page. Dropped
   entirely: .mail-table/.mail-th/.mail-td/.mail-row-menu/.mail-col-*.

   Two-column HR flag rows (2026-09-03 patch notes, "Patch plan:
   Connections", item 5): checkbox+title in the first column, the
   blurb (and, for a held-back flag, a small "Not yet enabled" badge on
   the label itself rather than a third paragraph) in the second - the
   release-gate sentence itself now renders once, above the whole list
   (hrFlagsSection() in connections.js), not per row. */
.hr-flag-row {
  display: grid;
  grid-template-columns: minmax(220px, 320px) 1fr;
  align-items: start;
  gap: 6px 16px;
  padding: 8px 0;
  border-top: 1px solid var(--line);
}
.hr-flag-row:first-child { border-top: none; }
.hr-flag-row .check-label { gap: 8px; }
.hr-flag-row .badge { margin-left: 8px; }
@media (max-width: 560px) {
  .hr-flag-row { grid-template-columns: 1fr; }
}

/* costing benches (041): rates + capacity rows, numbers-only inputs */
.cost-row { display: flex; align-items: center; gap: 8px;
  padding: 6px 0; flex-wrap: wrap; }
/* Related settings are native links with the same centered touch targets
   as buttons, not top-aligned text in an otherwise empty capsule. */
nav.cost-row > a.btn { display: inline-flex; align-items: center;
  justify-content: center; max-width: 100%; text-align: center;
  text-decoration: none; line-height: 1.4; }
.cost-row-label { flex: 1 1 160px; font-size: 0.88rem;
  font-family: var(--sans); }
.cost-row input { width: 90px; min-height: 44px; padding: 4px 8px;
  border: 1px solid var(--line); border-radius: var(--r-touch);
  background: var(--card); color: var(--ink);
  font: 15px var(--mono); font-variant-numeric: tabular-nums; }
.cost-row-cap input { width: 74px; }
/* Price slab (132): the live margin/floor read-outs sit inline, AFTER
   the fee/lab/supplies inputs, in a row whose leading label
   (.cost-row-label) grows to fill leftover space. Their text changes
   on every keystroke ("margin —" -> "margin $1,234.56 *"), and an
   unbounded span's width change reflows how much space is left for the
   label to grow into - which visibly shifts the inputs the owner is
   still typing in, even though they sit BEFORE the span in DOM order.
   A fixed min-width (generous past any real fee) plus tabular figures
   stops the recompute from ever changing this span's footprint, so the
   label - and everything typed to its right - stops moving. */
.proc-figure { min-width: 148px; display: inline-block;
  white-space: nowrap; font-variant-numeric: tabular-nums; }
/* Effective-from: one dated decision per card, named rather than assumed.
   Sits above the rows it governs because it applies to whichever row you
   save, and a control that governs several rows must not look like it
   belongs to one of them. */
.eff-from { display: flex; align-items: center; gap: 8px;
  flex-wrap: wrap; padding: 6px 0 10px; margin-bottom: 14px; }
.eff-date { min-height: 44px; padding: 4px 8px;
  border: 1px solid var(--line); border-radius: var(--r-touch);
  background: var(--card); color: var(--ink);
  font: 15px var(--mono); font-variant-numeric: tabular-nums; }
/* The server refuses a save with no date, so the field states the
   problem where the problem is rather than in a note elsewhere. */
.eff-date[aria-invalid="true"] { border-color: var(--warn);
  box-shadow: 0 0 0 2px var(--warn-bg); }
.btn-small { min-height: 32px; padding: 2px 8px; font-size: 9.5px; }

/* ---- Practice setup (021) ---- */
.setup-card { max-width: 720px; }
.setup-rail { display: flex; flex-wrap: wrap; gap: 6px; list-style: none;
  padding: 0; margin: 14px 0 18px; }
.setup-step { padding: 6px 10px; border: 1px solid var(--line);
  border-radius: var(--r-touch); font-size: 12px; cursor: pointer;
  min-height: 34px; display: flex; align-items: center; gap: 6px; }
.setup-step.on { background: var(--blue-bg); border-color: var(--blue);
  color: var(--ink); font-weight: 600; }
.setup-step.done { opacity: 0.75; }
.setup-step.skipped { opacity: 0.55; }
.setup-step .tag { font: 600 9px var(--mono); text-transform: uppercase;
  letter-spacing: 0.1em; color: var(--faint); }
.setup-step-name { font-size: 12px; }
.setup-choices { display: flex; flex-wrap: wrap; gap: 8px; margin: 10px 0; }
.setup-choices .choice { padding: 10px 14px; border: 1px solid var(--line);
  border-radius: var(--r-touch); background: var(--card); color: var(--ink);
  font: 13px var(--mono); min-height: 44px;
  cursor: pointer; text-transform: capitalize; }
.setup-choices .choice.on { background: var(--blue-bg); border-color: var(--blue);
  color: var(--ink); }
.setup-actions { display: flex; gap: 10px; margin-top: 16px; }
.setup-office-row { display: flex; gap: 10px; align-items: center;
  margin: 8px 0; }
.setup-office-row .setup-name { flex: 1; }
.setup-list { margin: 6px 0 12px; padding-left: 18px; }
.setup-procs { display: flex; flex-direction: column; gap: 6px;
  margin: 10px 0; }
.setup-proc { display: flex; gap: 8px; align-items: baseline; }
.setup-ref { margin-left: auto; font-size: 11px; }
/* a static status block, not a control - unboxed like .subcard, a
   surface tint carries it rather than a frame (round 5). */
.setup-conn { border: none; border-radius: var(--r);
  background: var(--surface2); padding: 12px 14px; margin: 10px 0; }
.setup-conn-head { display: flex; align-items: center; gap: 10px;
  justify-content: space-between; }
.setup-conn-steps { margin: 8px 0 2px; padding-left: 20px; }
.setup-conn-steps li { margin: 4px 0; color: var(--muted); }

/* ---- Leads (CRM) ---- */
.lead-row { padding: 14px 0; }
.lead-head { display: flex; align-items: center; gap: 8px; flex-wrap: wrap; }
.lead-acts { display: flex; gap: 6px; flex-wrap: wrap; margin-top: 8px; }
.lead-acts .input { min-height: 40px; }
.lead-acts .btn { min-height: 40px; padding: 0 10px; }
.lead-procs { display: flex; flex-wrap: wrap; gap: 4px 14px; margin: 6px 0; }
.lead-proc { display: inline-flex; align-items: center; gap: 4px;
  font-size: 0.88rem; color: var(--ink); }
.lead-note { display: flex; flex-wrap: wrap; gap: 8px; width: 100%;
  margin-top: 8px; }
.lead-note .input { flex: 1 1 140px; min-width: 140px; max-width: none; }
/* Sales assist: a quiet advisory block inside the lead card - text
   only, no control in it writes anything. */
.sales-assist { width: 100%; border-left: 3px solid var(--line);
  padding: 4px 0 4px 10px; }
.sales-assist p { margin: 4px 0; }
.huddle-calls .lead-why { color: var(--muted); font-size: 0.85rem; }

/* Sales assist, discoverability revision (owner ruling 2026-08-21): the
   same quiet advisory frame as .sales-assist, for the empty state shown
   when a lead carries no objection yet. */
.sales-assist-empty { width: 100%; border-left: 3px solid var(--line);
  padding: 4px 0 4px 10px; }
.sales-assist-empty p { margin: 4px 0; }

/* ---- motion: shared duration family from the token root ---- */
@keyframes rt-fade-up {
  from { opacity: 0; transform: translateY(6px); }
  to   { opacity: 1; transform: none; }
}
.card { animation: rt-fade-up var(--dur) var(--ease) backwards; }
.setup-pane > * { animation: rt-fade-up var(--dur) var(--ease) backwards; }
.setup-pane > *:nth-child(2) { animation-delay: 30ms; }
.setup-pane > *:nth-child(3) { animation-delay: 60ms; }
.setup-pane > *:nth-child(n+4) { animation-delay: 90ms; }
@keyframes rt-pop {
  0% { transform: scale(0.6); opacity: 0; }
  100% { transform: scale(1); opacity: 1; }
}
.setup-step .tag { animation: rt-pop var(--dur-fast) var(--ease) backwards; }
/* `opacity` added (N4, 2026-08-18) so tokens.css's `:disabled { opacity:
   .55 }` eases in rather than popping - the disabled-state application
   of the motion foundation, matching dashboard-app/port.css's button. */
button { transition: background-color var(--dur-fast) var(--ease),
                     color var(--dur-fast) var(--ease),
                     transform var(--dur-fast) var(--ease),
                     opacity var(--dur-fast) var(--ease); }
button:active { transform: scale(0.97); }
input, select { transition: border-color var(--dur-fast) var(--ease),
                            box-shadow var(--dur-fast) var(--ease),
                            opacity var(--dur-fast) var(--ease); }

/* ---- schedule: operatory grid (P2, 2026-08-19; column-fit fix
   2026-08-27) ----
   The manager's day read as columns - one per chair, time down the
   side - is the standard practice-management view, and the flat list
   OD3 shipped with was the scaffold, not the destination. Both the
   list (od-list, unchanged markup) and the grid (od-grid-scroll) are
   built every render; only CSS decides which one shows, keyed to the
   700px floor this sheet already uses for .cards. No JS branch has to
   guess "does the grid fit" - the browser just does.

   Local overflow affordance rather than dashboard-app/tokens.css's
   .scroll-x: that utility does not exist on this branch's tokens.css
   yet (it is mid-flight on a sibling branch), so this follows the
   pattern this sheet already has for the same problem - .table-scroll,
   a few sections up - a plain overflow-x:auto rather than inventing a
   second affordance.

   `.od-grid` fills its scroll wrapper (`width: 100%`, not the
   `max-content` this rule shipped with) so the browser's own grid
   track algorithm sizes each operatory column from the JS-side
   `minmax(104px, 320px)` (schedule.js's odGrid) using the REAL space
   available inside #main's 760px column, growing every column up
   toward its 320px ceiling only as far as room allows and shrinking
   them (down to the 104px floor) when it does not. Measured against
   this sheet's own #main/.card box model (#main max-width 760, 12px
   side padding; .card 14px padding + 1px border; .od-grid 1px border;
   1px column-gap × 4 gaps; a 56px fixed time gutter): four operatories
   land at ~161px each with zero overflow at any width #main's own cap
   reaches (any viewport from roughly 790px up - "typical laptop" and
   wider all land here, since #main never grows past 760 regardless of
   how wide the window is), and ~146px each at 700px viewport - the
   exact floor where this media query first swaps the flat list for the
   grid. Both stay inside [104, 320], so nothing here ever needed either
   bound. Before this fix, `max-content` measured the grid's OWN
   preferred (always-at-ceiling, 320px-per-column) size and ignored the
   wrapper's actual width entirely - four columns' preferred width
   (56+4x320+gaps, ~1341px) forced the 3rd/4th column off-screen behind
   the wrapper's own scrollbar, exactly the owner's report (2026-08-27).
   The 320px ceiling itself stays exactly as the comment on odGrid's
   gridTemplateColumns line explains: it is what stops a single-
   operatory day from stretching one appointment block across the whole
   page - `width: 100%` does not reintroduce that (a definite-width grid
   container still respects each track's own max, it just no longer
   forces every track TO that max). Below the 104px floor (very narrow
   content, or five or more operatories) the wrapper's own
   overflow-x:auto is the intentional fallback - never the page body,
   which never scrolls sideways. */
.od-list { display: block; }
.od-grid-scroll { display: none; }
@media (min-width: 700px) {
  .od-list { display: none; }
  .od-grid-scroll { display: block; overflow-x: auto; padding-bottom: 4px; }
}
/* Round 5: this grid drew its own hairlines by painting the container
   var(--line) and leaving a 1px gap for it to peek through between
   cells - a border with extra steps. The gap colour now matches the
   PAGE (var(--bg)), same as the outer border being gone: cells read as
   sitting on the page, no drawn rule between them. */
.od-grid {
  display: grid;
  column-gap: 1px;
  row-gap: 1px;
  background: var(--bg);
  border: none;
  width: 100%;
}
.od-grid-corner, .od-grid-colhead, .od-grid-time, .od-grid-slot {
  background: var(--card);
}
.od-grid-colhead {
  position: sticky;
  top: 0;
  z-index: 2;
  display: flex;
  align-items: center;
  justify-content: center;
  padding: 4px 6px;
  font: 600 10px var(--mono);
  letter-spacing: 0.06em;
  text-transform: uppercase;
  color: var(--ink);
  background: var(--surface2);
  text-align: center;
}
.od-grid-time {
  position: sticky;
  left: 0;
  z-index: 1;
  display: flex;
  align-items: flex-start;
  justify-content: flex-end;
  padding: 2px 6px 0 0;
  font: 10px var(--mono);
  font-variant-numeric: tabular-nums;
  color: var(--muted);
  background: var(--surface2);
}
.od-grid-time-half { color: var(--faint); }
.od-grid-corner {
  position: sticky;
  top: 0;
  left: 0;
  z-index: 3;
  background: var(--surface2);
}
.od-grid-slot { min-width: 0; }

/* od-grid-cluster: one wrapper per overlap cluster (schedule-durations
   slice, 2026-08-20). Spans the cluster's real slot range (grid-row set
   inline per-cluster in JS - true duration, not a fixed 30-minute
   block) and splits into `laneCount` nested columns so appointments
   overlapping in TIME share the operatory's width side by side rather
   than one covering the other. A cluster with only one appointment gets
   laneCount 1, i.e. the operatory's full width - identical to the old
   fixed-slot look for the common non-overlapping case. */
.od-grid-cluster {
  display: grid;
  gap: 1px;
  padding: 1px;
  overflow: hidden;
}
.od-block {
  min-width: 0;
  min-height: 0;
  border-radius: var(--r);
  border-left: 3px solid var(--blue);
  background: var(--blue-bg);
  padding: 2px 4px;
  overflow: hidden;
  display: flex;
  flex-direction: column;
  gap: 1px;
  font-size: 9.5px;
  line-height: 1.2;
  cursor: default;
}
/* Card-clipping fix: the block sits inside .od-grid-cluster's own
   overflow:hidden (and clips its own overflow too), so the page-wide
   :focus-visible OUTLINE (drawn outside the border box, 2px offset)
   would be cut away by an ancestor exactly when a keyboard user tabs
   to a clipped card to read its full text via aria-label/title - the
   one moment this ring most needs to show. An INSET box-shadow instead
   always draws inside the block's own box, so nothing can clip it. */
.od-block:focus-visible {
  outline: none;
  box-shadow: inset 0 0 0 2px var(--blue);
}
/* A row whose duration never synced (pre-durations history, or a row
   the hot window has not re-touched yet) renders as the honest
   pre-durations fixed one-slot block, marked - never a fabricated
   length dressed up as a real one. */
.od-block-noduration {
  border-left-style: dashed;
  border-left-color: var(--muted);
  background: var(--surface2);
}
.od-block-time {
  font: 600 9.5px var(--mono);
  font-variant-numeric: tabular-nums;
  color: var(--ink);
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}
.od-block-patient {
  font: 600 9px var(--sans);
  color: var(--ink);
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}
/* The procedure line (123): below the name, above the chips - catalog
   labels or bare synced codes, one muted line, ellipsized like its
   neighbours rather than growing the block. */
.od-block-proc {
  font: 9px var(--sans);
  color: var(--muted);
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}
.od-block-prov {
  font: 9px var(--sans);
  color: var(--muted);
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}
.od-block .badges { gap: 3px; flex-wrap: wrap; }
.od-block .badge {
  font-size: 7.5px;
  padding: 1px 4px;
  letter-spacing: 0.06em;
  max-width: 100%;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

/* ---- responsive ----
   375px is the floor, and the floor is the DEFAULT: everything above
   stacks in one column with no fixed widths, so nothing here has to
   un-break a narrow screen. */
@media (min-width: 700px) {
  .cards { gap: 16px; }
  #main { padding-top: 20px; }
}

/* ---- reduced motion: ONE standdown for everything ---- */
@media (prefers-reduced-motion: reduce) {
  *, *::before, *::after {
    animation: none !important;
    transition: none !important;
  }
}

/* ---- print / save-as-PDF (My Performance + huddle sheet) ----
   Chrome hidden; data cards print clean on white; renders exactly the
   on-screen, server-filtered view.

   The literals here are deliberate and are the ONE place this sheet
   leaves the token system: paper has no theme. Printing a dark-mode
   page through tokens would put graphite ink on a graphite field and
   empty a toner cartridge saying nothing. */
@media print {
  #dev-banner, #topbar, .row-actions, .btn, .no-print { display: none !important; }
  body { background: #fff; background-image: none; color: #000; }
  #main { padding: 0; max-width: none; }
  .card { border: 1px solid #999; box-shadow: none; break-inside: avoid;
    background: #fff; }
  .card::before, .card::after { display: none; }
  .table th, .table td { border-bottom: 1px solid #bbb; color: #000; }
  .muted { color: #333; }
}
