/* Zeze — default design system (light, clinical, keyboard-first, 8px grid).
   Fully offline: system sans stack (no CDN). Drop Inter .woff2 into /static/fonts and add an
   @font-face here to switch — font-src is locked to 'self' by CSP. */
:root {
  /* Brand palette (design/Brand-Guidelines.md v1.0): Zeze Teal leads, Ink/Slate text, Cloud
     background, Line hairlines. --warn is a text-safe ochre (brand Warning #E8A33D fails AA as
     text — it lives in --warn-accent for borders/markers); --warn-bg is a Moon-gold tint, the
     brand's single warm note. */
  --bg: #EEF3F6; --surface: #FFFFFF; --text: #1C2733; --muted: #5A6B78; --line: #E4E9EC;
  --primary: #0E7C8B; --primary-700: #0B4F63; --primary-50: #DFECEF;
  /* D4-11 — --warn-accent is a NON-TEXT contrast obligation (WCAG 1.4.11, 3:1), not a decorative
     tint. The brand amber #E8A33D was moved out of text use for failing AA there, and then used as
     the left bar on .fu-card.amber and .lic-banner.warn — where it measured 2.16:1 against --surface
     and 1.93:1 against --bg, so the marker that distinguishes "needs attention" from "ordinary" was
     itself under the line. Darkened for the LIGHT theme only (the two dark themes' ambers already
     clear 3:1 against their grounds): #B0740F is 3.31:1 on --surface and 2.96:1 on --bg. */
  --ok: #1E8E3E; --ok-bg: #EAF5EC; --warn: #94620C; --warn-bg: #FBF1DC; --warn-accent: #B0740F;
  --danger: #C5341E; --danger-bg: #FBECE8;
  --focus: #1793A3;
  --s1: 4px; --s2: 8px; --s3: 12px; --s4: 16px; --s5: 24px; --s6: 32px;
  --radius: 8px; --shadow: 0 1px 2px rgba(15,23,42,.06), 0 1px 3px rgba(15,23,42,.08);
  /* D4-9 — elevation as TOKENS, so the dark themes can darken them.
     --shadow and --scroll-shadow were themed per dark theme, under a comment saying they were
     "near-invisible at the light value" — so this failure was found once and fixed for two values.
     Six more were left inline as rgba(15,23,42,·): the modal scrim, the modal shadow, the two
     dropdown panels, the global-search results and the tile hover. rgb(15,23,42) IS the dark theme's
     --bg, so the modal scrim composited to a measured 1.00:1 — literally zero dimming — and the
     dialog's own edge against the page came to 1.22:1 (dark) / 1.02:1 (rose gold). Every form in
     this app is a modal, and the whole point of a scrim is the affordance that the rest of the app
     is unavailable. */
  /* D4-8 — the boundary of a control you have to find, as distinct from a decorative divider.
     --line measures 1.22:1 against --surface in light, 1.41:1 in both dark themes — under WCAG
     1.4.11's 3:1 in all three. That is fine for a table rule or a card edge, where the content either
     side does the work. It is NOT fine for a text field, because every form in this app lives in a
     .modal or a .card, both of which are ALSO --surface: the field fill and the container fill are
     the identical hex, so this hairline is the only thing saying "there is an input here". An older
     staff member at 125% scaling opening the Save patient dialog sees a block of labels and has to
     click around to find the boxes.
     The finding proposes raising --line itself — one token, wide effect. Deliberately narrower here:
     raising --line repaints every card edge, every table row divider, every tab underline and both
     dropdown panels, which is a visual redesign the review did not authorise and cannot measure. This
     token fixes the control that actually fails, at 3.22:1 on --surface and 2.88:1 on --bg (the
     latter is a field sitting directly on the page, which is rare and is bounded by its own fill
     contrast as well). --line stays exactly as it is. */
  --field-line: #869199;
  --scrim: rgba(15,23,42,.45);
  --elev-modal: 0 20px 50px rgba(15,23,42,.30);
  --elev-panel: 0 8px 30px rgba(15,23,42,.18);
  --elev-pop: 0 8px 24px rgba(0,0,0,.18);
  --elev-hover: 0 2px 8px rgba(15,23,42,.10);
  font-family: system-ui, -apple-system, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
}
* { box-sizing: border-box; }
html, body { margin: 0; height: 100%; }
/* D4-16 — THE BROWSER'S OWN FONT-SIZE PREFERENCE HAD NO EFFECT ANYWHERE IN THIS APP.
   The declaration on body (16 pixels, fixed) hard-pinned the base, and all 40 font-size declarations
   in this file were px, with not one rem or em — so the single accommodation an older user with reading glasses is most
   likely to have already turned on did nothing, while a third to three-quarters of the characters on
   any given screen render BELOW 16px. (Browser ZOOM still worked; the text-size preference did not,
   and they are different settings reached from different places.)
   100% means "whatever the user asked for", and every size below is now a rem multiple of it. The
   values are unchanged at the default 16px base — this is the same design, expressed so it can grow. */
body { background: var(--bg); color: var(--text); font-size: 100%; line-height: 1.5; }
.num, .num * { font-variant-numeric: tabular-nums lining-nums; }
/* --primary-700, not --primary. The v3.6.0 brand palette moved --primary #0E7490 -> #0E7C8B and
   --primary-50 #ECFEFF -> #DFECEF, which pushed link text UNDER AA on every tinted surface:
   teal on --primary-50 went 5.15 -> 4.07, teal on --bg went 5.12 -> 4.39, and even on white it is
   only 4.91. The app tints a lot of surfaces that hold links — grouped reminder rows, expanded
   fleet/mask/recall detail rows, .hl-row deep-link highlights, nav and search hover — so the old
   'just keep links inside cards' rule could not hold. Fixing the TOKEN fixes the whole class.
   -700 is a brand token and clears 4.5 on white, --bg and --primary-50 alike. */
a { color: var(--primary-700); }
:focus-visible { outline: 2px solid var(--focus); outline-offset: 2px; border-radius: 4px; }
@media (prefers-reduced-motion: no-preference) { .anim { transition: all .18s ease; } }

/* Layout ------------------------------------------------------------------ */
/* App-shell frame: #app owns the whole viewport as a vertical stack (topbar + shell). overflow:hidden
   means the PAGE never scrolls — only the .main content pane does. So the top bar and the left nav are
   structurally fixed and can never scroll away (no position:sticky, no magic offsets). */
#app { height: 100vh; display: flex; flex-direction: column; overflow: hidden; }
/* Loading feedback: a thin animated bar across the top while a screen is fetching. route() sets
   aria-busy=true at the start of navigation; shell() removes it once the new screen paints — so this
   needs no JS, just a rule keyed off the attribute the code already toggles. */
#app[aria-busy="true"]::after { content: ''; position: fixed; left: 0; top: 0; height: 3px; width: 100%; z-index: 100;
  background: linear-gradient(90deg, transparent, var(--primary), transparent); background-size: 40% 100%; background-repeat: no-repeat; animation: loadbar 0.9s linear infinite; }
