/* Design tokens (colors, spacing, radii, shadows, fonts) now live in
   static/css/tokens.css, which base.html loads immediately before this
   file. They used to be declared in a :root block right here, and a second,
   diverging copy lived in public.css for the marketing pages -- including a
   different brand teal (#0d6e6e vs this side's #1E9A83) and a duplicate teal
   tint. See the header comment in tokens.css for the full reasoning and for
   which token is safe in which role; the short version is that --primary is
   decorative only, --primary-strong is for white text on a filled surface,
   and --primary-text is for teal words.

   Nothing else in this file changed as part of that move: every var(--...)
   reference below resolves to the same value it always did, with the two
   deliberate exceptions noted at .btn-primary and --primary-text. */

* { box-sizing: border-box; }

/* Off-screen but still announced. For live regions whose text is only useful
   to a screen reader -- the guide's search result count, for instance, where
   the filtered list already shows a sighted user what matched.

   Deliberately NOT display:none or visibility:hidden: both remove the element
   from the accessibility tree entirely, so a live region using them announces
   nothing at all. That is the classic way this pattern gets silently broken.
   clip-path over a 1px box keeps it in the tree while taking it out of view. */
.sr-only {
  position: absolute !important;
  width: 1px; height: 1px;
  margin: -1px; padding: 0;
  overflow: hidden;
  clip-path: inset(50%);
  white-space: nowrap;
  border: 0;
}

/* ---------- Skip link ----------
   Off-screen until focused, then pinned to the top-left. Positioned rather
   than clip/width:1px so it can actually be seen and read once it takes focus.
   The signed-in shell puts up to 14 sidebar links before the page content, so
   a sighted keyboard user was tabbing through all of them on every single page
   load; the patient-facing pages had no landmark at all to skip to. */
.skip-link {
  position: absolute; left: -9999px; top: 0; z-index: 100;
  background: var(--primary-strong); color: #fff;
  padding: 0.7rem 1.1rem; border-radius: 0 0 var(--radius-sm) 0;
  font-size: 0.9rem; font-weight: 600; text-decoration: none;
}
.skip-link:focus {
  left: 0;
  outline: 2px solid var(--primary-dark); outline-offset: 2px;
}
.skip-link:hover { text-decoration: underline; }

body {
  margin: 0;
  background: var(--bg);
  color: var(--ink);
  font-family: 'Inter', -apple-system, BlinkMacSystemFont, sans-serif;
  font-size: 15px;
  line-height: 1.55;
  /* Safety net: nothing should legitimately need to push the page wider
     than the viewport (see the mobile nav overflow bug this was added
     alongside) -- if something does overflow anyway, clip it instead of
     letting the whole page shift/scroll sideways on mobile. */
  overflow-x: hidden;
}

h1, h2, h3 {
  font-family: var(--font-heading);
  /* 600, not 700. Headings, every button, every name and every table header
     were all bold, so a screen full of labels read as heavy. One step down
     across the board (see .btn, .table th, .tag) at the same sizes. */
  font-weight: 600;
  letter-spacing: -0.015em;
  margin: 0;
  color: var(--primary-dark);
}

h1 { font-size: 1.5rem; }
h2 { font-size: 1.08rem; margin-bottom: 1rem; }
h3 { font-size: 1rem; margin: 0 0 0.35rem; }

.mono { font-family: 'IBM Plex Mono', monospace; letter-spacing: -0.01em; }

a { color: var(--primary-text); text-decoration: none; transition: color 0.15s ease; }
a:hover { text-decoration: underline; }

/* ---------- Shell layout ---------- */
.shell { display: flex; min-height: 100vh; }

