/* Hyacinth MSO — the staff dashboard, wearing the company's own identity:
   navy and gold, Playfair over DM Sans, taken from the marketing site.

   The public /book pages are NOT part of this surface. They are Rose
   Pediatrics' and they stay pink. rose.css pins itself against everything
   below -- see the "pinned against the staff theme" block in that file before
   assuming a change here is confined to the dashboard. */

/*  Self-hosted, and that is not a preference. middleware.py sets a CSP of
    default-src 'self' with no font-src, so a Google Fonts link would be
    blocked twice over: once for the stylesheet, once for the woff2. main.py
    registers the font/woff2 MIME type before mounting /static because some
    hosts serve it as text/plain, which nosniff then rejects.
    inter.woff2 sits beside these two and belongs to the booking pages. */
@font-face {
  font-family: "DM Sans Local";
  src: url("/static/dmsans.woff2") format("woff2");
  font-weight: 100 1000;   /* variable: the UI uses 500/550/600/650 */
  font-style: normal;
  font-display: swap;
}
@font-face {
  font-family: "Playfair Local";
  src: url("/static/playfair.woff2") format("woff2");
  font-weight: 400 900;
  font-style: normal;
  font-display: swap;
}

:root {
  /*  Both stacks degrade deliberately. If a woff2 is ever missing the next
      entry takes over: system-ui is metrically close enough to the old stack
      that nothing reflows, and Georgia carries the editorial intent. */
  --font-body: "DM Sans Local", "DM Sans", system-ui, -apple-system, BlinkMacSystemFont,
               "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
  --font-display: "Playfair Local", "Playfair Display", Georgia, "Times New Roman", serif;

  --bg: #FAFAF8;            /* cream page ground */
  --surface: #FFFFFF;       /* cards */
  /*  New, and it fixes a real bug rather than adding a shade. Seven rules used
      --bg as an "on-white grey wash" (progress track, default badge, cancelled
      appointment, chip and row hovers). Cream on white is a 1.5% delta -- all
      seven would have effectively disappeared. This is the wash. */
  --surface-alt: #EEF2F7;
  /*  Translucent rather than opaque so one value composites correctly on both
      the cream page and the white cards. */
  --border: rgba(27, 46, 75, 0.15);
  --border-strong: rgba(27, 46, 75, 0.20);
  --text: #1B2E4B;
  --text-muted: rgba(27, 46, 75, 0.70);   /* ~5:1 on white, still AA for body */

  /*  --accent stays the workhorse and it is NAVY, not gold. Half its twenty
      uses are text on a light background and half are fills carrying white
      text. Gold is 2.28:1 on white and fails both roles. */
  --accent: #1B2E4B;
  --accent-soft: #E7EDF6;   /* one step bluer than --surface-alt, so an
                               accented surface reads as accented */

  /*  Gold is the brand layer, and it has one rule: never a fill behind text,
      never the only thing saying what state something is in. It is 2.19:1 on
      cream. Rules, edges, underlines and the input focus border -- that is the
      whole remit. --gold-ink is the darkened escape hatch (4.7:1) for the day
      something genuinely has to be gold AND readable. */
  --gold: #C9A84C;
  --gold-ink: #8A6D1F;

  /*  Violet means exactly one thing: an appointment at Temple Hills. It was
      already here as three untokenised literals on .cal-appt.th. It is never a
      button, badge, link or nav state. Safe as colour-only because the agenda
      prints the clinic name as text -- which is what renders on phones, where
      the month grid is hidden entirely. */
  --violet: #5B3D80;
  --violet-soft: #EFE9F6;
  --violet-line: #7A53A8;

  /*  Re-tinted cooler and deeper to sit beside navy, but still unmistakably
      green / amber / red -- this is a clinical tool and status legibility
      outranks brand purity. Each ink now clears AA on its own soft fill, which
      is why the three hardcoded "dark text" partners could be deleted. Amber
      is pushed to ~33deg, away from gold's ~45deg, so the two cannot be
      confused with each other. */
  --good: #18664A;  --good-soft: #E9F1EC;
  --warn: #8A5A16;  --warn-soft: #FBEEDB;
  --bad:  #9B2226;  --bad-soft:  #FAE9E7;

  --radius: 12px;     /* cards and tiles */
  /* The control radius -- buttons, inputs, notices, desktop nav items. It was a
     bare 6px in five places, which is how two of them drift apart. */
  --radius-sm: 4px;
  --shadow: 0 1px 2px 0 rgb(0 0 0 / 0.05);
  /* A second, softer tier for things that should sit off the page rather than on
     it. Two layers: a tight contact shadow plus a wide diffuse one. */
  --shadow-md: 0 4px 6px -1px rgb(0 0 0 / 0.1), 0 2px 4px -2px rgb(0 0 0 / 0.1);
  /* One duration for the whole app. Long enough to read as a transition, short
     enough that a fast clicker never waits on it. */
  --speed: 120ms;
  /*  These two are contracts, not theme values. rose.css reads --tap in four
      places, and .shell's bottom padding is computed from --tabbar-h. Changing
      either reaches the booking pages or hides the last row of every table. */
  --tap: 44px;        /* Apple HIG minimum touch target */
  --tabbar-h: 56px;   /* mobile bottom nav; .shell bottom padding depends on it */
}

/* Breakpoints are literals, not custom properties, because @media cannot
   evaluate var() -- a --bp-phone token would silently never match. The three
   tiers are 900px (tablet / touch), 760px (bottom tab bar), 640px (phone
   layout), plus a short-landscape tier. All of them live at the bottom of
   this file, followed by one query that is not a width tier at all
   (prefers-reduced-motion) and is last so it can flatten the others. */

* { box-sizing: border-box; }

html {
  /* iOS inflates text on rotation unless this is pinned. */
  -webkit-text-size-adjust: 100%;
  text-size-adjust: 100%;
}

body {
  margin: 0;
  background: var(--bg);
  color: var(--text);
  /*  Longhand, not the `font:` shorthand it replaced. The shorthand also
      resets weight, style and variant, and it makes family impossible to
      override on its own -- which rose.css has to do.
      15px/1.5 is load-bearing: the booking pages override only the family, so
      changing either number here reflows a page this theme must not touch. */
  font-family: var(--font-body);
  font-size: 15px;
  line-height: 1.5;
  -webkit-font-smoothing: antialiased;
  /* A tap with no feedback reads as a dropped tap. Accent-tinted, not none. */
  -webkit-tap-highlight-color: rgba(27, 46, 75, 0.12);
}

a { color: var(--accent); text-decoration: none; }
a:hover { text-decoration: underline; }

/*  An element selector on purpose. It is the only thing that beats the family
    `.rose` inherits from its own class rule -- which is exactly why rose.css
    has to pin `.rose h1..h4 { font-family: inherit }` to stay on Inter.
    Family, tracking and weight only: every existing font-size rule (.page-head
    h1, .card-head h2, .login-card h1, and the phone tier) still wins. */
h1, h2, h3, h4 {
  font-family: var(--font-display);
  letter-spacing: -0.01em;
  font-weight: 600;
}
.brand-text strong { font-family: var(--font-display); }