@keyframes loadbar { 0% { background-position: -40% 0; } 100% { background-position: 140% 0; } }
@media (prefers-reduced-motion: reduce) { #app[aria-busy="true"]::after { animation: none; background-position: 50% 0; } }
/* D4-5 — the top bar WRAPS at every width, not only below 760px.
   It was flex-wrap:nowrap above the mobile breakpoint while #app is overflow:hidden with no scroll,
   so between roughly 761 and 840 CSS px the right-hand group was clipped off the viewport — with the
   notification bell present, "Sign out" had zero visible pixels at 761–768px and a sliver up to 839px,
   and focusing it did not scroll it into view because there is nothing to scroll. That band is not
   exotic: it is a half-screen window on a 1366 laptop, or 175% Windows scaling on a 1440 panel.
   Wrapping is already the intended behaviour — it is what the mobile block does — so it becomes the
   base rule and the breakpoint stops being load-bearing. */
.topbar { display: flex; flex-wrap: wrap; align-items: center; gap: var(--s4); background: var(--surface); border-bottom: 1px solid var(--line);
  padding: 0 var(--s4) 0 0; min-height: 48px; flex: 0 0 auto; position: relative; z-index: 20; }
/* Brand occupies a column exactly the nav-sidebar width (208px), centred, with a right divider that
   continues the sidebar's border up through the top bar. */
.topbar .brand { font-weight: 700; color: var(--primary-700); letter-spacing: -.01em;
  flex: 0 0 208px; box-sizing: border-box; align-self: stretch; display: flex; align-items: center;
  justify-content: center; text-align: center; padding: 0 var(--s3); border-right: 1px solid var(--line); }
.topbar .spacer { flex: 1; }
/* ONE control height across the whole bar. It used to hold four (32/40/44/47px in a 48px bar), so
   nothing shared a top or bottom edge: the search box inherited the global input rule and came out
   44px with 2px of clearance, .who kept the base button's 40px min-height, and the rest were 32px
   .btn.sm. Four heights, four different optical baselines — that is what read as "awkward". */
.topbar .gsearch, .topbar .who, .topbar .btn.sm { min-height: 32px; height: 32px; }
.topbar .gsearch { font-size: 0.875rem; padding: 4px 10px; }
.topbar .who { color: var(--muted); font-size: 0.8125rem; padding: 4px 8px; }
/* Related controls sit together (8px) and the groups sit apart (the bar's own 16px gap), so the bar
   reads as "find | do | you" instead of five equally-spaced strangers. */
.topbar .tb-group { display: flex; align-items: center; gap: var(--s2); }
/* The search is the ONLY thing in the bar allowed to shrink (it has min-width:190px to stop at).
   At 1024px the bar's natural width leaves ~6px of slack, so without this the squeeze landed on the
   bell: its button was pinched a few px under its contents and the red count badge wrapped onto a
   second line, spilling out of the 32px button. Nothing else here has anywhere useful to shrink to. */
.topbar .tb-group > *, .topbar .who, .topbar .btn.sm { flex: 0 0 auto; }
.topbar button { white-space: nowrap; }
/* Notifications bell dropdown */
.notif-panel { position: absolute; right: 0; top: calc(100% + 8px); width: min(360px, 90vw); background: var(--surface); border: 1px solid var(--line);
  border-radius: 10px; border: 1px solid var(--field-line); box-shadow: var(--elev-panel); z-index: 60; max-height: 70vh; overflow: auto; }
.notif-hd { padding: 10px 14px; font-weight: 600; border-bottom: 1px solid var(--line); }
.notif-item { padding: 10px 14px; border-bottom: 1px solid var(--line); cursor: pointer; }
.notif-item:last-child { border-bottom: 0; }
.notif-item:hover { background: var(--primary-50); }
/* Fills the viewport height left under the topbar; its two columns (nav | main) each get that full
   height and manage their own scrolling. min-height:0 lets the grid children actually overflow-scroll
   instead of stretching the grid. */
/* minmax(0,1fr): a bare 1fr's min is min-content, so a wide table could force the track (and the
   whole page) past the viewport wherever .main isn't a scroll container (the mobile layout). */
.shell { display: grid; grid-template-columns: 208px minmax(0, 1fr); flex: 1 1 auto; min-height: 0; overflow: hidden; }
.nav { border-right: 1px solid var(--line); padding: var(--s3);
  /* The frame is fixed (see #app): the nav fills the shell's full height and never scrolls with the
     page — only .main scrolls. A nav taller than the viewport scrolls within itself — and the same
     scroll-shadow technique as .table-wrap makes that visible (on short counter screens the last
     items used to clip with no hint they existed). */
  overflow-y: auto;
  background:
    linear-gradient(var(--surface) 45%, transparent) top / 100% 24px,
    linear-gradient(transparent, var(--surface) 45%) bottom / 100% 24px,
    radial-gradient(farthest-side at 50% 0, var(--scroll-shadow, rgba(15,23,42,.22)), transparent) top / 100% 10px,
    radial-gradient(farthest-side at 50% 100%, var(--scroll-shadow, rgba(15,23,42,.22)), transparent) bottom / 100% 10px;
  background-repeat: no-repeat;
  background-attachment: local, local, scroll, scroll; background-color: var(--surface); }
.nav a { display: flex; align-items: center; gap: var(--s2); padding: var(--s2) var(--s3); border-radius: var(--radius);
  color: var(--text); text-decoration: none; font-weight: 500; margin-bottom: 2px; }
.nav a:hover { background: var(--primary-50); }
.nav a.active { background: var(--primary); color: #fff; }
/* Section headings inside the nav (Today / Patients / Money / Workspace / System) — plain text,
   sentence case per brand voice. Non-interactive; purely wayfinding. */
.nav-sec { color: var(--muted); font-size: 0.75rem; font-weight: 600; padding: 0 var(--s3); margin: var(--s3) 0 3px; }
.nav-sec:first-child { margin-top: var(--s1); }
.nav .badge { margin-left: auto; }
/* Short screens (720p counter PCs): tighten the nav so all five groups fit without scrolling.
   Must come after the base .nav a / .nav-sec rules — equal specificity, so source order decides. */
@media (max-height: 800px) {
  .nav { padding-top: var(--s2); padding-bottom: var(--s2); }
  .nav a { padding-top: 5px; padding-bottom: 5px; margin-bottom: 0; }
  .nav-sec { margin: 6px 0 2px; line-height: 1.2; }
}
/* D4-14 — scroll-padding-top, because .rem-bulk pins an opaque bar to the top of this pane.
   .main is the app's only scroll container, and once a bulk selection is active the Follow-ups screen
   sticks a ~40px bar at its top. Tabbing to a control from below scrolls it flush to the top of the
   scrollport — underneath the bar — so the focused element is invisible (WCAG 2.2 SC 2.4.11, AA).
   One line, and it fixes deep-link scrollIntoView landings on the same screen too. */
.main { padding: var(--s5); overflow-y: auto; min-height: 0; scroll-padding-top: 56px; } /* the ONLY scrolling pane */
.main > * { max-width: 1180px; margin-inline: auto; } /* centre the content in the available space rather than hugging the nav */
/* Subtle always-visible bottom bar (support / licence / version). flex:0 0 auto keeps it pinned below
   the shell in the #app column; the .main pane still owns all scrolling. */
.appfoot { flex: 0 0 auto; display: flex; align-items: center; gap: 12px; padding: 6px 16px;
  background: var(--surface); border-top: 1px solid var(--line); color: var(--muted); font-size: 0.75rem; }
.appfoot a { color: var(--muted); text-decoration: none; }
.appfoot a:hover { color: var(--primary); text-decoration: underline; }
.appfoot .grow { flex: 1; }
.appfoot .sep { opacity: .5; }

/* Trial countdown bar (licensing). Sits between the topbar and the shell; escalates amber -> red. */
.lic-banner { flex: 0 0 auto; display: flex; align-items: center; gap: 12px; padding: 6px 16px;
  background: var(--primary-50); border-bottom: 1px solid var(--line); color: var(--primary-700); font-size: 0.8125rem; }
.lic-banner.warn { background: var(--warn-bg); color: var(--warn); border-bottom-color: var(--warn-accent); }
.lic-banner.urgent { background: var(--danger-bg); color: var(--danger); border-bottom-color: var(--danger); }
.lic-banner .lic-cta { margin-left: auto; color: inherit; text-decoration: underline; font-weight: 600; cursor: pointer; font-size: 0.8125rem; }

/* Heidi Health consult-note import — the review dialog */
.heidi-patient { font-size: 0.9375rem; padding: 4px 0; }
/* D4-22 — ONE banner component, because "attention, non-urgent" was being said five different ways.
   Two of the five did it by taking .flag — an inline chip, 12px/700/1px-6px padding — and
   inline-overriding display and padding to fake a banner, which is how a chip ends up as the visual
   vocabulary for a block-level warning. .heidi-warn already had the right geometry, so it is promoted
   to a named component and the fakes point at it. .flag stays the inline chip its geometry describes. */
.notice { border-left: 4px solid var(--line); background: var(--bg); color: var(--text); padding: 9px 12px; border-radius: 0 8px 8px 0; margin: 6px 0 10px; font-size: 0.84rem; display: block; }
.notice.warn { background: var(--warn-bg); border-left-color: var(--warn-accent); color: var(--warn); }
.notice.danger { background: var(--danger-bg); border-left-color: var(--danger); color: var(--danger); }
.heidi-warn { background: var(--danger-bg); border-left: 4px solid var(--danger); color: var(--danger); padding: 9px 12px; border-radius: 0 8px 8px 0; margin: 6px 0 10px; font-size: 0.84rem; }
.heidi-cand { display: inline-block; margin: 4px 8px 4px 0; }
.heidi-sec { font-weight: 600; color: var(--primary-700); font-size: 0.8125rem; margin: 14px 0 4px; border-bottom: 1px solid var(--line); padding-bottom: 3px; } /* sentence case per brand voice */
.heidi-prop { display: flex; align-items: center; gap: 10px; padding: 6px 0; border-bottom: 1px solid var(--line); flex-wrap: wrap; }
.heidi-cbwrap { display: flex; align-items: center; gap: 8px; min-width: 200px; flex: 1; cursor: pointer; font-size: 0.875rem; }
.heidi-change { display: flex; align-items: center; gap: 8px; font-size: 0.8438rem; }
/* A8-24 — the value about to be OVERWRITTEN must be readable. This clipped at 220px with no title,
   on the one screen whose job is to show what a commit destroys: a real address ("Unit 4, 128 Warrego
   Highway, Gatton QLD 4343") measures ~298px at this size, so the operator saw "Unit 4, 128 Warrego
   High…", ticked the box, and the previous value was gone — heidi's audit records changed key NAMES
   only, never the prior value, so there is no recovery path. The row is already flex-wrap:wrap, so
   wrapping costs nothing but vertical space, and the client also carries the full value in a title. */
.heidi-cur { color: var(--muted); text-decoration: line-through; max-width: 320px; overflow-wrap: anywhere; }
/* A8-36 — `var(--mist)` is defined nowhere: no :root, no dark theme, no fallback. The declaration is
   invalid at computed-value time, so the arrow inherited full-strength body text instead of the muted
   tone it was written for — on the screen whose whole job is "this value becomes that one", where the
   separator competing with the values is precisely backwards. --muted is the real token. */
.heidi-arrow { color: var(--muted); }
.heidi-new { color: var(--primary-700); font-weight: 600; }
.heidi-new.backward { color: var(--warn); }
.heidi-val { max-width: 240px; }
h1 { font-size: 1.375rem; margin: 0 0 var(--s4); letter-spacing: -.01em; }
h2 { font-size: 1rem; margin: var(--s5) 0 var(--s3); }
.sub { color: var(--muted); font-size: 0.875rem; }

/* Tiles ------------------------------------------------------------------- */
.tiles { display: grid; grid-template-columns: repeat(auto-fill, minmax(220px, 1fr)); gap: var(--s3); }
.tile { background: var(--surface); border: 1px solid var(--line); border-left: 4px solid var(--line);
  border-radius: var(--radius); padding: var(--s4); box-shadow: var(--shadow); }
.tile.red { border-left-color: var(--danger); }
.tile.amber { border-left-color: var(--warn-accent); }
.tile.green { border-left-color: var(--ok); }
.tile .k { font-size: 1.75rem; font-weight: 700; }
.tile .l { color: var(--muted); font-size: 0.8125rem; font-weight: 600; } /* sentence case per brand voice (was uppercase) */
.tile.click { cursor: pointer; } .tile.click:hover { box-shadow: var(--elev-hover); }

/* Cards & tables ---------------------------------------------------------- */
.card { background: var(--surface); border: 1px solid var(--line); border-radius: var(--radius); box-shadow: var(--shadow); margin-bottom: var(--s4); }
.card .hd { padding: var(--s3) var(--s4); border-bottom: 1px solid var(--line); font-weight: 600; display: flex; align-items: center; gap: var(--s2); }
.card .bd { padding: var(--s2) 0; }
/* Reports' "On this page" jump nav. Plain underlined links on purpose: chips read as filters, and
   these navigate. Deliberately NOT sticky — pinned chrome was rejected on zoom, print and 1024px
   grounds, consistent with the stance at the top of this file. */
.jumpnav { display: flex; flex-wrap: wrap; align-items: baseline; gap: var(--s2) var(--s3); margin: 0 0 var(--s4); }
.jumpnav a { text-decoration: underline; } /* colour comes from the global `a` rule */
/* A jumped-to heading lands clear of the scroll pane's top edge instead of flush against it. */
.card .hd h2[id] { scroll-margin-top: var(--s4); }
table { width: 100%; border-collapse: collapse; }
th, td { text-align: left; padding: var(--s3) var(--s4); border-bottom: 1px solid var(--line); vertical-align: middle;
  overflow-wrap: anywhere; } /* long unbroken free text (mask types, notes, URLs) wraps instead of blowing out the layout */
td.num, th.num { white-space: nowrap; } /* dates & numbers stay on one line — never "03/07/202\n4" */
/* A wide table scrolls inside its card instead of breaking the page layout. The layered
   backgrounds are scroll shadows: the edge darkens only while more columns are hidden that side,
   so a clipped table no longer looks complete. Cell backgrounds are transparent, so the wrapper's
   shadow shows through; the covers scroll with the content (attachment: local) and hide the
   shadow at either end of travel. */
.table-wrap { overflow-x: auto;
  background:
    linear-gradient(90deg, var(--surface) 45%, transparent) left / 28px 100%,
    linear-gradient(270deg, var(--surface) 45%, transparent) right / 28px 100%,
    radial-gradient(farthest-side at 0 50%, var(--scroll-shadow, rgba(15,23,42,.22)), transparent) left / 12px 100%,
    radial-gradient(farthest-side at 100% 50%, var(--scroll-shadow, rgba(15,23,42,.22)), transparent) right / 12px 100%;
  background-repeat: no-repeat;
  background-attachment: local, local, scroll, scroll; }
/* Row action buttons (Follow-ups etc.): one tidy right-aligned group. WRAPS when tight (R6):
   the old nowrap + no-shrink cluster could not shrink, so below ~1300px it overflowed leftward and
   painted OVER the neighbouring cells — the defect that triggered the Follow-ups rebuild. Buttons
   keep their own nowrap (a label never breaks mid-word); the GROUP wraps to a second line instead. */
.row-actions { display: flex; gap: 5px; justify-content: flex-end; align-items: center; flex-wrap: wrap; }
.row-actions > * { flex: 0 0 auto; white-space: nowrap; }
/* Slightly tighter than a standalone .btn.sm so the actions stay compact in their column. */
.row-actions .btn.sm { padding: 4px 8px; }
th { font-size: 0.75rem; text-transform: uppercase; letter-spacing: .04em; color: var(--muted); font-weight: 600; }
tbody tr:hover { background: #F3F7F9; }
tbody tr:last-child td { border-bottom: 0; }
tr.stripe-red td:first-child { box-shadow: inset 3px 0 0 var(--danger); }

/* Follow-ups call cards (R6 panel pick): one card per patient — the card IS the phone call.
   Everything wraps inside the card, so overpaint is structurally impossible at any width. */
.fu-stack { display: block; }
.fu-cards { list-style: none; margin: 0; padding: 0; display: flex; flex-direction: column; gap: 10px; }
.fu-card { background: var(--surface); border: 1px solid var(--line); border-left-width: 4px;
  border-radius: var(--radius); box-shadow: var(--shadow); }
.fu-card.red { border-left-color: var(--danger); }
.fu-card.amber { border-left-color: var(--warn-accent); }
.fu-card.blue { border-left-color: var(--primary); }
/* A3-15 / D4-10 — DE-EMPHASIS BY OPACITY TAKES THE TEXT DOWN WITH IT.
   `opacity: .65` composites the whole card, live text included: .sub ("due DD/MM/YYYY") falls from
   5.5:1 to about 2.7:1 in the default light theme, and the gray "Snoozed → date" chip from 6.9:1 to
   about 3.1:1 — both under AA, on a card whose remaining job is to tell you WHEN it comes back.
   The app already has the right technique for this and uses it on calendar chips: recede the SURFACE,
   leave the text at full strength. The card still reads as snoozed — it says so, in words, in a chip,
   which is also what satisfies 1.4.1 without dimming anything. */
.fu-card.snoozed { border-left-color: var(--line); background: var(--bg); }
.fu-card > .hd { display: flex; gap: 8px; align-items: center; flex-wrap: wrap; padding: 8px 14px; }
.fu-card:not(.slim) > .hd { border-bottom: 1px solid var(--line); }
.fu-name { margin: 0; font-size: 0.9375rem; font-weight: 600; display: inline; }
.fu-card > .bd { padding: 2px 14px; }
.fu-item { display: flex; gap: 8px; align-items: center; flex-wrap: wrap; padding: 6px 0 6px 4px;
  border-bottom: 1px solid var(--line); font-size: 0.875rem; }
.fu-item:last-child { border-bottom: 0; }
.fu-card > .ft { padding: 8px 14px; border-top: 1px solid var(--line); background: var(--primary-50);
  border-radius: 0 0 var(--radius) var(--radius); }
.fu-acts, .fu-slim-acts { display: flex; gap: 5px; align-items: center; flex-wrap: wrap; justify-content: flex-end; }
/* aria-disabled, not disabled — stays focusable so the reason is reachable (activating shows it). */
/* D4-10 — .btn.is-disabled is NOT a disabled control (the app uses aria-disabled and keeps the button
   focusable so the reason can be read), so 1.4.3's exemption for disabled elements does not apply to
   it — and at .45 the label measured 2.70:1 in light and 3.61/3.86:1 in the two dark themes. Express
   "unavailable" with a muted token and a softened surface instead, which keeps the label legible for
   the person who has to work out WHY it is unavailable. */
.btn.is-disabled { color: var(--muted); background: var(--bg); border-color: var(--line); cursor: not-allowed; }
/* Flat view (R6, D-flat): fixed column budget — columns can never overpaint; long content
   ellipsises and the actions wrap inside their own guaranteed width. */
.rem-flat table { table-layout: fixed; }
.rem-flat th:nth-child(1) { width: 34px; }
.rem-flat th:nth-child(3) { width: 96px; }
.rem-flat th:nth-child(4) { width: 200px; }
/* D4-4 — THE ONLY COLUMN THAT NAMES THE PATIENT WAS THE ONE THAT SHRANK.
   This is the app's only table-layout:fixed table, and it reserved 650px of fixed width (34+96+200+320)
   for the checkbox, Due, Status and actions columns, leaving .fu-pi — the patient column — whatever
   was left. Measured on the booted app: 443px at 1366x768/100% (already clipping the amber
   "2 earlier missed" badge, i.e. the flag saying this person has been chased twice and missed, sliced
   in half at the DEFAULT resolution), 234px at 125% Windows scaling, 52px at 150%, and ZERO PIXELS at
   175% — a standard Windows 11 option on this panel. At that width every row in the sweep list is an
   anonymous "Due / Status / Snooze / Done" strip, and staff resolve, snooze and log contacts from it.
   The actions column loses its fixed width (it already wraps via .row-actions{flex-wrap:wrap}), and
   the patient column gets a floor in ch so it stops being the residual. */
.rem-flat th:nth-child(5) { width: auto; }
.rem-flat th:nth-child(2), .rem-flat td.fu-pi { min-width: 22ch; }
.rem-flat td.fu-pi { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.fu-contact-sub { margin-top: 2px; }

/* clickable brand → dashboard */
.topbar a.brand { text-decoration: none; cursor: pointer; }
.topbar a.brand:hover { color: var(--primary); }

/* global patient quick-search */
.gsearch-wrap { position: relative; flex: 0 1 380px; min-width: 190px; }
.gsearch { width: 100%; min-height: 36px; }
.gsearch-results { position: absolute; top: calc(100% + 4px); left: 0; right: 0; z-index: 60;
  background: var(--surface); border: 1px solid var(--line); border-radius: var(--radius);
  border: 1px solid var(--field-line); box-shadow: var(--elev-pop); max-height: 320px; overflow: auto; }
.gsearch-item { padding: 8px 12px; cursor: pointer; border-bottom: 1px solid var(--line); }
.gsearch-item:last-child { border-bottom: 0; }
.gsearch-item:hover, .gsearch-item:focus { background: var(--primary-50); outline: none; }

/* sortable + filterable tables */
th.sortable { cursor: pointer; user-select: none; }
th.sortable:hover { color: var(--text); }
th[data-sort] { color: var(--primary-700); }
.sort-ind { font-size: 0.625rem; }

/* questionnaire option buttons (segmented, pill-shaped, wrap on narrow screens) */
.q-item { padding: 10px 0; border-bottom: 1px solid var(--line); }
.q-item:last-of-type { border-bottom: 0; }
.q-text { margin-bottom: 8px; }
.opt-group { display: flex; flex-wrap: wrap; gap: 6px; }
.opt-btn { min-height: 34px; min-width: 46px; padding: 4px 16px; border: 1px solid var(--line);
  border-radius: 999px; background: var(--surface); color: var(--text); font-weight: 600; cursor: pointer; }
.opt-btn:hover { background: var(--primary-50); }
.opt-btn.sel { background: var(--primary); border-color: var(--primary); color: #fff; }
.opt-btn.sel:hover { background: var(--primary-700); }
:root[data-theme="dark"] .opt-btn.sel, :root[data-theme="rosegold"] .opt-btn.sel { color: #0F172A; }

/* Expandable-row height tween (fleet devices/masks, recall owners): grid-template-rows 0fr -> 1fr
   is the one CSS mechanism that animates to an unknown height. Vertical padding sits on .grow-pad
   so the closed state is truly zero-height; the tween itself only runs for motion-tolerant users. */
.grow-td { padding-top: 0; padding-bottom: 0; }
.grow-wrap { display: grid; grid-template-rows: 0fr; }
.grow-wrap.open { grid-template-rows: 1fr; }
.grow-inner { overflow: hidden; min-height: 0; }
.grow-pad { padding: var(--s3) 0; }
@media (prefers-reduced-motion: no-preference) { .grow-wrap { transition: grid-template-rows .22s ease; } }

tr.click { cursor: pointer; }
/* patient-name links inside tables — clear affordance without heavy underlines everywhere */
.plink { color: var(--primary-700); text-decoration: none; font-weight: 600; cursor: pointer; white-space: nowrap; /* keep a name on one line — never break "Christopher" mid-word or double the row height */
  max-width: 30ch; overflow: hidden; text-overflow: ellipsis; display: inline-block; vertical-align: bottom; } /* …but a pathological name ellipsises instead of widening the column (full name on hover) */
.phone-copy { font-size: 0.75rem; color: var(--primary-700); background: none; border: 0; padding: 0 2px; min-height: 0; cursor: pointer; }
.phone-copy:hover { text-decoration: underline; }
.plink:hover { text-decoration: underline; }

/* quick-link buttons */
.ql-grid { display: flex; flex-wrap: wrap; gap: 12px; margin-top: 14px; }
.ql-btn { display: inline-flex; align-items: center; justify-content: center; min-width: 170px; padding: 16px 22px;
  border: 1px solid var(--line); border-radius: var(--radius); background: var(--surface); color: var(--text);
  font-weight: 600; text-decoration: none; box-shadow: var(--shadow, 0 1px 2px rgba(0,0,0,.04)); }
.ql-btn:hover { background: var(--primary-50); border-color: var(--primary); color: var(--primary-700); }
.right { text-align: right; } .center { text-align: center; }

/* Status badges (always paired with a text label) ------------------------- */
.badge { display: inline-flex; align-items: center; gap: 6px; font-size: 0.75rem; font-weight: 600; padding: 2px 8px; border-radius: 999px; white-space: nowrap; }
.badge::before { content: ""; width: 8px; height: 8px; border-radius: 50%; background: currentColor; opacity: .9; }
.badge.green { color: #166534; background: var(--ok-bg); }
.badge.amber { color: #92400E; background: var(--warn-bg); }
.badge.red { color: #991B1B; background: var(--danger-bg); }
.badge.gray { color: #475569; background: #F1F5F9; }
.badge.blue { color: var(--primary-700); background: var(--primary-50); }
.pill { display: inline-block; font-size: 0.75rem; font-weight: 600; padding: 2px 8px; border-radius: 999px; color: #fff; }

/* Buttons & forms --------------------------------------------------------- */
button, .btn { font: inherit; font-weight: 600; border: 1px solid var(--line); background: var(--surface); color: var(--text);
  padding: 8px 14px; border-radius: var(--radius); cursor: pointer; min-height: 40px; }
button:hover { background: var(--primary-50); } /* themed token: readable in BOTH light and dark (a hardcoded light hex made dark-mode labels invisible on hover) */
.btn.primary { background: var(--primary); border-color: var(--primary); color: #fff; }
.btn.primary:hover { background: var(--primary-700); }
.btn.danger { background: var(--danger); border-color: var(--danger); color: #fff; }
.btn.danger:hover { background: var(--danger); filter: brightness(.92); } /* keep white-on-red readable (was falling through to the bare button:hover) */
.btn.sm { min-height: 32px; padding: 4px 10px; font-size: 0.8125rem; }
.btn[disabled] { opacity: .6; cursor: progress; }
/* An <a class="btn"> is inline by default, so min-height silently does nothing and the link renders
   shorter than the real buttons beside it — under the 44px touch target the rest of the UI keeps.
   inline-flex makes the 27 anchor-buttons match their <button> siblings. */
a.btn { display: inline-flex; align-items: center; justify-content: center; text-decoration: none; }
.bar { display: flex; gap: var(--s2); align-items: center; flex-wrap: wrap; margin-bottom: var(--s3); }
label { display: block; font-size: 0.8125rem; font-weight: 600; color: var(--muted); margin: var(--s3) 0 var(--s1); }
/* D4-8: --field-line, not --line. The field fill and its container fill are the same hex, so this
   border is the only thing identifying the control — see the token's definition for the numbers. */
input, select, textarea { font: inherit; width: 100%; padding: 9px 11px; border: 1px solid var(--field-line); border-radius: var(--radius);
  background: var(--surface); color: var(--text); min-height: 40px; }
/* Checkboxes/radios are NOT text fields — the rule above would stretch them to a 40px, full-width,
   padded box (and inflate every table row that has a select checkbox). Reset them to native size. */
input[type=checkbox], input[type=radio] { width: auto; min-height: 0; padding: 0; margin: 0; border: 0; }
/* Native size is ~13px — fiddly at a counter. 18px + the themed accent keeps them native but hittable. */
input[type=checkbox] { width: 18px; height: 18px; accent-color: var(--primary); cursor: pointer; }
input[type=radio] { width: 16px; height: 16px; accent-color: var(--primary); cursor: pointer; }
/* D4-2 — the focus ring, on the controls that needed it most.
   This rule used to REPLACE the app's ring: `input:focus` is (0,1,1) and beats the global
   `:focus-visible` at (0,1,0), on keyboard and mouse focus alike, substituting a 2px --primary-50
   halo that measures 1.21:1 (light) / 1.18:1 (dark) / 1.13:1 (rose gold) against --surface. That is
   under any perceptual threshold in all three themes, so what actually remained was a 1px border
   colour change — in an app where every button, tab, row and link gets a 2px ring at 2px offset, and
   where EVERY FORM IS A MODAL, so this was the focus indicator for essentially all data entry. A
   staff member tabbing the 14-field patient dialog at 125% scaling could not tell which field was
   focused from a metre back at the counter. WCAG 2.2 SC 2.4.11 wants a 2px perimeter.
   The border tint stays as a bonus cue; the ring comes back. */
input:focus, select:focus, textarea:focus { border-color: var(--focus); outline: none; }
input:focus-visible, select:focus-visible, textarea:focus-visible {
  border-color: var(--focus); outline: 2px solid var(--focus); outline-offset: 2px;
}
/* A4-45 — the paired fields stack when there is no room for two columns. This was the only .row2 rule
   in the stylesheet and no media block touched it — including the 760px mobile block, which collapses
   .kv but left this at two fixed columns at every width. Every add/edit dialog in the app uses it, so
   at 400% zoom (WCAG 1.4.10 Reflow, AA) the second column of each pair went off the edge. auto-fit
   rather than a breakpoint: a modal's width is not the viewport's, so the field pair should react to
   ITS box, not to the window. */
.row2 { display: grid; grid-template-columns: repeat(auto-fit, minmax(180px, 1fr)); gap: var(--s3); }
.err { color: var(--danger); font-size: 0.875rem; margin: var(--s2) 0; min-height: 1.2em; }
.empty { padding: var(--s6); text-align: center; color: var(--muted); }

/* Overlay / modal --------------------------------------------------------- */
/* The overlay is position:fixed at viewport height, so a dialog taller than the screen used to overflow
   with nothing able to scroll — the page can't (the overlay is fixed) and the overlay didn't. On a 1280x720
   counter PC that put "Save patient", "Book it" and "Save machine" entirely below the fold and unclickable.
   (Enter still submitted, via the footer's primary button — but nobody discovers that at the counter.)
   Two independent guards, because either alone leaves a gap:
     overflow-y on the overlay   — the dialog can always be scrolled to, whatever its height;
     max-height + column flex on the modal — it stays inside the screen and scrolls its BODY instead, so
       the footer with the submit button is pinned and visible without scrolling at all.
   align-items:flex-start matters here: centring a taller-than-viewport child in a scroll container clips
   its top OUT of the scrollable area, so the header becomes unreachable — the same bug moved upward. */
.overlay { position: fixed; inset: 0; background: var(--scrim); display: flex; align-items: flex-start; justify-content: center; z-index: 50; padding: 8vh 0 24px; overflow-y: auto; }
/* D4-9 — a BORDER, not just a scrim and a shadow.
   Measured after theming the scrim: dark still dims the page only 1.12:1 and rose gold 1.03:1, and it
   cannot do better — compositing black over a near-black page is arithmetic, not a tuning problem. So
   on a dark theme the dialog's edge came to 1.36:1 / 1.10:1 against the page behind it, and .modal had
   no border at all: its only edge cue was fill-vs-page plus a shadow that is itself invisible there.
   Every form in this app is a modal, and the affordance a scrim exists to give — "the rest of the app
   is unavailable" — was not being given.
   Measured after: the border reads 4.17:1 (dark) and 3.36:1 (rose gold) against the dimmed page,
   which is the failure closed. In LIGHT it composites to 1.00:1 against the scrim and effectively
   disappears — deliberately left, because light was never the failing case: a white dialog on a
   dimmed grey page is 3.21:1 by FILL, and the fill is the edge cue there. A border that adds nothing
   in one theme and fixes two others is the right trade; a per-theme border colour to make it visible
   in all three would be decoration. */
.modal { background: var(--surface); border: 1px solid var(--field-line); border-radius: 12px; width: min(560px, 92vw); box-shadow: var(--elev-modal);
  max-height: 100%; display: flex; flex-direction: column; }
.modal.wide { width: min(760px, 94vw); } /* content-heavy dialogs (e.g. the Heidi review) */
.modal .hd { padding: var(--s4); border-bottom: 1px solid var(--line); font-weight: 700; display:flex; align-items:center; flex: 0 0 auto; }
/* min-height:0 is what actually lets the body shrink and scroll — a flex item's default min-height:auto
   refuses to go below its content, which silently defeats overflow-y in a column flex container. */
.modal .bd { padding: var(--s4); overflow-y: auto; flex: 1 1 auto; min-height: 0; }
.modal .ft { padding: var(--s3) var(--s4); border-top: 1px solid var(--line); display: flex; justify-content: flex-end; gap: var(--s2); flex: 0 0 auto; }
.x { margin-left: auto; background: none; border: 0; font-size: 1.25rem; min-height: auto; padding: 0 6px; color: var(--muted); }

/* Login ------------------------------------------------------------------- */
/* A9-6 — THE THREE SCREENS OUTSIDE THE APP SHELL HAD NO SCROLL CONTAINER AT ALL.
   Login, first-run setup and the licence lock bypass shell() and append straight into #app, which is
   height:100vh; overflow:hidden — and html/body are height:100%, so the document cannot scroll
   either. A card taller than the viewport was therefore clipped equally top and bottom with no way to
   reach the hidden part. The @media(max-width:760px) escape hatch that restores #app{overflow:visible}
   is keyed on WIDTH, so it never fires in the failure band.
   What that cost, measured: a 1366x768 pharmacy laptop at Windows 150% scaling is ~910x430 CSS px —
   width 910, so the mobile rules stay off — and the ~550px first-run setup card loses its bottom 60px,
   which is the only "Create account & continue" button. First-run setup could not be completed at all.
   At 200% browser zoom the licence-lock card loses the block carrying "This computer's ID", which is
   exactly the string the pharmacist has to read out to obtain a key. WCAG 1.4.4 at AA.
   Same two-guard shape the .overlay/.modal rules already use, and for the same reason: scroll the
   wrapper, and start at the top rather than centring, or a tall card is centred out of reach. */
.login-wrap { min-height: 100vh; display: grid; place-items: center; overflow-y: auto; padding: 5vh 0 24px; background: linear-gradient(180deg, #DFECEF, #EEF3F6); }
@media (max-height: 700px) { .login-wrap { align-items: start; } }
.login { background: var(--surface); border: 1px solid var(--line); border-radius: 14px; box-shadow: var(--shadow); padding: var(--s6); width: min(380px, 92vw); }
.login h1 { text-align: center; }
.login .brand { display: flex; align-items: center; justify-content: center; gap: 9px; color: var(--primary-700); font-weight: 700; margin-bottom: var(--s3); }
/* Brand lockup: ascending-Zzz mark + "zeze" wordmark (Segoe UI, teal — the app is offline). */
.brand-mark { height: 26px; width: 26px; display: block; flex: 0 0 auto; }
.brand-word { font-weight: 600; letter-spacing: -.02em; font-size: 1.25rem; line-height: 1; }
.topbar .brand { gap: 8px; }
.login .brand .brand-mark { height: 36px; width: 36px; }
.login .brand .brand-word { font-size: 1.875rem; }

/* Toast ------------------------------------------------------------------- */
/* One fixed host stacks toasts bottom-centre (newest nearest the bottom, older pushed up) so they never
   overlap. pointer-events:none on the host keeps the empty column from blocking clicks. */
#toast-host { position: fixed; bottom: var(--s5); left: 50%; transform: translateX(-50%); z-index: 80;
  display: flex; flex-direction: column; align-items: center; gap: 8px; pointer-events: none;
  max-width: min(92vw, 480px); }
.toast { background: #0E1B2B; color: #fff; padding: 10px 16px; border-radius: 999px; font-size: 0.875rem; /* brand Midnight */
  box-shadow: var(--shadow); pointer-events: auto; max-width: 100%; text-align: center; }
/* Error toasts must not look like success — red, and they linger longer (set in toast()). */
.toast-err { background: #C5341E; font-weight: 600; }

/* Note bodies keep the line breaks / blank lines the author typed or pasted (e.g. from Heidi AI) instead of
   collapsing to one block; overflow-wrap stops a pasted URL blowing out the column. */
.note-body { white-space: pre-wrap; overflow-wrap: anywhere; }

/* Visually-hidden text for screen readers (e.g. the patient name on grouped reminder rows,
   where the visible cell is deliberately empty because the name sits on the group header). */
.sr-only { position: absolute; width: 1px; height: 1px; padding: 0; margin: -1px; overflow: hidden; clip: rect(0 0 0 0); white-space: nowrap; border: 0; }
/* Reminders (Round-3 targeted fixes): the bulk bar stays in view while scrolling a long queue;
   the flat "sweep" view runs denser rows than the grouped call view. */
.rem-bulk { position: sticky; top: 4px; z-index: 5; box-shadow: var(--shadow); }
.rem-flat td { padding-top: 5px; padding-bottom: 5px; }

/* Skip-to-content link — visually hidden until keyboard-focused (WCAG 2.4.1 bypass). */
.skip-link { position: absolute; left: -9999px; top: 0; z-index: 100; background: var(--primary); color: #fff;
  padding: 8px 14px; border-radius: 0 0 6px 0; font-weight: 600; text-decoration: none; }
.skip-link:focus { left: 8px; }
/* Dark and rose-gold --primary is a light tint there, so the link needs dark text (same rule as .nav a.active). */
:root[data-theme="dark"] .skip-link { color: #0F172A; }
:root[data-theme="rosegold"] .skip-link { color: #1A0E0F; }
.main:focus { outline: none; } /* focus moved here programmatically by the skip link — no ring on the whole pane */

/* Required-field marker + deep-link row highlight + plain topbar button ----- */
.req { color: var(--warn, #94620C); font-weight: 700; }
.hl-row > td { background: var(--primary-50) !important; box-shadow: inset 3px 0 0 var(--primary); }
button.btn-plain { border: 0; background: none; padding: 4px 6px; cursor: pointer; border-radius: 6px; }
button.btn-plain:hover { text-decoration: underline; background: var(--primary-50); }
/* Keyboard-focusable rows (rowKeyable) need a visible focus ring. */
tr.click:focus-visible, tr[tabindex]:focus-visible { outline: 2px solid var(--primary); outline-offset: -2px; }
/* Combobox: the arrow-key-highlighted option (aria-activedescendant target). */
.cb-active { outline: 2px solid var(--primary); outline-offset: -2px; background: var(--primary-50); }
/* SMS-consent tri-state control: yellow = not recorded, green = consented, red = declined.
   Fills are the -700 shades so the white button text meets WCAG AA (the -600s measured 2.8–4.0:1). */
.consent-dot { display: inline-block; width: 12px; height: 12px; border-radius: 50%; }
.consent-amber, .consent-dot.consent-amber { background: #B45309; }
.consent-green, .consent-dot.consent-green { background: #15803D; }
.consent-red,   .consent-dot.consent-red   { background: #B91C1C; }
.btn.consent-amber, .btn.consent-green, .btn.consent-red { color: #fff; border-color: transparent; font-weight: 600; }

/* Dropdown menu (Documents ▾, Sell / trial ▾, Copy SMS ▾) — same surface language as the bell. */
.menu-panel { position: absolute; left: 0; top: calc(100% + 6px); min-width: 210px; background: var(--surface); border: 1px solid var(--line);
  border-radius: 10px; border: 1px solid var(--field-line); box-shadow: var(--elev-panel); z-index: 60; padding: 4px; }
.menu-item { display: block; width: 100%; text-align: left; border: 0; background: none; font: inherit; font-weight: 500;
  padding: 8px 10px; border-radius: 6px; cursor: pointer; min-height: 0; color: var(--text); white-space: nowrap; }
.menu-item:hover { background: var(--primary-50); }
.menu-item[disabled] { color: var(--muted); cursor: default; white-space: normal; }
.menu-item[disabled]:hover { background: none; }

/* Patient screen (panel-pick layout) ---------------------------------------
   One identity card (facts + consent + appointments + due strip), a constant action row with
   44px targets, count chips on the tabs. At >=1280px the identity card pins to a left column
   while the tabs take the rest; below that it is the plain stack. */
.patient-actions .btn { min-height: 44px; }
.duebar { padding: 9px 16px; border-top: 1px solid var(--line); display: flex; gap: 8px; flex-wrap: wrap; align-items: center; }
.tabs .cnt { font-size: 0.75rem; font-weight: 600; color: var(--muted); background: var(--bg); border-radius: 999px; padding: 0 6px; margin-left: 5px; }
.tabs button.active .cnt { color: var(--primary-700); background: var(--primary-50); }
@media (min-width: 1280px) {
  .patient-cols { display: grid; grid-template-columns: 300px minmax(0, 1fr); gap: 14px; align-items: start; }
  .patient-left { position: sticky; top: 0; }
  .patient-left .kv { grid-template-columns: 105px 1fr; } /* the 160px label column doesn't fit a 300px card */
  /* Dashboard (panel-pick layout): action column left, planned column right. Below this width it
     stacks back into the familiar single column — the safe fallback IS the old layout. */
  .dash-cols { display: grid; grid-template-columns: minmax(0, 7fr) minmax(0, 5fr); gap: 14px; align-items: start; }
}

/* Tabs -------------------------------------------------------------------- */
.tabs { display: flex; gap: 2px; border-bottom: 1px solid var(--line); margin-bottom: var(--s4); flex-wrap: wrap; }
.tabs button { border: 0; border-bottom: 2px solid transparent; border-radius: 0; background: none; color: var(--muted); min-height: 36px; }
.tabs button.active { color: var(--primary-700); border-bottom-color: var(--primary); }
.kv { display: grid; grid-template-columns: 160px 1fr; gap: var(--s1) var(--s3); }
.kv div.k { color: var(--muted); font-size: 0.8125rem; }
.note { border-bottom: 1px solid var(--line); padding: var(--s3) 0; }
.note .meta { color: var(--muted); font-size: 0.75rem; }
.flag { background: var(--warn-bg); color: var(--warn); font-size: 0.75rem; font-weight: 700; padding: 1px 6px; border-radius: 4px; }
/* Dark theme ------------------------------------------------------------- */
:root[data-theme="dark"] {
  color-scheme: dark;
  --bg: #0F172A; --surface: #1E293B; --text: #E2E8F0; --muted: #94A3B8; --line: #334155;
  --primary: #22D3EE; --primary-700: #67E8F9; --primary-50: #15384A;
  --ok: #4ADE80; --ok-bg: #14321F; --warn: #FBBF24; --warn-bg: #3A2C0A; --warn-accent: #FBBF24; --danger: #F87171; --danger-bg: #3A1414;
  --focus: #22D3EE;
  --shadow: 0 1px 2px rgba(0,0,0,.4), 0 1px 3px rgba(0,0,0,.5);
  --scroll-shadow: rgba(0,0,0,.6); /* table-edge scroll hint — near-invisible at the light value */
  /* D4-9 — the same reasoning as the two lines above, applied to the six elevation values that were
     left inline. rgb(15,23,42) IS this theme's --bg, so the light scrim dimmed the page by exactly
     nothing (measured 1.00:1) and the modal's edge against the page came to 1.22:1. Pure black at a
     higher alpha is what actually reads on a dark ground. */
  --field-line: #63748A; /* D4-8: 3.06:1 on --surface, 3.74:1 on --bg */
  --scrim: rgba(0,0,0,.62);
  --elev-modal: 0 20px 50px rgba(0,0,0,.70);
  --elev-panel: 0 8px 30px rgba(0,0,0,.60);
  --elev-pop: 0 8px 24px rgba(0,0,0,.60);
  --elev-hover: 0 2px 8px rgba(0,0,0,.50);
}
:root[data-theme="dark"] .nav a.active, :root[data-theme="dark"] .btn.primary, :root[data-theme="dark"] .pill,
:root[data-theme="dark"] .btn.danger { color: #0F172A; } /* --danger is LIGHT in this theme; white text measured 2.77:1 */
:root[data-theme="dark"] tbody tr:hover { background: #243044; }
:root[data-theme="dark"] .login-wrap { background: linear-gradient(180deg, #0B2530, #0F172A); }
:root[data-theme="dark"] .badge.green { color: #86EFAC; }
:root[data-theme="dark"] .badge.amber { color: #FCD34D; }
:root[data-theme="dark"] .badge.red { color: #FCA5A5; }
:root[data-theme="dark"] .badge.blue { color: #67E8F9; }
:root[data-theme="dark"] .badge.gray { color: #CBD5E1; background: #334155; }
:root[data-theme="dark"] .flag { background: #3A2C0A; }

/* "Black & rose gold" ------------------------------------------------------
   Renamed from "Leanne mode" (token: leanne) on 2026-07-31. currentTheme() migrates any
   localStorage value still set to the old token. */
:root[data-theme="rosegold"] {
  color-scheme: dark;
  --bg: #0A0708; --surface: #17100F; --text: #F2E3DC; --muted: #BBA09A; --line: #3B2C2A;
  --primary: #B76E79; --primary-700: #D9A0A0; --primary-50: #2A1A1B;
  --ok: #84CBA3; --ok-bg: #16271D; --warn: #E2B585; --warn-bg: #2E2314; --warn-accent: #E2B585; --danger: #E88A86; --danger-bg: #2E1615;
  --focus: #D9A0A0;
  --shadow: 0 1px 2px rgba(0,0,0,.55), 0 1px 4px rgba(0,0,0,.65);
  --scroll-shadow: rgba(0,0,0,.6);
  /* D4-9: rose gold is darker still — the dialog edge measured 1.02:1 against the scrimmed page. */
  --field-line: #745C57; /* D4-8: 3.05:1 on --surface, 3.26:1 on --bg */
  --scrim: rgba(0,0,0,.68);
  --elev-modal: 0 20px 50px rgba(0,0,0,.78);
  --elev-panel: 0 8px 30px rgba(0,0,0,.68);
  --elev-pop: 0 8px 24px rgba(0,0,0,.68);
  --elev-hover: 0 2px 8px rgba(0,0,0,.55);
}
:root[data-theme="rosegold"] .nav a.active, :root[data-theme="rosegold"] .btn.primary, :root[data-theme="rosegold"] .pill,
:root[data-theme="rosegold"] .btn.danger { color: #1A0E0F; } /* white text measured 2.50:1 here */
:root[data-theme="rosegold"] tbody tr:hover { background: #241A1A; }
:root[data-theme="rosegold"] .login-wrap { background: linear-gradient(180deg, #1C0F12, #0A0708); }
:root[data-theme="rosegold"] .badge.green { color: #A6E3C2; }
:root[data-theme="rosegold"] .badge.amber { color: #F2C896; }
:root[data-theme="rosegold"] .badge.red { color: #F2ABA7; }
:root[data-theme="rosegold"] .badge.blue { color: #E6BBC0; }
:root[data-theme="rosegold"] .badge.gray { color: #DBC7C0; background: #3B2C2A; }
:root[data-theme="rosegold"] .flag { background: #2E2314; }

/* ---------- calendar (appointment book) ---------- */
.cal-grid { display: grid; gap: 0; border: 1px solid var(--line); border-radius: var(--radius); overflow: hidden; background: var(--surface); }
.cal-headrow { display: contents; }
.cal-head { padding: 8px 6px; text-align: center; font-weight: 600; font-size: 0.8125rem; border-bottom: 1px solid var(--line); border-left: 1px solid var(--line); background: var(--bg); }
.cal-head.today { color: var(--primary-700); box-shadow: inset 0 -2px 0 var(--primary); } /* -700: teal-on-Cloud misses AA */
.cal-head .sub { font-weight: 400; }
.cal-timecol { position: relative; border-left: 0; font-size: 0.75rem; color: var(--muted); }
.cal-timecol .tick { position: absolute; right: 6px; transform: translateY(-50%); background: var(--surface); padding: 0 2px; }
.cal-col { position: relative; border-left: 1px solid var(--line); cursor: pointer; }
.cal-col.today { background: var(--primary-50); }
.cal-line { position: absolute; left: 0; right: 0; border-top: 1px solid var(--line); pointer-events: none; }
.cal-now { position: absolute; left: 0; right: 0; border-top: 2px solid var(--danger); pointer-events: none; z-index: 3; }
.cal-appt { position: absolute; overflow: hidden; border-radius: 6px; padding: 2px 6px; font-size: 0.75rem; line-height: 1.25;
  border-left: 3px solid var(--type-colour, var(--primary)); background: color-mix(in srgb, var(--type-colour, var(--primary)) 14%, var(--surface));
  cursor: pointer; z-index: 2; }
.cal-appt:hover { z-index: 4; box-shadow: var(--shadow); }
/* Match the hover bump on focus, or the 2px ring at outline-offset:2px is painted over by the
   adjacent block and the keyboard user cannot see where they are. */
.cal-appt:focus-visible { z-index: 5; }
/* Flex so the status chip is never what gets truncated: the name shrinks and ellipsises, the chip
   holds its width. A clipped status word is the same defect as no status word. */
.cal-appt .t { font-weight: 600; display: flex; align-items: baseline; gap: 4px; min-width: 0; }
.cal-appt .t .nm { flex: 0 1 auto; min-width: 0; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.cal-appt .w { color: var(--muted); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
/* Fade the CONTENT, not the block. These two used `opacity` on .cal-appt, which cascades to every
   child — including the status chip added below, measured at 2.94:1 (done) and 2.08:1 (cancelled)
   against a 4.5:1 AA floor. No child style can escape a parent's opacity, so the recede is done with
   muted text and a softened tint instead: the block still steps back, but the one word explaining
   WHY it has stepped back stays readable. */
.cal-appt.st-done { background: color-mix(in srgb, var(--type-colour, var(--primary)) 6%, var(--surface));
  border-left-color: color-mix(in srgb, var(--type-colour, var(--primary)) 40%, var(--surface)); }
.cal-appt.st-done .nm, .cal-appt.st-done .w { color: var(--muted); }
.cal-appt.st-cancelled { background: var(--surface); border-left-color: var(--line); }
.cal-appt.st-cancelled .nm, .cal-appt.st-cancelled .w { color: var(--muted); text-decoration: line-through; }
.cal-appt.st-dna { border-left-color: var(--danger); background: color-mix(in srgb, var(--danger) 12%, var(--surface)); }
.cal-appt.st-arrived { outline: 2px solid color-mix(in srgb, var(--type-colour, var(--primary)) 55%, var(--surface)); }
/* The status as a WORD on the block. Everything else that distinguishes a status here is colour or
   opacity (WCAG 1.4.1), and the only place it was named was the mouse-only title tooltip. Sits
   inside .t, which is the one line that renders at every block height. */
/* GEOMETRY ONLY — the colour comes from the .badge tone class beside it, so the chip inherits the
   whole themed badge palette (including the per-theme text overrides at :root[data-theme] below)
   for free. An earlier version rolled its own colours and failed AA in dark and rose-gold: --danger
   and --ok are LIGHT foreground colours in those themes, so white text on them measured 2.77:1 and
   3.32:1 (dark) and 2.50:1 and 3.58:1 (rose-gold). Never give this chip a private palette. */
.cal-appt .st-chip { flex: 0 0 auto; font-size: 0.625rem; padding: 0 4px; border-radius: 3px; gap: 0; }
/* .st-cancelled inherits the block's line-through; re-stating it on the chip reads as struck-out
   twice, so the chip only carries the word. */
.cal-appt.st-cancelled .st-chip { text-decoration: none; }
.cal-closed { display: grid; place-items: center; color: var(--muted); font-size: 0.8125rem; }

/* Micro-interactions ------------------------------------------------------
   Subtle, feedback-oriented motion only. EVERYTHING here is gated behind
   prefers-reduced-motion: no-preference, so a user (or clinic PC) that asks the OS
   for reduced motion gets an instant, completely static UI. Durations are short
   (90–220ms) and movements tiny (1–10px): press feedback, a gentle screen settle,
   and soft entrances for modals/toasts — nothing that draws the eye during real work. */
@media (prefers-reduced-motion: no-preference) {
  /* Theme switch melts rather than slams: the shell's colour-bearing surfaces cross-fade. Listed
     BEFORE the interactive rule below so hover/press transitions still win on elements in both. */
  body, .topbar, .nav, .appfoot, .card, .tile, .login-wrap { transition: background-color .22s ease, color .22s ease, border-color .22s ease; }
  /* Smooth every hover / focus / colour change on interactive elements (they used to snap). */
  button, .btn, .nav a, .opt-btn, .ql-btn, .tile.click, .plink, .phone-copy, .tabs button,
  input, select, textarea, .badge, .notif-item, .gsearch-item, .x, a, tbody tr {
    transition: background-color .14s ease, border-color .14s ease, color .14s ease,
      box-shadow .16s ease, transform .09s ease, opacity .14s ease, filter .14s ease;
  }
  /* Press feedback: a gentle push-down so a click feels registered. */
  button:not([disabled]):active, .btn:not([disabled]):active,
  .opt-btn:active, .ql-btn:active, .nav a:active, .tabs button:active { transform: translateY(1px); }
  /* Hover lift on the raised, clickable surfaces. */
  .tile.click:hover, .ql-btn:hover { transform: translateY(-1px); }
  /* A new screen settles into place; the login card and modals ease up; toasts rise. */
  .main  { animation: zzIn    .18s ease both; }
  .login { animation: zzUp    .24s cubic-bezier(.2,.7,.3,1) both; }
  .overlay { animation: zzFade .16s ease both; }
  .modal   { animation: zzUp   .2s  cubic-bezier(.2,.7,.3,1) both; }
  .toast   { animation: zzToast .22s cubic-bezier(.2,.7,.3,1) both; }
  .notif-panel { animation: zzDrop .15s ease both; }
}
@keyframes zzIn    { from { opacity: 0; transform: translateY(6px); }  to { opacity: 1; transform: none; } }
@keyframes zzUp    { from { opacity: 0; transform: translateY(-8px) scale(.985); } to { opacity: 1; transform: none; } }
@keyframes zzFade  { from { opacity: 0; } to { opacity: 1; } }
@keyframes zzToast { from { opacity: 0; transform: translateY(10px); } to { opacity: 1; transform: none; } }
@keyframes zzDrop  { from { opacity: 0; transform: translateY(-5px); } to { opacity: 1; transform: none; } }

/* Counter/tablet widths: the 208px nav + 24px gutters left the worklist tables clipping their
   rightmost action buttons behind an unhinted scroll at 1024px. Tighten the chrome before content. */
@media (max-width: 1100px) {
  .shell { grid-template-columns: 168px minmax(0, 1fr); }
  .topbar .brand { flex-basis: 168px; } /* keep the brand column and its divider aligned with the nav */
  .main { padding: var(--s3); }
  th, td { padding: var(--s3); }
}
@media (max-width: 760px) {
  /* On a phone the fixed app-shell gives way to natural page scroll: the nav becomes a scrollable top
     strip and the whole document scrolls again. */
  #app { height: auto; overflow: visible; }
  .shell { grid-template-columns: minmax(0, 1fr); overflow: visible; }
  /* The nav strip holds 14 items in a phone width — the same scroll-shadow trick as .table-wrap
     signals the hidden ones (links are transparent, so the wrapper's shadow shows through). */
  .nav { display: flex; gap: 4px; overflow-x: auto; height: auto;
    background:
      linear-gradient(90deg, var(--surface) 45%, transparent) left / 28px 100%,
      linear-gradient(270deg, var(--surface) 45%, transparent) right / 28px 100%,
      radial-gradient(farthest-side at 0 50%, var(--scroll-shadow, rgba(15,23,42,.22)), transparent) left / 12px 100%,
      radial-gradient(farthest-side at 100% 50%, var(--scroll-shadow, rgba(15,23,42,.22)), transparent) right / 12px 100%;
    background-repeat: no-repeat;
    background-attachment: local, local, scroll, scroll; background-color: var(--surface); }
  .nav-sec { display: none; } /* headings would eat width in the horizontal strip */
  .main { overflow: visible; } .kv { grid-template-columns: 1fr; }
  /* The sidebar collapses to a top strip on mobile, so the fixed 208px brand column no longer aligns —
     let it size to its text, drop the divider, and allow the top bar to wrap. */
  .topbar { flex-wrap: wrap; padding: var(--s2) var(--s3); }
  .topbar .brand { flex: 0 0 auto; align-self: center; border-right: 0; padding: 0; justify-content: flex-start; } }
/* A7-23 — a header that exists only on paper. The Reports screen already has its own <h1> and period
   chips, so repeating the clinic name on screen would be noise; on PAPER it is the difference between
   a filed document and four pages of anonymous figures. */
.print-head { display: none; }
/* A7-11 — data that exists only on paper, for a cell whose on-screen content is a control. The print
   block hides every <button>, so a column whose only content was a button printed empty. */
.print-only { display: none; }
@media print {
  .print-head { display: block !important; }
  .print-only { display: inline-block !important; }
  /* .jumpnav joins the controls: on paper an in-page anchor list navigates nowhere. */
  .topbar, .nav, .bar, .btn, button, .appfoot, .jumpnav { display: none !important; }
  /* Undo the fixed app-shell so the full document flows onto paper instead of one viewport-height pane. */
  #app { height: auto; overflow: visible; display: block; }
  .shell { display: block; overflow: visible; } .main { max-width: none; overflow: visible; }
  /* Paper is white regardless of the on-screen theme — force the LIGHT palette so Dark / Black-&-rose-gold users
     don't print pale text on a white page. */
  /* D4-1 — ALL fifteen colour tokens, plus the twelve selector-level per-theme overrides.
     This block reset eight. The seven it missed are the status colours, so on a Dark or
     Black-&-rose-gold machine the Reports page printed like this, measured on paper:
       .badge.blue kept its dark literal #67E8F9 on white = 1.45:1 (rose gold #E6BBC0 = 1.72:1),
       which renders as roughly #D6D6D6 on a mono office laser — "Active: 412" and every stalled
       stage badge came out as EMPTY PILLS;
       .badge.green/amber/red/gray kept their unreset dark *-bg fills (#14321F, #3A2C0A, #3A1414,
       #334155), printing as solid near-black ink blocks;
       the stripe-red bar marking a high-severity stalled sleep test, and the tile borders, measured
       --danger 2.77:1 / --warn-accent 1.67:1 / --ok 1.74:1 against a 3:1 non-text floor.
     Exactly the opposite of this block's own stated intent, two lines up.
     The badge colours cannot be fixed by resetting tokens alone, because those rules set literal hex
     on .badge.green etc. — so they are reset here by selector, which is why this block is longer than
     a token list.
     TEST WITH "BACKGROUND GRAPHICS" OFF. That is the browser default and the worse case: the fills
     vanish and only the text colour carries the meaning. */
  :root, :root[data-theme="dark"], :root[data-theme="rosegold"] {
    --bg: #FFFFFF; --surface: #FFFFFF; --text: #1C2733; --muted: #5A6B78; --line: #B9C2C8;
    --primary: #0E7C8B; --primary-700: #0B4F63; --primary-50: #FFFFFF;
    --ok: #14682D; --ok-bg: #FFFFFF; --warn: #7A5009; --warn-bg: #FFFFFF; --warn-accent: #7A5009;
    --danger: #A32717; --danger-bg: #FFFFFF; --focus: #0B4F63;
    --field-line: #6B7885; /* 4.52:1 on paper */
    --scrim: transparent; --elev-modal: none; --elev-panel: none; --elev-pop: none; --elev-hover: none;
    --shadow: none; --scroll-shadow: transparent;
  }
  /* The twelve selector-level colour rules the token reset cannot reach. Ink-on-white, each measured
     at or above 6.8:1 as text (green 6.89, amber 7.05, red 7.34, blue 9.08, gray 7.61).
     Being straight about what this does NOT achieve: on a mono laser these five are within 0.04 of
     each other in relative luminance, so they print as much the same grey. That is acceptable here
     and only here, because none of these badges is colour-only — each one carries its own words
     ("Overdue 5d", "Due today", "Closed"), and the border makes the pill visible even with the
     browser's default "Background graphics: off". Colour is reinforcement on paper, not the message. */
  .badge, .flag, .pill {
    background: #FFFFFF !important; border: 1px solid #6B7885 !important; color: #1C2733 !important;
  }
  .badge.green, :root[data-theme="dark"] .badge.green, :root[data-theme="rosegold"] .badge.green { color: #14682D !important; border-color: #14682D !important; }
  .badge.amber, :root[data-theme="dark"] .badge.amber, :root[data-theme="rosegold"] .badge.amber { color: #7A5009 !important; border-color: #7A5009 !important; }
  .badge.red,   :root[data-theme="dark"] .badge.red,   :root[data-theme="rosegold"] .badge.red   { color: #A32717 !important; border-color: #A32717 !important; }
  .badge.blue,  :root[data-theme="dark"] .badge.blue,  :root[data-theme="rosegold"] .badge.blue  { color: #0B4F63 !important; border-color: #0B4F63 !important; }
  .badge.gray,  :root[data-theme="dark"] .badge.gray,  :root[data-theme="rosegold"] .badge.gray  { color: #4A5560 !important; border-color: #6B7885 !important; }
  :root[data-theme="dark"] .flag, :root[data-theme="rosegold"] .flag { background: #FFFFFF !important; }
  /* Row and tile status marks are non-text: a 3:1 floor, and a printed hairline that survives
     "Background graphics off" because a border is not a background. */
  tr.stripe-red { box-shadow: inset 3px 0 0 #A32717 !important; }
  .tile.red { border-left-color: #A32717 !important; } .tile.amber { border-left-color: #7A5009 !important; }
  .tile.green { border-left-color: #14682D !important; }
  :root[data-theme="dark"] tbody tr:hover, :root[data-theme="rosegold"] tbody tr:hover { background: #FFFFFF !important; }
  body { background: #FFFFFF; }
  /* Wide GP tables (compliance/therapy: Nights ≥ 4h %, Leak 95th, AHI…) sit inside .table-wrap{overflow-x:auto}
     for on-screen scroll — on paper that clips the right-hand columns. Let them reflow, keep rows intact,
     and repeat the header on each page. */
  .table-wrap { overflow: visible !important; background: none !important; } /* no scroll-shadow smudges on paper */
  table { page-break-inside: auto; } tr { page-break-inside: avoid; } thead { display: table-header-group; }
}

/* Follow-up card verbs (v3.47.0): Contact · Done/Replaced · Snooze · Dismiss ------------------------
   One width for the verbs, so the columns line up from row to row: "Snooze" used to land in a different
   spot depending on whether "Done" or "Replaced" sat beside it. rem, so the browser's text-size
   preference scales the button with its label (D4-16). */
.btn.act-fixed { min-width: 5.5rem; justify-content: center; }
.row-actions .btn.act-fixed, .fu-acts .btn.act-fixed { padding-left: 6px; padding-right: 6px; }
.btn.contact { border-color: var(--primary); color: var(--primary-700); font-weight: 600; }
.btn.contact:hover { background: var(--primary-50); }
.btn.quiet { color: var(--muted); }
.btn.quiet:hover { color: var(--danger); border-color: var(--danger); }

/* Contact dialog ---------------------------------------------------------------------------------- */
.cw-who { display: flex; flex-wrap: wrap; align-items: center; gap: 6px 12px; padding: 10px 12px; border: 1px solid var(--line); border-radius: var(--radius); background: var(--bg); margin-bottom: var(--s3); }
.cw-who .cw-name { font-weight: 600; font-size: 1rem; }
.cw-who .cw-ctx { flex-basis: 100%; color: var(--muted); font-size: 0.8125rem; }
.cw-tabs { display: flex; gap: 2px; border-bottom: 1px solid var(--line); margin-bottom: var(--s3); flex-wrap: wrap; }
.cw-tabs [role=tab] { border: 0; border-bottom: 2px solid transparent; border-radius: 0; background: none; color: var(--muted); min-height: 40px; padding: 6px 12px; font-weight: 600; }
.cw-tabs [role=tab][aria-selected=true] { color: var(--primary-700); border-bottom-color: var(--primary); }
.cw-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(280px, 1fr)); gap: var(--s4); align-items: start; }
.cw-grid > * > :first-child > label { margin-top: 0; } /* only the FIRST field of each column loses its top margin; every field label is :first-child of its own wrapper */
.cw-meter { display: flex; flex-wrap: wrap; gap: 4px 8px; color: var(--muted); font-size: 0.8125rem; margin-top: 4px; min-height: 1.2em; }
.cw-meter .over { color: var(--danger); font-weight: 600; }
.cw-chips { display: flex; flex-wrap: wrap; gap: 6px; margin-top: 6px; }
.cw-chip { display: inline-flex; flex-direction: column; align-items: flex-start; min-height: 40px; padding: 3px 8px; border: 1px solid var(--line); border-radius: 8px; background: var(--surface); color: var(--text); font-size: 0.75rem; line-height: 1.3; text-align: left; cursor: pointer; }
.cw-chip code { font-family: ui-monospace, Consolas, monospace; font-size: 0.75rem; color: var(--primary-700); }
.cw-chip small { color: var(--muted); max-width: 11rem; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.cw-chip:hover { border-color: var(--primary); background: var(--primary-50); }
.cw-phone { border: 1px solid var(--line); border-radius: 18px; padding: 12px 12px 14px; background: var(--surface); box-shadow: var(--shadow); }
.cw-phone .to { color: var(--muted); font-size: 0.75rem; text-align: center; margin-bottom: 10px; }
.cw-bubble { background: var(--primary-50); color: var(--text); border-radius: 16px 16px 16px 4px; padding: 10px 12px; white-space: pre-wrap; overflow-wrap: anywhere; font-size: 0.9375rem; line-height: 1.45; max-width: 92%; min-height: 2.6em; }
.cw-unfilled { background: var(--warn-bg); color: var(--warn); border-radius: 4px; padding: 0 3px; font-family: ui-monospace, Consolas, monospace; font-size: 0.875em; }
.cw-note { border-radius: var(--radius); padding: 8px 12px; font-size: 0.875rem; margin-top: var(--s3); border: 1px solid transparent; display: flex; flex-wrap: wrap; align-items: center; gap: 6px 10px; }
.cw-note.amber { background: var(--warn-bg); color: var(--warn); border-color: var(--warn-accent); }
.cw-note.red { background: var(--danger-bg); color: var(--danger); border-color: var(--danger); }
.cw-note.green { background: var(--ok-bg); color: var(--ok); border-color: var(--ok); }
.cw-outcomes { display: grid; grid-template-columns: repeat(auto-fit, minmax(210px, 1fr)); gap: var(--s2); margin: var(--s3) 0; }
.cw-outcome { min-height: 56px; text-align: left; padding: 8px 12px; display: flex; flex-direction: column; align-items: flex-start; justify-content: center; gap: 2px; }
.cw-outcome small { color: var(--muted); font-weight: 400; font-size: 0.75rem; }
.cw-done { text-align: center; padding: var(--s5) var(--s3); }
.cw-done .tick { width: 56px; height: 56px; border-radius: 50%; background: var(--ok-bg); color: var(--ok); display: inline-flex; align-items: center; justify-content: center; font-size: 1.75rem; font-weight: 700; margin-bottom: var(--s3); }
.cw-done h3 { margin: 0 0 6px; font-size: 1.125rem; }
.cw-done p { margin: 4px 0 0; color: var(--muted); }

/* Follow-ups bulk bar (v3.47.0): ALWAYS in the layout, so the first tick never pushes the list down.
   Idle = nothing ticked: quieter fill, dashed edge, greyed verbs (aria-disabled, still focusable). */
.rem-bulk { display: flex; background: var(--primary-50); padding: 8px 12px; border-radius: 8px; min-height: 48px; }
.rem-bulk.idle { background: var(--bg); border: 1px dashed var(--line); }
.rem-bulk.idle .sub { color: var(--muted); }
.rem-bulk.idle { box-shadow: none; }
/* A greyed verb is grey whatever its normal colour: .btn.danger and .btn.primary sit later in this
   file than .btn.is-disabled at the same specificity, so an idle "Mark done / replaced" stayed red. */
.btn.danger.is-disabled, .btn.primary.is-disabled, .btn.contact.is-disabled { background: var(--bg); border-color: var(--line); color: var(--muted); filter: none; font-weight: 500; }

/* Vertical rhythm (v3.47.0) ------------------------------------------------------------------------
   A control bar that directly follows form fields gets the same 12px the labels above them have. Four
   Settings cards had their Save button touching the last input: .bar carries a bottom margin and no
   top one, and an input has no bottom margin, so nothing separated them. */
.row2 + .bar, div:has(> label) + .bar, input + .bar, select + .bar, textarea + .bar, .table-wrap + .bar, table + .bar { margin-top: var(--s3); }
/* An inline error under a control INSIDE A TABLE CELL reserves no height until it has something to
   say. The reserved line (min-height + margin, right for a dialog where the form must not jump) made
   that cell taller than its neighbours, and vertical-align: middle then floated the input 16px above
   the controls beside it (Settings > Reminders > check-in prompts). */
td > .err { margin: 4px 0 0; min-height: 0; }
td > .err:empty { display: none; }
/* The "add one" row at the foot of an editable table: same columns as the rows above, quieter fill. */
tr.add-row td { background: var(--bg); border-bottom: 0; }

/* Consent block on the patient record (v3.47.0): a setting with a state, not a dot and three loose buttons. */
.consent { margin-top: 12px; padding-top: 12px; border-top: 1px solid var(--line); }
.consent-head { display: flex; align-items: center; justify-content: space-between; gap: 8px; margin-bottom: 8px; }
.consent-title { font-weight: 600; }
.consent-note { margin: 8px 0 0; font-size: 0.8125rem; }
/* Segmented control: one pill, the chosen segment filled in its state colour. */
.seg { display: inline-flex; max-width: 100%; border: 1px solid var(--field-line); border-radius: 999px; overflow: hidden; background: var(--surface); }
.seg-btn { border: 0; border-radius: 0; background: none; color: var(--muted); font-weight: 600; font-size: 0.8125rem; min-height: 36px; padding: 6px 14px; }
.seg-btn + .seg-btn { border-left: 1px solid var(--line); }
.seg-btn:hover { background: var(--bg); color: var(--text); }
.seg-btn:focus-visible { outline-offset: -3px; }
.seg-btn[aria-checked=true].yes { background: var(--ok-bg); color: var(--ok); }
.seg-btn[aria-checked=true].no { background: var(--danger-bg); color: var(--danger); }
.seg-btn[aria-checked=true].none { background: var(--warn-bg); color: var(--warn); }
/* .cw-note is display:flex, which beats the browser's [hidden]{display:none} — so an emptied note
   painted as a bare amber bar under the Email and Log-a-call tabs. */
.cw-note[hidden] { display: none; }