.sidebar {
  width: 248px;
  background: linear-gradient(180deg, #0D332E 0%, #081F1C 100%);
  color: #EAF2F0;
  display: flex;
  flex-direction: column;
  padding: 1.2rem 0.9rem;
  flex-shrink: 0;
}

.sidebar-top { display: flex; align-items: center; justify-content: space-between; margin-bottom: 1.4rem; }
.brand { display: flex; align-items: center; gap: 0.65rem; padding: 0 0.3rem; text-decoration: none; min-width: 0; }
.brand:hover .brand-name { color: #fff; opacity: 0.85; }
.brand-name { font-family: var(--font-heading); font-weight: 700; font-size: 1.04rem; line-height: 1.2; letter-spacing: -0.02em; color: #fff; }

/* Hamburger toggle -- hidden entirely outside the mobile breakpoint below,
   where the sidebar is a top bar instead of a full-height column and needs
   a way to reveal the nav/footer without them taking up permanent space. */
.nav-toggle {
  display: none; flex-direction: column; justify-content: center; align-items: center;
  gap: 4px; width: 34px; height: 34px; flex-shrink: 0;
  background: none; border: none; border-radius: var(--radius-sm); cursor: pointer; padding: 0;
}
.nav-toggle:hover { background: rgba(234, 242, 240, 0.07); }
.nav-toggle-bar { display: block; width: 19px; height: 2px; border-radius: 2px; background: #EAF2F0; transition: transform 0.2s ease, opacity 0.2s ease; }
.sidebar.nav-open .nav-toggle-bar:nth-child(1) { transform: translateY(6px) rotate(45deg); }
.sidebar.nav-open .nav-toggle-bar:nth-child(2) { opacity: 0; }
.sidebar.nav-open .nav-toggle-bar:nth-child(3) { transform: translateY(-6px) rotate(-45deg); }

.nav { display: flex; flex-direction: column; gap: 0.15rem; flex: 1; }
.nav-link {
  display: flex; align-items: center; gap: 0.5rem;
  color: #A9C4BD;
  padding: 0.5rem 0.75rem;
  border-radius: var(--radius-sm);
  font-size: 0.9rem;
  font-weight: 500;
  transition: background 0.15s ease, color 0.15s ease;
}
.nav-link:hover { background: rgba(234, 242, 240, 0.07); text-decoration: none; color: #fff; }
.nav-link.active { background: rgba(30, 154, 131, 0.35); color: #fff; font-weight: 600; box-shadow: inset 0 0 0 1px rgba(234, 242, 240, 0.08); }
/* The number beside a nav link: how many things that screen is waiting on
   (booking requests to confirm, LOAs awaiting the HMO). Rendered only when
   non-zero, so a quiet clinic has a quiet sidebar. */
.nav-count {
  margin-left: auto; min-width: 1.35rem; padding: 0.05rem 0.4rem; border-radius: 999px;
  background: rgba(234, 242, 240, 0.14); color: #fff; font-size: 0.72rem; font-weight: 600;
  text-align: center; line-height: 1.5; font-variant-numeric: tabular-nums;
}
.nav-link.active .nav-count { background: rgba(255, 255, 255, 0.22); }
/* Section labels inside the nav (HMO, Clinic, Platform). aria-hidden in the
   markup: they group visually; the links read fine without them. */
.nav-group {
  font-size: 0.66rem; letter-spacing: 0.1em; text-transform: uppercase; color: #7FA49A;
  font-weight: 600; margin: 0.8rem 0.75rem 0.15rem;
}
/* Phone-only search in the green header; hidden at desktop width where the
   top bar carries the same form. */
.sidebar-search { display: none; }

.sidebar-footer { border-top: 1px solid rgba(234, 242, 240, 0.12); padding-top: 1.1rem; margin-top: 0.5rem; }
.user-chip { display: flex; align-items: center; gap: 0.65rem; margin-bottom: 0.9rem; }
.user-avatar {
  width: 34px; height: 34px; border-radius: 50%;
  /* --primary-strong for the same reason as .btn-primary: the initials in
     here are white at 0.85rem/700 (~13.6px), which WCAG counts as normal
     text needing 4.5:1. On --primary that was 3.50:1; this is 4.83:1.
     The circle still reads clearly against the dark sidebar (2.84:1 at the
     top of its gradient, 3.56:1 at the bottom) and keeps the pale ring
     below, so it loses nothing as a shape. */
  background: var(--primary-strong); color: #fff;
  display: flex; align-items: center; justify-content: center;
  font-weight: 700; font-size: 0.85rem; flex-shrink: 0;
  box-shadow: 0 0 0 2px rgba(234, 242, 240, 0.15);
}
.user-name { font-size: 0.87rem; font-weight: 500; color: #fff; }
.user-role { font-size: 0.75rem; color: #8CAAA2; text-transform: capitalize; }
.signout { font-size: 0.82rem; color: #8CAAA2; display: block; margin-top: 0.45rem; transition: color 0.15s ease; }
.signout:first-of-type { margin-top: 0; }
.signout:hover { color: #fff; }

.main { flex: 1; padding: 1rem 2rem 2.2rem; max-width: 1240px; min-width: 0; }

/* ---------- Top bar ---------- */
/* Search plus the two most common actions, in the same place on every page.
   Sticky so the search is one keystroke away however far down a long record
   someone has scrolled; the page background behind it so content slides
   under rather than showing through. */
.topbar {
  display: flex; align-items: center; gap: 0.8rem; flex-wrap: wrap;
  position: sticky; top: 0; z-index: 5; background: var(--bg);
  padding: 0.5rem 0 0.7rem; margin-bottom: 0.9rem;
}
.topbar-search {
  flex: 1 1 320px; max-width: 560px; min-width: 0;
  display: flex; align-items: center; gap: 0.5rem;
  background: var(--surface); border: 1px solid var(--border); border-radius: 8px;
  padding: 0 0.7rem; height: 38px;
}
.topbar-search:focus-within { outline: 2px solid var(--primary); outline-offset: 1px; border-color: var(--primary); }
.topbar-search-icon { color: var(--muted); flex-shrink: 0; }
.topbar-search input {
  flex: 1; min-width: 0; border: 0; background: transparent; font: inherit; font-size: 0.9rem;
  color: var(--ink); height: 100%; padding: 0;
}
.topbar-search input:focus { outline: none; }
.topbar-search input::-webkit-search-decoration { -webkit-appearance: none; }
.topbar-search kbd {
  font-family: 'IBM Plex Mono', monospace; font-size: 0.68rem; color: var(--muted);
  border: 1px solid var(--border); border-radius: 4px; padding: 0 0.35rem; line-height: 1.5;
}
.topbar-actions { display: flex; gap: 0.5rem; margin-left: auto; }

/* ---------- Flash messages ---------- */
.flash-stack { margin-bottom: 1.2rem; display: flex; flex-direction: column; gap: 0.5rem; }
.flash-stack-solo { max-width: 420px; margin: 1.5rem auto 0; }
.flash {
  padding: 0.65rem 0.9rem;
  border-radius: 8px;
  font-size: 0.88rem;
  font-weight: 500;
}
.flash-success { background: var(--primary-tint); color: var(--primary-dark); }
.flash-error { background: var(--danger-tint); color: var(--danger-text); }
.flash-warning { background: var(--accent-tint); color: var(--accent-text); }

/* ---------- Billing reminder banner ---------- */
.billing-alert {
  padding: 0.75rem 1rem; border-radius: 10px; font-size: 0.88rem;
  margin-bottom: 1.2rem; line-height: 1.5;
}
.billing-alert strong { display: inline; margin-right: 0.3rem; }
/* Same contrast issue as the status tags above (this banner's text is 0.88rem
   -- too small to qualify for WCAG's "large text" 3:1 exemption, so it needs
   the full 4.5:1) -- same --accent-text/--danger-text fix. */
.billing-alert-due_soon { background: var(--accent-tint); color: var(--accent-text); }
.billing-alert-overdue { background: var(--danger-tint); color: var(--danger-text); }
.billing-alert-link {
  font-weight: 700; text-decoration: underline; text-underline-offset: 2px;
  margin-left: 0.4rem; color: inherit;
}
.billing-alert-link:hover { opacity: 0.8; }

/* ---------- Page header ---------- */
.page-header {
  display: flex; align-items: flex-end; justify-content: space-between;
  margin-bottom: 1.1rem; gap: 1rem; flex-wrap: wrap;
}
.eyebrow {
  text-transform: uppercase; font-size: 0.7rem; letter-spacing: 0.09em;
  color: var(--muted); font-weight: 600; margin: 0 0 0.2rem;
}
.header-actions { display: flex; gap: 0.6rem; }

/* ---------- Buttons ---------- */
.btn {
  display: inline-flex; align-items: center; justify-content: center;
  padding: 0.5rem 0.95rem;
  border-radius: var(--radius-sm);
  font-size: 0.86rem;
  font-weight: 500;
  letter-spacing: -0.005em;
  border: 1px solid transparent;
  cursor: pointer;
  font-family: inherit;
  transition: background 0.15s ease, border-color 0.15s ease, box-shadow 0.15s ease, transform 0.1s ease;
}
.btn:hover { text-decoration: none; }
.btn:active { transform: translateY(1px); }
.btn-block { width: 100%; }
/* --primary-strong, not --primary. This button is white text at 0.87rem/600
   (~13.9px) -- normal-size text by WCAG's reckoning, so it needs 4.5:1, and
   it is nowhere near the large-text exemption (18.7px bold / 24px regular).
   White on --primary (#1E9A83) measures 3.50:1 and fails; white on
   --primary-strong (#19806D) measures 4.83:1 and passes. Same hue, one step
   darker -- see the ramp in tokens.css.

   Worth stating plainly because this file already got the same fix right
   three times over for text colors (--primary-text, --accent-text,
   --danger-text): the button was simply the one place the accessible
   variant never got applied, and it is the most-clicked element in the
   product. */
.btn-primary { background: var(--primary-strong); color: #fff; box-shadow: 0 1px 2px rgba(11,46,42,0.08), 0 6px 14px -6px rgba(30,154,131,0.45); }
/* Hover stays --primary-dark (white on it: 14.60:1). Resting is now darker
   than it was, so the hover step is a smaller jump than before -- still a
   clearly visible state change, and the shadow lift reinforces it. */
.btn-primary:hover { background: var(--primary-dark); box-shadow: 0 2px 4px rgba(11,46,42,0.1), 0 10px 20px -8px rgba(30,154,131,0.5); }
.btn-secondary { background: var(--surface); color: var(--primary-dark); border-color: var(--border); }
.btn-secondary:hover { border-color: var(--primary); background: var(--primary-tint); }
.btn-ghost { background: transparent; color: var(--muted); }
.btn-ghost:hover { color: var(--ink); background: var(--surface-alt); }
.btn-danger { background: var(--danger-tint); color: var(--danger-text); }
.btn-danger:hover { background: var(--danger); color: #fff; }
/* min-height plus vertical padding takes this from ~13px to the 24px WCAG
   2.2 SC 2.5.8 minimum. These are destructive or state-changing actions
   (Confirm, Cancel, Void, Delete) sitting in flex button rows rather than
   inline in a sentence, so the inline exception does not cover them.
   inline-flex + align-items keeps the label optically centred while the
   text itself still determines the width. */
.link-danger {
  background: none; border: none; color: var(--danger);
  font-size: 0.83rem; cursor: pointer; font-family: inherit; text-decoration: underline;
  display: inline-flex; align-items: center;
  min-height: 24px; padding: 0.25rem 0.2rem;
}
.link-danger:focus-visible {
  outline: 2px solid var(--primary); outline-offset: 2px; border-radius: 4px;
}
.inline-form { display: inline; }

/* ---------- Stat cards ---------- */
.stat-row { display: flex; gap: 0.8rem; margin-bottom: 1.1rem; }
.stat-card {
  background: var(--surface); border: 1px solid var(--border); border-radius: var(--radius);
  padding: 0.85rem 1.05rem; flex: 1; min-width: 0; box-shadow: var(--shadow);
  transition: box-shadow 0.15s ease, transform 0.15s ease;
}
.stat-value { display: block; font-family: var(--font-heading); font-weight: 600; font-size: 1.6rem; letter-spacing: -0.02em; color: var(--primary-dark); }
.stat-label { font-size: 0.85rem; color: var(--muted); }

/* ---------- Panels ---------- */
.panel {
  background: var(--surface); border: 1px solid var(--border); border-radius: var(--radius);
  padding: 1.1rem 1.25rem; box-shadow: var(--shadow);
  /* Defensive: if anything inside a panel (a table, the weekly calendar grid,
     a long unbreakable string) is ever wider than the panel itself, scroll
     it inside its own box instead of forcing the whole page wider than the
     viewport -- the mobile nav overflow bug taught us that a single overflow
     anywhere can shift/clip everything else on the page. */
  overflow-x: auto;
}
/* h1/h2/h3 reset to margin:0 sitewide (see top of file) -- panels rely on
   their h2 title having *some* space before whatever follows (a chart,
   a card grid, a table), so give panel titles their gap back here instead
   of patching it per-page. Panels that truly want the title flush against
   the next element already override this inline (see admin_billing.html). */
.panel h2 { margin-bottom: 0.7rem; }
.grid-2 { display: grid; grid-template-columns: 1fr 1fr; gap: 0.9rem; }
.grid-detail { display: grid; grid-template-columns: 300px 1fr; gap: 0.9rem; align-items: start; }

/* ---------- Table ---------- */
/* A table's minimum width is its content, not its container: width:100% is a
   ceiling and never a floor. When the columns cannot fit, the table grows past
   its card, the PAGE grows past the viewport, and everything laid out against
   the page's right edge -- every .form-actions button on the screen -- goes
   off-screen with it. That is one overflowing table clipping controls in
   unrelated cards, which is exactly how it was reported: a cut-off "Make
   default" AND a cut-off "Upload logo" two panels away.

   The wrapper contains it. Scrolling a table sideways is a poorer answer than
   a table that fits, so the phone rules further down make the roster fit --
   but this is what guarantees no table can ever push the page wider again,
   whatever somebody puts in a cell later. */
.table-scroll { overflow-x: auto; -webkit-overflow-scrolling: touch; }

.table { width: 100%; border-collapse: collapse; }
.table th {
  text-align: left; font-size: 0.72rem; text-transform: uppercase; letter-spacing: 0.06em;
  color: var(--muted); font-weight: 500; padding: 0 0.6rem 0.45rem; border-bottom: 1px solid var(--border);
}
.table td { padding: 0.5rem 0.6rem; border-bottom: 1px solid var(--border); font-size: 0.9rem; }
/* cursor: pointer belongs only on rows that actually navigate.
   It used to sit on every .table row, including tables with no data-row-href
   (the appointments list, staff.html) -- so users learned from the patient list
   that a pointer means "clickable row" and then found it lied elsewhere. Only
   .diagram-table-reference had opted out, one table at a time. */
.table tbody tr { cursor: default; }
.table tbody tr[data-row-href] { cursor: pointer; }
.table tbody tr:hover { background: var(--primary-tint); }
/* Keyboard equivalent of the hover tint: when the row's link takes focus, the
   whole row highlights, so tabbing through gives the same "which row am I on"
   feedback the mouse gets. */
.table tbody tr:focus-within { background: var(--primary-tint); }
.table tbody tr:last-child td { border-bottom: none; }

/* The anchor that makes a data-row-href row keyboard-reachable. Deliberately
   inherits body colour rather than looking like a teal link: the whole row is
   the affordance, and turning one cell blue would imply the rest of the row
   isn't clickable. Underline on hover/focus keeps it discoverable as a link. */
.row-link { color: inherit; text-decoration: none; font-weight: 500; }
.row-link:hover { text-decoration: underline; }
.row-link:focus-visible {
  outline: 2px solid var(--primary); outline-offset: 2px; border-radius: 3px;
}

/* A plain reference table (e.g. the Data Diagram's table list, admin_guide.html
   Sec.10) isn't a clickable row list like the rest of .table's uses, so it
   opts out of the pointer cursor and hover tint above. */
.diagram-table-reference tbody tr { cursor: default; }
.diagram-table-reference tbody tr:hover { background: none; }
.diagram-table-reference tr.diagram-table-group td {
  padding-top: 1.1rem; font-size: 0.78rem; text-transform: uppercase; letter-spacing: 0.05em;
  color: var(--primary-text); border-bottom: 1px solid var(--border);
}
.diagram-table-reference tr.diagram-table-group:first-child td { padding-top: 0.65rem; }
.diagram-table-reference td:first-child { white-space: nowrap; }
.diagram-table-reference code { background: var(--bg); padding: 0.1rem 0.35rem; border-radius: 4px; font-size: 0.85em; }

/* ---------- Invoice line items (billing) ---------- */
.invoice-items-table { width: 100%; border-collapse: collapse; }
.invoice-items-table th {
  text-align: left; font-size: 0.72rem; text-transform: uppercase; letter-spacing: 0.06em;
  color: var(--muted); font-weight: 600; padding: 0 0.4rem 0.5rem; border-bottom: 1px solid var(--border);
}
.invoice-items-table td { padding: 0.35rem 0.4rem; vertical-align: top; }
.invoice-items-table input {
  width: 100%; padding: 0.5rem 0.6rem; border: 1px solid var(--border); border-radius: 8px;
  font-size: 0.9rem; font-family: inherit; box-sizing: border-box;
}
.invoice-items-table .qty-input { width: 4.5rem; }
.invoice-items-table .price-input { width: 6.5rem; }
.invoice-items-table .payer-input { width: 6.5rem; }
.invoice-items-table .remove-row {
  padding: 0.4rem 0.6rem; font-size: 1rem; line-height: 1;
}

/* ---------- Mini list (dashboard) ---------- */
.mini-list { list-style: none; margin: 0; padding: 0; display: flex; flex-direction: column; gap: 0.55rem; }
.mini-list li { border-bottom: none; }
/* Scoped to direct-child anchors only (li > a) — the Today's Appointments
   widget nests its link one level deeper (li > div > a) with its own layout,
   so this card treatment doesn't collide with that row's status tag/actions. */
.mini-list > li > a {
  display: block; position: relative; color: var(--ink);
  padding: 0.6rem 2.3rem 0.6rem 0.9rem;
  border: 1px solid var(--border); border-radius: var(--radius-sm); background: var(--surface-alt);
  transition: border-color 0.15s ease, background 0.15s ease, box-shadow 0.15s ease, transform 0.15s ease;
}
.mini-list > li > a::after {
  content: '\2192'; color: var(--muted); font-size: 1rem;
  position: absolute; right: 1rem; top: 50%; transform: translateY(-50%);
  transition: transform 0.15s ease, color 0.15s ease;
}
.mini-list > li > a:hover {
  text-decoration: none; border-color: var(--primary); background: var(--primary-tint);
  box-shadow: var(--shadow-sm); transform: translateY(-1px);
}
.mini-list > li > a:hover::after { transform: translateY(-50%) translateX(3px); color: var(--primary-text); }
.mini-list-title { font-weight: 500; font-size: 0.9rem; display: block; }
.mini-list-sub { font-size: 0.85rem; color: var(--muted); display: block; margin-top: 0.2rem; }

/* ---------- Contact cards (Support page) ---------- */
/* There are three contact cards (Facebook, WhatsApp, Email) -- this was
   hard-set to two columns from when there were only two channels, which
   left the third (Email) sitting alone on its own row below a pair. */
.contact-card-grid { display: grid; grid-template-columns: 1fr 1fr 1fr; gap: 1rem; }
.contact-card {
  display: flex; align-items: flex-start; gap: 1rem; min-width: 0;
  background: var(--surface); border: 1px solid var(--border); border-radius: var(--radius);
  padding: 1.4rem; color: var(--ink);
  transition: border-color 0.15s ease, box-shadow 0.2s ease, transform 0.15s ease;
}
.contact-card:hover {
  text-decoration: none; border-color: transparent; box-shadow: var(--shadow-lg); transform: translateY(-2px);
}
.contact-icon {
  width: 44px; height: 44px; border-radius: 12px; flex-shrink: 0;
  display: flex; align-items: center; justify-content: center;
  font-size: 1.3rem; color: #fff;
}
.contact-icon-facebook { background: #1877F2; }
.contact-icon-whatsapp { background: #25D366; }
.contact-icon-email { background: var(--primary); }
.contact-card-body { min-width: 0; }
.contact-card-title { font-weight: 700; font-size: 0.98rem; color: var(--ink); display: flex; align-items: center; gap: 0.4rem; }
.contact-card-sub {
  /* text-overflow only clips block-level boxes -- this is a <span> (inline
     by default), so overflow/ellipsis below were silently no-ops until
     this was added, and the un-wrapping URL just spilled out of the card
     instead of truncating. */
  display: block;
  font-size: 0.85rem; color: var(--muted); margin-top: 0.25rem;
  white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
}
.contact-card-arrow { margin-left: auto; color: var(--muted); font-size: 1.1rem; align-self: center; transition: transform 0.15s ease, color 0.15s ease; }
.contact-card:hover .contact-card-arrow { transform: translateX(3px); color: var(--primary-text); }

/* The Support page's logged-out view embeds these cards inside the narrow
   (~480px) auth panel, not the wide main content column — always stack them
   there regardless of viewport width, or a long URL squeezed into half of
   480px wraps letter-by-letter. */
.auth-panel .contact-card-grid { grid-template-columns: 1fr; }

/* Step down to 2 columns before the full single-column stack below, so
   three cards don't get squeezed uncomfortably narrow in the gap between
   "plenty of room for 3" and "only room for 1". */
@media (max-width: 1000px) {
  .contact-card-grid { grid-template-columns: 1fr 1fr; }
}

@media (max-width: 640px) {
  .contact-card-grid { grid-template-columns: 1fr; }
}

.empty { color: var(--muted); font-size: 0.9rem; padding: 0.5rem 0; }

/* ---------- Payment method selector (Pay now) ---------- */
.pay-method-grid { display: flex; gap: 0.6rem; flex-wrap: wrap; margin: 0.8rem 0 1.2rem; align-items: center; }
.pay-method-btn-logo { display: flex; align-items: center; justify-content: center; width: 108px; padding: 0.5rem 0.6rem; }
.pay-method-btn-logo svg { display: block; width: 100%; height: 26px; }
.pay-method-panel { margin-top: 0.8rem; }
.pay-qr-placeholder {
  width: 220px; height: 220px; display: flex; flex-direction: column; align-items: center; justify-content: center;
  gap: 0.6rem; text-align: center; padding: 1rem; border: 2px dashed var(--border); border-radius: var(--radius);
  color: var(--muted); font-size: 0.85rem; background: var(--surface); margin-bottom: 0.8rem;
}
.pay-qr-placeholder[hidden] { display: none; }
.pay-qr-spinner {
  width: 28px; height: 28px; border-radius: 50%;
  border: 3px solid var(--border); border-top-color: var(--primary);
  animation: pay-qr-spin 0.8s linear infinite;
}
@keyframes pay-qr-spin { to { transform: rotate(360deg); } }
.pay-qr-image {
  display: block; width: 220px; height: 220px; max-width: 100%; object-fit: contain;
  border: 1px solid var(--border); border-radius: var(--radius); background: var(--surface);
  padding: 0.6rem; margin-bottom: 0.8rem;
}

/* ---------- Chips, avatars, quick filters ---------- */
/* The shared vocabulary for state across the app: teal = fine, amber = needs a
   decision, red = money or safety, blue = HMO, grey = information. Same AA-checked
   text/tint pairs as .tag; these are sentence case and a touch larger, for
   phrases ("Allergy: penicillin") rather than one-word statuses. */
.chip {
  display: inline-block; padding: 0.1rem 0.55rem; border-radius: 999px;
  font-size: 0.76rem; font-weight: 500; white-space: nowrap; line-height: 1.6;
  background: var(--surface-alt); border: 1px solid var(--border); color: var(--muted);
}
.chip-ok { background: var(--primary-tint); border-color: transparent; color: var(--primary-dark); }
.chip-warn { background: var(--accent-tint); border-color: transparent; color: var(--accent-text); }
.chip-bad { background: var(--danger-tint); border-color: transparent; color: var(--danger-text); }
.chip-info { background: #E4EEF8; border-color: transparent; color: #2A5D9F; }
.chip-muted { background: var(--surface-alt); color: var(--muted); }
/* Initials in a circle. Decorative (aria-hidden in the markup): the name is
   right beside it. Two letters give a list a scannable left edge. */
.avatar {
  display: inline-flex; align-items: center; justify-content: center; flex-shrink: 0;
  width: 30px; height: 30px; border-radius: 50%; background: var(--primary-tint);
  color: var(--primary-deep); font-size: 0.72rem; font-weight: 600; letter-spacing: 0.02em;
}
.quick-filters { display: flex; gap: 0.4rem; align-items: center; flex-wrap: wrap; margin-bottom: 0.8rem; }
.chip-filter {
  display: inline-flex; align-items: center; gap: 0.35rem; padding: 0.3rem 0.7rem;
  border-radius: 999px; border: 1px solid var(--border); background: var(--surface);
  color: var(--ink); font-size: 0.82rem; font-weight: 500; text-decoration: none; min-height: 32px;
}
.chip-filter:hover { text-decoration: none; border-color: var(--primary); }
.chip-filter.is-on { background: var(--primary-tint); border-color: var(--primary); color: var(--primary-deep); }
.chip-filter .chip-count { color: var(--muted); font-variant-numeric: tabular-nums; }
.chip-filter.is-on .chip-count { color: var(--primary-text); }
.quick-filters-note { margin-left: auto; font-size: 0.8rem; color: var(--muted); }
.btn-sm { padding: 0.3rem 0.7rem; font-size: 0.8rem; }
.num { font-variant-numeric: tabular-nums; white-space: nowrap; }
/* A panel whose table runs edge to edge; the padding moves to the cells. */
.panel-flush { padding: 0; }
.panel-flush .table th:first-child, .panel-flush .table td:first-child { padding-left: 1.1rem; }
.panel-flush .table th:last-child, .panel-flush .table td:last-child { padding-right: 1.1rem; }
.panel-flush .table th { padding-top: 0.7rem; }
.panel-foot { padding: 0.2rem 1.1rem 0.8rem; }
.panel-foot .pager { margin-top: 0.5rem; }
.panel-pad { padding: 1rem 1.1rem; margin: 0; }

/* ---------- Patient directory ---------- */
.directory th { white-space: nowrap; }
.directory td { vertical-align: middle; }
.directory-name { display: flex; align-items: center; gap: 0.65rem; }
.directory-name-text { display: flex; flex-direction: column; min-width: 0; }
.directory-name .row-link { font-weight: 500; }
.directory-drn { font-size: 0.74rem; color: var(--muted); }
.directory-flags { display: flex; flex-wrap: wrap; gap: 0.3rem; }
.directory-open { text-align: right; width: 1%; white-space: nowrap; }

/* ---------- Search bar ---------- */
.search-bar { display: flex; gap: 0.6rem; margin-bottom: 1.2rem; }
.search-bar input {
  flex: 1; min-width: 0; padding: 0.6rem 0.8rem; border: 1px solid var(--border); border-radius: 8px;
  font-family: inherit; font-size: 0.9rem; background: var(--surface);
}

/* ---------- Forms ---------- */
/* minmax(0, 1fr), not 1fr -- see .intake-checkbox-grid. `1fr` is
   minmax(auto, 1fr), and `auto` as a track minimum is min-content, so the
   column refuses to shrink and its contents overflow instead. This is the
   layout nearly every form in the app is built on, which makes it the widest
   reach this bug has. */
.form-grid { display: grid; grid-template-columns: minmax(0, 1fr) minmax(0, 1fr); gap: 1rem 1.2rem; }
.form-grid label {
  display: flex; flex-direction: column; gap: 0.35rem;
  font-size: 0.82rem; font-weight: 500; color: var(--muted);
}
.form-grid input, .form-grid select, .form-grid textarea {
  padding: 0.55rem 0.7rem; border: 1px solid var(--border); border-radius: 7px;
  font-family: inherit; font-size: 0.92rem; color: var(--ink); background: var(--surface);
  line-height: 1.4;
}
/* Reported as "list fields are smaller than the others" (book_intake.html's
   Yes/No dropdowns next to its text inputs): a <select> has its own
   browser/OS-native chrome (the dropdown arrow, its own internal vertical
   metrics), which doesn't reliably respect the shared padding/line-height
   rule above the way <input>/<textarea> do -- even with identical CSS, a
   native select can render visibly shorter. appearance: none hands full
   control of the box back to this stylesheet; the background-image below
   redraws the arrow that removes, sized/positioned/colored (--muted) to
   match the rest of the UI. Scoped to .form-grid select only, so
   input/textarea keep their native rendering. */
.form-grid select {
  appearance: none; -webkit-appearance: none; -moz-appearance: none;
  padding-right: 2rem;
  background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16'%3E%3Cpath fill='none' stroke='%235F7369' stroke-width='1.6' stroke-linecap='round' stroke-linejoin='round' d='M4 6l4 4 4-4'/%3E%3C/svg%3E");
  background-repeat: no-repeat;
  background-position: right 0.7rem center;
  background-size: 13px;
}
.form-grid input:focus, .form-grid select:focus, .form-grid textarea:focus,
.search-bar input:focus, .auth-form input:focus {
  outline: 2px solid var(--primary); outline-offset: 1px; border-color: var(--primary);
}
/* The visible half of the field-error marking (aria-invalid is set by
   field_error_attrs() in app.py). Attribute selector rather than :invalid,
   because :invalid also fires on the browser's own constraint validation --
   which paints every empty required field red before the user has typed
   anything, and is why :invalid styling is usually a mistake. This only ever
   appears after a real submission the server rejected.

   Not colour alone: the border goes to 2px as well, so the marking survives
   greyscale and the ~8% of men with a red-green deficiency (SC 1.4.1). The
   message above the form carries the actual information either way -- this is
   a pointer to the field, not the error text. */
.form-grid [aria-invalid="true"],
.auth-form [aria-invalid="true"],
input[aria-invalid="true"], select[aria-invalid="true"], textarea[aria-invalid="true"] {
  border: 2px solid var(--danger);
}
/* The focus ring has to stay legible on top of the error border -- without
   this the 2px danger border and the 2px primary outline sit adjacent and read
   as one muddy 4px edge. Keeping the border red under focus also means the
   field does not appear to "fix itself" the moment it is focused. */
.form-grid [aria-invalid="true"]:focus,
.auth-form [aria-invalid="true"]:focus,
input[aria-invalid="true"]:focus, select[aria-invalid="true"]:focus,
textarea[aria-invalid="true"]:focus {
  outline: 2px solid var(--danger); outline-offset: 2px; border-color: var(--danger);
}
/* Checkboxes need their own rule, and this is not redundancy: the custom
   checkbox at .checkbox-item input[type="checkbox"] scores 0,2,1 on
   specificity, so it beats the 0,1,1 attribute selector above and the error
   border would simply never paint on the consent boxes -- the one place in the
   app where an unticked box is a validation error. */
.checkbox-item input[type="checkbox"][aria-invalid="true"] {
  border: 2px solid var(--danger);
}
.checkbox-item input[type="checkbox"][aria-invalid="true"]:focus-visible {
  outline: 2px solid var(--danger); outline-offset: 1px;
}
/* An author `display` rule beats the browser's own `[hidden] { display: none }`
   -- not on specificity but on origin: any author declaration outranks the
   user-agent stylesheet. So `.form-grid label { display: flex }` silently
   defeated `element.hidden = true`, and the visit form's clinician text box,
   which JavaScript "hid" whenever a roster name was picked, was in fact
   visible the whole time. Both fields showed, holding one value twice.

   .pay-qr-placeholder[hidden] two hundred lines above is the same bug patched
   for one class, by someone who hit it once and fixed it where they stood.
   This is the general form. !important because the whole point is to outrank
   whatever display a component sets on itself. */
[hidden] { display: none !important; }

.span-2 { grid-column: span 2; }
.form-actions { display: flex; justify-content: flex-end; gap: 0.6rem; margin-top: 0.4rem; }

/* ---------- Patient record ---------- */
.record-crumb { font-size: 0.8rem; color: var(--muted); margin: 0 0 0.6rem; }
.record-crumb a { color: var(--muted); }
.record-hero {
  display: grid; grid-template-columns: auto minmax(0, 1fr) auto; gap: 1rem; align-items: center;
  background: var(--surface); border: 1px solid var(--border); border-radius: var(--radius);
  padding: 0.9rem 1.2rem; box-shadow: var(--shadow); margin-bottom: 0.8rem;
}
.avatar-lg { width: 48px; height: 48px; font-size: 1rem; }
.record-hero h1 { font-size: 1.35rem; margin: 0; }
.record-meta { display: flex; flex-wrap: wrap; gap: 0.3rem 0.9rem; margin: 0.25rem 0 0; font-size: 0.84rem; color: var(--muted); align-items: center; }
.record-meta b { color: var(--ink); font-weight: 600; }
.record-chips { display: flex; flex-wrap: wrap; gap: 0.35rem; margin: 0.5rem 0 0; }
.record-chips .chip { white-space: normal; line-height: 1.4; padding-top: 0.15rem; padding-bottom: 0.15rem; }
.record-chips a.chip:hover { text-decoration: none; border-color: var(--primary); }
.record-actions { display: flex; gap: 0.5rem; flex-wrap: wrap; justify-content: flex-end; }

.tabs { display: flex; gap: 0.1rem; border-bottom: 1px solid var(--border); margin-bottom: 0.9rem; overflow-x: auto; scrollbar-width: none; }
.tab {
  padding: 0.55rem 0.85rem; color: var(--muted); font-weight: 500; font-size: 0.88rem; white-space: nowrap;
  border-bottom: 2px solid transparent; margin-bottom: -1px; text-decoration: none;
}
.tab:hover { color: var(--ink); text-decoration: none; }
.tab.is-on { color: var(--primary-deep); border-bottom-color: var(--primary); }
.tab b { font-weight: 500; color: var(--muted); margin-left: 0.25rem; font-variant-numeric: tabular-nums; }
.tab-panel > .panel + .panel, .tab-panel > .panel + .freehand-accordion, .tab-panel > .freehand-accordion + .freehand-accordion { margin-top: 0.9rem; }

.record-cols { display: grid; grid-template-columns: minmax(0, 1fr) 300px; gap: 0.9rem; align-items: start; }
.record-side { display: grid; gap: 0.9rem; }
.record-timeline { list-style: none; margin: 0; padding: 0; }
.record-timeline li {
  display: grid; grid-template-columns: 5.6rem minmax(0, 1fr) auto; gap: 0.7rem; align-items: start;
  padding: 0.6rem 1.1rem; border-bottom: 1px solid var(--border); font-size: 0.88rem;
}
.record-timeline li:last-child { border-bottom: 0; }
.record-timeline li.is-upcoming { background: var(--primary-tint); }
.rt-date { font-size: 0.78rem; color: var(--muted); padding-top: 0.15rem; white-space: nowrap; }
.rt-body { display: flex; flex-direction: column; min-width: 0; }
.rt-title { font-weight: 500; color: var(--ink); }
a.rt-title:hover { text-decoration: underline; }
.rt-sub { font-size: 0.82rem; color: var(--muted); }
.record-empty { padding: 1rem 1.1rem; color: var(--muted); font-size: 0.88rem; display: flex; flex-wrap: wrap; gap: 0.6rem; align-items: center; margin: 0; }
.kv-list { margin: 0; padding: 0.3rem 1.1rem 0.5rem; }
.kv-list div { display: flex; justify-content: space-between; gap: 0.8rem; padding: 0.4rem 0; border-bottom: 1px solid var(--border); font-size: 0.85rem; }
.kv-list div:last-child { border-bottom: 0; }
.kv-list dt { color: var(--muted); white-space: nowrap; }
.kv-list dd { margin: 0; text-align: right; }
.kv-rows { list-style: none; margin: 0; padding: 0.2rem 1.1rem 0.5rem; }
.kv-rows li { display: flex; justify-content: space-between; gap: 0.8rem; padding: 0.35rem 0; border-bottom: 1px solid var(--border); font-size: 0.85rem; }
.kv-rows li:last-child { border-bottom: 0; }
.record-foot { display: flex; gap: 1rem; align-items: center; flex-wrap: wrap; margin-top: 1.2rem; font-size: 0.8rem; color: var(--muted); }
.record-foot .link-danger { font-size: 0.8rem; text-decoration: none; color: var(--muted); }
.record-foot .link-danger:hover { color: var(--danger); text-decoration: underline; }

/* ---------- Payment plan (installments) on the Billing tab ---------- */
.plan-box { margin: 0.5rem 0 0.4rem; padding: 0.6rem 0.8rem; border: 1px solid var(--border); border-radius: 8px; background: var(--surface-alt); }
.plan-summary { font-size: 0.88rem; margin-bottom: 0.5rem; display: flex; flex-wrap: wrap; gap: 0.3rem 0.4rem; align-items: center; }
.plan-summary .sub { color: var(--muted); font-size: 0.82rem; }
.plan-table { font-size: 0.84rem; }
.plan-table th, .plan-table td { padding-top: 0.3rem; padding-bottom: 0.3rem; }
.plan-table .num { text-align: right; }
.plan-row-overdue td { background: var(--danger-tint); }
.inline-details { display: inline; }
.inline-details summary::-webkit-details-marker { display: none; }
.write-off-form { display: flex; gap: 0.4rem; align-items: center; margin-top: 0.5rem; flex-wrap: wrap; }
.write-off-form input { flex: 1 1 16rem; min-width: 0; padding: 0.4rem 0.6rem; border: 1px solid var(--border); border-radius: 6px; font: inherit; font-size: 0.85rem; }

/* ---------- Patient detail ---------- */
.detail-list { display: flex; flex-direction: column; gap: 0.7rem; margin: 0 0 1.3rem; }
.detail-list div { display: flex; justify-content: space-between; gap: 1rem; border-bottom: 1px dashed var(--border); padding-bottom: 0.5rem; }
.detail-list dt { color: var(--muted); font-size: 0.85rem; }
.detail-list dd { margin: 0; font-weight: 500; text-align: right; }
.detail-alert dd { color: var(--danger); }

/* ---------- Timeline (signature element) ---------- */
/* A patient's Dental history only grows over time, and used to render every
   visit fully expanded -- for a long-time patient that meant scrolling past
   a wall of old Treatment/Medications/Notes text just to reach the next
   entry, and the panel itself grew tall enough to push everything below it
   down the page indefinitely (same shape of problem as the calendar's
   hour grid earlier). Two changes address it together: each entry is a
   native <details>/<summary> (patient_detail.html) collapsed to one
   compact line by default -- date, visit type, diagnosis -- with the most
   recent entry pre-expanded; and the panel itself is capped and scrolls
   internally instead of growing the page, same pattern as
   .calendar-grid-wrap. <details> needs no JS, is keyboard-operable out of
   the box, and only the entries someone actually opens cost any real
   vertical space. */
.timeline-panel {
  max-height: calc(100vh - 280px); overflow-y: auto;
  scrollbar-width: thin; scrollbar-color: var(--border) transparent;
}
.timeline-panel::-webkit-scrollbar { width: 8px; }
.timeline-panel::-webkit-scrollbar-track { background: transparent; }
.timeline-panel::-webkit-scrollbar-thumb { background: var(--border); border-radius: 8px; }
.timeline-panel::-webkit-scrollbar-thumb:hover { background: var(--muted); }
.timeline { list-style: none; margin: 0; padding: 0; position: relative; }
.timeline::before {
  content: ''; position: absolute; left: 5px; top: 6px; bottom: 6px; width: 2px; background: var(--border);
}
.timeline-item { position: relative; padding-left: 1.8rem; padding-bottom: 1.6rem; }
.timeline-item:last-child { padding-bottom: 0; }
.timeline-marker {
  position: absolute; left: 0; top: 4px; width: 12px; height: 12px; border-radius: 50%;
  background: var(--primary); border: 2px solid var(--surface); box-shadow: 0 0 0 2px var(--primary);
}
.timeline-head {
  display: flex; flex-wrap: wrap; align-items: center; gap: 0.6rem; margin-bottom: 0.25rem;
  cursor: pointer; list-style: none;
}
.timeline-head::-webkit-details-marker { display: none; }
.timeline-head::before {
  content: '\25B8'; color: var(--muted); font-size: 0.7rem; flex-shrink: 0;
  transition: transform 0.15s ease;
}
.timeline-body[open] > .timeline-head::before { transform: rotate(90deg); }
.timeline-summary-title {
  font-family: var(--font-heading); font-weight: 600; color: var(--primary-dark); font-size: 1rem;
}
.timeline-details { padding-top: 0.15rem; }
.timeline-date { font-family: 'IBM Plex Mono', monospace; font-size: 0.78rem; color: var(--muted); }
.tag {
  /* --accent-text, not --accent: plain --accent only clears ~2.7:1 as text
     here, well under WCAG AA's 4.5:1 minimum for small text -- see the
     --accent-text definition at the top of this file. */
  background: var(--accent-tint); color: var(--accent-text); font-size: 0.72rem; font-weight: 600;
  text-transform: uppercase; letter-spacing: 0.04em; padding: 0.15rem 0.5rem; border-radius: 5px;
}
.tag.pay-status-paid { background: var(--primary-tint); color: var(--primary-dark); }
.tag.pay-status-pending { background: var(--accent-tint); color: var(--accent-text); }
.tag.pay-status-overdue { background: var(--danger-tint); color: var(--danger-text); }
/* Not a payment status. Reusing .pay-status-paid for "this is the current one" worked
   visually and lied in the markup, which is how a payment colour ends up being changed
   and a clinician roster changing with it. Same tokens as pay-status-paid deliberately:
   --primary-dark on --primary-tint is the pair already checked for AA contrast. */
.tag.tag-current { background: var(--primary-tint); color: var(--primary-dark); }

/* ---------- Help guide ---------- */
.help-toc { display: flex; flex-wrap: wrap; gap: 0.4rem 1.1rem; margin: 0 0 1.6rem; padding: 0; list-style: none; }
.help-toc a { font-size: 0.85rem; font-weight: 600; }
.help-section { margin-top: 1.6rem; padding-top: 1.4rem; border-top: 1px solid var(--border); }
.help-section:first-of-type { margin-top: 0; padding-top: 0; border-top: none; }
.help-subsection { margin-top: 1.2rem; }
.help-subsection h3 { margin: 0 0 0.4rem; }
.help-callout {
  padding: 0.65rem 0.9rem; border-radius: 8px; font-size: 0.88rem; line-height: 1.5;
  margin: 0.6rem 0 1rem; border-left: 3px solid var(--primary); background: var(--primary-tint);
}
.help-callout.help-callout-tip { border-left-color: var(--accent); background: var(--accent-tint); }
.help-callout.help-callout-warn { border-left-color: var(--danger); background: var(--danger-tint); }
/* System Check results list (/system-check, static/js/system-check.js) --
   reuses .tag/.pay-status-* for the pass/warn/fail badge, same colors as
   the Access Log's action badges, just in a plain stacked list rather than
   a table row. */
.syscheck-list { list-style: none; margin: 0; padding: 0; display: flex; flex-direction: column; gap: 0.5rem; }
.syscheck-item { display: flex; align-items: flex-start; gap: 0.6rem; }
.syscheck-item .tag { flex-shrink: 0; margin-top: 0.1rem; }
.syscheck-item-body { font-size: 0.92rem; line-height: 1.5; }
.ui-label {
  display: inline-block; background: var(--surface-alt); border: 1px solid var(--border);
  border-radius: 6px; padding: 0.05rem 0.4rem; font-size: 0.85em; font-weight: 600; color: var(--primary-dark);
}
.help-code {
  font-family: 'IBM Plex Mono', monospace; font-size: 0.82rem; line-height: 1.7;
  background: var(--surface-alt); border: 1px solid var(--border); border-radius: 8px;
  padding: 0.7rem 0.9rem; margin: 0.5rem 0 1rem; overflow-x: auto; white-space: pre;
  color: var(--primary-dark);
}

/* ---------- Pager ----------
   Shared by the patient directory and the access log (templates/_pager.html).
   The status line sits opposite the links so the count reads as a caption on the
   table rather than as part of the controls; on a narrow screen they stack, since
   two flex children at opposite ends of 360px leave the page number crushed. */
.pager {
  display: flex; justify-content: space-between; align-items: center;
  gap: 0.8rem; margin-top: 1rem; flex-wrap: wrap;
}
.pager-status { font-size: 0.85rem; color: var(--muted); margin: 0; }
.pager-links { display: flex; gap: 0.6rem; }

/* ---------- Analytics ---------- */
.analytics-controls { display: flex; gap: 0.5rem; margin-bottom: 1.4rem; }
.analytics-group-heading {
  font-size: 1.3rem; font-weight: 600; color: var(--primary-dark);
  /* margin-top adds breathing room *above* the border-top rule itself --
     without it, the gap before this divider was only whatever bottom
     margin the preceding element happened to have (e.g. .analytics-note's
     1rem), which reads as the note text nearly touching the rule below it. */
  margin: 0.8rem 0 1rem; padding-top: 1.8rem; border-top: 1px solid var(--border);
}
.analytics-group-heading:first-of-type { padding-top: 0; border-top: none; margin-top: 0; }
.analytics-section { margin-top: 1.6rem; }
.analytics-section:first-of-type { margin-top: 0; }
/* Analytics has three logical heading levels -- group ("Reports"), section
   ("Revenue"), panel ("Collected per day") -- but all three were authored as
   <h2>, so a screen reader met about twenty undifferentiated siblings under
   one <h1> with no way to tell a section from a card inside it, and no way to
   skip a section. The tags now carry the real hierarchy (h2 / h3 / h4).

   The sizes below deliberately reproduce what the flat <h2> markup already
   rendered, so this is a semantic change with no visual one. h3 and h4 are
   styled here rather than in the global h1/h2/h3 reset at the top of the file
   because that reset is sitewide and pricing.html already styles its own
   <h4> -- widening it would silently restyle a public marketing page. */
.analytics-section h3,
.analytics-section h4 {
  font-family: var(--font-heading);
  font-weight: 600;
  letter-spacing: -0.015em;
  color: var(--primary-dark);
  font-size: 1.08rem;
  margin: 0 0 0.9rem;
}

/* The text alternative for each chart (see the chart_data macros in
   analytics.html). Collapsed by default so the page looks unchanged; the
   summary is the only thing that occupies space, and it sits tight under the
   chart it belongs to. Long ranges ("This year", "All time") produce a few
   hundred rows, so the opened table scrolls inside its own box rather than
   pushing the rest of the page down a screenful. */
.chart-data { margin: -0.7rem 0 1.2rem; }
.chart-data > summary {
  cursor: pointer;
  width: fit-content;
  padding: 0.15rem 0;
  font-size: 0.85rem;
  color: var(--muted);
}
.chart-data > summary:hover { color: var(--primary-dark); }
.chart-data-scroll { max-height: 320px; overflow-y: auto; margin-top: 0.5rem; }
.chart-data table { font-size: 0.85rem; }
.chart-data caption {
  text-align: left;
  font-size: 0.8rem;
  color: var(--muted);
  padding-bottom: 0.4rem;
}
/* A small "what is this for" disclosure beside a section heading. Three paragraphs of
   explanation were sitting permanently above the Clinicians table, saying things the
   User Guide already says at more length -- so the explanation folds away and the form
   is the thing you see.

   A <details>, not a hover tooltip. A tooltip shown on hover is invisible on a phone,
   unreachable from a keyboard, and gone the moment the pointer moves -- WCAG 2.2 SC
   1.4.13 exists because of exactly that. This opens on click, tap, Enter or Space,
   stays open, and reads out as a disclosure to a screen reader, with no JavaScript. */
.field-note { margin: -0.1rem 0 0.9rem; }
.field-note > summary {
  cursor: pointer;
  width: fit-content;
  padding: 0.15rem 0;
  font-size: 0.85rem;
  color: var(--muted);
}
.field-note > summary:hover { color: var(--primary-dark); }
.field-note > div {
  font-size: 0.85rem;
  color: var(--muted);
  line-height: 1.5;
  max-width: 60ch;
  margin-top: 0.35rem;
  padding-left: 0.7rem;
  border-left: 2px solid var(--border);
}
.field-note > div p { margin: 0 0 0.5rem; }
.field-note > div p:last-child { margin-bottom: 0; }

.chart-wrap { position: relative; height: 260px; margin: 0.6rem 0 1.2rem; }
.chart-wrap.chart-wrap-sm { height: 200px; }
.analytics-note { font-size: 0.85rem; color: var(--muted); margin: -0.4rem 0 1rem; }
/* The -0.4rem top margin above is tuned for .analytics-note right after
   .analytics-controls (pulls it in closer to the range buttons) -- after a
   .grid-2 of chart panels it instead reads as the caption nearly touching
   the bottom edge of the cards, since .grid-2 itself has no bottom margin
   of its own to push against. Give that specific pairing real breathing
   room instead. */
.grid-2 + .analytics-note { margin-top: 0.9rem; }
.timeline-body p { margin: 0.25rem 0; font-size: 0.88rem; }
.timeline-notes { color: var(--muted); font-style: italic; }
.timeline-lines { list-style: none; margin: 0 0 0.4rem; padding: 0; max-width: 28rem; font-size: 0.92rem; }
.timeline-lines li { display: flex; justify-content: space-between; gap: 1rem; padding: 0.1rem 0; border-bottom: 1px dashed var(--border); }
.timeline-lines li span:last-child { font-variant-numeric: tabular-nums; white-space: nowrap; }
.timeline-meta { display: flex; gap: 0.9rem; margin-top: 0.5rem; font-size: 0.85rem; color: var(--muted); align-items: center; }

/* ---------- Auth screen ---------- */
.auth-screen { min-height: 100vh; display: flex; align-items: center; justify-content: center; background: var(--bg); padding: 1rem; }
.auth-panel {
  background: var(--surface); border: 1px solid var(--border); border-radius: 14px;
  padding: 2.4rem 2.2rem; width: 340px; max-width: 100%; min-width: 0; box-shadow: var(--shadow); text-align: center;
  box-sizing: border-box;
}
.auth-mark {
  width: 44px; height: 44px; border-radius: 10px; background: var(--accent); color: var(--primary-dark);
  display: flex; align-items: center; justify-content: center; margin: 0 auto 1rem;
  font-family: var(--font-heading); font-weight: 700; font-size: 1.4rem;
}
/* The brand line above an auth panel's heading. This replaced a 56px logo
   image on 2026-09-10: the mark did not survive being small, and these panels
   had no other brand element, so the site's name in type does the job the
   picture was doing. --primary-deep is 7.34:1 on the panel surface, so it
   reads as brand colour without being a heading in disguise. */
.auth-wordmark {
  display: block; margin: 0 auto 1.1rem;
  font-family: var(--font-heading); font-weight: 700; font-size: 1.02rem;
  letter-spacing: -0.02em; color: var(--primary-deep);
}
a:hover > .auth-wordmark { color: var(--primary-dark); }
.auth-panel h1 { font-size: 1.5rem; }
.auth-sub { color: var(--muted); font-size: 0.85rem; margin: 0.2rem 0 1.6rem; }
.auth-form { display: flex; flex-direction: column; gap: 0.9rem; text-align: left; }
.auth-form label { font-size: 0.82rem; font-weight: 600; color: var(--muted); display: flex; flex-direction: column; gap: 0.35rem; }
.auth-form input { padding: 0.6rem 0.75rem; border: 1px solid var(--border); border-radius: 7px; font-family: inherit; font-size: 0.92rem; }
.auth-form .btn { margin-top: 0.4rem; }
.auth-hint { font-size: 0.75rem; color: var(--muted); margin-top: 1.2rem; line-height: 1.5; }
.auth-hint code { background: var(--bg); padding: 0.1rem 0.35rem; border-radius: 4px; }
/* The two-factor invitation sits below the "forgot your password" line and needs to
   read as a suggestion, not a warning -- so it borrows the same muted hint colour
   rather than any alert tone, and is separated by a rule instead of a coloured
   panel. It is the last thing on the page on purpose: it must never compete with
   the sign-in form for attention. */
/* Dashboard two-factor prompt. Uses the ordinary panel surface rather than a warning
   colour: this is a suggestion about an account that is working fine, and dressing it
   as a problem would train people to dismiss real warnings the same way. */
/* One slim row per notice, not a card with a paragraph in it: two of these
   used to push today's appointments below the fold. The sentence carries the
   point; the button carries the rest. */
.notice {
  display: flex; gap: 0.8rem; align-items: center; flex-wrap: wrap;
  padding: 0.5rem 0.9rem; margin-bottom: 0.6rem; font-size: 0.86rem;
  background: var(--surface); border: 1px solid var(--border); border-left: 3px solid var(--primary);
  border-radius: 8px;
}
.notice-warn { border-left-color: var(--accent); }
.notice-text { flex: 1 1 24ch; min-width: 0; }
.notice-text strong { font-weight: 600; }
.notice-actions { display: flex; gap: 0.4rem; align-items: center; margin-left: auto; }
.notice-actions .btn { padding: 0.35rem 0.75rem; font-size: 0.82rem; }
/* Kept for any template still using the old names; same as .notice. */
.mfa-notice { display: flex; gap: 0.8rem; align-items: center; flex-wrap: wrap; border-left: 3px solid var(--primary); margin-bottom: 0.6rem; }
.mfa-notice p { margin: 0.2rem 0 0; color: var(--muted); font-size: 0.85rem; max-width: 62ch; }
.mfa-notice-actions { display: flex; gap: 0.5rem; align-items: center; }

.auth-hint-mfa {
  margin-top: 1rem; padding-top: 1rem; border-top: 1px solid var(--border);
}

/* ---------- Responsive ---------- */
/* This used to be 3 separate @media (max-width: 860px) blocks scattered
   through the file (here, plus two more further down, one for the
   odontogram tooth size -- since removed with the feature -- and one for
   the intake checkbox grid) -- same
   breakpoint, same behavior, just harder to see at a glance that they're
   all the same condition. Consolidated into one block down where the third
   one used to live (see the "Responsive (consolidated)" comment below) so
   there's exactly one 860px query to update if this breakpoint ever
   changes. Moving each rule set later in the file rather than earlier is
   what keeps this safe -- CSS resolves same-specificity conflicts by
   source order, and every rule below already came *after* its own
   unconditional (non-media) counterpart at its old position, so moving it
   further down can only keep that override winning, never break it.
   Nothing here changes what actually renders at any screen width.
*/

@media (prefers-reduced-motion: reduce) {
  * { transition: none !important; animation: none !important; }
}

/* ---------- Shared panel header and hint text ---------- */
/* Named for the odontogram because that is where they were written. The tooth
   chart itself was retired on 2026-08-18 and its styles removed on 2026-08-30;
   these two outlived it because every panel header and every small grey hint
   line in the app uses them -- .odontogram-hint appears in 33 templates. Not
   renamed: a rename touching 33 files to fix a name is a large diff that buys
   nothing a comment does not. */
.odontogram-header {
  display: flex; align-items: center; justify-content: space-between;
  flex-wrap: wrap; gap: 0.8rem; margin-bottom: 0.8rem;
}
.odontogram-hint { font-size: 0.85rem; color: var(--muted); margin: 0 0 1.2rem; max-width: 640px; }

/* ---------- Appointment booking (public page) ---------- */
.booking-screen { min-height: 100vh; display: flex; align-items: center; justify-content: center; background: var(--bg); padding: 2.5rem 1rem; }
.booking-panel {
  background: var(--surface); border: 1px solid var(--border); border-radius: 14px;
  padding: 2.2rem 2rem; width: 100%; max-width: 560px; box-shadow: var(--shadow);
}
.booking-sub { color: var(--muted); font-size: 0.85rem; margin: 0.3rem 0 1.4rem; }

.booking-existing-banner {
  background: var(--primary-tint); color: var(--primary-dark); padding: 0.7rem 0.9rem;
  border-radius: 8px; font-size: 0.88rem; margin-bottom: 1.2rem;
}

/* ---------- Appointments (staff queue) ---------- */
.appointment-filters { display: flex; gap: 0.5rem; margin-bottom: 1.2rem; }
.appointment-actions { display: flex; gap: 0.7rem; }
/* min-height plus vertical padding takes this from ~13px to the 24px WCAG
   2.2 SC 2.5.8 minimum. These are destructive or state-changing actions
   (Confirm, Cancel, Void, Delete) sitting in flex button rows rather than
   inline in a sentence, so the inline exception does not cover them.
   inline-flex + align-items keeps the label optically centred while the
   text itself still determines the width. */
.link-confirm {
  background: none; border: none; color: var(--primary-text);
  font-size: 0.83rem; cursor: pointer; font-family: inherit; text-decoration: underline;
  display: inline-flex; align-items: center;
  min-height: 24px; padding: 0.25rem 0.2rem;
}
.link-confirm:focus-visible {
  outline: 2px solid var(--primary); outline-offset: 2px; border-radius: 4px;
}
.appt-status-pending { background: var(--accent-tint); color: var(--accent-text); }
.appt-status-confirmed { background: var(--primary-tint); color: var(--primary-dark); }
.appt-status-cancelled { background: var(--danger-tint); color: var(--danger-text); }
/* How a booking says it will be paid (docs/HMO_CLAIMS_DESIGN.md). Same three
   AA-checked pairs as the statuses above, so nothing new to measure. */
.tag.payer-hmo { background: var(--primary-tint); color: var(--primary-dark); }
.tag.payer-cash { background: var(--accent-tint); color: var(--accent-text); }
.tag.payer-unsure { background: var(--danger-tint); color: var(--danger-text); }
/* LOA status (release 3). 'expired' is derived at render time, never stored. */
.tag.loa-status-requested { background: var(--accent-tint); color: var(--accent-text); }
.tag.loa-status-approved, .tag.loa-status-used { background: var(--primary-tint); color: var(--primary-dark); }
.tag.loa-status-denied, .tag.loa-status-expired, .tag.loa-status-void { background: var(--danger-tint); color: var(--danger-text); }
/* Claim status (release 4). */
.tag.claim-status-draft, .tag.claim-status-submitted, .tag.claim-status-acknowledged, .tag.claim-status-resubmitted { background: var(--accent-tint); color: var(--accent-text); }
.tag.claim-status-paid { background: var(--primary-tint); color: var(--primary-dark); }
.tag.claim-status-short_paid, .tag.claim-status-denied, .tag.claim-status-written_off { background: var(--danger-tint); color: var(--danger-text); }

/* ---------- Appointments (weekly calendar) ---------- */
.view-toggle { display: flex; gap: 0.5rem; margin-left: auto; }

.calendar-toolbar {
  display: flex; align-items: center; justify-content: space-between;
  margin-bottom: 1rem; flex-wrap: wrap; gap: 0.6rem;
}
.calendar-range { margin: 0; }
.calendar-nav { display: flex; gap: 0.4rem; }

/* Contained inside its own frame, with its own scrollbar, instead of the
   whole page growing tall enough to need the browser's page-level scrollbar
   -- the 37rem-tall hour grid below otherwise pushed total page height past
   most viewports, and the resulting full-height scrollbar down the right
   edge of the whole screen read as a stray UI element rather than part of
   the calendar. max-height is sized against the viewport, leaving room for
   the page header/toolbar above; the scrollbar itself is styled slim
   instead of the browser's default full-width track. */
.calendar-grid-wrap {
  overflow-x: auto; overflow-y: auto; max-height: calc(100vh - 280px);
  scrollbar-width: thin; scrollbar-color: var(--border) transparent;
}
.calendar-grid-wrap::-webkit-scrollbar { width: 8px; height: 8px; }
.calendar-grid-wrap::-webkit-scrollbar-track { background: transparent; }
.calendar-grid-wrap::-webkit-scrollbar-thumb { background: var(--border); border-radius: 8px; }
.calendar-grid-wrap::-webkit-scrollbar-thumb:hover { background: var(--muted); }
.calendar-grid {
  display: grid;
  grid-template-columns: 52px repeat(7, minmax(104px, 1fr));
  min-width: 660px;
}

.calendar-corner { border-bottom: 1px solid var(--border); }
.calendar-day-head {
  text-align: center; padding: 0.4rem 0 0.6rem; border-bottom: 1px solid var(--border);
  display: flex; flex-direction: column; gap: 0.15rem;
}
.calendar-day-head.is-today .calendar-day-num { color: var(--primary-text); font-weight: 700; }
.calendar-day-label { font-size: 0.72rem; color: var(--muted); text-transform: uppercase; letter-spacing: 0.06em; font-weight: 600; }
.calendar-day-num { font-family: var(--font-heading); font-weight: 600; font-size: 1.1rem; color: var(--ink); }

/* The corner label sits in a 52px column; "UNSCHEDULED" in small caps with
   letter-spacing is wider than that and was clipped to "EDULED". Two lines,
   no tracking, and min-width: 0 so the grid track cannot be forced wider. */
.calendar-unscheduled-label {
  font-size: 0.62rem; color: var(--muted); text-transform: uppercase; letter-spacing: 0;
  font-weight: 600; display: flex; align-items: flex-start; min-width: 0;
  justify-content: flex-end; text-align: right; padding: 5px 6px 0 0; border-bottom: 1px solid var(--border);
  line-height: 1.15; overflow: hidden;
}
.calendar-unscheduled-label span { display: block; }
.calendar-unscheduled-cell {
  /* 0.25rem (4px) was too tight for the SC 2.5.8 spacing exception once the
     chips themselves were under 24px; 0.35rem keeps them comfortably apart
     now that each chip is 24px in its own right. */
  display: flex; flex-direction: column; gap: 0.35rem; padding: 0.35rem;
  border-bottom: 1px solid var(--border); border-left: 1px solid var(--border);
  /* Tall enough that the 7 AM axis label, centred on the first hour line just
     below, no longer rises into this row and over the corner label. */
  min-height: 2.4rem;
}
/* Was ~19px tall with a 4px gap between chips, which is under the 24px target
   minimum and too tightly packed to qualify for the spacing exception either.
   These open the appointment popover, so a mis-tap acts on the wrong
   appointment. Height comes from min-height rather than a taller font so the
   chip text stays the same size in a dense week view. */
.calendar-chip {
  display: flex; align-items: center; width: 100%; font-size: 0.74rem;
  padding: 0.2rem 0.4rem; min-height: 24px; border-radius: 6px;
  white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
  appearance: none; -webkit-appearance: none; border: none; text-align: left;
  font-family: inherit; cursor: pointer;
}
.calendar-chip:focus-visible {
  outline: 2px solid var(--primary); outline-offset: 1px;
}
.calendar-chip:hover { filter: brightness(0.97); }

/* 37rem over a 12-hour day gave each hour ~3.1rem, so a 30-minute appointment
   was ~25px tall -- shorter than its own contents. Every block rendered time,
   name and (for a super admin) clinic, about 39px of text, and clipped the
   difference. 58rem gives the hour ~4.8rem and the default appointment ~39px,
   so the common case now fits instead of being cropped. The grid already
   scrolls (see .calendar-grid-wrap above), so this costs page height, not
   layout. */
.calendar-time-axis, .calendar-day-col { position: relative; height: 58rem; }
.calendar-day-col { border-left: 1px solid var(--border); }
.calendar-hour-label {
  position: absolute; left: 0; right: 6px; transform: translateY(-50%);
  font-size: 0.72rem; color: var(--muted); text-align: right;
}
.calendar-hour-line { position: absolute; left: 0; right: 0; border-top: 1px solid var(--border); }
/* Today's column is tinted and carries a line at the current time, so the
   desk can see where the day is without reading the axis. */
.calendar-day-col.is-today { background: rgba(30, 154, 131, 0.05); }
.calendar-now-line { position: absolute; left: 0; right: 0; border-top: 2px solid var(--danger); z-index: 1; pointer-events: none; }
.calendar-now-line::before { content: ""; position: absolute; left: -4px; top: -5px; width: 8px; height: 8px; border-radius: 50%; background: var(--danger); }

.appt-block {
  /* left/width are set inline per-block (see appointments.html) from the
     lane assignment _build_week_calendar() computes in app.py -- two
     appointments booked for the same or overlapping time (allowed on
     purpose, not a bug -- see the clash-warning in book_confirm()) get
     their own side-by-side lane instead of both being drawn at full column
     width in the same spot, where the second one used to completely and
     invisibly cover the first. 3px here is only a fallback for the
     unexpected case where lane data is somehow missing. */
  position: absolute; left: 3px; right: 3px; min-height: 32px; overflow: hidden;
  border-radius: 6px; border-left: 3px solid currentColor; padding: 2px 6px;
  font-size: 0.7rem; line-height: 1.15; display: flex; flex-direction: column;
  /* flex-start, not center. When content is taller than the block, centering
     overflows it EQUALLY in both directions and overflow:hidden then crops the
     top and bottom -- which is why a clipped block showed a slice through the
     middle of the patient's name rather than simply losing the last line.
     Starting at the top means anything cropped is cropped off the bottom, so
     the time and the name survive and the clinic line is what goes. */
  justify-content: flex-start; color: inherit; box-shadow: var(--shadow);
  appearance: none; -webkit-appearance: none; border-top: none; border-right: none;
  border-bottom: none; text-align: left; font-family: inherit; cursor: pointer;
}
.appt-block:hover { filter: brightness(0.97); text-decoration: none; }
.appt-block-time { font-weight: 600; font-size: 0.68rem; }
.appt-block-name { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.appt-block-clinic { font-size: 0.65rem; opacity: 0.8; }
/* Three or more appointments sharing a slot each get a third of the column or
   less -- about 30px, where no label is legible. The template drops the time
   and clinic lines at that point and keeps the name, which is the one thing
   worth reading; the row position already communicates the time, and the full
   detail is a hover away in the title and a click away in the popover. */
.appt-block.is-narrow { padding: 2px 3px; font-size: 0.64rem; }

.calendar-popover-backdrop { position: fixed; inset: 0; background: transparent; z-index: 40; }
.calendar-popover {
  position: absolute; z-index: 41; width: 260px;
  background: var(--surface); border: 1px solid var(--border); border-radius: var(--radius);
  box-shadow: var(--shadow); padding: 0.9rem 1rem;
}
.calendar-popover-head { display: flex; align-items: center; justify-content: space-between; margin-bottom: 0.3rem; }
.calendar-popover-time { font-size: 0.78rem; color: var(--muted); font-weight: 600; }
/* Was ~19x19. A dismiss control on a panel whose other buttons change
   appointment status, so it wants to be comfortably hittable -- 28px square,
   with the glyph kept at its original size. */
.calendar-popover-close {
  background: none; border: none; padding: 0; color: var(--muted); font-size: 1.2rem;
  line-height: 1; cursor: pointer;
  display: inline-flex; align-items: center; justify-content: center;
  width: 28px; height: 28px; border-radius: 6px; margin: -0.2rem -0.4rem -0.2rem 0;
}
.calendar-popover-close:focus-visible {
  outline: 2px solid var(--primary); outline-offset: 1px;
}
.calendar-popover-close:hover { color: var(--ink); }
.calendar-popover h3 { margin: 0 0 0.15rem; }
.calendar-popover-meta { margin: 0 0 0.2rem; font-size: 0.85rem; color: var(--muted); }
.calendar-popover-reason { margin: 0.3rem 0; font-size: 0.85rem; }
.calendar-popover-actions { display: flex; flex-wrap: wrap; gap: 0.4rem; margin-top: 0.7rem; }
.calendar-popover-actions .btn { padding: 0.4rem 0.75rem; font-size: 0.82rem; }

/* Generic centered modal (currently just the "Cancel appointment" dialog
   in appointments.html) -- distinct from .calendar-popover above, which is
   anchored to its trigger rather than centered, and doesn't dim the rest
   of the page. */
.modal-backdrop { position: fixed; inset: 0; background: rgba(11, 46, 42, 0.45); z-index: 60; }
.modal-panel {
  position: fixed; top: 50%; left: 50%; transform: translate(-50%, -50%); z-index: 61;
  width: min(420px, calc(100vw - 2rem));
  background: var(--surface); border: 1px solid var(--border); border-radius: var(--radius);
  box-shadow: var(--shadow); padding: 1.3rem 1.4rem;
}
.modal-panel h3 { margin: 0 0 0.3rem; }
.modal-panel .muted { margin: 0 0 0.8rem; font-size: 0.85rem; color: var(--muted); }
.modal-panel textarea {
  width: 100%; resize: vertical; font-family: inherit; font-size: 0.9rem; color: var(--ink);
  padding: 0.55rem 0.7rem; border: 1px solid var(--border); border-radius: 7px; background: var(--surface);
}
.modal-panel textarea:focus { outline: 2px solid var(--primary); outline-offset: 1px; border-color: var(--primary); }
.modal-actions { display: flex; justify-content: flex-end; gap: 0.5rem; margin-top: 1rem; }

/* ---------- Dashboard ---------- */
/* Four small numbers, each a link to the screen it counts. Tone follows the
   chip vocabulary: amber when there is something to do, red when money is
   over 30 days. */
.kpi-row { display: grid; grid-template-columns: repeat(4, minmax(0, 1fr)); gap: 0.7rem; margin-bottom: 0.9rem; }
.kpi {
  display: flex; flex-direction: column; gap: 0.1rem; min-width: 0;
  background: var(--surface); border: 1px solid var(--border); border-radius: var(--radius);
  padding: 0.75rem 0.95rem; color: var(--ink); text-decoration: none; box-shadow: var(--shadow);
}
.kpi:hover { text-decoration: none; border-color: var(--primary); }
.kpi-label { font-size: 0.74rem; font-weight: 500; color: var(--muted); letter-spacing: 0.02em; }
.kpi-value { font-family: var(--font-heading); font-weight: 600; font-size: 1.45rem; letter-spacing: -0.01em; color: var(--primary-dark); font-variant-numeric: tabular-nums; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.kpi-value small { font-size: 0.8rem; font-weight: 500; color: var(--muted); font-family: inherit; }
.kpi-sub { font-size: 0.76rem; color: var(--muted); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.kpi-warn .kpi-value { color: var(--accent-text); }
.kpi-bad .kpi-value { color: var(--danger-text); }

.dash-grid { display: grid; grid-template-columns: minmax(0, 1.5fr) minmax(0, 1fr); gap: 0.9rem; align-items: start; }
.dash-side { display: grid; gap: 0.9rem; }
.panel-head {
  display: flex; align-items: center; justify-content: space-between; gap: 0.8rem;
  padding: 0.7rem 1.1rem; border-bottom: 1px solid var(--border);
}
.panel-head h2 { margin: 0; font-size: 1rem; }
.panel-count { font-weight: 500; font-size: 0.82rem; color: var(--muted); margin-left: 0.35rem; }
.panel-link { font-size: 0.84rem; font-weight: 500; white-space: nowrap; }
.panel-foot-note { margin: 0; padding: 0.5rem 1.1rem 0.7rem; font-size: 0.8rem; color: var(--muted); }

/* Today's list: time, who, state, one action. The next slot is tinted. */
.today-list { list-style: none; margin: 0; padding: 0; }
.today-row {
  display: grid; grid-template-columns: 4.6rem minmax(0, 1fr) auto auto; gap: 0.7rem; align-items: center;
  padding: 0.55rem 1.1rem; border-bottom: 1px solid var(--border);
}
.today-row:last-child { border-bottom: 0; }
.today-row.is-next { background: var(--primary-tint); }
.today-time { font-size: 0.8rem; font-weight: 600; color: var(--ink); white-space: nowrap; }
.today-row.is-next .today-time { color: var(--primary-text); }
.today-row.is-next .avatar { background: var(--surface); }
.today-who { display: flex; align-items: center; gap: 0.6rem; min-width: 0; }
.today-who-text { display: flex; flex-direction: column; min-width: 0; }
.today-name { color: var(--ink); font-weight: 500; }
.today-sub { font-size: 0.8rem; color: var(--muted); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.today-action { min-width: 6.4rem; text-align: right; }
.today-action .btn { white-space: nowrap; }

.decision-list, .activity-list { list-style: none; margin: 0; padding: 0; }
.decision-list li, .activity-list li {
  display: flex; align-items: center; gap: 0.6rem; padding: 0.5rem 1.1rem;
  border-bottom: 1px solid var(--border); font-size: 0.88rem;
}
.decision-list li:last-child, .activity-list li:last-child { border-bottom: 0; }
.decision-text, .activity-text { flex: 1; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.decision-text a, .activity-text a { color: var(--ink); font-weight: 500; }
.decision-meta, .activity-when { font-size: 0.78rem; color: var(--muted); white-space: nowrap; }
.activity-list .avatar { width: 26px; height: 26px; font-size: 0.66rem; }

/* ---------- Dashboard: today's appointments (older widget styles) ---------- */
.todays-appt-row {
  display: flex; align-items: center; justify-content: space-between;
  gap: 1rem; padding: 0.6rem 0.2rem;
}
.todays-appt-row .todays-appt-link {
  flex: 1; min-width: 0; padding: 0; display: flex !important; flex-direction: column;
}
.todays-appt-time { font-size: 0.78rem; color: var(--primary-text); font-weight: 700; }
.todays-appt-actions { display: flex; align-items: center; gap: 0.6rem; flex-shrink: 0; }

/* ---------- Freehand chart notes ---------- */
.freehand-toolbar { display: flex; align-items: center; gap: 0.6rem; flex-wrap: wrap; }
.freehand-colors { display: flex; gap: 0.4rem; margin-right: 0.4rem; }
.color-swatch {
  width: 26px; height: 26px; border-radius: 50%; border: 2px solid transparent;
  cursor: pointer; padding: 0; box-shadow: 0 0 0 1px var(--border);
}
.color-swatch.active { border-color: var(--surface); box-shadow: 0 0 0 2px var(--primary); }
#freehand-erase.active { background: var(--danger); color: #fff; border-color: var(--danger); }
.btn:disabled { opacity: 0.45; cursor: not-allowed; }

.freehand-canvas-wrap { position: relative; touch-action: none; }
.freehand-canvas-wrap canvas {
  position: absolute; top: 0; left: 0; width: 100%; height: 100%;
  pointer-events: none;
  /* The canvas is a transparent layer sitting on top of the tooth-chart
     image, not part of the same bitmap -- by default a stroke drawn over a
     black outline paints fully opaque and blots the line out underneath it.
     multiply blends the stroke color with whatever's already there instead
     of covering it: black stays black no matter what color is drawn over
     it, and marks over white areas keep their full color, so linework stays
     readable under any freehand marking. */
  mix-blend-mode: multiply;
}
.freehand-canvas-wrap.editing canvas { pointer-events: auto; cursor: crosshair; }

/* The chart background: a plain, non-clickable image.
   The comment here used to say this reused "the interactive tooth shapes" --
   an earlier design that was already gone. It is an <img>, which is why
   removing the retired odontogram's SVG styles on 2026-08-30 left the freehand
   chart untouched. */
.freehand-bg-image {
  display: block; width: 100%; height: auto; user-select: none; pointer-events: none;
  border-radius: 8px;
}
/* ---------- Freehand chart notes ----------
   The chart's text equivalent. Styled as a normal form field rather than
   something secondary: it is the only part of a chart that a screen reader can
   read, that the exported PDF carries in words, and that could ever be searched.
   readonly (outside edit mode, and always for the staff role) is shown by a
   muted background rather than the disabled opacity used on buttons, which
   would push the text below a readable contrast ratio. */
.freehand-notes { margin-top: 1rem; }
.freehand-notes label {
  display: flex; flex-direction: column; gap: 0.35rem;
  font-size: 0.85rem; font-weight: 600; color: var(--primary-dark);
}
.freehand-notes-optional { font-weight: 500; color: var(--muted); }
.freehand-notes textarea {
  width: 100%; resize: vertical; font-family: inherit; font-size: 0.92rem;
  line-height: 1.5; color: var(--ink); background: var(--surface);
  padding: 0.6rem 0.75rem; border: 1px solid var(--border); border-radius: 7px;
}
.freehand-notes textarea:focus {
  outline: 2px solid var(--primary); outline-offset: 1px; border-color: var(--primary);
}
.freehand-notes textarea[readonly] {
  background: var(--surface-alt); color: var(--ink);
}

.chart-attribution {
  font-size: 0.75rem; color: var(--muted); margin-top: 0.6rem;
}

/* ---------- Freehand chart accordion (adult / pediatric) ---------- */
.freehand-accordion-summary {
  cursor: pointer; list-style: none; display: flex; align-items: center; justify-content: space-between;
  font-family: var(--font-heading); font-weight: 600; font-size: 1.05rem; color: var(--primary-dark);
}
.freehand-accordion-summary::-webkit-details-marker { display: none; }
.freehand-accordion-summary::marker { content: ""; }
.freehand-accordion-summary:hover { color: var(--primary-text); }
.accordion-chevron {
  color: var(--muted); font-size: 0.75rem; transition: transform 0.15s ease; flex-shrink: 0; margin-left: 0.8rem;
}
.freehand-accordion[open] .accordion-chevron { transform: rotate(180deg); }
.freehand-accordion-body { margin-top: 1rem; }
.chart-attribution a { color: var(--muted); text-decoration: underline; }

/* ---------- Generic collapsible panel (Billing, Documents, Platform
   Billing's sections, Site Guide, etc.) -- same <details>/<summary>
   open/close mechanics as the freehand chart accordion above, and
   reuses its .accordion-chevron, just generalized under a plain name so
   any .panel can become collapsible without a chart-specific class. */
.panel-accordion-summary {
  cursor: pointer; list-style: none; display: flex; align-items: center; justify-content: space-between;
}
.panel-accordion-summary::-webkit-details-marker { display: none; }
.panel-accordion-summary::marker { content: ""; }
.panel-accordion-summary h2 { margin-bottom: 0; }
.panel-accordion[open] .accordion-chevron { transform: rotate(180deg); }
.panel-accordion-body { margin-top: 0.9rem; }

/* ---------- Patient intake form ---------- */
.intake-panel { max-width: 720px; text-align: left; }
.intake-panel .booking-form { text-align: left; }
.intake-section-title {
  margin: 1.8rem 0 0.9rem; padding-top: 1rem; border-top: 1px solid var(--border);
}
.intake-panel form > .intake-section-title:first-of-type,
.intake-section-title:first-child { margin-top: 0.6rem; padding-top: 0; border-top: none; }
.intake-subhead {
  font-weight: 700; font-size: 0.92rem; color: var(--primary-dark);
  margin: 1.2rem 0 0.6rem;
}
/* Groups of related questions on the intake form, as real fieldsets so the
   question is programmatically attached to its checkboxes rather than merely
   sitting above them. The browser's default fieldset border and padding are
   stripped: the legend alone reads as the group here, and the surrounding
   panel already provides the boundary. min-width:0 because a fieldset has an
   intrinsic min-width that otherwise stops the grids inside it from shrinking
   on narrow screens. */
.intake-group {
  border: none; margin: 0; padding: 0; min-width: 0;
}
/* legend does not accept flex/grid children in every browser, and it sits
   outside the normal flow, so the subhead styling is applied to it directly
   rather than inherited. Same visual result as the <p> it replaced. */
.intake-group > legend.intake-subhead {
  padding: 0;
  float: none;
  width: auto;
}

/* Required-field marker. aria-hidden because the control's own `required`
   attribute already conveys this to assistive tech -- without that, a screen
   reader announces "First name star, required", saying it twice. This marker
   is purely for sighted users, who previously had nothing at all: every form
   here marked required fields with the attribute only. */
.req {
  color: var(--danger-text); font-weight: 700; margin-left: 0.15rem;
}
/* The key that explains the marker, shown once per form. */
.required-key {
  font-size: 0.82rem; color: var(--muted); margin: 0 0 1rem;
}
.required-key .req { margin-left: 0; }

/* minmax(0, 1fr), not 1fr. `1fr` is shorthand for `minmax(auto, 1fr)`, and
   `auto` as a track minimum means min-content -- so the track REFUSES to
   shrink below its widest item and the item overflows the grid instead. It is
   the same trap as `min-width: auto` on a flex item, one level up, and it is
   why the "Other:" box on intake question 8 still sat over the edge of the
   form after min-width: 0 was added to the input itself. The input could
   shrink; the column it lived in could not. Measured in Chrome at 280-860px
   across one, two and three columns: 42px of overflow before, none after. */
.intake-checkbox-grid {
  display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 0.5rem 1rem; margin-bottom: 0.6rem;
}
.intake-checkbox-grid-3col { grid-template-columns: repeat(3, minmax(0, 1fr)); }
.checkbox-item {
  display: flex; align-items: center; gap: 0.45rem; font-size: 0.85rem; color: var(--ink);
  font-weight: 400; cursor: pointer;
  /* Wrap rather than overflow. When the row genuinely cannot fit -- a three
     column grid on a narrow phone -- the "Other:" input drops onto its own
     line under the label instead of running out of the card. A basis of 7rem
     is what stops it wrapping while there is still room for it. */
  flex-wrap: wrap;
}
/* .checkbox-item is a ROW -- box, then its words. Inside a .form-grid it is
   also a `label`, and `.form-grid label` sets flex-direction: column at
   higher specificity (a class plus a type beats a bare class), so the tick
   rendered centred ABOVE its own text on Settings' document-footer switch.
   Scoped here rather than by loosening either rule: the column is right for
   the grid's ordinary fields, where the caption belongs above its input. */
.form-grid .checkbox-item {
  flex-direction: row; align-items: center; justify-content: flex-start;
}

.checkbox-item .inline-text-input { flex: 1 1 7rem; min-width: 0; }
.checkbox-item input[type="checkbox"] {
  appearance: none; -webkit-appearance: none;
  width: 18px; height: 18px; flex-shrink: 0; margin: 0;
  border: 1px solid var(--border); border-radius: 5px; background: var(--surface);
  display: inline-grid; place-content: center; cursor: pointer;
  transition: background 0.12s ease, border-color 0.12s ease;
}
.checkbox-item input[type="checkbox"]::before {
  content: ""; width: 10px; height: 10px; background: var(--surface);
  clip-path: polygon(14% 44%, 0 65%, 50% 100%, 100% 16%, 80% 0%, 43% 62%);
  transform: scale(0); transition: transform 0.1s ease-in-out;
}
.checkbox-item input[type="checkbox"]:checked { background: var(--primary); border-color: var(--primary); }
.checkbox-item input[type="checkbox"]:checked::before { transform: scale(1); }
.checkbox-item input[type="checkbox"]:focus-visible { outline: 2px solid var(--primary); outline-offset: 1px; }
/* min-width: 0 is what makes `flex: 1` mean "take the space that is left".
   A flex item defaults to min-width:auto, and a text input's auto minimum is
   its intrinsic size -- about twenty characters -- so without this the input
   REFUSES to shrink and runs out past the card instead. That is how the
   intake form's "Other:" box ended up over the edge of the form on a phone.
   .search-bar input already carried this fix; it just was not carried across. */
.inline-text-input {
  flex: 1; min-width: 0; padding: 0.25rem 0.5rem; border: 1px solid var(--border);
  border-radius: 5px; font-family: inherit; font-size: 0.82rem; margin-left: 0.3rem;
}

/* ---------- Signature options (intake form) ----------
   The two ways to sign -- draw or type -- presented as real fieldsets so the
   grouping is programmatic and not just visual. These are the first fieldsets
   in the codebase; the styling deliberately strips the browser's default border
   and inherits the surrounding panel, since the legend alone is enough to read
   as a group here. */
.signature-option {
  border: none; margin: 0; padding: 0; min-width: 0;
}
.signature-option > legend {
  font-weight: 700; font-size: 0.92rem; color: var(--primary-dark);
  padding: 0; margin-bottom: 0.35rem;
}
.signature-option label {
  display: flex; flex-direction: column; gap: 0.35rem;
  font-size: 0.82rem; font-weight: 600; color: var(--muted);
}
.signature-option input[type="text"] {
  padding: 0.6rem 0.75rem; border: 1px solid var(--border); border-radius: 7px;
  font-family: inherit; font-size: 1rem; color: var(--ink); background: var(--surface);
}
.signature-option input[type="text"]:focus {
  outline: 2px solid var(--primary); outline-offset: 1px; border-color: var(--primary);
}
/* The "or" between the two options. aria-hidden in the markup -- the legends
   already convey that these are alternatives, so announcing a bare "or"
   between them adds noise rather than meaning. */
.signature-or {
  margin: 0.9rem 0; font-size: 0.85rem; font-weight: 600; color: var(--muted);
  text-transform: uppercase; letter-spacing: 0.08em; display: flex;
  align-items: center; gap: 0.8rem;
}
.signature-or::before,
.signature-or::after {
  content: ""; flex: 1; height: 1px; background: var(--border);
}

.signature-pad-wrap {
  border: 1px dashed var(--border); border-radius: 8px; background: #FAFBFA;
  height: 160px; position: relative; touch-action: none; margin-bottom: 0.6rem;
}
.signature-pad-wrap canvas { position: absolute; top: 0; left: 0; width: 100%; height: 100%; cursor: crosshair; }
.signature-actions { display: flex; justify-content: flex-end; margin-bottom: 0.4rem; }

/* ---------- Responsive (consolidated) ---------- */
/* See the comment near the top of the file (where this used to be the
   first of three separate 860px blocks) -- this is now the one place all
   of the app's max-width: 860px rules live. */
@media (max-width: 860px) {
  .shell { flex-direction: column; }
  /* The nav has up to 6 links (Dashboard, Patients, Appointments, Analytics,
     Settings, and Platform billing for super admins) -- that never fits on
     one unwrapping row on a phone-width screen. Previously .sidebar and
     .nav both stayed as a single flex row here, so the links silently
     overflowed past the sidebar's own background and pushed the whole page
     wider than the viewport, breaking the layout everywhere below it (see
     the "blank dashboard" mobile screenshot this fixed). Stacking the brand
     above a wrapping nav row keeps everything inside the sidebar's own
     width, no matter how many links are showing for a given user. */
  /* The sidebar becomes a slim top bar, not a full-height column -- with a
     phone screen this tall, showing the full nav (up to 6 links) and the
     full footer (account chip + 4 more links) at all times, as the earlier
     fix did, pushed real page content below the fold before you'd even
     scrolled. Collapsed by default, revealed on tap via .nav-toggle. */
  .sidebar { width: 100%; flex-direction: column; align-items: stretch; padding: 0.7rem 1rem; }
  .sidebar-top { margin-bottom: 0; }
  .nav-toggle { display: flex; }
  .nav {
    flex-direction: column; gap: 0.15rem; max-height: 0; overflow: hidden;
    margin-top: 0; transition: max-height 0.2s ease, margin-top 0.2s ease;
  }
  .sidebar-footer {
    max-height: 0; overflow: hidden; border-top: none; margin-top: 0; padding-top: 0;
    transition: max-height 0.2s ease;
  }
  /* The open state must be able to SCROLL, not just be taller.
     `max-height: 420px` with `overflow: hidden` was a menu that silently
     truncated: a super admin with the orthodontics add-on has 10 links and 4
     group headings, about 600px of them, so Installments, Platform billing and
     the Patient Access Log were cut off with nothing to scroll to reach them --
     the links were in the page, correctly gated, and simply could not be
     touched. Reported from a phone on 4 September 2026.

     The height is now a fraction of the viewport rather than a pixel count
     guessed from today's link list, and overflow-y is auto, so the menu holds
     whatever the signed-in role and the enabled add-ons put in it: a longer
     list scrolls instead of disappearing. `overflow: hidden` stays on the
     CLOSED state, where it is what makes the collapse work. */
  .sidebar.nav-open .nav {
    max-height: 80vh; margin-top: 0.9rem; overflow-y: auto; -webkit-overflow-scrolling: touch;
  }
  .sidebar.nav-open .sidebar-footer {
    max-height: 80vh; margin-top: 0.9rem; padding-top: 0.9rem;
    overflow-y: auto; -webkit-overflow-scrolling: touch;
    border-top: 1px solid rgba(234, 242, 240, 0.12);
  }
  .main { padding: 0.8rem 1rem 1.6rem; }
  /* The search moves into the green header (one tap, no menu); the top bar
     keeps only the two action buttons, side by side. Sticky is dropped on
     the phone: a sticky bar over a short screen costs more than it gives. */
  .sidebar-search { display: block; margin-top: 0.6rem; }
  .sidebar-search input {
    width: 100%; height: 40px; padding: 0 0.8rem; border-radius: 8px; border: 1px solid rgba(234, 242, 240, 0.18);
    background: rgba(234, 242, 240, 0.1); color: #fff; font: inherit; font-size: 0.95rem;
  }
  .sidebar-search input::placeholder { color: #A9C4BD; }
  .sidebar-search input:focus { outline: 2px solid var(--primary); outline-offset: 1px; }
  .topbar { position: static; padding: 0 0 0.6rem; margin-bottom: 0.6rem; }
  .topbar-search { display: none; }
  .topbar-actions { margin-left: 0; width: 100%; }
  .topbar-actions .btn { flex: 1; min-height: 44px; }
  .nav-link { min-height: 44px; }
  .grid-2, .grid-detail, .form-grid { grid-template-columns: minmax(0, 1fr); }
  .span-2 { grid-column: span 1; }
  /* Flex rows that assume desktop-width content and don't already wrap --
     without flex-wrap, any row like this that doesn't fit on one line
     pushes the whole page wider than the viewport (the same root cause as
     the earlier nav overflow bug), so these wrap on phone-width screens too. */
  .analytics-controls, .appointment-filters, .search-bar { flex-wrap: wrap; }
  .search-bar input { flex-basis: 100%; }
  /* Stat cards: at the 2rem desktop size, an amount like "PHP 12,850.00"
     is wider than a card can be once 3-4 of them share a phone-width row --
     the text doesn't clip, it just visually spills past the card's own
     right edge into whatever's next to it. Wrapping the row into a 2-column
     grid plus a smaller value size keeps every card self-contained. */
  /* The record on a phone: header stacks, Log visit is the one full-width
     action and the rest sit in a row under it; tabs scroll sideways. */
  .record-hero { grid-template-columns: auto minmax(0, 1fr); }
  .record-actions { grid-column: 1 / -1; display: grid; grid-template-columns: repeat(3, minmax(0, 1fr)); gap: 0.4rem; }
  .record-actions .btn-primary { grid-column: 1 / -1; order: -1; min-height: 44px; }
  .record-actions .btn { min-height: 40px; }
  .record-cols { grid-template-columns: minmax(0, 1fr); }
  .record-timeline li { grid-template-columns: 4.4rem minmax(0, 1fr); }
  .record-timeline li .chip { grid-column: 2; justify-self: start; }
  .tab { min-height: 44px; display: inline-flex; align-items: center; }
  .kpi-row { grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 0.5rem; }
  .kpi { padding: 0.6rem 0.75rem; }
  .kpi-value { font-size: 1.25rem; }
  .dash-grid { grid-template-columns: minmax(0, 1fr); }
  .today-row { grid-template-columns: 3.6rem minmax(0, 1fr); row-gap: 0.35rem; padding: 0.6rem 1rem; }
  .today-row .chip { grid-column: 2; justify-self: start; }
  .today-action { grid-column: 2; text-align: left; min-width: 0; }
  .today-action .btn { min-height: 40px; }
  .stat-row { flex-wrap: wrap; }
  .stat-card { flex: 1 1 calc(50% - 0.5rem); padding: 1rem 1.1rem; }
  .stat-value { font-size: 1.4rem; overflow-wrap: break-word; }

  /* Patient intake form's condition checkboxes: single column instead of a
     multi-column grid, which gets too cramped at phone width. */
  .intake-checkbox-grid, .intake-checkbox-grid-3col { grid-template-columns: minmax(0, 1fr); }

  /* Table cells stop insisting on one line. `white-space: nowrap` on an
     actions cell is right on a desktop -- a button wrapping mid-label looks
     like a rendering fault -- and is what sets the table's minimum width on a
     phone, where there is no width to insist on. Buttons stack instead, which
     is what makes the roster fit a 360px screen rather than merely scroll. */
  /* !important because the cells that force the overflow carry the nowrap as an
     INLINE style, and inline beats a class selector. Narrowing it to
     [style*="nowrap"] would miss the next one written as a class instead;
     every cell may wrap on a phone, and there is no width here to insist on. */
  .table th, .table td { white-space: normal !important; }

  /* The invoice line-item grid. A text input carries an intrinsic min-content
     width of roughly twenty characters, so `width: 100%` on it is a ceiling
     and the CELL still refuses to go below ~180px -- three inputs and a
     remove button is a ~440px floor on a ~360px screen. table-layout: fixed
     makes the declared widths authoritative instead of advisory, and
     min-width: 0 is what lets the description input actually shrink to its
     column. Without both, the table is contained by its wrapper and scrolls
     rather than fits. */
  .invoice-items-table { table-layout: fixed; }
  .invoice-items-table input { min-width: 0; }
  .invoice-items-table th, .invoice-items-table td { padding-left: 0.2rem; padding-right: 0.2rem; }
  .invoice-items-table .qty-input { width: 100%; }
  .invoice-items-table .price-input { width: 100%; }
  .invoice-items-table col.col-qty { width: 3.4rem; }
  .invoice-items-table col.col-price { width: 5rem; }
  .invoice-items-table col.col-payer { width: 5rem; }
  .invoice-items-table col.col-remove { width: 2.2rem; }



  /* The file input's 200px floor is right on a desktop, where it keeps
     "Choose file / No file chosen" readable, and is simply more than a 360px
     screen has to give once the label and button share the row. */
  .upload-form input[type="file"] { min-width: 0; }
  .table td .btn, .table td .link-danger { display: block; width: 100%; text-align: center; }
  .table td form { display: block !important; }
  .table td form + form { margin-top: 0.35rem; }
  .table th, .table td { padding-left: 0.45rem; padding-right: 0.45rem; }
}

/* Below 640px the four-column grid stops being worth its own cost. Qty,
   price and the remove button need about 195px of the 343px a 412px phone
   actually offers, which left the description -- the only field anybody
   types a sentence into -- with 167px. Reported as "description field too
   small", and it was.

   So on a phone the row stops behaving like a table row: description takes
   a full-width line of its own, and qty, price and remove share the line
   under it. Measured in Chrome: the description input goes from 167px to
   380px at 412px wide, with no overflow anywhere from 320px up.

   The <thead> is hidden here, so each cell carries its own label via
   data-label. Removing the headings without replacing them would leave
   three unlabelled boxes -- the aria-labels on the inputs cover screen
   readers, and these cover everyone else. */
/* The directory on a phone: not a six-column table squeezed into 360px, but
   one card per patient -- name and DRN beside the initials, then age, mobile
   and next visit on one line, then the flag chips. The <thead> goes; each
   cell that still shows carries its label as a prefix. Last visit is dropped
   here on purpose: the phone screen is the one at the chair, where "next"
   matters and "last" is a tap away on the record. */
@media (max-width: 640px) {
  .directory thead { display: none; }
  .directory, .directory tbody { display: block; width: 100%; }
  .directory tr {
    display: block; padding: 0.7rem 1rem;
    border-bottom: 1px solid var(--border);
  }
  .directory tr:last-child { border-bottom: 0; }
  .directory td { display: block; padding: 0 !important; border: 0; font-size: 0.86rem; }
  .directory td[data-label="Patient"] { margin-bottom: 0.2rem; }
  /* No Open button on the phone: the whole card is the tap target
     (data-row-href), and the name is the link. */
  .directory .directory-open { display: none; }
  .directory td[data-label="Age"], .directory td[data-label="Mobile"], .directory td[data-label="Next"] {
    display: inline; color: var(--muted); font-size: 0.8rem;
  }
  .directory td[data-label="Age"]::before { content: "Age "; }
  .directory td[data-label="Mobile"]::before { content: " · "; }
  .directory td[data-label="Next"]::before { content: " · Next "; }
  .directory td[data-label="Last visit"], .directory td[data-label="Clinic"] { display: none; }
  .directory .directory-flags { margin-top: 0.3rem; }
  .quick-filters { flex-wrap: nowrap; overflow-x: auto; padding-bottom: 0.2rem; scrollbar-width: none; }
  .quick-filters-note { display: none; }
  .chip-filter { flex-shrink: 0; min-height: 40px; }

  .invoice-items-table thead,
  .invoice-items-table colgroup { display: none; }
  .invoice-items-table, .invoice-items-table tbody { display: block; width: 100%; }
  .invoice-items-table tr.invoice-item-row {
    display: grid; grid-template-columns: minmax(0, 1fr) 5.5rem 5rem 2.4rem;
    gap: 0.3rem 0.4rem; align-items: end;
    padding: 0.5rem 0; border-bottom: 1px solid var(--border);
  }
  .invoice-items-table td { display: block; padding: 0; }
  .invoice-items-table td:first-child { grid-column: 1 / -1; }
  .invoice-items-table td[data-label]::before {
    content: attr(data-label); display: block;
    font-size: 0.68rem; text-transform: uppercase; letter-spacing: 0.06em;
    color: var(--muted); font-weight: 600; margin-bottom: 0.15rem;
  }
  .invoice-items-table .qty-input, .invoice-items-table .price-input, .invoice-items-table .payer-input { width: 100%; }
}


/* ---------- Multi-tenant clinic context ---------- */
.clinic-context {
  font-size: 0.72rem; color: #9FBAB5; margin: -0.15rem 0 1.5rem 0.3rem;
  text-transform: uppercase; letter-spacing: 0.06em; font-weight: 600;
}

.clinic-badge {
  display: inline-block; font-size: 0.72rem; text-transform: uppercase; letter-spacing: 0.06em;
  font-weight: 700; color: var(--primary-dark); background: var(--primary-tint);
  padding: 0.15rem 0.55rem; border-radius: 5px;
}

.reset-form { display: flex; gap: 0.6rem; align-items: center; padding: 0.5rem 0; }
/* Same reason as .inline-text-input: max-width caps it, min-width:0 is what
   lets it shrink. A cap without a floor is only half a constraint. */
.reset-form input { flex: 1; min-width: 0; max-width: 280px; padding: 0.5rem 0.7rem; border: 1px solid var(--border); border-radius: 7px; font-family: inherit; }

/* ---------- Document upload ---------- */
.upload-form {
  display: flex; align-items: center; gap: 0.7rem; flex-wrap: wrap;
  margin-top: 1rem; padding-top: 1rem; border-top: 1px solid var(--border);
}
/* The upload label. Given a full-width row of its own rather than being
   visually hidden: the control genuinely benefits from a visible name, and a
   hidden-label pattern here would only be papering over the same gap. */
.upload-form-label {
  flex-basis: 100%; font-size: 0.82rem; font-weight: 600; color: var(--muted);
}
.upload-form input[type="file"] {
  flex: 1; min-width: 200px; font-size: 0.85rem; color: var(--muted);
  padding: 0.4rem 0; font-family: inherit;
}

/* ---------- Consent modals ---------- */
.consent-checkbox-row { display: flex; align-items: baseline; gap: 0.6rem; margin-bottom: 0.7rem; font-size: 0.88rem; flex-wrap: wrap; }
.consent-checkbox-row .checkbox-item { margin: 0; }
.consent-link { color: var(--primary-text); font-weight: 600; }
.consent-modal-overlay {
  display: none; position: fixed; inset: 0; background: rgba(28, 38, 36, 0.55);
  z-index: 1000; align-items: center; justify-content: center; padding: 1.5rem;
}
.consent-modal-overlay.open { display: flex; }
.consent-modal {
  background: var(--surface); border-radius: 12px; max-width: 640px; width: 100%;
  max-height: 85vh; display: flex; flex-direction: column; box-shadow: var(--shadow);
}
.consent-modal-header {
  display: flex; align-items: center; justify-content: space-between;
  padding: 1rem 1.3rem; border-bottom: 1px solid var(--border); flex-shrink: 0;
}
.consent-modal-header h3 { margin: 0; font-family: var(--font-heading); font-weight: 600; color: var(--primary-dark); }
.consent-modal-close {
  background: none; border: none; font-size: 1.6rem; line-height: 1; cursor: pointer; color: var(--muted);
  padding: 0 0.3rem;
}
.consent-modal-close:hover { color: var(--ink); }
.consent-modal-body { padding: 1.1rem 1.3rem; overflow-y: auto; font-size: 0.86rem; color: var(--ink); line-height: 1.5; }
.consent-modal-body h4 { font-size: 0.92rem; margin: 1rem 0 0.3rem; color: var(--primary-dark); }
.consent-modal-body h4:first-child { margin-top: 0; }
.consent-modal-body p { margin: 0.3rem 0; }
.consent-modal-body ul { margin: 0.3rem 0 0.6rem; padding-left: 1.2rem; }
.consent-modal-body li { margin-bottom: 0.3rem; }

/* ---------- Orthodontics (docs/ORTHO_DESIGN.md) ---------- */
/* The module reuses the record hero, the tabs, the chips and the tables, so
   what is here is only the three things it genuinely adds: a case's money line,
   the records grid, and the upload slots. */

.ortho-search { display: flex; gap: 0.5rem; margin: 0 0 0.9rem; }
.ortho-search input[type="search"] {
  /* min-width: 0 -- a flex item cannot shrink below the intrinsic width of a
     text input without it, which overflows the page on a phone. */
  flex: 1 1 auto; min-width: 0; max-width: 340px; padding: 0.45rem 0.7rem;
  border: 1px solid var(--border); border-radius: var(--radius-sm); font: inherit;
}
.ortho-search input[type="search"]:focus { outline: 2px solid var(--primary); outline-offset: 1px; }

.case-no { font-size: 0.82rem; color: var(--muted); font-weight: 500; margin-left: 0.4rem; }
/* The one sentence that answers "how much have I paid?" without opening
   anything -- the thing clinics currently keep in a notebook. */
.case-money {
  margin: 0.45rem 0 0; font-size: 0.88rem; color: var(--ink);
  font-variant-numeric: tabular-nums;
}
.case-field { margin: 0 0 0.8rem; white-space: pre-wrap; }
.case-field:last-child { margin-bottom: 0; }
.case-label {
  display: block; font-size: 0.76rem; letter-spacing: 0.04em; text-transform: uppercase;
  color: var(--muted); margin-bottom: 0.15rem;
}
.case-list { list-style: none; margin: 0; padding: 0; display: grid; gap: 0.7rem; }
.case-list li { border-bottom: 1px dashed var(--border); padding-bottom: 0.6rem; }
.case-list li:last-child { border-bottom: none; padding-bottom: 0; }
.case-list .sub { display: block; color: var(--muted); font-size: 0.8rem; margin-top: 0.15rem; }

.phase-actions { display: flex; align-items: center; flex-wrap: wrap; gap: 0.7rem; }
.phase-hint { color: var(--muted); font-size: 0.84rem; flex: 1 1 12rem; }

/* Records. The grid is thumbnails only -- forty full-size X-rays to show them
   at 120px is the difference between a page that opens and one the desk stops
   using -- and every tile is a link to the original. */
.panel-head-bare { margin-bottom: 0.9rem; }
.records-add { display: flex; flex-wrap: wrap; gap: 0.4rem; }
.records-grid {
  display: grid; grid-template-columns: repeat(auto-fill, minmax(140px, 1fr));
  gap: 0.8rem; margin-top: 0.3rem;
}
.record-tile { margin: 0; }
.record-tile a {
  display: block; aspect-ratio: 4 / 3; overflow: hidden; background: var(--surface-alt);
  border: 1px solid var(--border); border-radius: var(--radius-sm);
}
.record-tile img { width: 100%; height: 100%; object-fit: cover; display: block; }
.record-tile-file {
  display: flex; align-items: center; justify-content: center; height: 100%;
  color: var(--muted); font-size: 0.82rem; font-weight: 600; letter-spacing: 0.06em;
}
.record-tile figcaption { font-size: 0.78rem; margin-top: 0.3rem; line-height: 1.35; }
.record-tile figcaption .sub { display: block; color: var(--muted); font-size: 0.74rem; }

/* The compare strip: one row of columns, one column per set-on-a-day, the same
   view in each. Columns are equal and the images are letterboxed rather than
   cropped -- a cropped intra-oral photo is a different photo. */
.compare-views { display: flex; flex-wrap: wrap; gap: 0.4rem; margin-bottom: 0.9rem; }
.compare-strip { margin-bottom: 0.9rem; }
.compare-columns {
  display: grid; grid-template-columns: repeat(var(--compare-cols, 2), minmax(0, 1fr));
  gap: 0.7rem;
}
.compare-cell { margin: 0; }
.compare-cell figcaption { font-size: 0.82rem; margin-bottom: 0.35rem; display: flex; gap: 0.5rem; align-items: baseline; }
.compare-moment { font-weight: 600; }
.compare-cell figcaption .sub { color: var(--muted); font-size: 0.76rem; }
.compare-cell a { display: block; background: var(--surface-alt); border: 1px solid var(--border); border-radius: var(--radius-sm); aspect-ratio: 4 / 3; overflow: hidden; }
.compare-cell img { width: 100%; height: 100%; object-fit: contain; display: block; }
.compare-empty {
  aspect-ratio: 4 / 3; display: flex; flex-direction: column; align-items: center; justify-content: center;
  gap: 0.3rem; border: 1px dashed var(--border); border-radius: var(--radius-sm); color: var(--muted); font-size: 0.82rem;
}

.record-slots { border: none; padding: 0; margin: 0 0 1.1rem; }
.record-slots legend {
  font-size: 0.76rem; letter-spacing: 0.04em; text-transform: uppercase;
  color: var(--muted); padding: 0; margin-bottom: 0.4rem;
}
.record-slot-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(220px, 1fr)); gap: 0.6rem; }
.record-slot {
  display: flex; flex-direction: column; gap: 0.25rem; font-size: 0.84rem;
  border: 1px dashed var(--border); border-radius: var(--radius-sm); padding: 0.55rem 0.65rem;
}
.record-slot:focus-within { border-color: var(--primary); border-style: solid; }
.record-slot-label { font-weight: 500; }
.record-slot input[type="file"] { font-size: 0.78rem; max-width: 100%; }

/* The adjustment visit's "what was done" list: a row of checkboxes rather than
   a multi-select, because it is ticked at the chair with a thumb. */
.checkbox-grid { border: none; padding: 0; margin: 0; }
.checkbox-grid legend {
  font-size: 0.76rem; letter-spacing: 0.04em; text-transform: uppercase;
  color: var(--muted); padding: 0; margin-bottom: 0.4rem;
}
.checkbox-grid .checkbox-inline {
  display: inline-flex; align-items: center; gap: 0.35rem; margin: 0 0.9rem 0.45rem 0;
  font-size: 0.88rem; font-weight: 400; min-height: 32px;
}
.checkbox-grid input[type="checkbox"] { width: 1rem; height: 1rem; }

.field-hint { display: block; color: var(--muted); font-size: 0.78rem; font-weight: 400; margin-top: 0.2rem; }
.plan-table .plan-part { display: none; }
.plan-late-fee { display: block; margin-top: 0.2rem; }

@media (max-width: 720px) {
  /* The installment schedule on a phone. Seven columns in a scroll box read as
     a table cut off mid-word -- Balance became "BALA" -- and nothing said it
     scrolled. Paid and Balance go: the status chip already says paid/overdue/
     due and an unpaid row's balance is its amount. The one row where those two
     columns said something new, a part-paid one, gets a sub-line under the
     amount instead. Dates stay on one line. */
  .plan-table .plan-col-paid, .plan-table .plan-col-balance { display: none; }
  .plan-table td, .plan-table th { padding-left: 0.4rem; padding-right: 0.4rem; }
  /* !important to beat the phone-wide "every cell may wrap" rule above. That
     rule is right for buttons and names; a date broken as "2026-" / "09-04" is
     not wrapping, it is a different number. With two columns gone the table
     has the width to keep it on one line. */
  .plan-table td:nth-child(2), .plan-table th:nth-child(2) { white-space: nowrap !important; }
  .plan-table td:first-child, .plan-table th:first-child { width: 1%; }
  .plan-table .chip { font-size: 0.7rem; padding: 0.1rem 0.4rem; }
  .plan-box { padding-left: 0.5rem; padding-right: 0.5rem; }
  .plan-table .plan-part { display: block; white-space: normal; font-size: 0.74rem; color: var(--muted); }
  .plan-summary { font-size: 0.86rem; }

  /* The Cases list becomes one card per case, the same way the directory does:
     a nine-column table in a scroll box is not a calling list anyone reads on a
     phone. Patient and case no. on top, then the phase and progress, then the
     money, then the flags -- the order the desk asks the questions in. */
  .case-directory thead { display: none; }
  .case-directory, .case-directory tbody { display: block; width: 100%; }
  .case-directory tr {
    display: block; padding: 0.7rem 0.9rem; border-bottom: 1px solid var(--border);
  }
  .case-directory tr:last-child { border-bottom: 0; }
  .case-directory td { display: block; padding: 0 !important; border: 0; font-size: 0.86rem; text-align: left; }
  .case-directory td[data-label="Patient"] { margin-bottom: 0.15rem; }
  .case-directory td[data-label="Patient"] .sub { display: inline; margin-left: 0.4rem; }
  .case-directory td[data-label="Case"], .case-directory td[data-label="Phase"],
  .case-directory td[data-label="Progress"] {
    display: inline-block; margin-right: 0.6rem; vertical-align: middle;
  }
  .case-directory td[data-label="Case"] .sub, .case-directory td[data-label="Progress"] .sub,
  .case-directory td[data-label="Balance"] .sub { display: inline; margin-left: 0.25rem; }
  .case-directory td[data-label="Progress"]::before { content: "· "; color: var(--muted); }
  .case-directory td[data-label="Balance"] { display: block; margin-top: 0.2rem; font-weight: 500; }
  .case-directory td[data-label="Balance"]::before { content: ""; }
  .case-directory td[data-label="Last visit"] { display: block; color: var(--muted); font-size: 0.8rem; margin-top: 0.15rem; }
  .case-directory td[data-label="Last visit"]::before { content: "Last visit "; }
  .case-directory td[data-label="Next"] { display: inline; color: var(--muted); font-size: 0.8rem; }
  .case-directory td[data-label="Next"]::before { content: " · next "; }
  .case-directory td[data-label="Clinic"] { display: none; }
  .case-directory td[data-label="Flags"] { margin-top: 0.35rem; }
  .case-directory td.directory-open { display: none; }

  .records-grid { grid-template-columns: repeat(auto-fill, minmax(110px, 1fr)); }
  /* On a phone the strip scrolls sideways two columns at a time rather than
     shrinking five sets to thumbnails nobody can read. */
  .compare-columns { display: flex; overflow-x: auto; -webkit-overflow-scrolling: touch; scroll-snap-type: x mandatory; }
  .compare-cell { flex: 0 0 78%; scroll-snap-align: start; }
  .record-slot-grid { grid-template-columns: minmax(0, 1fr); }
  .ortho-search input[type="search"] { max-width: none; }
}


/* Add-to-home-screen card on the dashboard (installable web app, 7 Sep 2026). */
.pwa-install { display: flex; align-items: center; justify-content: space-between; gap: 1rem; flex-wrap: wrap; margin-bottom: 1.2rem; }
.pwa-install-body { display: flex; align-items: center; gap: 0.9rem; flex: 1 1 18rem; }
.pwa-install-body img { border-radius: 10px; flex: 0 0 auto; }
.pwa-install-actions { display: flex; gap: 0.5rem; flex: 0 0 auto; }
.pwa-install[hidden] { display: none; }


/* ---------------------------------------------------------------------------
   Platform Directory admin screen (docs/CLINIC_DIRECTORY_DESIGN.md).

   The PUBLIC directory pages moved to public.css on 10 Sep 2026, when they
   moved onto public_base.html -- see the note at the top of that file. What is
   left here is what /admin/directory and its form need, and only that: those
   two are super-admin screens inside the application and should go on looking
   like the rest of it.
   --------------------------------------------------------------------------- */
.directory-scoreboard { flex-wrap: wrap; }
.page-header-actions { display: flex; gap: 0.5rem; align-items: center; flex-wrap: wrap; }
.btn-lg { padding: 0.7rem 1.3rem; font-size: 0.95rem; }
.label-hint { color: var(--muted); font-weight: 400; font-size: 0.78rem; }
.filter-sep { flex: 0 0 100%; height: 0; }
.form-panel { max-width: 46rem; }
.tag.tag-on { background: var(--primary-tint); color: var(--primary-dark); font-weight: 600; }

/* Procedure typeahead on the invoice line (P-01, static/js/procedure-picker.js).
   The cell is the positioning context so the list hangs off its own input
   rather than the page -- an invoice can have many rows and only one list is
   ever open, but the open one has to belong to the row being typed in. */
.procedure-picker-cell { position: relative; }

.procedure-suggestions {
  position: absolute;
  z-index: 30;
  top: 100%;
  left: 0;
  right: 0;
  margin: 2px 0 0;
  padding: 0;
  list-style: none;
  max-height: 16rem;
  overflow-y: auto;
  background: var(--surface, #fff);
  border: 1px solid var(--border, #d7dbe0);
  border-radius: 6px;
  box-shadow: 0 6px 18px rgba(16, 24, 40, 0.12);
}

.procedure-option {
  display: flex;
  justify-content: space-between;
  align-items: baseline;
  gap: 0.75rem;
  padding: 0.45rem 0.6rem;
  cursor: pointer;
}

.procedure-option:hover,
.procedure-option.is-active {
  background: var(--surface-alt, #eef2f7);
}

.procedure-option-name { flex: 1 1 auto; }

.procedure-option-meta {
  flex: 0 0 auto;
  font-variant-numeric: tabular-nums;
  font-size: 0.85em;
  opacity: 0.7;
}

/* The procedure ledger on the patient record (P-02 piece 3).
   Both controls are <details> so they work with no JavaScript at all: the
   cancel form needs a reason typed before it posts, and a reason box that only
   appears once a script has run is a cancellation a clinic cannot record on a
   tablet whose script failed. */
.ledger-add { margin-top: 1.1rem; }
.ledger-add > summary { display: inline-block; list-style: none; cursor: pointer; }
.ledger-add > summary::-webkit-details-marker { display: none; }
.ledger-add[open] > summary { margin-bottom: 0.9rem; }

.ledger-cancel { display: inline-block; margin-left: 0.4rem; }
.ledger-cancel > summary { cursor: pointer; font-size: 0.85rem; }
.ledger-cancel form {
  display: flex;
  flex-wrap: wrap;
  gap: 0.4rem;
  margin-top: 0.4rem;
  min-width: 0;
}
.ledger-cancel input[type="text"] {
  flex: 1 1 11rem;
  min-width: 0;
  padding: 0.35rem 0.5rem;
  border: 1px solid var(--border);
  border-radius: var(--radius-sm, 6px);
  font: inherit;
  font-size: 0.85rem;
}

/* One procedure slot on the visit form (P-02 piece 4). A fieldset rather than a
   plain div so the rows are announced as groups: "Procedure 2" is the only
   thing distinguishing three identical sets of inputs to a screen reader. */
.ledger-slot {
  border: 1px solid var(--border);
  border-radius: var(--radius-sm, 6px);
  padding: 0.9rem 1rem 1rem;
  margin-bottom: 1rem;
  min-width: 0;
}
.ledger-slot > legend {
  padding: 0 0.4rem;
  font-size: 0.85rem;
  font-weight: 600;
  color: var(--muted);
}

/* One row's site-fill form in the needs-a-tooth queue (P-02 piece 6). Inline so
   the whole queue reads as a list somebody works down, not as a stack of forms. */
.site-fill { display: flex; flex-wrap: wrap; gap: 0.4rem; align-items: center; min-width: 0; }
.site-fill input[type="text"] { flex: 0 1 7rem; min-width: 0; }
.site-fill input[type="text"], .site-fill select {
  padding: 0.35rem 0.5rem;
  border: 1px solid var(--border);
  border-radius: var(--radius-sm, 6px);
  font: inherit;
  font-size: 0.85rem;
}