/* ---------- Top bar ---------- */
.topbar {
  display: flex;
  align-items: center;
  gap: 28px;
  padding: 0 24px;
  height: 60px;
  /*  Cream, like the site's navbar. Background and box-shadow are the ONLY
      things this rule may gain. transform / filter / backdrop-filter /
      will-change / contain each make .topbar the containing block for the
      position:fixed .nav below 760px -- and the bottom tab bar lands inside
      the header. See the note in the 760px tier. */
  background: var(--bg);
  border-bottom: 1px solid var(--border);
  box-shadow: var(--shadow);
  position: sticky;
  top: 0;
  z-index: 20;
}
.brand { display: flex; align-items: center; gap: 10px; }
/*  A raster roundel, not a letter tile. The artwork is an opaque square on a
    lavender ground, so the circle is cut here rather than in the file -- there
    is no transparency to preserve and nothing installed that could add any.

    The four hyacinth-*.png files are derived from a 1024px master kept OUT of
    this repo (~/Documents/Hyacinth/brand/hyacinth-mark-source-1024.png -- 1.4MB
    of binary that would sit in the history forever, and it carries a C2PA
    "AI generated" manifest we do not want to ship). To regenerate:

      sips -c 780 780 <master> --out /tmp/m.png          # centred crop; drops
                                                         # the corner watermark
      ffmpeg -i /tmp/m.png -vf scale=N:N:flags=lanczos \
             -pix_fmt rgb24 -y app/static/<name>.png      # N = 96/32/16/180

    rgb24 rather than rgba: the source alpha is uniformly opaque, so dropping it
    is lossless and saves ~25% of the bytes. border-radius clips regardless.

    The old `background: var(--accent)` is gone on purpose: behind an opaque
    circular image it could only ever flash as a navy disc before decode.
    box-sizing:border-box means the ring eats into the 30px instead of growing
    it, so the bar's metrics are exactly what they were. */
.brand-mark {
  width: 30px; height: 30px;
  flex: 0 0 auto;          /* the 761-900px band is the tightest in this file */
  display: block;
  border-radius: 50%;
  object-fit: cover;
  border: 1px solid rgba(27, 46, 75, 0.20);
  background: var(--accent-soft);   /* placeholder disc while decoding */
}
.brand-text { display: flex; flex-direction: column; line-height: 1.15; }
.brand-text strong { font-size: 14px; }
.brand-text small { color: var(--text-muted); font-size: 11.5px; }

.nav { display: flex; gap: 4px; margin-left: 8px; }
.nav a {
  padding: 7px 13px; border-radius: var(--radius-sm);
  color: var(--text-muted); font-weight: 500; font-size: 14px;
}
/*  A gold underline instead of a filled pill, drawn with an inset shadow so it
    costs no layout. Padding, weight and casing are untouched on purpose: the
    900px band is the tightest in this file (see the tier below), and uppercase
    or extra tracking here is what pushes "Sign out" out of the bar.

    Gold is 2.19:1 on cream, so the underline cannot legally be what tells you
    which tab is active. The weight bump to 600 and the muted -> navy colour
    shift are the accessible signals; the gold sits on top of them. */
.nav a:hover { background: transparent; color: var(--accent); box-shadow: inset 0 -2px 0 var(--gold); text-decoration: none; }
.nav a.active { background: transparent; color: var(--accent); font-weight: 600; box-shadow: inset 0 -2px 0 var(--gold); }
.logout-form { margin-left: auto; }
/*  Sign out is chrome, not something anyone hunts for, so it takes the smaller
    size. That is not taste -- it is the 761-900px band. With seven manager tabs
    the bar clears 761px by a couple of pixels, and an uppercase label at the
    full .btn size ate most of the headroom it used to have. */
.logout-form .btn { padding: 7px 12px; font-size: 11px; letter-spacing: 0.08em; }

/* The open-pile count on the Review tab -- most of the reason that tab exists.
   Warn-coloured because "waiting on a person" already looks like this elsewhere in
   the app (.badge-review); a sixth colour for the same meaning would be a new
   vocabulary word for an idea that already has one. */
.nav-count {
  display: inline-block;
  min-width: 18px; padding: 0 5px; margin-left: 6px;
  border-radius: 999px;
  background: var(--warn-soft); color: var(--warn);
  border: 1px solid color-mix(in srgb, var(--warn) 28%, #fff);
  font-size: 11px; font-weight: 700; line-height: 16px;
  text-align: center; font-variant-numeric: tabular-nums;
}
.nav a.active .nav-count {
  background: var(--warn); color: #fff; border-color: var(--warn);
}

/* ---------- Layout ---------- */
.shell { max-width: 1320px; margin: 0 auto; padding: 26px 24px 60px; }
/* 100vh first as the fallback: iOS's dynamic toolbar makes 100vh taller than
   the visible area, so the login card starts below the fold and jumps as the
   toolbar collapses. dvh is the actual viewport. */
.shell-bare { display: grid; place-items: center; min-height: 100vh; min-height: 100dvh; padding: 24px; }

.page-head {
  display: flex; align-items: flex-end; justify-content: space-between;
  gap: 16px; flex-wrap: wrap; margin-bottom: 20px;
}
.page-head h1 { margin: 0; font-size: 23px; letter-spacing: -0.01em; }
/* A section heading INSIDE a page, for a screen that carries several. 17px sits
   between the page title's 23px and a card head's 15px.
   Without this rule an <h2> here renders at the UA default -- roughly 24px bold
   with 0.83em margins -- which is LOUDER than the page title above it. The review
   page shipped in exactly that state. */
.page-head h2 { margin: 0; font-size: 17px; letter-spacing: -0.01em; }
.page-head p { margin: 4px 0 0; color: var(--text-muted); font-size: 13.5px; }

.card {
  background: var(--surface);
  border: 1px solid var(--border);
  border-radius: var(--radius);
  box-shadow: var(--shadow-md);
}
/*  18px/20px, not the site's 32px. That is a marketing-card measure; here it
    would cost ~24px of usable width per side on the six-column tables, and the
    phone tier already steps this to 16px -- which would become a 2x jump. */
.card-pad { padding: 18px 20px; }
.card + .card { margin-top: 18px; }
.card-head {
  padding: 14px 20px; border-bottom: 1px solid var(--border);
  display: flex; align-items: center; justify-content: space-between; gap: 12px;
}
/* h3 as well as h2: a card on a page that already has section headings has to
   drop a level to stay below them, and both must look the same -- a card title is
   a card title whatever depth the page needed it to be. One rule rather than two
   so they cannot drift. */
.card-head h2, .card-head h3 { margin: 0; font-size: 15px; font-weight: 600; }

/*  The site's signature: a short gold rule above a title. One selector list so
    the three title contexts cannot drift apart.
    Deliberately NOT on .page-head h2 -- that is a section heading, not a title,
    and four gold ticks stacked down one page stops being a motif and starts
    being wallpaper. .card-head is a flex row, so the ::before renders as a
    block inside the heading's own box and the row simply re-centres. */
.page-head h1::before,
.card-head h2::before,
.card-head h3::before,
.login-card h1::before {
  content: "";
  display: block;
  width: 2.5rem;
  height: 0.125rem;
  background: var(--gold);
  margin-bottom: 10px;
}

/* ---------- Tiles ---------- */
.tiles {
  display: grid; gap: 14px; margin-bottom: 18px;
  grid-template-columns: repeat(auto-fit, minmax(190px, 1fr));
}
.tile {
  background: var(--surface); border: 1px solid var(--border);
  border-radius: var(--radius); padding: 16px 18px; box-shadow: var(--shadow);
}
/* Tiles are links; box-shadow is already in the transition list further down. */
.tile:hover { box-shadow: var(--shadow-md); }
.tile .label { color: var(--text-muted); font-size: 12.5px; font-weight: 500; }
/*  tabular-nums is load-bearing, not polish. Playfair's default figures are
    proportional, so a count going 7 -> 11 would change the number's width and
    the tile would twitch between page loads. */
.tile .value {
  font-family: var(--font-display);
  font-size: 30px; font-weight: 500; letter-spacing: -0.01em; margin-top: 4px;
  font-variant-numeric: tabular-nums;
}
.tile .sub { color: var(--text-muted); font-size: 12px; margin-top: 2px; }
.tile.alert .value { color: var(--warn); }
.tile a.value { display: block; }

.breakdown { display: flex; flex-direction: column; gap: 9px; }
.breakdown-row { display: flex; align-items: center; gap: 10px; font-size: 13.5px; }
.breakdown-row .name { min-width: 130px; color: var(--text-muted); }
/*  --surface-alt, not --bg. The track sits on a white card, and cream on white
    is a 1.5% delta -- the empty part of the bar would simply vanish. */
.breakdown-row .bar { flex: 1; height: 7px; background: var(--surface-alt); border-radius: 4px; overflow: hidden; }
.breakdown-row .bar span { display: block; height: 100%; background: var(--accent); border-radius: 4px; }
.breakdown-row .count { min-width: 42px; text-align: right; font-variant-numeric: tabular-nums; font-weight: 600; }

/* ---------- Buttons ----------
 *
 *  Uppercase and letter-spaced, from the marketing site. This applies to .btn
 *  and nothing else -- not chips, not nav items, not badges. Tabs and filters
 *  are read at a glance dozens of times a shift and mixed case is faster to
 *  scan; a button is read once, deliberately.
 *
 *  The font-size drops 13.5 -> 12 (and 12.5 -> 11 for -sm) to pay for it:
 *  uppercase at .09em tracking is roughly 1.15x wider, and without the drop
 *  the action cells in the wider tables overflow their column.
 *
 *  There is deliberately no .btn-gold. The marketing site defines its gold CTA
 *  as navy-band-only, and this app has no navy band -- so the variant could
 *  only ever be used wrong, at 2.28:1 on cream.
 */
.btn {
  display: inline-flex; align-items: center; gap: 7px;
  padding: 9px 18px; border-radius: var(--radius-sm);
  border: 1px solid var(--accent); background: var(--surface);
  color: var(--accent); font-size: 12px; font-weight: 600;
  text-transform: uppercase; letter-spacing: 0.09em;
  cursor: pointer; font-family: inherit;
}
/*  The site's secondary button inverts on hover rather than tinting. Declared
    before the variants below so it never leaks into them. */
.btn:hover { background: var(--accent); color: #fff; text-decoration: none; }
.btn-primary { background: var(--accent); border-color: var(--accent); color: #fff; }
/*  Derived, not hand-darkened. This was #26496e -- a manually darkened copy of
    the old --accent that would have gone on pointing at the previous blue.
    Navy at 88% over the cream page is the marketing site's `hover:bg-navy/90`. */
.btn-primary:hover { background: color-mix(in srgb, var(--accent) 88%, var(--bg)); }
/*  background too, not just the border. .btn fills with --surface, which used
    to be invisible because the topbar was also white -- on the cream bar a
    ghost button was suddenly a white box. */
.btn-ghost { background: transparent; border-color: transparent; color: var(--text-muted); }
.btn-ghost:hover { background: color-mix(in srgb, var(--accent) 7%, transparent); color: var(--text); }
.btn-danger { color: var(--bad); border-color: color-mix(in srgb, var(--bad) 28%, #fff); }
.btn-danger:hover { background: var(--bad-soft); color: var(--bad); }
.btn-sm { padding: 6px 12px; font-size: 11px; letter-spacing: 0.08em; }
.btn[disabled] { opacity: 0.55; cursor: not-allowed; }
.btn-row { display: flex; gap: 9px; flex-wrap: wrap; align-items: center; }

.spinner {
  width: 13px; height: 13px; border-radius: 50%;
  border: 2px solid rgba(255,255,255,0.35); border-top-color: #fff;
  animation: spin 0.7s linear infinite;
}
.spinner.dark { border-color: var(--border-strong); border-top-color: var(--accent); }
@keyframes spin { to { transform: rotate(360deg); } }

/* ---------- Forms ---------- */
/*  Uppercase at .08em, the same treatment as `th` and `.detail-item .k`. Those
    three are all "the name of a field" and had drifted into three different
    looks; this is the one they now share. */
label { display: block; font-size: 11.5px; font-weight: 600; text-transform: uppercase; letter-spacing: 0.08em; color: var(--text-muted); margin-bottom: 5px; }
input[type=text], input[type=password], input[type=search], input[type=date],
input[type=time], input[type=tel], select, textarea {
  width: 100%; padding: 8px 11px; font: inherit; font-size: 14px;
  border: 1px solid var(--border-strong); border-radius: var(--radius-sm);
  background: var(--surface); color: var(--text);
}
/*  Gold border on focus, from the site. The slate halo stays and is doing the
    real work: gold on white is 2.28:1, nowhere near the 3:1 that WCAG 1.4.11
    wants from a focus indicator, so the halo is what makes this compliant and
    the gold is the brand layer on top of it. The keyboard :focus-visible
    outline further down stays navy for the same reason. */
input:focus, select:focus, textarea:focus {
  outline: none; border-color: var(--gold);
  box-shadow: 0 0 0 3px var(--accent-soft);
}
textarea { min-height: 74px; resize: vertical; }
.form-grid { display: grid; gap: 14px; grid-template-columns: repeat(auto-fit, minmax(220px, 1fr)); }
.form-actions { display: flex; gap: 9px; margin-top: 18px; }
.field-full { grid-column: 1 / -1; }

/* ---------- Toolbar ---------- */
.toolbar {
  display: flex; gap: 10px; align-items: center; flex-wrap: wrap;
  padding: 13px 20px; border-bottom: 1px solid var(--border);
}
.toolbar .grow { flex: 1; }
.toolbar .search { margin-left: auto; min-width: 290px; max-width: 400px; flex: 1; }
.chips { display: flex; gap: 5px; flex-wrap: wrap; }
.chip {
  padding: 6px 11px; border-radius: 999px; font-size: 12.5px; font-weight: 550;
  border: 1px solid var(--border-strong); color: var(--text-muted); background: var(--surface);
}
.chip:hover { background: color-mix(in srgb, var(--accent) 7%, transparent); text-decoration: none; }
.chip.active { background: var(--accent); border-color: var(--accent); color: #fff; }

/* The Follow-up, To do and Calendar sub-navs. Same chip shape as the queue
   filters but separated by a rule, because these navigate between pages while
   the chips below them filter one -- two different actions should not look
   identical. */
.chips.subnav {
  margin-bottom: 16px; padding-bottom: 12px;
  border-bottom: 1px solid var(--border);
}

/* How much is waiting in the pile a To do sub-tab points at. The nav badge is
   the sum of these four, so this is where a staff member finds out WHICH pile
   the number on the tab is in -- without it the grouping would hide exactly what
   promoting Review to a top-level tab was meant to expose (see app/nav.py).

   Inherits colour from the chip rather than using --warn like .nav-count: on an
   active chip the background is already the accent, and a warn-tinted pill on it
   reads as an error. Absent rather than "0", so a cleared pile is visibly
   clear. */
.chip-count {
  margin-left: 6px; padding: 0 5px;
  border-radius: 999px; background: color-mix(in srgb, var(--accent) 10%, transparent);
  font-size: 11px; font-weight: 700; font-variant-numeric: tabular-nums;
}
.chip.active .chip-count { background: rgba(255, 255, 255, 0.24); }

/* A candidate chart whose date of birth matches the patient's. On the review
   screen this is the row a person should look at hardest: it is either this child
   filed under a different surname, or a sibling. Tinted rather than bordered so
   the table still reads as a table. */
.row-match > td { background: color-mix(in srgb, var(--accent) 7%, transparent); }

/* ---------- Needs review ----------
   Several different questions on one page. The job of the styling is to make them
   read as separate piles rather than one wall of identical cards, because the
   decisions are not interchangeable: one attaches another child's chart
   permanently, one picks a date, one is only an acknowledgement.

   Every rule here replaces an inline style attribute -- see the note above
   .two-col for why that matters. This page carried ten of them, the most in the
   repo. */

.review-section + .review-section { margin-top: 30px; }

/* The section's question, in a tinted strip. Reuses .page-head's flex shape; the
   tint is what stops a section heading being read as a second page title. */
.section-head {
  align-items: baseline;
  margin-bottom: 14px;
  padding: 12px 16px;
  background: var(--accent-soft);
  /* the .badge-info border -- same family, same 22% derivation */
  border: 1px solid color-mix(in srgb, var(--accent) 22%, #fff);
  border-radius: var(--radius);
}
/* Prose past about 70 characters a line is measurably harder to scan, and these
   hints are the sentence that tells somebody what they are deciding. */
.section-head p { max-width: 70ch; }

/* One card per decision, carrying its kind on its edge, so scrolling past the
   section heading never leaves you unsure which question you are answering.
   `kind` is not free text: group_by_kind maps anything it does not recognise to
   'unknown', so this can only ever be one of these four. */
.review-card { border-left: 3px solid var(--border-strong); box-shadow: var(--shadow-md); }
.review-chart_match     { border-left-color: var(--bad); }
.review-physical_date   { border-left-color: var(--warn); }
.review-oa_disagreement { border-left-color: var(--accent); }
.review-unknown         { border-left-color: var(--text-muted); }

/* What we know: the candidate charts, the visit list, the two dates. */
.review-evidence { margin-top: 14px; }

/* ---------- The candidate grid, folded away ----------
   A chart-match card used to print its entire search grid inline: 58 rows on
   average, 100 on 76 of the 204 cards, 11,788 name rows in one response. Nobody
   could work the queue, and every one of those rows is another child's name and
   date of birth rendered whether or not anyone needed it.

   <details>/<summary> and no script, per the house rule. The fold is
   presentational only -- the rows are all in the HTML, so Ctrl-F still finds a
   surname inside a closed card. */
.review-more { margin-top: 14px; }

.review-more > summary {
  cursor: pointer;
  display: flex; align-items: baseline; gap: 8px; flex-wrap: wrap;
  padding: 10px 12px;
  border: 1px solid var(--border);
  border-radius: var(--radius-sm);
  background: var(--surface-alt);
  font-size: 13px;
}
/* Both spellings. A replacement affordance is drawn below, and leaving the UA
   marker in place would put two disclosure triangles on one row. */
.review-more > summary { list-style: none; }
.review-more > summary::-webkit-details-marker { display: none; }
.review-more > summary:hover { border-color: var(--border-strong); }

/* Open/closed is carried by three things and none of them is colour alone: the
   verb changes word, the chevron rotates, and the open header grows a gold seam
   along its top edge -- an edge, which is the whole of gold's remit here. */
.review-more[open] > summary {
  border-radius: var(--radius-sm) var(--radius-sm) 0 0;
  box-shadow: inset 0 2px 0 var(--gold);
}

.review-more-verb { font-weight: 600; color: var(--accent); }
.review-more-verb .when-open { display: none; }
.review-more[open] > summary .when-closed { display: none; }
.review-more[open] > summary .when-open { display: inline; }

/* Built from borders rather than a glyph, so it rotates rather than swapping
   character -- a swap reads as two different controls, a rotation reads as one
   control in two states. */
.review-more-verb::before {
  content: "";
  display: inline-block;
  width: 6px; height: 6px;
  margin-right: 7px;
  border-right: 1.5px solid currentColor;
  border-bottom: 1.5px solid currentColor;
  transform: translateY(-1px) rotate(-45deg);
  transition: transform var(--speed) ease;
}
.review-more[open] > summary .review-more-verb::before {
  transform: translateY(-2px) rotate(45deg);
}

/* The triage line: how many charts, what the matcher proved about the date of
   birth, and -- only when it differs -- how many rows can actually be chosen.
   This is what lets somebody skip a card without opening it. */
.review-more-facts { color: var(--text-muted); }

/* The grid already carries .review-evidence's top margin; a second one inside an
   open fold doubles the gap under the summary. */
.review-more > .review-evidence {
  margin-top: 0;
  border-radius: 0 0 var(--radius-sm) var(--radius-sm);
}

/* The reasoning layer read the same search and disagreed with the refusal.
   Sits above the fold and outside every form, and contains no control -- see the
   template for why that is structural rather than stylistic. */
.review-dissent { margin-top: 14px; }
.review-dissent-why { margin-top: 8px; font-size: 12.5px; }
.review-dissent-why .k { display: block; font-weight: 600; }

/* The bulk bar. Dismiss only; the form it submits contains no radios at all.
   Deliberately not `position: sticky; bottom: 0` -- below 760px that lands on top
   of the fixed tab bar, and a submit button hidden under navigation on a screen
   that writes to medical records is not a trade worth making for convenience. */
.review-bulk {
  display: flex; gap: 10px; align-items: center; flex-wrap: wrap;
  margin-bottom: 14px; padding: 12px 16px;
  background: var(--surface);
  border: 1px solid var(--border);
  border-radius: var(--radius);
}
.review-bulk .muted { font-size: 12.5px; max-width: 70ch; }

/* The per-card tick, in .card-head and bound to the bulk form by `form=`.
   margin-right:auto on the title so the flex row reads tick + name on the left
   and the badges on the right, rather than space-between pushing the name into
   the middle of the header. */
.review-tick { flex: 0 0 auto; margin: 0; }
.review-card .card-head h3 { margin-right: auto; }
.review-flags { display: flex; gap: 6px; align-items: center; flex-wrap: wrap; }

/* What you are about to do. Deliberately NOT .toolbar: that carries a
   border-BOTTOM because it is a card header strip everywhere else it is used, and
   under a submit button that rule draws a line beneath nothing. This is a footer,
   so the rule goes on top -- which is also the separation the card wanted anyway,
   between the evidence and the decision. */
.review-actions {
  display: flex; gap: 10px; align-items: center; flex-wrap: wrap;
  margin-top: 16px; padding-top: 14px;
  border-top: 1px solid var(--border);
}
/* (0,2,1), so it beats the base `input[type=text] { width: 100% }` at (0,1,1).
   .w-auto cannot be reused here for exactly that reason: it is (0,1,0) and loses
   to the attribute selector -- it has only ever worked because both its callers
   are <select>. */
.review-actions input[type=text] { flex: 1 1 240px; min-width: 0; width: auto; }
.review-hint { margin: 8px 0 0; font-size: 12.5px; color: var(--text-muted); max-width: 70ch; }

/* "None of these" / "Looked at it -- leave as is". Below the confirm row and left
   plain, because it is the quieter of the two decisions on the card. */
.review-dismiss {
  display: flex; gap: 10px; align-items: center; flex-wrap: wrap;
  margin-top: 12px;
}

/* The row a keyboard user is on. The whole interaction on this page is choosing a
   radio, and the radios had no focus affordance of any kind. Same tint mechanism
   as .row-match above, one step stronger so the two are tellable apart when they
   land on the same row. */
.review-evidence tbody tr:focus-within > td {
  background: color-mix(in srgb, var(--accent) 12%, transparent);
}

/* ---------- Tables ---------- */
/* overscroll-behavior-x matters on iOS: without it a horizontal drag inside a
   table hands itself to Safari's edge-swipe-back gesture mid-scroll. */
.table-wrap { overflow-x: auto; -webkit-overflow-scrolling: touch; overscroll-behavior-x: contain; }
table { width: 100%; border-collapse: collapse; font-size: 13.5px; }
th, td { padding: 10px 14px; text-align: left; border-bottom: 1px solid var(--border); }
th { font-size: 11.5px; text-transform: uppercase; letter-spacing: 0.08em; color: var(--text-muted); font-weight: 650; white-space: nowrap; background: var(--surface-alt); }
th a { color: inherit; display: inline-flex; align-items: center; gap: 4px; }
th a:hover { color: var(--accent); text-decoration: none; }
/*  A wash rather than a solid: at <=640px the rows become stacked cards on the
    white card surface, and an opaque near-white would paint over them. */
tbody tr:hover { background: color-mix(in srgb, var(--accent) 4%, transparent); }
tbody tr:last-child td { border-bottom: none; }
td.num { font-variant-numeric: tabular-nums; }
td.actions { text-align: right; white-space: nowrap; }
/*  Call / Text on a patients row. Each button is its own POST form, which
    .inline-form would otherwise lay out as a block per line. The result of a
    press is drawn on the row that was pressed, under the buttons. */
td.actions .remind-form { display: inline-flex; vertical-align: middle; }
td.actions .remind-last { display: block; margin-top: 4px; font-size: 12px; }
td.actions .remind-result {
  margin: 8px 0 0; padding: 7px 10px; font-size: 12.5px;
  text-align: left; white-space: normal;
}
tbody tr.remind-focus { background: color-mix(in srgb, var(--accent) 6%, transparent); }

/* ---------- Badges ---------- */
.badge {
  display: inline-block; padding: 2px 8px; border-radius: 999px;
  font-size: 11.5px; font-weight: 600; white-space: nowrap;
  background: var(--surface-alt); color: var(--text-muted); border: 1px solid var(--border);
}
/*  Each semantic colour used to be a three-part set -- soft fill, border, dark
    text -- of which only the fill was a token. The other two were literals, so
    re-tinting an ink left its border pointing at the old hue. The border is now
    derived from the ink at a fixed 28% (22% for the navy family, which is
    darker and would otherwise read heavier than the rest). */
.badge-review { background: var(--warn-soft); color: var(--warn); border-color: color-mix(in srgb, var(--warn) 28%, #fff); }
.badge-good { background: var(--good-soft); color: var(--good); border-color: color-mix(in srgb, var(--good) 28%, #fff); }
.badge-bad { background: var(--bad-soft); color: var(--bad); border-color: color-mix(in srgb, var(--bad) 28%, #fff); }
.badge-info { background: var(--accent-soft); color: var(--accent); border-color: color-mix(in srgb, var(--accent) 22%, #fff); }

/* ---------- Notices ---------- */
.notice {
  padding: 11px 15px; border-radius: var(--radius-sm); font-size: 13.5px;
  border: 1px solid var(--border); background: var(--surface); margin-bottom: 16px;
}
/*  The three hardcoded text colours that used to live here are gone. They
    existed because the old inks were too light to read on their own soft fills;
    the re-tinted ones clear AA unaided (warn 4.85:1, bad 6.6:1, good 5.95:1),
    so the ink token is now the text as well. Three literals fewer to drift. */
.notice-warn { background: var(--warn-soft); border-color: color-mix(in srgb, var(--warn) 28%, #fff); color: var(--warn); }
.notice-bad { background: var(--bad-soft); border-color: color-mix(in srgb, var(--bad) 28%, #fff); color: var(--bad); }
.notice-good { background: var(--good-soft); border-color: color-mix(in srgb, var(--good) 28%, #fff); color: var(--good); }

.empty { padding: 46px 20px; text-align: center; color: var(--text-muted); font-size: 14px; }
.muted { color: var(--text-muted); }
.mono { font-variant-numeric: tabular-nums; font-family: ui-monospace, SFMono-Regular, Menlo, monospace; font-size: 12.5px; }

/* ---------- Login ---------- */
.login-card { width: 100%; max-width: 380px; padding: 30px; }
.login-card h1 { margin: 0 0 5px; font-size: 21px; }
.login-card .sub { color: var(--text-muted); font-size: 13.5px; margin: 0 0 22px; }
.login-card .field { margin-bottom: 14px; }
.login-card .btn { width: 100%; justify-content: center; }
.login-brand { display: flex; align-items: center; gap: 10px; margin-bottom: 20px; }

/* ---------- Calendar ---------- */
/* minmax(0, 1fr) rather than 1fr: a bare 1fr is minmax(auto, 1fr), so each
   track's floor is its min-content -- and .cal-appt below is white-space:
   nowrap, whose min-content is the full untruncated "9:30 AM Rasmussen,
   Celestine". overflow:hidden and text-overflow:ellipsis do not reduce that
   contribution, so one long patient name pushed the seven tracks past the
   card and scrolled the whole page sideways, at every width including
   desktop. With short names the rendering is identical. */
.cal-grid { display: grid; grid-template-columns: repeat(7, minmax(0, 1fr)); border-top: 1px solid var(--border); }
.cal-dow {
  padding: 8px; text-align: center; font-size: 11.5px; font-weight: 650;
  color: var(--text-muted); text-transform: uppercase; letter-spacing: 0.04em;
  border-right: 1px solid var(--border); border-bottom: 1px solid var(--border); background: var(--surface-alt);
}
.cal-dow:last-child { border-right: none; }
.cal-cell {
  min-height: 108px; padding: 6px; border-right: 1px solid var(--border);
  border-bottom: 1px solid var(--border); vertical-align: top;
}
.cal-cell:nth-child(7n) { border-right: none; }
/* Not this month: the page ground showing through, which is what it means. */
.cal-cell.other-month { background: var(--bg); }
.cal-cell.today { background: var(--accent-soft); }
.cal-daynum { font-size: 12px; font-weight: 650; color: var(--text-muted); margin-bottom: 4px; }
.cal-cell.today .cal-daynum { color: var(--accent); }
.cal-appt {
  display: block; padding: 3px 6px; margin-bottom: 3px; border-radius: 4px;
  font-size: 11.5px; line-height: 1.3;
  background: var(--accent-soft); color: var(--accent);
  border-left: 2px solid var(--accent);
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
.cal-appt:hover { text-decoration: none; filter: brightness(0.96); }
/*  Temple Hills. The only place violet is allowed to mean anything -- see the
    token block. Colour is never the sole carrier: the agenda prints the clinic
    name as text, and the agenda is what renders on phones where this grid is
    hidden outright. */
.cal-appt.th { background: var(--violet-soft); color: var(--violet); border-left-color: var(--violet-line); }
.cal-appt.cancelled { background: var(--surface-alt); color: var(--text-muted); border-left-color: var(--border-strong); text-decoration: line-through; }
.cal-more { font-size: 11px; color: var(--text-muted); padding-left: 6px; }

.list-day { padding: 9px 20px; background: var(--surface-alt); border-bottom: 1px solid var(--border); font-size: 12.5px; font-weight: 650; color: var(--text-muted); }

/* ---------- Profile ---------- */
.detail-grid { display: grid; gap: 16px 26px; grid-template-columns: repeat(auto-fit, minmax(190px, 1fr)); }
.detail-item .k { font-size: 11.5px; color: var(--text-muted); font-weight: 600; text-transform: uppercase; letter-spacing: 0.08em; }
.detail-item .v { font-size: 14.5px; margin-top: 3px; }

.pager { display: flex; gap: 8px; align-items: center; justify-content: space-between; padding: 12px 20px; border-top: 1px solid var(--border); font-size: 13px; color: var(--text-muted); }

/* ---------- Follow-up queue ---------- */
/* The per-row status form. Kept on one line so a long queue stays scannable;
   the table's own overflow-x handles narrow screens. */
.inline-form {
  display: flex; gap: 6px; align-items: center; flex-wrap: nowrap;
}
.inline-form select,
.inline-form input[type="date"],
.inline-form input[type="text"] {
  font-size: 12.5px; padding: 5px 7px; min-width: 0;
}
.inline-form input[type="text"] { width: 130px; }
.inline-form input[type="date"] { width: 132px; }
.chips { margin-bottom: 14px; }

/* ---------- Classes lifted out of inline style attributes ----------
   An inline style="..." beats every stylesheet rule for that property, so a
   media query can never reach it. These four carry the exact declarations they
   replaced -- desktop rendering is unchanged -- purely so the mobile tiers
   below can override them. */
.two-col { display: grid; gap: 18px; grid-template-columns: repeat(auto-fit, minmax(330px, 1fr)); }

/* The one-time password handout. Large and selectable because it is read off
   this screen and typed by somebody else, once, and there is no second chance
   to get it right. `user-select: all` makes one click select the whole thing
   rather than a word of it. */
.password-handout {
  font-size: 26px;
  letter-spacing: 0.06em;
  user-select: all;
  margin: 14px 0 18px;
  word-break: break-all;
}
.w-auto { width: auto; }
.map-select { width: auto; min-width: 210px; }
.check-row { display: flex; align-items: center; gap: 8px; font-weight: 400; padding: 6px 0; }
.check-row input[type="checkbox"] { width: auto; }
/* Typeahead result buttons, built by the script in calendar.html. */
.patient-hit { display: block; width: 100%; text-align: left; margin-bottom: 4px; }

/* ---------- Touch-only affordances ----------
   Hidden by default and revealed on touch-sized screens. A native title=
   tooltip never fires on a touch device, so anything only reachable by hover
   needs a visible counterpart there. */
.touch-note { display: none; }
.sort-bar { display: none; }
.cal-agenda { display: none; }

/* ---------- Motion and focus ----------
   Two things this file has never had.

   Nothing animated here can reflow: every property in the list is a colour or a
   shadow. Layout properties are deliberately absent -- a transition on width or
   padding turns a fast click into a wait.

   :focus-visible rather than :focus, so a mouse click does not leave a ring
   sitting behind it. That side effect is why the buttons had no ring at all, and
   why a keyboard user has until now had to guess where they were. The radio and
   checkbox rules are not cosmetic: on the review screen the entire interaction is
   choosing a radio, and they had no focus affordance whatsoever. */
a, .btn, .chip, .nav a, .badge, .cal-appt, .tile, summary,
input[type=text], input[type=password], input[type=search], input[type=date],
input[type=time], input[type=tel], select, textarea {
  transition: background-color var(--speed) ease, border-color var(--speed) ease,
              color var(--speed) ease, box-shadow var(--speed) ease;
}

/* No border-radius in here on purpose: an outline already follows the element's
   own radius, and setting one would square off the .chip and .badge pills for as
   long as they held focus. */
.btn:focus-visible,
.chip:focus-visible,
.nav a:focus-visible,
a:focus-visible,
summary:focus-visible,
input[type=radio]:focus-visible,
input[type=checkbox]:focus-visible {
  outline: 2px solid var(--accent);
  outline-offset: 2px;
}
/* The text controls already draw a ring of their own -- border-color plus
   box-shadow under `input:focus` above -- and a second outline on top of it is
   two rings. The radio and checkbox selectors above are (0,2,1) and survive this
   (0,1,1), which is the only reason they can be cleared here in bulk. */
input:focus-visible, select:focus-visible, textarea:focus-visible { outline: none; }

/* ==================================================================
   Mobile. Everything below is an override; the rules above are the
   desktop rendering and must stay untouched by these tiers.
     A  <= 900px  touch targets, tablet
     B  <= 760px  bottom tab bar
     C  <= 640px  phone layout (card stacks, 16px controls, agenda)
     D  short landscape
   and then, after D and outside this numbering because it keys on an OS
   preference rather than a viewport, a prefers-reduced-motion block. It is last
   so it can flatten anything the four tiers introduce -- do not fold it into D.
   No !important anywhere -- every override reuses a selector of equal or
   greater specificity and sits later in the file. If a rule below needs
   !important, an inline style attribute was missed.
   ================================================================== */

/* ---------- Tier A: anything touch-sized, including iPad portrait ---------- */
@media (max-width: 900px) {
  /* Kills the 300ms double-tap-zoom delay on every control. */
  a, button, .btn, .chip, .nav a, .cal-appt, .cal-more, th a { touch-action: manipulation; }

  /* Five nav items for clinic staff, seven for a manager. Between 761 and 900px
     this is still the desktop bar with .brand-text visible, so the extra tabs
     have to come out of the chrome's own padding rather than out of the viewport
     -- brand + nav + sign-out otherwise runs to roughly the full width at the low
     end of this range. Below 760px the tab-bar rules replace this entirely.

     This band is the tightest one, and it is why the nav was consolidated on
     2026-08-16: at eleven items the bar wanted about 1250px and simply pushed
     "Sign out" out, because .nav has no wrap and .topbar no longer has
     overflow-x. Anything added back here reopens that.

     nowrap because the failure mode here is not overflow, it is a label breaking
     mid-word -- "Follow-" over "up" was what prompted the consolidation, and a
     seven-tab manager bar still did it at exactly 900px without this. */
  .nav a { padding: 7px 9px; white-space: nowrap; }
  .logout-form .btn { white-space: nowrap; }

  /* The brand wordmark goes at 900px rather than 760px, which is where it used
     to go. It is the least load-bearing thing in the bar -- the "H" mark still
     says whose app this is -- and the ~130px it returns is what lets a manager's
     seven tabs and "Sign out" sit on one line through this whole band. */
  .brand-text { display: none; }

  /* Apple HIG minimum. Before this: .cal-more ~16px, th a ~17px, .cal-appt
     ~21px, .btn-sm ~31px, .chip ~33px, .nav a ~35px, .btn ~38px. */
  .btn { min-height: var(--tap); }
  /* 12px rather than 14px of side padding: the uppercase labels are ~1.15x
     wider than the mixed case they replaced, and the action cells are tight. */
  .btn-sm { min-height: var(--tap); padding: 8px 12px; }
  /* gap, because inline-flex makes the label and the count separate flex
     items and drops the whitespace between them ("Never seen0"). */
  .chip { min-height: var(--tap); display: inline-flex; align-items: center; gap: 5px; padding: 10px 14px; }
  th a { min-height: var(--tap); padding: 6px 0; }
  .cal-appt { min-height: 30px; padding: 6px; display: flex; align-items: center; }
  .cal-more { min-height: var(--tap); display: flex; align-items: center; padding: 0 6px; }
  .patient-hit { min-height: var(--tap); }
  /* Tapping the count is the whole point of a tile. Note the base rule
     `.tile a.value` does not match this markup -- the anchor sits inside
     .value rather than carrying the class -- so this is the only rule
     sizing it. */
  .tile .value a { display: inline-flex; align-items: center; min-height: var(--tap); }

  /* Deliberately NOT forced to 44px: links that sit inside a sentence
     (.notice "Review them", the .page-head breadcrumb, "Assign a clinic").
     WCAG 2.5.8 exempts inline links in a block of text, and making them
     44px inline-flex would break the line box they sit in. Each one has a
     redundant >=44px path: the "Needs review" chip, the Patients tab, and
     the Edit button respectively. */

  /* A link in a table cell is a standalone target -- the patient name is the
     way into the record. Bare, these are 16px. Excludes .btn and .badge,
     which are sized (or unclickable) already. */
  table td a:not(.btn):not(.badge) {
    display: inline-flex; align-items: center; min-height: var(--tap);
  }

  /* Form controls. iPad runs the same Safari as an iPhone, so the 16px
     focus-zoom threshold and the 44px target apply from here, not just at
     phone widths. Base is 14px / ~39px tall, and number + file are absent
     from the base selector list entirely (browser default, ~11px on iOS). */
  input[type=text], input[type=password], input[type=search], input[type=date],
  input[type=time], input[type=tel], input[type=number], input[type=email],
  select, textarea {
    font-size: 16px; padding: 11px 12px; min-height: var(--tap);
  }
  textarea { min-height: 96px; }
  input[type=file] { font-size: 16px; width: 100%; padding: 10px 0; min-height: var(--tap); }
  /* Not select: appearance:none there removes the native chevron, and there
     is no custom arrow to replace it with. Only the text-like controls, where
     iOS otherwise forces a pill shape on type=search. */
  input[type=text], input[type=password], input[type=search], input[type=date],
  input[type=time], input[type=tel], input[type=number], input[type=email], textarea {
    -webkit-appearance: none; appearance: none; border-radius: var(--radius-sm);
  }
  .check-row { min-height: var(--tap); padding: 10px 0; }
  .check-row input[type="checkbox"] { width: 22px; height: 22px; }

  /* The base `.inline-form select` is (0,2,0) and would beat the bare
     `select` above, leaving the follow-up queue's controls at 12.5px on an
     iPad. Match its specificity here. */
  .inline-form select,
  .inline-form input[type="date"],
  .inline-form input[type="text"] {
    font-size: 16px; padding: 11px 12px; min-height: var(--tap);
  }

  /* Promoted out of the old 760px block: a 290px minimum search plus two chip
     groups plus two selects stops fitting on one line well before 900px. */
  .toolbar .search { min-width: 100%; margin-left: 0; }

  /* 330px minimum tracks force two columns until ~700px, which squeezes the
     breakdown bars to nothing. */
  .two-col { grid-template-columns: 1fr; }

  .touch-note { display: block; font-size: 12px; color: var(--text-muted); line-height: 1.4; margin-top: 3px; }

  /* Month grid survives at 820px: 7 columns is ~117px a day. Tighten it. */
  .cal-cell { min-height: 92px; }
  .cal-appt { font-size: 11px; }
}

/* ---------- Tier B: the nav becomes a bottom tab bar ---------- */
@media (max-width: 760px) {
  /* .topbar's old overflow-x:auto existed because .nav overflowed it. .nav is
     fixed now, and an ancestor's overflow must not be able to clip it.
     .topbar must never gain transform / filter / will-change / contain either,
     or it would become the containing block for the fixed .nav. */
  .topbar { gap: 12px; padding: 0 14px; overflow: visible; height: 54px; }
  /* .brand-text is already hidden by Tier A from 900px down. */

  .nav {
    position: fixed; left: 0; right: 0; bottom: 0; z-index: 30;
    margin: 0; gap: 0; justify-content: space-around;
    background: var(--surface);
    border-top: 1px solid var(--border);
    box-shadow: 0 -1px 3px rgba(27, 46, 75, 0.08);
    padding-bottom: env(safe-area-inset-bottom);
  }
  /* Five cells for clinic staff -- the people actually holding a phone. At 320px
     that is about 64px each, so the label gives up a point of type and most of
     its side padding rather than the tab giving up its label, which is why the
     nav says "To do" while the pages it opens say "Bookings", "Missed" and so on.

     A manager gets seven (Schedule and Users), or about 45px each at 320px. That
     is knowingly tight and judged acceptable: managers run the practice from a
     desk. If it ever needs fixing, fold those two into a Settings tab rather than
     shrinking the type again.

     It was eleven cells until 2026-08-16 -- 29px each, for labels needing 66 --
     which is what prompted the consolidation.

     `gap` is load-bearing: this anchor is display:flex, which makes the label and
     the count separate flex items and drops the whitespace between them. Without
     it the To do tab reads "To do175". */
  .nav a {
    flex: 1 1 0; min-width: 0; min-height: var(--tabbar-h);
    display: flex; align-items: center; justify-content: center; gap: 4px;
    padding: 8px 3px; border-radius: 0;
    font-size: 11.5px; font-weight: 600; white-space: nowrap;
    letter-spacing: -0.01em;
  }
  /*  box-shadow:none is not tidiness. The desktop rule draws the active/hover
      gold as an inset bottom border, and this block does not otherwise reset
      it -- so a tapped tab would keep a gold bar under it after your finger
      left, on a tab that is not the active one. */
  .nav a:hover { background: transparent; color: var(--text-muted); box-shadow: none; }
  .nav a.active {
    background: transparent; color: var(--accent);
    /* Inset from the TOP here: this bar is at the bottom of the screen, so the
       marker belongs on the edge nearest the content it labels. */
    box-shadow: inset 0 2px 0 var(--gold);
  }
  /* Smaller and unbordered down here: at 64px of cell, the ring around a
     two-digit number is a pixel of noise the label needs back. margin-left goes
     because the flex gap above already separates them. */
  .nav-count {
    min-width: 16px; padding: 0 4px; margin-left: 0;
    font-size: 10px; line-height: 14px;
    border-color: transparent;
  }

  /* Clear the fixed bar so the last table row and the pager stay reachable. */
  .shell {
    padding: 18px calc(14px + env(safe-area-inset-right)) calc(var(--tabbar-h) + 24px + env(safe-area-inset-bottom)) calc(14px + env(safe-area-inset-left));
  }
  .topbar {
    padding-left: calc(14px + env(safe-area-inset-left));
    padding-right: calc(14px + env(safe-area-inset-right));
  }
  /* Login carries class="shell shell-bare" and .shell-bare is declared after
     .shell above, so it must be re-declared after .shell here too or the login
     page inherits 80px of padding for a tab bar it never renders. */
  .shell-bare { padding: 24px; }
}

/* ---------- Tier C: phone layout ---------- */
@media (max-width: 640px) {

  /* Clinic staff get five cells here and need none of this. A manager gets
     seven -- about 53px each at 375px -- and at Tier B's 11.5px "Dashboard"
     runs past its cell into "Calendar", while the To do badge lands on its own
     label.

     The label is what must survive, so the type and the side padding give way
     instead. A tab that cannot be read is worse than one set a point small, and
     truncating to "Dash..." would be worse than either.

     If an eighth tab is ever proposed, this is where it fails: there is no room
     left to take. Fold Schedule and Users into a Settings tab before adding
     one. */
  .nav a { font-size: 10.5px; padding: 8px 1px; gap: 3px; letter-spacing: -0.02em; }
  .nav-count { min-width: 14px; padding: 0 3px; font-size: 9.5px; line-height: 13px; }

  /* ---- Wide tables become one labelled card per row ----
     Sideways scroll inside a page is the thing a thumb cannot discover, so
     below this width no table scrolls -- every row stacks. Labels come from
     data-label, set from the same t() key as the matching <th>. */
  .table-wrap { overflow-x: visible; }

  table.stack { display: block; font-size: 14px; }
  table.stack thead { display: none; }
  table.stack tbody { display: block; }
  /* Flex column rather than block so the card's identity can lead regardless
     of the column order the table uses -- the calendar agenda puts Time
     first, but on a card the patient name is the heading. */
  table.stack tr {
    display: flex; flex-direction: column; padding: 14px 16px;
    border-bottom: 1px solid var(--border);
  }
  table.stack tr:last-child { border-bottom: none; }
  table.stack tbody tr:hover { background: transparent; }

  table.stack td {
    display: grid; grid-template-columns: 8.5em 1fr; gap: 2px 12px;
    align-items: start; padding: 3px 0; border-bottom: none;
    text-align: left;      /* neutralises td.num and td.actions */
    white-space: normal;   /* neutralises the nowrap on th/td.actions */
  }
  table.stack td[data-label]::before {
    content: attr(data-label);
    font-size: 11.5px; text-transform: uppercase; letter-spacing: 0.04em;
    font-weight: 650; color: var(--text-muted); padding-top: 2px;
  }
  /* Cells with more than one child (the due date carries a date, an overdue
     badge and the rule that produced it) generate one grid item each, so
     without this the second child wraps into the label column. Pinning every
     element child to column 2 stacks them under the value instead; bare text
     nodes become anonymous items and auto-place into column 2 ahead of them. */
  table.stack td[data-label] > * { grid-column: 2; }
  /* A badge is a pill around its own text; as a grid item it would stretch
     the full column. Prose is left to fill the column and wrap. */
  table.stack td[data-label] > .badge { justify-self: start; }
  /* The row's identity: no label, reads as the card's heading, and hoisted to
     the top of the card by flex order. */
  table.stack td.cell-title { display: block; font-size: 16px; font-weight: 600; padding: 0 0 8px; order: -1; }
  table.stack td.cell-badge { display: block; padding: 0 0 6px; order: -2; }
  /* The card heading keeps its 44px target from tier A. A link inside a
     labelled row does not: the only one in the app is the patients list's
     first-name cell, which points at the same record as the heading directly
     above it, so it is a duplicate of an already-large target. Forcing it to
     44px also made it a centred inline-flex box that sat visibly lower than
     its own label. */
  table.stack td[data-label] a:not(.btn):not(.badge) {
    display: inline; min-height: 0;
  }
  /* Actions and the follow-up form sit below the facts, ruled off. */
  table.stack td.actions,
  table.stack td.cell-form {
    margin-top: 8px; padding-top: 10px;
    border-top: 1px solid var(--border);
  }
  table.stack td.actions {
    display: flex; flex-wrap: wrap; gap: 8px; align-items: center;
  }
  table.stack td.cell-form { display: block; }
  table.stack td.actions .btn { min-height: var(--tap); }
  /* On a card, the last-contacted hint and a press result take their own line. */
  table.stack td.actions .remind-last,
  table.stack td.actions .remind-result { flex-basis: 100%; }

  /* Sort lives here once thead is hidden. */
  .sort-bar {
    display: flex; align-items: center; gap: 10px; flex-wrap: wrap;
    padding: 12px 16px; border-bottom: 1px solid var(--border);
  }
  .sort-bar .chips { margin-bottom: 0; }
  .sort-bar-label {
    font-size: 11.5px; text-transform: uppercase; letter-spacing: 0.04em;
    font-weight: 650; color: var(--text-muted);
  }

  /* ---- Calendar: the 7-column grid gives a day ~53px here. Show the agenda
     that the month branch renders alongside it instead. ---- */
  .cal-grid { display: none; }
  .cal-agenda { display: block; }

  /* Form-control sizing lives in tier A -- iPad needs it too. Only the
     go-full-width part is phone-specific. */
  .w-auto, .toolbar .w-auto, .map-select { width: 100%; min-width: 0; }

  .form-actions { flex-direction: column; align-items: stretch; }
  .form-actions .btn { width: 100%; justify-content: center; }

  /* The review screen's action rows. A note field and a button side by side at
     phone width leaves neither usable, so they stack full-width. */
  .section-head { padding: 11px 14px; }
  .review-actions { flex-direction: column; align-items: stretch; }
  /* (0,2,1) again, matching the base rule it overrides, and later in the file. */
  .review-actions input[type=text] { flex: 1 1 auto; width: 100%; }
  .review-actions .btn { width: 100%; justify-content: center; }
  .review-dismiss { align-items: flex-start; }
  .review-dismiss .btn { width: 100%; justify-content: center; }
  .page-head .btn-row { width: 100%; }
  .page-head .btn-row .btn { flex: 1 1 auto; justify-content: center; }
  .card-head { flex-wrap: wrap; }
  .pager { flex-direction: column; align-items: flex-start; gap: 10px; }
  .toolbar { padding: 12px 16px; }
  .card-pad { padding: 16px; }

  /* Two columns of key/value still reads well at 375px. */
  .detail-grid { gap: 14px 18px; grid-template-columns: repeat(auto-fit, minmax(150px, 1fr)); }
  .breakdown-row { flex-wrap: wrap; }
  .breakdown-row .name { min-width: 0; flex: 1 1 auto; }

  /* ---- The follow-up queue's per-row form. Field names and the CSRF token
     are untouched; only the wrapping and sizing change. ---- */
  .inline-form { flex-wrap: wrap; gap: 8px; }
  .inline-form select,
  .inline-form input[type="date"],
  .inline-form input[type="text"] {
    flex: 1 1 100%;
    width: 100%;   /* beats the 130px / 132px above -- same selector, later */
  }
  .inline-form .btn { flex: 1 1 100%; justify-content: center; min-height: var(--tap); }
  .inline-form .touch-note { flex: 1 1 100%; margin: 0 0 -4px; }

  /* The keyboard shoves a centred card off-screen; pin it near the top. */
  .shell-bare {
    align-items: start;
    padding: 8vh 16px calc(24px + env(safe-area-inset-bottom));
  }
  .login-card { padding: 24px 20px; }
  .page-head h1 { font-size: 20px; }
  .empty { padding: 34px 16px; }
}

/* ---------- Tier D: phone landscape, where vertical space is the scarce axis ---------- */
@media (max-width: 900px) and (orientation: landscape) and (max-height: 460px) {
  .topbar { position: static; }
  .nav a { min-height: var(--tap); font-size: 11.5px; }
  .shell { padding-bottom: calc(var(--tap) + 20px + env(safe-area-inset-bottom)); }
}

/* ---------- Not a width tier ----------
   Deliberately last, so it can flatten anything the tiers above introduce. It is
   a media query, but it keys on an operating-system preference rather than a
   viewport, which is why it sits outside the A-D numbering.

   .spinner is named rather than left to the universal selector: `*` is (0,0,0)
   and cannot beat that rule's own (0,1,0) `animation` shorthand. Naming it obeys
   this file's rule -- equal or greater specificity, later in the file -- where
   !important would be breaking that rule for one selector. A still ring still
   reads as "working". */
@media (prefers-reduced-motion: reduce) {
  *, *::before, *::after {
    transition-duration: 0.01ms;
    animation-duration: 0.01ms;
    animation-iteration-count: 1;
  }
  .spinner { animation: none; }
}

/* A booking we have approved that Office Ally does not know about yet. Dashed
   rather than solid because it is a promise, not a fact on the clinic's own
   schedule -- and it must never be mistaken for a confirmed appointment. */
.cal-appt.cal-pending {
  border: 1px dashed var(--warn);
  background: transparent;
  color: var(--text-muted);
}
tr.row-pending td { background: var(--warn-soft); }

/* The patient-facing booking page. Wider touch targets and larger type than the
   staff app: it is read on a phone, once, by somebody who has never seen it.

   DO NOT RE-THEME. These five rules live in app.css but render only inside
   `.rose`, so they are part of the Rose Pediatrics surface, not the staff one.
   They survive a staff re-theme today only because they carry no colour --
   keep it that way. Anything visual for these classes belongs in rose.css,
   where it is pinned. */
.selfserve { max-width: 560px; margin: 32px auto 18px; }
.selfserve h1 { margin: 0 0 4px; font-size: 22px; }
.slot-day { margin-top: 18px; }
.slot-day-label { font-weight: 600; margin-bottom: 8px; }
.slot-row { display: flex; flex-wrap: wrap; gap: 8px; }
.slot-row form { margin: 0; }
.slot-btn { min-height: var(--tap); min-width: 92px; justify-content: center; }

/* ---------- Run a test call (admin, /followup) ----------
   A <details> panel, no script, matching the disclosure pattern already used
   elsewhere in the app. Collapsed by default on purpose: it is a box that dials
   a phone, and the follow-up queue is read over shoulders all day.

   Prefix is tcall- because app.css already owns the .cal-* namespace and the
   public booking pages own rose-*. Deliberately thin -- the panel reuses
   .notice*, .badge*, .table-wrap, .stack, .mono, .muted, .btn* and .page-head
   rather than restating them. */
.tcall {
  margin-bottom: 18px;
  border: 1px solid var(--border);
  border-radius: var(--radius);
  background: var(--surface);
}
.tcall > summary {
  display: flex;
  gap: 10px;
  align-items: center;
  flex-wrap: wrap;
  padding: 12px 18px;
  cursor: pointer;
  font-size: 13.5px;
  min-height: var(--tap);
  list-style: none;
}
.tcall > summary::-webkit-details-marker { display: none; }
.tcall[open] > summary { border-bottom: 1px solid var(--border); }
.tcall-verb { font-weight: 600; color: var(--accent); }
.tcall-facts { color: var(--text-muted); }
.tcall-body { padding: 16px 18px 18px; }
.tcall-body > .muted { max-width: 70ch; }
.tcall-grid {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(220px, 1fr));
  gap: 14px;
  margin: 14px 0;
}
.tcall-grid label {
  display: block;
  margin-bottom: 4px;
  font-size: 12.5px;
  font-weight: 600;
}
.tcall-grid input,
.tcall-grid select { width: 100%; }
.tcall-grid .muted { margin-top: 4px; font-size: 12px; }
.tcall-actions { margin-top: 4px; }
/* The strip is a card footer in all but name -- rule on top, not bottom. */
.tcall-strip {
  margin-top: 18px;
  padding-top: 14px;
  border-top: 1px solid var(--border);
}
