/*
 * Clinic Panel — base styles.
 *
 * Mobile first, per docs/03: laid out for 380px, widened at breakpoints.
 * Tap targets are deliberately large (min 44px) because the front desk uses
 * this one-handed on a phone between patients.
 */

/*
 * Colour, in two layers.
 *
 * The first is a palette: one cool neutral ramp and one brand ramp, each step
 * chosen so any two of them are a deliberate pair. Before this there were
 * eleven one-off hex values plus another dozen written inline through the
 * file, which is why a border, a divider and a disabled row could all be
 * *nearly* the same grey and never quite agree.
 *
 * The second is the old names, kept as aliases. Six hundred lines refer to
 * `--line` and `--bg`, and repointing them at the ramp is what let this be a
 * palette change rather than a rewrite.
 */
:root {
  /* Neutrals. 25 is barely off-white; 900 is text. */
  --n-0: #fff;
  --n-25: #fafbfd;
  --n-50: #f4f6fa;
  --n-100: #eceff5;
  --n-200: #dfe4ed;
  --n-300: #c6cedd;
  --n-400: #97a1b4;
  --n-500: #6b7688;
  --n-600: #4d5768;
  --n-700: #363f4e;
  --n-800: #212936;
  --n-900: #131a24;

  /*
   * Brand. Taken from the nquiri mark, replacing the navy this shipped with.
   *
   * The ramp is not invented: `--b-600` is the wordmark's own green, sampled
   * from the logo file, and `--b-accent` is the bright green of the
   * magnifying-glass handle and the two dots on the i's. Everything else is a
   * step along the same hue so the interface and the mark read as one thing.
   *
   * The first cut of this redesign used a teal ramp around `#0d9488`. It was
   * close enough to look deliberate and wrong enough to look broken beside the
   * real logo: the mark sits at hue 167°, a true green, and that ramp sat at
   * 174–185°, which leans blue. A sidebar at `#0c2e31` next to a 167° wordmark
   * reads as two brands sharing a screen.
   *
   * Contrast, since these carry white text: 600 on white is 7.6:1 and 500 is
   * 4.9:1, so both clear AA. `--b-accent` is 2.7:1 and therefore **never a
   * background for text** — it is an edge, an icon tint, a highlight.
   *
   * Every name below is unchanged, which is what let this be a repoint rather
   * than a rewrite: six hundred lines already say `--b-600`.
   */
  --b-50: #e9f6f2;
  --b-100: #cfebe2;
  --b-200: #a2d8c8;
  --b-500: #00806a;
  --b-600: #01604b;
  --b-700: #024b3b;
  --b-800: #06342a;
  --b-900: #04231b;
  --b-accent: #00b393;

  /* Semantic. Each has a text tone, a wash and a border that belong together. */
  --good: #0b6e43;
  --good-bg: #e6f6ee;
  --good-line: #a9dcc2;
  --bad: #a8241d;
  --bad-bg: #fdedeb;
  --bad-line: #f0bfba;
  --warn: #7a5800;
  --warn-bg: #fff6dd;
  --warn-line: #e6d19b;

  /* --- the names the rest of the file uses ------------------------------ */
  /*
   * `--navy` is a historical name, kept because repointing it is what made the
   * palette change a two-line edit rather than a rewrite. It now names the
   * strongest green a *control* should use — not the darkest one in the ramp.
   *
   * That distinction is new. On the navy ramp, 800 was a deep blue that read
   * as "selected" on a chip or a chosen radio; on the green ramp 800 is the
   * sidebar, nearly black, and a selected segment wearing it looked switched
   * off rather than switched on. Chrome uses 800 by name (`--side-bg`);
   * controls use this.
   */
  --navy: var(--b-700);
  --navy-soft: var(--b-600);
  --ink: var(--n-900);
  --muted: var(--n-500);
  --line: var(--n-200);
  --bg: var(--n-50);
  --white: var(--n-0);

  /*
   * Elevation as tokens rather than four hand-written box-shadows. Both are
   * two-layer: a tight shadow for the edge, a wide soft one for the lift.
   * Tinted with the brand rather than black, so a shadow on a cool page does
   * not read as grey sludge.
   */
  --shadow-1: 0 1px 2px rgb(15 37 71 / 5%), 0 1px 3px rgb(15 37 71 / 4%);
  --shadow-2: 0 1px 2px rgb(15 37 71 / 4%), 0 8px 20px rgb(15 37 71 / 6%);
  --shadow-pop: 0 10px 28px rgb(15 37 71 / 16%);

  --radius: 12px;
  --radius-sm: 9px;
  --radius-pill: 999px;

  --tap: 44px;

  /*
   * The desktop sidebar's width, and the tones that only exist inside it.
   *
   * A token rather than a repeated 224px because four rules depend on it
   * agreeing exactly — the rail itself, the topbar's offset, the page's
   * offset, and the point the layout collapses back to a dock.
   */
  /*
   * How wide the page is allowed to get, at this breakpoint.
   *
   * One variable because two rules depend on it agreeing exactly: the page
   * centres itself inside it, and the topbar centres its own contents on the
   * same figure so the clinic name lines up with the cards below it. They were
   * two hand-written copies of 1080px and 1200px, which is one edit away from
   * a header that no longer aligns with its own page.
   */
  --page-max: 560px;

  --side-w: 224px;
  --side-bg: var(--b-800);
  /* Green-tinted greys, not neutral ones: neutral grey on a green rail reads
     as dirty. 10:1 against the rail, so the labels are not a squint. */
  --side-ink: #c9e3d8;
  --side-ink-dim: #8ab3a3;
  /* The active item wears the wordmark's own green. */
  --side-on: #0b5442;
  --side-rule: #10493a;

  /* One focus ring everywhere, so it is recognisable rather than a surprise. */
  --focus: 0 0 0 3px var(--b-200);
}

* { box-sizing: border-box; }

body {
  margin: 0;
  background: var(--bg);
  color: var(--ink);
  /*
   * `system-ui` first: on Android that resolves to Roboto and on Windows to
   * Segoe UI Variable, both of which render better than the named fallbacks
   * that used to lead. No web font — `font-src` is 'self' (DECISIONS §21) and
   * a typeface download is not a trade worth making on a mid-range phone.
   *
   * 1.55 rather than 1.5: these screens are read in a hurry, at arm's length,
   * often one-handed.
   */
  font: 16px/1.55 system-ui, -apple-system, "Segoe UI Variable Text", "Segoe UI",
        Roboto, "Helvetica Neue", Arial, sans-serif;
  -webkit-font-smoothing: antialiased;
  -moz-osx-font-smoothing: grayscale;
  -webkit-text-size-adjust: 100%;
  /* Numbers in a column should line up wherever they appear. */
  font-variant-numeric: tabular-nums;
}

.wrap {
  max-width: var(--page-max);
  margin: 0 auto;
  padding: 16px 16px 40px;
}

/*
 * Type scale. Headings are tightened and set slightly heavier: at these sizes
 * default tracking reads loose, and a heading that is only a little larger than
 * the body text has to earn the distinction some other way.
 */
h1 { font-size: 1.5rem; font-weight: 700; letter-spacing: -0.018em; line-height: 1.25; margin: 0 0 4px; }
h2 { font-size: 1.0625rem; font-weight: 650; letter-spacing: -0.011em; margin: 0 0 8px; }
h3 { font-size: 0.9375rem; font-weight: 650; letter-spacing: -0.008em; margin: 18px 0 8px; }
.sub { color: var(--muted); margin: 0 0 20px; }
.muted { color: var(--muted); font-size: 0.9125rem; }

/* ---------------------------------------------------------------- chrome */

.banner--staging {
  background: var(--warn-bg);
  border-bottom: 1px solid var(--warn-line);
  color: var(--warn);
  font-size: 0.8125rem;
  font-weight: 600;
  letter-spacing: 0.02em;
  padding: 7px 16px;
  text-align: center;
}

.topbar {
  align-items: center;
  /*
   * A shallow gradient rather than one flat navy. It is barely perceptible as
   * a gradient — the point is that the bar stops looking like a coloured
   * rectangle and starts looking like a surface.
   */
  background: linear-gradient(180deg, var(--b-700), var(--b-800));
  box-shadow: inset 0 -1px 0 rgb(255 255 255 / 8%);
  color: var(--white);
  display: flex;
  justify-content: space-between;
  padding: 12px 16px;
}
/*
 * A clinic name is as long as the clinic chose to make it, and this sits beside
 * a branch switcher and a menu button on a 360px screen. Ellipsis rather than
 * wrapping: a two-line bar would push the whole page down.
 *
 * `min-width: 0` is what makes the ellipsis work at all — a flex item's default
 * minimum is its content width, so without it the name refuses to shrink and
 * shoves the menu button off the edge instead of truncating.
 */
.topbar__name {
  font-weight: 650;
  letter-spacing: -0.008em;
  min-width: 0;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
  /* An <a> since it goes to Today, dressed as the label it always was. */
  color: inherit;
  text-decoration: none;
}
.topbar__out { margin: 0; }

/*
 * The user menu — docs/03 puts Billing, Settings and Export behind one.
 *
 * Built on <details>, so it opens and closes with no JavaScript: nothing to
 * load, nothing to go wrong on a slow phone, and no exception needed in the
 * Content-Security-Policy.
 */
/* The clinic's own picklists — Clinic, Marketing and Laboratories (§72). */
/*
 * Wraps, because the row grew a third control. On a 375px phone a treatment
 * name, an interval dropdown and Save cannot share one line: the name input is
 * the flexible one, so it took the whole squeeze and ended up 33px wide —
 * unreadable, and unusable for the rename the row exists to do. A flex-basis
 * it will not shrink below sends the dropdown to a second line instead.
 */
.listrow { align-items: center; display: flex; flex: 1; flex-wrap: wrap; gap: 8px; margin: 0; min-width: 0; }
.listrow input[name="label"] { flex: 1 1 150px; min-width: 0; }
.listrow__chase {
  align-items: center;
  display: flex;
  font-size: 0.8125rem;
  gap: 5px;
  margin: 0;
  white-space: nowrap;
}
.listrow__chase input { min-height: auto; width: auto; }
/*
 * When a treatment brings the patient back. Same row, same weight as the
 * chase tick it sits beside — both are behaviour rather than labelling.
 */
.listrow__recall {
  align-items: center;
  display: flex;
  font-size: 0.8125rem;
  gap: 5px;
  margin: 0;
  white-space: nowrap;
}
.listrow__recall select { min-height: auto; padding: 4px 6px; width: auto; }
.listrow__recall-label { color: var(--muted); }
/*
 * Which branches an option belongs to — §80.
 *
 * Only drawn on a multi-branch clinic's all-branches view, so most clinics
 * never see it. Wraps as a group after the name, and each tick keeps its
 * branch label beside it: a row of bare boxes would need a header to read, and
 * this row already has three other controls competing for the line.
 */
.listrow__branches { display: flex; flex-wrap: wrap; gap: 4px 12px; }
.listrow__branch {
  align-items: center;
  color: var(--muted);
  display: flex;
  font-size: 0.8125rem;
  gap: 5px;
  margin: 0;
  white-space: nowrap;
}
.listrow__branch input { min-height: auto; width: auto; }
/*
 * Save and Remove, one cluster at the line's end.
 *
 * `margin-left: auto` is what makes the rows read as a table: however wide
 * the name and however many controls a row carries, the actions end at the
 * same right edge on every row. When the controls wrap on a phone the ticks
 * keep the line's left and the actions keep its right, which is the layout
 * the separate-form markup could never produce.
 */
.listrow__actions { align-items: center; display: flex; gap: 8px; margin-left: auto; }

/*
 * The picklist row, opted in to vertical centring.
 *
 * `.row` deliberately does not centre — a dozen screens top-align their two
 * halves — and the picklists were living with the consequence: the Remove
 * form is a separate flex child from the update form (it posts elsewhere, so
 * it cannot share the element), and without centring it hung from the top of
 * the line while Save sat mid-row. One class, only where every child is
 * one control tall.
 */
.row--pick { align-items: center; }

/* Retired options stay visible so they can be put back, but read as inactive. */
.row--off, .listrow--off { opacity: 0.55; }
.listadd { align-items: flex-end; display: flex; flex-wrap: wrap; gap: 10px; margin-top: 14px; }
.listadd .field { flex: 1; margin: 0; min-width: 190px; }

/*
 * A row of buttons under the thing they act on.
 *
 * Named in eight views since Phase 3 and never styled, so every one of them
 * was stacking its buttons as blocks — visible on Laboratories, where Save sat
 * on its own line above a Close link on another. §73.
 */
.row-actions { align-items: center; display: flex; flex-wrap: wrap; gap: 10px; }

/*
 * The retire/reopen form that follows a record row. Its own <form>, because it
 * posts somewhere other than Save does and so cannot share that row's element;
 * this just stops it sitting flush against the next record.
 */
.listrow__toggle { margin: 6px 0 4px; }

/*
 * A record that is a paragraph, not a line — an aftercare note.
 *
 * These were `.listrow`, which is the flex line the picklists use for a single
 * short input. A textarea in that layout collapses to its min-content width:
 * the note bodies were rendering about 185px wide with their own scrollbars,
 * and each field's label wrapped away from the field it belonged to. A note is
 * a name and a paragraph, so it gets a grid that says exactly that — stacked
 * on a phone, name beside body once there is room.
 */
.noterow { border-top: 1px solid var(--line); margin-top: 18px; padding-top: 18px; }
.noterow:first-of-type { border-top: 0; margin-top: 0; padding-top: 0; }
.noterow__fields { display: grid; gap: 0 18px; grid-template-columns: minmax(0, 1fr); }
@media (min-width: 760px) {
  .noterow__fields { grid-template-columns: minmax(0, 280px) minmax(0, 1fr); }
}
/*
 * Save and Retire on one line. They are two sibling <form>s — Retire posts
 * to its own endpoint, so it cannot live inside Save's form — and as blocks
 * they stacked, Save on one line and Retire alone on the next. The note's
 * form dissolves (display: contents) so the fields and both buttons lay out
 * on the row's own flex line: fields across the top, the two buttons side
 * by side beneath them.
 */
.noterow { align-items: center; display: flex; flex-wrap: wrap; gap: 10px; }
.noterow > form:first-of-type { display: contents; }
.noterow .noterow__fields { flex: 1 1 100%; }
.noterow > .listrow__toggle { margin: 0; }

.usermenu { position: relative; }
.usermenu__button {
  align-items: center;
  border-radius: 6px;
  color: var(--white);
  cursor: pointer;
  display: flex;
  font-size: 1.15rem;
  justify-content: center;
  /* The 44px tap target the rest of the app uses. */
  min-height: var(--tap);
  min-width: var(--tap);
  list-style: none;
}
/* Safari draws its own disclosure triangle without this. */
.usermenu__button::-webkit-details-marker { display: none; }
.usermenu__button:focus-visible { outline: 2px solid var(--white); outline-offset: 2px; }

/*
 * The trigger has two faces: ☰ on a phone, initials-and-name at desk width.
 *
 * One <summary> wearing either, rather than two controls — a second element
 * would be a second thing to keep in step, and only one of them can own the
 * open state anyway.
 */
.usermenu__avatar, .usermenu__who, .usermenu__caret { display: none; }

@media (min-width: 1024px) {
  .usermenu__burger { display: none; }
  .usermenu__button { gap: 10px; justify-content: flex-end; padding: 0 6px 0 0; }
  .usermenu__avatar {
    align-items: center;
    background: var(--b-500);
    border-radius: 50%;
    color: var(--white);
    display: flex;
    flex: 0 0 auto;
    font-size: 0.8125rem;
    font-weight: 700;
    height: 34px;
    justify-content: center;
    letter-spacing: 0.02em;
    width: 34px;
  }
  .usermenu__who { display: block; line-height: 1.2; text-align: left; }
  .usermenu__name { display: block; font-size: 0.875rem; font-weight: 600; }
  .usermenu__role { color: var(--muted); display: block; font-size: 0.75rem; }
  .usermenu__caret { color: var(--muted); display: block; font-size: 0.7rem; }

  /*
   * The clinic's own destinations leave this menu at desk width — they have a
   * permanent door in the sidebar's Setup group. What is left is what the
   * menu should always have been: the things that are about *you*.
   */
  .usermenu__clinic { display: none; }
}

/*
 * Hidden until opened, and stated explicitly.
 *
 * The browser hides a closed <details>'s children with its own rule, but a
 * class selector outranks it — so `display: flex` here alone left the panel
 * permanently on screen with the page showing through it.
 */
.usermenu__panel { display: none; }
.usermenu[open] .usermenu__panel { display: flex; }

.usermenu__panel {
  background: var(--white);
  border: 1px solid var(--line);
  border-radius: var(--radius);
  box-shadow: var(--shadow-pop);
  flex-direction: column;
  min-width: 190px;
  padding: 6px;
  position: absolute;
  right: 0;
  top: calc(100% + 6px);
  z-index: 30;
}
.usermenu__item {
  background: none;
  border: 0;
  border-radius: 6px;
  color: var(--ink);
  display: flex;
  align-items: center;
  font: inherit;
  min-height: var(--tap);
  padding: 0 12px;
  text-align: left;
  text-decoration: none;
  width: 100%;
}
.usermenu__item:hover { background: var(--bg); }
.usermenu__item--button { cursor: pointer; color: var(--bad); }
.usermenu__rule { border-top: 1px solid var(--line); margin: 6px 4px; }

.foot {
  color: var(--muted);
  font-size: 0.8125rem;
  padding: 24px 16px 32px;
  text-align: center;
}
.foot__by { align-items: center; display: inline-flex; gap: 7px; }

/*
 * The vendor's mark. Height is set on the element and width left automatic, so
 * one rule fits whatever proportions the artwork turns out to have — a wordmark
 * and a square badge both work without touching CSS.
 *
 * `max-width: 100%` matters on a 320px phone: a wide wordmark would otherwise
 * push the footer sideways and give the whole page a horizontal scrollbar.
 */
.brandmark { display: block; max-width: 100%; width: auto; }
.brandmark--lg { height: 30px; }

/* Above a sign-in card, where WEBMAGIC LAB is what is being identified. */
.brandtop {
  align-items: center;
  display: flex;
  justify-content: center;
  padding: 18px 0 4px;
}

/* ----------------------------------------------------------------- cards */

.card {
  background: var(--white);
  border: 1px solid var(--line);
  border-radius: var(--radius);
  box-shadow: var(--shadow-1);
  margin-bottom: 16px;
  padding: 20px;
}
.card--narrow { margin-top: 24px; }

/* ----------------------------------------------------------- embed pages */

/*
 * The framed booking form (views/book). Tighter rhythm than the app proper:
 * every saved pixel is scrolling a patient does not do inside somebody's
 * website, and the fallback iframe height in the Settings snippet only has to
 * be right when the clinic's page editor stripped the resizer script.
 * The 88px dock allowance on .foot is for a bottom bar these pages never have.
 */
.wrap--embed { max-width: 520px; padding: 10px 12px 0; }
.wrap--embed .card { margin-bottom: 12px; padding: 16px; }
.wrap--embed .card--narrow { margin-top: 0; }
.wrap--embed h1 { font-size: 1.375rem; }
.wrap--embed .field { margin-bottom: 12px; }
.wrap--embed + .foot { padding: 4px 0 14px; }

/* ---------------------------------------------------------------- fields */

.field { display: block; margin-bottom: 16px; }
.field__label { display: block; font-size: 0.875rem; font-weight: 600; margin-bottom: 5px; }
.field__opt { color: var(--muted); font-weight: 400; }
.field__hint { color: var(--muted); display: block; font-size: 0.8125rem; margin-top: 4px; }
.field__err { color: var(--bad); display: block; font-size: 0.8125rem; margin-top: 4px; }

/*
 * The list was text, email, password and untyped, which meant every `date`
 * input in the product — the filter rows, the booking form, the recall anchor —
 * had been rendering as an unstyled browser default: no border of ours, no
 * radius, and 13px text. The last one matters most, because the 16px below is
 * not a taste: under it, iOS Safari zooms the whole page when the field takes
 * focus, which is the thing this rule was written to stop.
 *
 * Found while adding the search boxes to the Records tabs (§52) — a styled
 * search box beside an unstyled date field is what made it visible.
 */
input[type="text"], input[type="email"], input[type="password"],
input[type="search"], input[type="date"], input[type="time"],
input[type="tel"], input[type="number"],
input:not([type]) {
  background: var(--white);
  border: 1px solid var(--n-300);
  border-radius: var(--radius-sm);
  color: var(--ink);
  /* 16px prevents iOS Safari zooming the whole page on focus. */
  font-size: 16px;
  min-height: var(--tap);
  padding: 10px 12px;
  transition: border-color 0.12s, box-shadow 0.12s;
  width: 100%;
}
::placeholder { color: var(--n-400); }

/*
 * One focus treatment for every control: the border darkens to brand and a
 * soft ring sits outside it. `outline` alone was a hard 2px square around a
 * rounded field, which reads as an error state rather than as focus.
 */
input:focus-visible, select:focus-visible, textarea:focus-visible {
  border-color: var(--b-500);
  box-shadow: var(--focus);
  outline: none;
}

/* ---------------------------------------------------------------- buttons */

.btn {
  background: var(--white);
  border: 1px solid var(--n-300);
  border-radius: var(--radius-sm);
  box-shadow: var(--shadow-1);
  color: var(--n-700);
  cursor: pointer;
  display: inline-block;
  font-size: 1rem;
  font-weight: 600;
  letter-spacing: -0.006em;
  min-height: var(--tap);
  padding: 11px 18px;
  text-align: center;
  text-decoration: none;
  transition: background-color 0.12s, border-color 0.12s, box-shadow 0.12s, transform 0.06s;
}
.btn:hover { background: var(--n-25); border-color: var(--n-400); }

/*
 * A secondary button used to be a bare transparent box, which on a white card
 * left "Cancel" and "Book" looking like two different kinds of thing rather
 * than two buttons.
 */
/*
 * Stepped in by one after the palette moved to green.
 *
 * These were 600→800 and 500→700, which on the navy ramp was a barely-there
 * darkening. The green ramp reaches much further down — 800 is the sidebar,
 * nearly black — so the same pair became a button that faded from green into
 * charcoal and read as muddy. 500→600 keeps it one colour with a light on it,
 * which is all the gradient was ever for.
 */
.btn--primary {
  background: linear-gradient(180deg, var(--b-500), var(--b-600));
  border-color: var(--b-600);
  color: var(--white);
}
.btn--primary:hover {
  background: linear-gradient(180deg, var(--b-600), var(--b-700));
  border-color: var(--b-700);
}
/* Pressing it moves. On a phone that is most of the feedback there is. */
.btn:active { box-shadow: none; transform: translateY(1px); }
.btn:focus-visible { box-shadow: var(--focus); outline: none; }
.btn--block { display: block; width: 100%; }
.btn[disabled] { box-shadow: none; cursor: not-allowed; opacity: 0.55; transform: none; }

/*
 * The row-sized button. Named in eight views since Phase 3 and, like
 * `.row-actions` before it, never actually defined — so every "small" button
 * rendered at full size, and the picklist rows fell back to underlined links
 * because a 44px button beside every treatment was visibly wrong. Sized to sit
 * inside a list row: smaller type, tighter padding, but still a 36px target,
 * because the finger that taps Remove is on the same phone as everything else.
 */
.btn--small {
  font-size: 0.875rem;
  font-weight: 600;
  min-height: 36px;
  padding: 6px 13px;
}

/*
 * The destructive action in a row — Remove, Retire, Close this lab.
 *
 * Quiet until approached: red text on the card, filling red on hover. Solid
 * `.btn--bad` shouts from every row of a thirty-treatment list; a bare red
 * link (what these were) gave a destructive action the smallest tap target on
 * the screen. Every use sits on a form with data-confirm, so the colour warns
 * and the dialog protects.
 */
.btn--bad-ghost { border-color: var(--bad-line); color: var(--bad); }
.btn--bad-ghost:hover { background: var(--bad-bg); border-color: var(--bad); }

.linkish {
  background: none;
  border: 0;
  border-radius: 4px;
  color: inherit;
  cursor: pointer;
  font: inherit;
  padding: 0;
  text-decoration: underline;
  text-decoration-color: var(--n-300);
  text-underline-offset: 2px;
}
.linkish:hover { text-decoration-color: currentcolor; }
.linkish:focus-visible { box-shadow: var(--focus); outline: none; }

/* §123: the maintenance page's one card, floated down from the top edge. */
.waitcard { margin-top: 12vh; }

/*
 * §127: a support thread. The vendor's messages carry the brand's green edge,
 * so whose turn it was reads at a glance without bubbles or avatars.
 */
.thread { margin-bottom: 20px; }
.msg { border-top: 1px solid var(--line); padding: 12px 0 12px 10px; }
.msg:first-child { border-top: 0; }
.msg--vendor { border-left: 3px solid var(--b-500); }
.msg__meta { font-size: 0.8125rem; margin: 0 0 4px; }
.msg__body { margin: 0; white-space: pre-wrap; }

/*
 * §123: a message from the vendor, shown inside the clinic app. Blue on
 * purpose — good, bad and warn are all spoken for, and news is none of them.
 */
.alert--news {
  align-items: center;
  background: #eef4fc;
  border: 1px solid #c9dcf3;
  border-left: 3px solid #2b6cb0;
  color: #1d4d7c;
  display: flex;
  flex-wrap: wrap;
  gap: 10px;
  justify-content: space-between;
}
.alert--news form { margin: 0; }

/* ---------------------------------------------------------------- alerts */

/* A 3px edge on the leading side, so the kind of message is legible before the
   words are — and so the three alerts differ by more than a pale wash. */
.alert {
  border-radius: var(--radius-sm);
  font-size: 0.9375rem;
  margin-bottom: 16px;
  padding: 12px 14px;
}
.alert--good { background: var(--good-bg); border: 1px solid var(--good-line);
               border-left: 3px solid var(--good); color: var(--good); }
.alert--bad { background: var(--bad-bg); border: 1px solid var(--bad-line);
              border-left: 3px solid var(--bad); color: var(--bad); }
.alert--warn { background: var(--warn-bg); border: 1px solid var(--warn-line);
               border-left: 3px solid var(--warn); color: var(--warn); }

/*
 * The hidden attribute must always win. The browser's own `[hidden]` rule is
 * ordinary specificity, so any class that sets display — .btn's inline-block —
 * quietly overrides it. That is how the Settings install button managed to
 * render alongside the "already installed" text it was mutually exclusive
 * with: ui.js set `hidden` faithfully and the stylesheet undid it.
 */
[hidden] { display: none !important; }

/* ------------------------------------------------------------ lists/rows */

.plain { list-style: none; margin: 0 0 16px; padding: 0; }
.plain > li { border-bottom: 1px solid var(--line); padding: 12px 0; }
.plain > li:last-child { border-bottom: 0; }

/*
 * The Settings hub — §130.
 *
 * `.hubhead` is a card's title beside its one action, with none of
 * .branchrow__hours' inset background — a header is not a sub-panel.
 * `.hubrow` is a whole-row link: title and status on the left, a chevron on
 * the right, the entire strip tappable — on a phone a 44px-wide Open button
 * beside 300px of dead text is a small target next to a large excuse.
 */
.hubhead { align-items: center; display: flex; gap: 12px; justify-content: space-between; }
.hubhead h2 { margin: 0; }
.hubrow {
  align-items: center;
  color: inherit;
  display: flex;
  gap: 12px;
  justify-content: space-between;
  text-decoration: none;
}
.hubrow__chev { color: var(--n-300); flex: none; font-size: 1.5rem; line-height: 1; }

/* Stacks on a phone, sits side by side once there is room. */
.row { display: flex; flex-wrap: wrap; gap: 8px; justify-content: space-between; }
/*
 * The right-hand end of a list row: a status chip and the one fact that
 * qualifies it — due Thursday, four days late, no date promised. It had no
 * rule at all, so it was a block whose spans ran together on word spacing and
 * whose baseline did not meet the chip's.
 */
/*
 * A status chip sitting directly in a list row.
 *
 * `.row` is a flex container and its items stretch by default, so a pill beside
 * a two-line block was being pulled to the full height of that block — a 100px
 * by 44px box with a 999px radius, which draws as a large ellipse with the
 * label stranded at the top of it. Visible on Enquiries, where every row has an
 * outcome chip beside a name and a number.
 *
 * Centred rather than `align-items: center` on `.row` itself: that rule is used
 * by a dozen screens whose rows deliberately top-align their two halves.
 */
.row > .pill { align-self: center; }

/*
 * The next free slots — §78.
 *
 * Grouped under a day heading and laid out as a wrapping row of times, because
 * that is how the answer gets said out loud: "Thursday — ten, quarter past
 * ten, half past". A vertical list of twenty rows makes the front desk do that
 * grouping in their head while somebody waits on the phone.
 */
.freeday {
  border-top: 1px solid var(--line);
  color: var(--muted);
  font-size: 0.8125rem;
  font-weight: 500;
  margin: 18px 0 10px;
  padding-top: 14px;
  text-transform: none;
}
.freeday:first-of-type { border-top: 0; margin-top: 6px; padding-top: 0; }

.freeslot {
  align-items: baseline;
  background: var(--b-50);
  border: 1px solid transparent;
  border-radius: 10px;
  color: var(--b-600);
  display: inline-flex;
  gap: 8px;
  margin: 0 8px 8px 0;
  min-height: 44px;
  padding: 8px 14px;
  text-decoration: none;
}
.freeslot:hover, .freeslot:focus-visible { border-color: var(--b-500); }
.freeslot .muted { font-size: 0.8125rem; }

/*
 * The week — §77.
 *
 * Seven columns of counts, which is the only thing that fits: 7 × 40px plus a
 * band gutter is 340px, and a phone reports 360. Names never would, which is
 * exactly why this screen shows none.
 */
.wk { border-collapse: collapse; width: 100%; }
.wk th, .wk td { padding: 6px 4px; text-align: center; vertical-align: middle; }
.wk thead th { border-bottom: 1px solid var(--line); padding-bottom: 10px; }
.wk__bandhead, .wk__band {
  color: var(--muted);
  font-size: 0.8125rem;
  font-weight: 500;
  min-width: 78px;
  text-align: left;
}
.wk__day { color: var(--muted); display: block; font-size: 0.75rem; }
.wk__date { display: block; font-size: 1.0625rem; font-weight: 650; }
.wk tfoot th, .wk tfoot td { border-top: 1px solid var(--line); padding-top: 10px; }

/* Today's column, marked the whole way down so the eye keeps its place. */
.wk__today { background: var(--b-50); }
.wk thead .wk__today .wk__date { color: var(--b-600); }

/*
 * The count. Four steps of fill and no more — a finer gradient is one nobody
 * can compare across a row by eye, which is the only thing this screen is for.
 * Shaded against the week's own busiest cell, so a quiet clinic's full morning
 * still reads as full.
 */
.wk__cell {
  border-radius: 8px;
  display: block;
  font-size: 0.9375rem;
  font-weight: 600;
  line-height: 34px;
  min-width: 34px;
  text-decoration: none;
}
.wk__cell--0 { background: var(--n-25); color: var(--n-400); }
.wk__cell--1 { background: #e1f5ee; color: #085041; }
.wk__cell--2 { background: #9fe1cb; color: #085041; }
.wk__cell--3 { background: #5dcaa5; color: #04342c; }
.wk__cell:hover, .wk__cell:focus-visible { box-shadow: 0 0 0 2px var(--b-500); }

/* Not open then — a dash, never a zero, which would read as a gap to fill. */
.wk__shut { color: var(--n-300); font-size: 0.8125rem; }

/*
 * §90: how full a weekday's band ran across a whole range. The emptiest
 * blocks get the warning tint, because they are the ones the card exists to
 * point at — everything else stays quiet.
 */
.wk__pct {
  border-radius: var(--radius);
  display: inline-block;
  font-size: 0.8125rem;
  font-variant-numeric: tabular-nums;
  padding: 4px 6px;
}
.wk__pct--low { background: #fdeceb; color: var(--bad); font-weight: 650; }

/*
 * Tightened until seven days actually fit a phone, which is the whole claim
 * this screen makes. 52px of gutter plus seven 34px columns is 290 inside the
 * 294 a 360px screen leaves after the page padding. It still sits in .scrollx,
 * because a clinic on a 320px phone should scroll a table rather than lose the
 * end of its week.
 */
@media (max-width: 480px) {
  .wk th, .wk td { padding: 5px 1px; }
  .wk__bandhead, .wk__band { font-size: 0.6875rem; min-width: 0; padding-right: 4px; }
  .wk__cell { font-size: 0.875rem; line-height: 30px; min-width: 30px; }
  .wk__date { font-size: 0.9375rem; }
}

/*
 * The calendar, drawn to scale — §76.
 *
 * A row that only exists because something is booked in it: outside the stated
 * hours, or on a day the clinic shuts. Dimmed and struck with a tint rather
 * than hidden, because this screen never conceals a booking.
 */
.cal__row--closed > th, .cal__row--closed > td { background: var(--n-25); }
.cal__outside {
  color: var(--muted);
  display: block;
  font-size: 0.6875rem;
  font-weight: 400;
}

/*
 * The + in an empty slot. Nearly invisible until the row is approached, so a
 * grid of free half hours does not read as a grid of buttons — the bookings
 * are the content, and these are the spaces between them.
 */
.cal__free {
  align-items: center;
  border: 1px dashed var(--line);
  border-radius: 8px;
  color: var(--n-400);
  display: flex;
  font-size: 1rem;
  justify-content: center;
  min-height: 34px;
  opacity: 0.5;
  text-decoration: none;
}
.cal__free:hover, .cal__free:focus-visible {
  background: var(--b-50);
  border-color: var(--b-500);
  border-style: solid;
  color: var(--b-600);
  opacity: 1;
}
/* A touch screen has no hover, so they cannot rely on it to be findable. */
@media (hover: none) { .cal__free { opacity: 0.75; } }

/*
 * The same control on a slot that already holds somebody — §39 allows the
 * double booking, so §76 offers the tap. Quieter than the free one and with no
 * border: it must never make a taken time look available. It comes up to full
 * strength on hover and focus, where the intent is unambiguous.
 */
.cal__free--busy {
  border-color: transparent;
  margin-top: 2px;
  min-height: 22px;
  opacity: 0.25;
}
.cal__free--busy:hover, .cal__free--busy:focus-visible { opacity: 1; }
@media (hover: none) { .cal__free--busy { opacity: 0.4; } }

/*
 * A half hour inside a booking that started earlier. A rule rather than a
 * label: the appointment above it already says who and until when, and
 * repeating that in every row it spans would bury the day in its own text.
 */
.cal__busy {
  color: var(--n-300);
  display: block;
  font-size: 1rem;
  line-height: 34px;
  text-align: center;
}

/*
 * The opening-hours line on each branch in Settings — §75.
 *
 * Its own block rather than another clause in the grey run of address and
 * phone, which is where it shipped first and where nobody found it. Same
 * shape as every other thing on that page: a label, what is set, a button.
 */
.branchrow__hours {
  align-items: center;
  background: var(--n-25);
  border-radius: var(--radius);
  display: flex;
  flex-wrap: wrap;
  gap: 10px 16px;
  justify-content: space-between;
  margin: 12px 0;
  padding: 12px 14px;
}

/*
 * Opening hours, seven days of them — §75.
 *
 * A row per day: the name on the left, its two sessions on the right. Below
 * 700px the name goes above the times, because four time inputs and two "to"s
 * do not share a 360px line with a day name and stay tappable.
 *
 * The times wrap as a pair rather than individually — a closing time that
 * wraps away from its opening time reads as a separate fact.
 */
.hours__day {
  border-top: 1px solid var(--line);
  display: flex;
  flex-wrap: wrap;
  gap: 6px 20px;
  padding: 14px 0;
}
.hours__day:first-of-type { border-top: 0; padding-top: 0; }
.hours__name { align-items: baseline; display: flex; flex: 0 0 130px; gap: 8px; }
.hours__state { color: var(--muted); font-size: 0.8125rem; }
.hours__day--closed .hours__state { color: var(--muted); }

.hours__slots { display: flex; flex: 1 1 auto; flex-wrap: wrap; gap: 10px 22px; }
.hours__slot { align-items: center; display: flex; gap: 8px; }
.hours__slotlabel { color: var(--muted); font-size: 0.8125rem; min-width: 46px; }
.hours__to { color: var(--muted); font-size: 0.8125rem; }
/*
 * `width: auto` because a bare `input[type=time]` inherits the full-width rule
 * every other input on the page wants, and four of those stack into a column.
 */
.hours__slot input { min-width: 0; width: auto; }

/*
 * The ✕ that empties a session — §75, and the whole reason it exists: a
 * `type="time"` field cannot be cleared again on Chrome or on Android.
 *
 * 32px and not the 44px a primary target wants: it sits between two time
 * inputs that are themselves the tap targets, and at full size it competed
 * with them. Its own hit area is padded past its glyph.
 */
.hours__clear {
  background: none;
  border: 0;
  border-radius: 50%;
  color: var(--muted);
  cursor: pointer;
  font-size: 1.125rem;
  height: 32px;
  line-height: 1;
  padding: 0;
  width: 32px;
}
.hours__clear:hover { background: var(--n-50); color: var(--ink); }
.hours__clear:focus-visible { box-shadow: var(--focus); outline: none; }

.hours__dayactions { display: flex; flex: 0 0 auto; gap: 14px; }
.hours__dayactions .linkish { font-size: 0.8125rem; }

/* A closed day reads as one, without hiding the boxes that reopen it. */
.hours__day--closed .hours__slots { opacity: 0.6; }

@media (max-width: 699px) {
  .hours__name { flex: 1 0 100%; }
  .hours__slots { gap: 8px 16px; }
  .hours__slot { flex: 1 0 100%; }
  .hours__dayactions { flex: 1 0 100%; }
}

.row__meta { align-items: center; display: flex; flex-wrap: wrap; gap: 8px; justify-content: flex-end; }
.row__actions { display: flex; flex-wrap: wrap; gap: 8px; align-items: center; }
.row__actions form { margin: 0; }

/*
 * The per-row action set — WhatsApp, ✓ Done, snooze, Skip — on every chase
 * card: follow-ups, recalls, pending balances, and the WhatsApp link beside
 * an appointment's status chip.
 *
 * These were bare underlined links with an emoji in front: three different
 * treatments sharing one row, and a thumb target no taller than the text.
 * Small pill buttons instead. Pills rather than .btn rectangles, so they stay
 * visually lighter than the card-level buttons and sit comfortably beside the
 * status chips, which share the shape.
 *
 * 36px, not --tap's 44: three of these share one row of the narrow side
 * column, and at full button height the actions outweigh the row they act on.
 * Still roughly double the hit area the links gave.
 */
.act {
  align-items: center;
  background: var(--white);
  border: 1px solid var(--n-300);
  border-radius: var(--radius-pill);
  box-shadow: var(--shadow-1);
  color: var(--n-700);
  cursor: pointer;
  display: inline-flex;
  font: inherit;
  font-size: 0.8125rem;
  font-weight: 600;
  gap: 5px;
  min-height: 36px;
  padding: 4px 13px;
  text-decoration: none;
  white-space: nowrap;
}
.act:hover { background: var(--n-25); border-color: var(--n-400); }
.act:active { box-shadow: none; transform: translateY(1px); }
.act:focus-visible { box-shadow: var(--focus); outline: none; }

/*
 * "This opens WhatsApp" — said by the mark, not by the button.
 *
 * These used to be filled #128c7e. On a chase list that is a dozen saturated
 * green slabs down one edge of the screen, all shouting equally, none of them
 * the thing you are meant to read. The mark inside the button (partials/
 * waicon.ejs) carries WhatsApp's actual green in the place a borrowed brand
 * colour belongs — its own logo — and the button goes back to being a button.
 *
 * The green hairline is what keeps it identifiable at a glance in a row of
 * otherwise identical pills, without filling anything.
 */
.act--wa { border-color: #b7e3ce; color: var(--n-700); }
.act--wa:hover { background: #f2fbf6; border-color: #8fd4b4; color: var(--ink); }
/*
 * `nowrap`, and it is fixing a regression rather than tidying.
 *
 * Adding the mark put 16px of icon plus a 6px gap inside buttons that were
 * already near their container's width on a phone, so the label wrapped and
 * "Chase lab" rendered 97px wide by **74px tall** — a two-line button in a
 * row of one-line ones. The icon must push the button wider, never taller.
 */
.act--wa, .btn--wa, .act--sms, .btn--sms {
  align-items: center;
  display: inline-flex;
  gap: 6px;
  white-space: nowrap;
}
.waicon { display: block; flex: 0 0 auto; }
.chatbtn__icon { flex: 0 0 auto; font-size: 0.95em; line-height: 1; opacity: 0.75; }

/*
 * The visit clock — DECISIONS §156.
 *
 * "Waiting 25 minutes" and "In chair since 2:40 PM", on the appointment row and
 * at the head of the visit screen.
 *
 * No colour, and that is the decision rather than an omission. Amber would have
 * matched the Arrived chip beside it, but the chip's amber means "not finished"
 * and this line means "here is the number" — §45's rule, and the two would have
 * been one wash saying two things. Weight and the ink tone are enough to lift
 * it off the muted lines it sits under, which is all it needs: nothing else on
 * the row changes while the screen is open, so the eye finds it anyway.
 *
 * Tabular figures so a list of waiting times reads as a column of numbers
 * rather than a ragged edge, and so a minute ticking over does not shuffle the
 * characters after it.
 */
.chairclock {
  color: var(--ink);
  font-size: 0.8125rem;
  font-variant-numeric: tabular-nums;
  font-weight: 600;
  margin-top: 2px;
}
/* On the visit screen it is a paragraph in the header, not a line on a row. */
.chairclock--head { margin: 4px 0 0; }

/*
 * A destructive pill. Text-only red rather than a filled button: it sits on
 * every row of a list, and a column of solid red would read as the list being
 * the problem. Filled on hover, so the intent is unmistakable at the moment of
 * the click without shouting before it.
 */
.act--bad { color: var(--bad); border-color: var(--bad-line); }
.act--bad:hover { background: var(--bad); border-color: var(--bad); color: var(--white); }

/*
 * The SMS button, for a patient not on WhatsApp — §60.
 *
 * Brand navy, not a second green. WhatsApp's green is a borrowed brand colour
 * and means "this hands off to WhatsApp"; SMS has no brand to borrow, and
 * §45's rule cuts both ways — two greens a shade apart on the same list would
 * read as one meaning with a rendering glitch rather than two channels.
 */
/*
 * SMS: no colour at all, just the envelope — §60's distinction, rebuilt.
 *
 * It used to be filled brand navy against WhatsApp's green, which was a clear
 * two-channel signal. Once the brand became green that stopped working: a
 * brand-tinted SMS pill and a green-hairlined WhatsApp pill sat side by side
 * in the same list looking like one control rendered twice. The channels are
 * told apart by the mark now — WhatsApp has a logo, SMS has an envelope — and
 * the only green left in the pair belongs to WhatsApp, which is whose colour
 * it actually is.
 */
.act--sms { border-color: var(--n-300); color: var(--n-700); }
.act--sms:hover { background: var(--n-25); border-color: var(--n-400); color: var(--ink); }

/* The tick carries the "resolve" meaning; green does it without shouting. */
.act__tick { color: var(--good); font-weight: 700; }

.pill {
  background: var(--b-50);
  border: 1px solid var(--b-100);
  border-radius: var(--radius-pill);
  color: var(--b-600);
  font-size: 0.75rem;
  font-weight: 600;
  margin-left: 6px;
  padding: 2px 8px;
  text-transform: capitalize;
}

/*
 * A pill that leads its line rather than trailing it — §68's tag on a patient
 * row, where the code would otherwise be. `.pill` was written for counts that
 * follow a word ("Recalls 2") so its 6px sits on the left, and used in front
 * of a name it indented every tagged row past the untagged ones. The margin
 * changes sides.
 */
.pill--lead { margin-left: 0; margin-right: 6px; }

/*
 * Never been in and nothing booked either. Quieter than `.pill`, because this
 * is the absence of a state rather than one worth the eye: an appointment that
 * was cancelled or missed is a row the front desk may want to tidy away, not a
 * thing to act on today.
 */
.pill--quiet { background: var(--n-50); border-color: var(--line); color: var(--muted); }

.linkish--bad { color: var(--bad); }

/*
 * Two classes the views have been asking for since before this file knew about
 * them — §48.
 *
 * `.text--bad` appears on "3 days late" in the lab list and on Today, and
 * `.small` on hint text beside a missing lab number. Both were written by
 * someone who reasonably assumed they existed; neither matched a rule, so the
 * markup said "this is overdue" and the screen said nothing. Defining them
 * turns that text red and that hint small in the places already asking.
 */
.text--bad { color: var(--bad); }
.small { font-size: 0.8125rem; }

/* A heading inside a card, under its <h2> — for a card with distinct parts. */
.subhead {
  font-size: 0.9375rem;
  font-weight: 650;
  margin: 20px 0 8px;
}
.subhead:first-of-type { margin-top: 12px; }

/*
 * The only button in the product that destroys data. Coloured like it, so it
 * cannot be mistaken for the primary action on the page it sits on.
 */
.btn--bad { background: var(--bad); border-color: var(--bad); color: var(--white); }
.btn--bad:hover { background: #96201a; border-color: #96201a; }

/* Temporary password handout — shown once, meant to be read aloud. */
.handout { margin-top: 10px; }
.handout > div { padding: 2px 0; }
.handout__k { color: var(--muted); display: inline-block; min-width: 84px; }
.handout code { background: #fff; border: 1px solid var(--line); border-radius: 4px;
                font-size: 1rem; padding: 2px 6px; }

/* Segmented control: docs/03 prefers these to dropdowns under six options. */
.seg { border: 0; margin: 0 0 16px; padding: 0; }
.seg__opt { display: inline-block; margin: 0 8px 4px 0; }
.seg__opt input { position: absolute; opacity: 0; pointer-events: none; }
.seg__opt span {
  background: var(--white);
  border: 1px solid var(--n-300);
  border-radius: var(--radius-sm);
  box-shadow: var(--shadow-1);
  display: inline-block;
  min-height: var(--tap);
  line-height: 22px;
  padding: 10px 16px;
  transition: background-color 0.12s, border-color 0.12s;
}
.seg__opt span:hover { background: var(--n-25); }
.seg__opt input:checked + span {
  background: var(--b-600);
  border-color: var(--b-600);
  box-shadow: none;
  color: var(--white);
}
.seg__opt input:focus-visible + span { box-shadow: var(--focus); }

select {
  background: var(--white);
  border: 1px solid var(--n-300);
  border-radius: var(--radius-sm);
  font-size: 16px;
  min-height: var(--tap);
  padding: 10px 12px;
  transition: border-color 0.12s, box-shadow 0.12s;
  width: 100%;
}
.topbar select { min-height: 34px; padding: 4px 8px; width: auto; }
.topbar__branch { margin: 0; }
.topbar__branch-name { font-size: 0.875rem; opacity: 0.85; }

textarea {
  background: var(--white);
  border: 1px solid var(--line);
  border-radius: 8px;
  font: inherit;
  font-size: 16px;
  padding: 10px 12px;
  resize: vertical;
  width: 100%;
}
.input--short { max-width: 120px; }

.head-row {
  align-items: center;
  display: flex;
  flex-wrap: wrap;
  gap: 12px;
  justify-content: space-between;
  margin-bottom: 4px;
}
.head-row h1 { margin: 0; }

/*
 * A heading with something small sitting beside it, rather than at the far end
 * of the row like .head-row does. For a link that qualifies the heading — the
 * calendar beside the appointment count — where the width of the card should
 * not decide how far away it lands.
 */
.titleline { align-items: baseline; display: flex; flex-wrap: wrap; gap: 10px; margin-bottom: 4px; }
.titleline h2 { margin: 0; }
/* Its own size: .linkish is font: inherit, and this sits next to an h2. */
.titleline__link { font-size: 0.8125rem; }

.plainlink { color: inherit; text-decoration: none; }
.plainlink:hover { text-decoration: underline; }

/* ------------------------------------------------------------- typeahead */

.typeahead { list-style: none; margin: 10px 0 0; padding: 0; }
.typeahead > li { border-top: 1px solid var(--line); }
.typeahead button {
  background: none;
  border: 0;
  cursor: pointer;
  font: inherit;
  min-height: var(--tap);
  padding: 11px 4px;
  text-align: left;
  width: 100%;
}
.typeahead button:hover, .typeahead button:focus-visible { background: var(--bg); }
.typeahead__empty { color: var(--muted); padding: 11px 4px; }

/*
 * §87: the home branch on a cross-branch match. Quiet like .pill--quiet —
 * it is orientation, not a state worth the eye.
 */
.typeahead__branch {
  background: var(--n-50);
  border: 1px solid var(--line);
  border-radius: var(--radius-pill);
  color: var(--muted);
  font-size: 12px;
  margin-left: 8px;
  padding: 1px 8px;
  white-space: nowrap;
}

/* ------------------------------------------------------------ profile */

/*
 * Tabs, as a segmented control rather than an underlined row.
 *
 * One component, four screens: the Patients list, a patient's profile, the
 * Chase tabs and the Records tabs. It used to be text with a 2px underline on
 * the active one, which on a page that also has a sidebar, a card and a
 * heading was the fourth different way of saying "this one" — and the least
 * visible of them. A raised pill in a track is the same shape the nav uses at
 * tablet width, so the product has one idea of what "currently selected"
 * looks like.
 *
 * `align-self: flex-start` so the track is as wide as its tabs, not as wide as
 * the page: a two-tab control stretched across 1500px reads as a broken
 * toolbar.
 */
.tabs {
  align-items: center;
  align-self: flex-start;
  background: var(--n-100);
  border-radius: var(--radius-pill);
  display: inline-flex;
  gap: 2px;
  margin: 16px 0 16px;
  max-width: 100%;
  overflow-x: auto;
  padding: 3px;
}
/*
 * The selected tab is ink, not brand.
 *
 * It was `--b-700`, a dark green, which put the one word the reader is meant
 * to read in a different colour from every other word on the screen — and dark
 * enough that it looked less like a highlight than like a rendering fault
 * beside the black headings around it. The white pill and the weight already
 * say which tab is on; the colour was a third statement of the same thing, in
 * the wrong hue.
 */
.tabs__on {
  background: var(--white);
  border-radius: var(--radius-pill);
  box-shadow: 0 0 0 1px var(--n-200), var(--shadow-1);
  color: var(--ink);
  font-weight: 650;
  padding: 6px 14px;
  white-space: nowrap;
}
.tabs__off {
  border-radius: var(--radius-pill);
  color: var(--n-400);
  cursor: default;
  padding: 6px 14px;
  white-space: nowrap;
}
/* The count inside a tab rides on the tab, so it must not carry its own wash. */
.tabs .pill { background: none; padding-left: 2px; padding-right: 0; }
.tabs__on .pill { color: var(--n-500); }

/*
 * §104: the pane under a #tabs anchor must be at least a screen tall, or the
 * anchor cannot reach the top and the strip lands somewhere different on
 * every tab — a short pane (one plan card) stopped mid-header while a long
 * one scrolled clean. The subtraction leaves room for the strip itself; a
 * little overshoot is fine, a shortfall brings the inconsistency back.
 * dvh where it exists, because vh on a phone includes the browser chrome.
 */
.tabpane { min-height: calc(100vh - 72px); }
@supports (height: 100dvh) {
  .tabpane { min-height: calc(100dvh - 72px); }
}

/*
 * ...and none of that is a desk's problem.
 *
 * §104 is a phone fix answering a phone's complaint: a profile header a full
 * screen tall, so a tab click that landed at the top of the document meant
 * scrolling back down past it every single time. On a desk the header is
 * around 180px and entirely above the fold, so the anchor was solving nothing
 * and charging for it twice — the jump threw the patient's name off the top of
 * the window on every tab click, and the min-height hung a screen of empty
 * white under a two-row timeline.
 *
 * The margin is how a fragment jump is declined without touching the href: the
 * browser targets `top - scroll-margin-top`, a viewport's worth above a strip
 * that sits a few hundred pixels down the page, which clamps to the top of the
 * document. Clicking a tab from the top therefore scrolls nowhere at all.
 *
 * In CSS rather than JS on purpose — the anchor works with scripting off, and
 * so must its cancellation.
 */
@media (min-width: 720px) {
  #tabs { scroll-margin-top: 100vh; }
  .tabpane { min-height: 0; }
}

/*
 * The avatar disc on a patient row and at the head of a profile.
 *
 * Purely a landmark. A list of thirty rows that are all "code · name · phone"
 * has nothing for the eye to travel down, and initials give each row a shape
 * before the reader has parsed a single word. It carries no information the
 * row does not already state, which is why it is `aria-hidden` in the markup —
 * a screen reader hearing "AK" before "Anita Kumar" is being slowed down.
 */
.avatar {
  align-items: center;
  background: var(--b-50);
  border-radius: 50%;
  color: var(--b-700);
  display: flex;
  flex: 0 0 auto;
  font-size: 0.8125rem;
  font-weight: 700;
  height: 40px;
  justify-content: center;
  letter-spacing: 0.02em;
  width: 40px;
}
.avatar--lg { font-size: 1.125rem; height: 60px; width: 60px; }
/* A row whose left edge is a disc and whose text sits beside it. */
.hasavatar { align-items: center; display: flex; gap: 12px; min-width: 0; }
.hasavatar > div { min-width: 0; }

/*
 * The disc's tint carries the row's state on the patients list — §71.
 *
 * Red for lapsed, amber for money owed, and both are redundant with the tag and
 * the figure on the same row. That redundancy is the point: colour is the fast
 * path for somebody scanning, never the only statement of a fact.
 */
.avatar--lapsed { background: var(--bad-bg); color: var(--bad); }
.avatar--owing { background: var(--warn-bg); color: var(--warn); }

/*
 * ---------------------------------------------- the patients list, as columns
 *
 * §71. The list used to be two stacked lines of "code · name" and "phone ·
 * source · branch", which is not what anybody looks a patient up for. Last
 * visit, next appointment and balance are, and they need columns to be
 * comparable down the page — the whole value of "14 months ago" is seeing it
 * against the "3 weeks ago" above it.
 *
 * Grid rather than a table element: the same cells have to become a stacked
 * card on a phone, and a <table> cannot reflow like that without
 * `display: block` overrides that leave it a table in name only.
 *
 * The column list is written once in a custom property so the header and the
 * rows cannot disagree — they are separate elements, and two hand-kept copies
 * of a track list is the classic way a header drifts out of line with its own
 * body. The balance column is dropped for staff, who never see money (hard
 * rule 6), so the property is redefined rather than the columns hidden.
 */
/*
 * A page header that carries a search box — §71.
 *
 * Wraps on a phone into title / search / button, which is three rows and still
 * less than the labelled card it replaced. The search is `flex: 1` so it takes
 * whatever the title and the button leave, rather than being given a width that
 * is wrong at one breakpoint or another.
 */
.pagehead {
  align-items: center;
  display: flex;
  flex-wrap: wrap;
  gap: 12px 16px;
  margin-bottom: 16px;
}
.pagehead__title { flex: 0 0 auto; }
.pagehead__title h1 { margin: 0; }
.pagehead__count { color: var(--muted); font-size: 0.875rem; margin: 2px 0 0; }
.pagehead > .btn { flex: 0 0 auto; }

.pagesearch { flex: 1 1 260px; min-width: 0; position: relative; }

/*
 * On a phone the header is two rows, not three: title and the action share the
 * first, search takes the second. Left to wrap on its own the search claimed
 * the second row and pushed "Add patient" onto a third, which is a whole row of
 * screen spent on one button.
 */
@media (max-width: 719px) {
  .pagehead { gap: 10px 12px; }
  /*
   * `order` and not just widths. The search is full width here, so in document
   * order it wraps to the second line and pushes the button onto a third —
   * three rows, one of them holding a single button. Ordering the button ahead
   * of the search puts it beside the title where there is room for it, and the
   * search takes the whole of the line below.
   */
  .pagehead__title { order: 0; }
  .pagehead > .btn { margin-left: auto; order: 1; }
  .pagesearch { flex: 1 0 100%; order: 2; }
}
.pagesearch input {
  padding-left: 40px;
  width: 100%;
}
.pagesearch__icon {
  color: var(--muted);
  font-size: 1.125rem;
  left: 14px;
  line-height: 1;
  pointer-events: none;
  position: absolute;
  top: 50%;
  transform: translateY(-50%);
}
/*
 * The result panel floats over the page rather than pushing it down.
 *
 * In the old card it was in flow, which was fine there — the card grew and the
 * list moved down with it. In a header, growth would shove the whole page on
 * every keystroke, so it is absolute and gets a surface of its own.
 */
.pagesearch .typeahead {
  background: var(--white);
  border: 1px solid var(--line);
  border-radius: var(--radius);
  box-shadow: var(--shadow-pop);
  left: 0;
  margin: 6px 0 0;
  max-height: 320px;
  overflow-y: auto;
  padding: 4px;
  position: absolute;
  right: 0;
  top: 100%;
  z-index: 25;
}
.pagesearch .typeahead > li { border-top: 0; }
.pagesearch .typeahead button { border-radius: 8px; padding-left: 10px; }

/* Tabs, the filter disclosure and the owner's link share one line. */
.listbar { align-items: center; display: flex; flex-wrap: wrap; gap: 12px; margin-bottom: 16px; }
.listbar .tabs { margin: 0; }
.listbar__end { color: var(--muted); font-size: 0.8125rem; margin-left: auto; }

/*
 * A disclosure that rides on the tab line and takes the whole width once open.
 *
 * Closed it is a pill beside the tabs; open it becomes a full-width panel
 * underneath them, because five fields cannot sit in a flex row next to a
 * segmented control. `flex: 1 0 100%` on `[open]` is what moves it — the
 * element does not change place in the document, only how much of the line it
 * claims, so nothing about the disclosure's own behaviour is scripted.
 */
.foldup--inline {
  background: var(--white);
  border: 1px solid var(--line);
  border-radius: var(--radius-pill);
  flex: 0 0 auto;
  padding: 6px 14px;
}
.foldup--inline > summary { font-size: 0.8125rem; font-weight: 600; gap: 6px; min-height: 26px; }
.foldup--inline > summary::after { font-size: 0.75rem; margin-left: 4px; }
.foldup--inline[open] {
  border-radius: var(--radius);
  flex: 1 0 100%;
  padding: 14px 16px;
}
.foldup--inline[open] > summary { margin-bottom: 10px; }

/*
 * §146: the header strip is a column header at a desk and a sort control on a
 * phone, out of one piece of markup.
 *
 * It used to be `display: none` below 1024px, which is correct for headings
 * over columns that are not there. But the sort links live in it, and the
 * front desk works this screen on a phone — shipping "you can order the list
 * by who owes most" as a desk-only feature would be shipping half of it.
 *
 * So below desk width the three headings that sort survive and the three that
 * do not are dropped: Patient and Phone are not sortable, and the last cell is
 * the empty one that lines up with the buttons. `first-child`/`nth-child(2)`/
 * `last-child` rather than counting the middle, because the balance column is
 * absent for anyone who may not see money and the count changes under them.
 */
.ptable__head {
  align-items: center;
  color: var(--muted);
  display: flex;
  flex-wrap: wrap;
  font-size: 0.75rem;
  gap: 4px 14px;
  margin-bottom: 10px;
}
.ptable__head::before { content: 'Sort by'; }
.ptable__head > span:first-child,
.ptable__head > span:nth-child(2),
.ptable__head > span:last-child { display: none; }
/*
 * §147: on a phone these are controls, so they are the size of controls.
 *
 * They shipped as bare 12px text links with no padding — about an 18px tap
 * target, against docs/03's 44px floor and CLAUDE.md's reason for it, which is
 * a receptionist working one-handed between patients. A heading over a column
 * can be 12px of text; a thing you tap cannot.
 *
 * 36px rather than 44px is the same judgement `.act` makes at 36px and for the
 * same reason: these sit in a row of their own with 8px between them, the
 * consequence of a mis-tap is a list in the wrong order, and one more tap
 * fixes it. The targets that carry docs/03's full 44px are the ones that write
 * something.
 */
.ptable__head .ptable__sort {
  background: var(--white);
  border: 1px solid var(--line);
  border-radius: var(--radius-pill);
  display: inline-flex;
  align-items: center;
  gap: 5px;
  min-height: 36px;
  padding: 4px 13px;
}
.ptable__head .ptable__sort:hover { background: var(--n-25); text-decoration: none; }
.ptable__head .ptable__sort--on {
  background: var(--b-50);
  border-color: var(--b-200);
}

@media (min-width: 1024px) {
  /* At a desk they are headings again, over the columns they name — so the
     pill shape comes off and they go back to being words in a header row. */
  .ptable__head::before { content: none; }
  .ptable__head > span:first-child,
  .ptable__head > span:nth-child(2),
  .ptable__head > span:last-child { display: block; }
  .ptable__head .ptable__sort {
    background: none;
    border: 0;
    display: inline;
    min-height: 0;
    padding: 0;
  }
  .ptable__head .ptable__sort:hover { background: none; text-decoration: underline; }

  .ptable,
  .ptable__head {
    --ptable-cols: minmax(0, 2.2fr) 128px 116px 116px 96px 188px;
  }
  /* Staff: no balance column, and its width goes back to the name. */
  .ptable--nomoney,
  .ptable__head--nomoney {
    --ptable-cols: minmax(0, 2.4fr) 128px 116px 116px 188px;
  }

  .ptable__head {
    align-items: center;
    color: var(--n-400);
    display: grid;
    font-size: 0.6875rem;
    font-weight: 700;
    gap: 12px;
    grid-template-columns: var(--ptable-cols);
    letter-spacing: 0.06em;
    /*
     * The same -8px the list itself carries. `.card .plain` pulls the rows out
     * so their hover highlight reaches the card's edge, and `.plain > li` pads
     * the content back in; the header sits outside that <ul> and so inherited
     * neither. It came out 16px narrower, which the flexible first column
     * absorbed — so every heading sat a little left of the column it named,
     * which is the one thing a header must not do.
     */
    margin: 0 -8px;
    padding: 0 8px 8px;
    text-transform: uppercase;
  }

  .ptable > li {
    align-items: center;
    display: grid;
    gap: 12px;
    grid-template-columns: var(--ptable-cols);
  }
  /* The label each cell carries for the phone; the header says it here. */
  .ptable__cell::before { display: none; }
  .ptable__num { text-align: right; }
  .ptable .row__actions { justify-content: flex-end; }
}

.ptable__name { display: block; }
.ptable__sub { display: block; font-size: 0.8125rem; }

/*
 * §146: the three comparable columns sort.
 *
 * The heading keeps the header's own type — small, spaced, uppercase — and
 * only stops being grey when it is the one in force, so a glance at the strip
 * says which order the list is in without reading the arrow. The arrow is
 * aria-hidden and the link's accessible name carries the same fact in words.
 */
.ptable__sort {
  border-radius: 4px;
  color: inherit;
  text-decoration: none;
  white-space: nowrap;
}
.ptable__sort:hover { color: var(--ink); text-decoration: underline; }
.ptable__sort--on { color: var(--b-600); }
/* The idle ⇅ says "this sorts" and must not say it as loudly as the heading
   itself; the chosen ↑ or ↓ is a statement about the list and keeps its
   weight. */
.ptable__sort:not(.ptable__sort--on) span { opacity: 0.45; }
.ptable__sort:hover span { opacity: 0.8; }
.ptable__sort:focus-visible { box-shadow: var(--focus); outline: none; }

/*
 * §146: where somebody who cannot find a patient goes looking.
 */
.listfoot { color: var(--muted); font-size: 0.8125rem; margin: 16px 0 0; }

/*
 * §147: the filters that are on, as chips that take themselves off.
 *
 * One line under the tabs, outside the fold, so a narrowed list explains
 * itself without spending 380px of phone screen on the form that narrowed it.
 * Wraps rather than scrolls: a chip off the edge is a filter the reader does
 * not know is on, which is the whole problem this is here to fix.
 *
 * 34px, not the 44px of a primary control. These are secondary — removing a
 * filter is cheap and instantly visible, and a row of full-size buttons above
 * the list would out-shout the list. The × is inside the tap area, not a
 * second target beside it.
 */
.chips { display: flex; flex-wrap: wrap; gap: 8px; margin-bottom: 16px; align-items: center; }
.chipoff {
  align-items: center;
  background: var(--white);
  border: 1px solid var(--line);
  border-radius: var(--radius-pill);
  color: var(--n-700);
  display: inline-flex;
  font-size: 0.8125rem;
  font-weight: 600;
  gap: 7px;
  min-height: 34px;
  padding: 4px 12px;
  text-decoration: none;
}
.chipoff:hover { background: var(--n-25); border-color: var(--n-400); color: var(--ink); }
.chipoff:focus-visible { box-shadow: var(--focus); outline: none; }
.chipoff__x { color: var(--n-400); font-size: 1rem; line-height: 1; }
.chipoff:hover .chipoff__x { color: var(--bad); }
/* The two that carry a state wear its colour, the same one the row's tag and
   its figure already use. */
.chipoff--warn { background: var(--warn-bg); border-color: var(--warn-line); color: var(--warn); }
.chipoff--warn:hover { background: #fff0c4; border-color: #d9c084; color: var(--warn); }
.chipoff--warn .chipoff__x { color: var(--warn); }
.chipoff--bad { background: var(--bad-bg); border-color: var(--bad-line); color: var(--bad); }
.chipoff--bad:hover { background: #fbdedb; border-color: #e5a9a3; color: var(--bad); }
.chipoff--bad .chipoff__x { color: var(--bad); }

/*
 * Below desk width the columns become one line of facts under the name.
 *
 * They were labelled with the column names — "Phone: … Last visit: … Next: …
 * Balance: —" — which was right to do, because without the header "3 weeks
 * ago" and "Tue 18 Aug" on one line are two dates with no stated meaning. But
 * the column name is not the only way to say it, and it is the longest: the
 * labels ran to about forty characters a row, on the width with the least to
 * spare.
 *
 * §146 keeps the meaning and drops the bulk. A phone number and a relative
 * date say what they are; a second date and an amount do not, so they carry a
 * word rather than a heading, and the line reads as a sentence:
 *
 *     98765 43210 · 3 weeks ago · next Tue 26 Aug · owes ₹500
 *
 * The separator rides on `+`, which matches hidden siblings too — that is
 * exactly what makes it safe here, because the two cells that can vanish
 * (`--none`) are the last two, so no visible cell is ever left with a leading
 * dot and nothing in front of it.
 */
@media (max-width: 1023px) {
  .ptable > li { display: block; }
  .ptable .hasavatar { margin-bottom: 8px; }
  .ptable__cell {
    color: var(--n-700);
    display: inline;
    font-size: 0.8125rem;
    margin: 0;
  }
  .ptable__cell::before { color: var(--n-400); content: attr(data-label); }
  .ptable__cell + .ptable__cell::before { content: '· ' attr(data-label); }
  /* Nothing booked, and nothing owed. True of most patients, and saying it
     twenty times down a phone screen is twenty things to read past. The
     desktop column keeps its words: it has a heading that would otherwise
     stand over an empty strip. */
  .ptable__cell--none { display: none; }
  .ptable .row__actions { margin-top: 8px; }
}

/*
 * The head of a patient's profile: who they are, and what can be done with
 * them, in one card.
 *
 * These were four stacked blocks — heading, a line of facts, a row of buttons,
 * and a remove link — reading as four unrelated things about the same person.
 * The remove control stays outside the button row on purpose (§ profile.ejs):
 * it is the one destructive action here and it must not sit one mis-tap from
 * Payment.
 */
/* `flex-start`, or Edit stretches to the height of the identity block beside
   it and renders as a 62px-tall button. */
.profilehead {
  align-items: flex-start;
  display: flex;
  flex-wrap: wrap;
  gap: 16px;
  justify-content: space-between;
}
.profilehead__who { align-items: center; display: flex; gap: 16px; min-width: 0; }
.profilehead__who h1 { margin: 0 0 2px; }
.profilehead .sub { margin: 0; }
/* Full width on its own line: the identity and Edit share the first row, the
   actions get the second however wide the card is. */
.profilehead .chain { flex: 1 0 100%; margin-top: 4px; }

.detail { display: grid; gap: 2px 16px; grid-template-columns: 130px 1fr; margin: 0; }
.detail dt { color: var(--muted); font-size: 0.9375rem; }
.detail dd { margin: 0 0 8px; }

/*
 * ------------------------------------------- the profile's actions, as tiles
 *
 * One chat button and six tiles, replacing seven equal buttons.
 *
 * The chain gave a phone four rows of buttons — 213px at 375px wide, between
 * the patient's name and the allergy banner under it — and said nothing about
 * which of the seven anybody actually presses. The chat button takes the full
 * width because it is the verb this product is built around, and the rest
 * become icon-over-label tiles three across: 172px for the same six actions
 * plus the chat.
 *
 * The tile renders at 54px against the buttons' 47px, both above docs/03's
 * 44px floor. This section is not making the targets smaller; it is removing
 * the space between them. `min-height` is 52 and the content settles at 54 —
 * the floor is what is promised, not the exact pixel.
 */
.pactions { flex: 1 0 100%; margin-top: 12px; }
.pactions__chat { margin-bottom: 8px; width: 100%; }
.ptiles { display: grid; gap: 8px; grid-template-columns: repeat(3, minmax(0, 1fr)); }
/* The intake form wraps a button that has to sit in the grid, not beside it. */
.ptiles > form { display: contents; }
.ptile {
  align-items: center;
  background: var(--white);
  border: 1px solid var(--line);
  border-radius: 11px;
  box-shadow: var(--shadow-1);
  color: var(--n-700);
  cursor: pointer;
  display: flex;
  flex-direction: column;
  font: inherit;
  font-size: 0.8125rem;
  font-weight: 600;
  gap: 4px;
  justify-content: center;
  line-height: 1.2;
  min-height: 52px;
  padding: 6px 4px;
  text-align: center;
  text-decoration: none;
  width: 100%;
}
.ptile:hover { background: var(--n-25); border-color: var(--n-400); color: var(--ink); }
.ptile:active { box-shadow: none; transform: translateY(1px); }
.ptile:focus-visible { box-shadow: var(--focus); outline: none; }
.ptile[disabled] { box-shadow: none; cursor: not-allowed; opacity: 0.55; transform: none; }
/* The icon is a landmark, not a second label: quieter than the word under it. */
.tileicon { color: var(--n-500); display: block; flex: 0 0 auto; }
.ptile:hover .tileicon { color: var(--b-600); }

/*
 * A desk has the width for one row, so the tiles lie down and become pills and
 * the chat button stops being full-bleed. Same six controls, same order.
 */
@media (min-width: 720px) {
  .pactions { align-items: center; display: flex; flex-wrap: wrap; gap: 10px; }
  .pactions__chat { margin-bottom: 0; width: auto; }
  .ptiles { display: flex; flex-wrap: wrap; gap: 10px; }
  .ptile {
    flex-direction: row;
    gap: 7px;
    min-height: 40px;
    padding: 6px 14px;
    width: auto;
  }
}

/*
 * ------------------------------------------------ the Details tab, as groups
 *
 * Contact, About, Medical, Notes. The label above the value rather than beside
 * it, which is what lets short answers pair up: Gender, Age and Date of birth
 * take one line between them instead of three, and a 130px label column stops
 * pushing every value into the same narrow gutter.
 */
.dgroups { display: grid; gap: 16px; }
.dgroup__label {
  color: var(--muted);
  font-size: 0.6875rem;
  font-weight: 700;
  letter-spacing: 0.07em;
  margin: 0 0 6px 4px;
  text-transform: uppercase;
}
.dgroup__card { margin: 0; }
/*
 * The gutter is padding inside the field, not a flex gap: halves are exactly
 * 50% and thirds exactly a third, so a gap would push the second one onto its
 * own line. Padding keeps the arithmetic whole and still separates the values.
 */
.dfields { display: flex; flex-wrap: wrap; margin: 0; row-gap: 14px; }
.dfield { flex: 1 0 100%; min-width: 0; padding-right: 14px; }
.dfield--half { flex: 1 0 50%; }
.dfield--third { flex: 1 0 33.333%; }
.dfield dt {
  color: var(--muted);
  font-size: 0.6875rem;
  font-weight: 700;
  letter-spacing: 0.06em;
  margin-bottom: 2px;
  text-transform: uppercase;
}
.dfield dd { font-size: 0.9375rem; margin: 0; overflow-wrap: anywhere; }
/* The sentence that replaced six rows of "—". Quiet, because it is the
   absence of information and not information. */
.dgroup__missing { color: var(--muted); font-size: 0.8125rem; margin: 14px 0 0; }
.dgroup__missing:first-child { margin-top: 0; }
/*
 * Medical wears the banner's amber. The two say the same thing in the same
 * colour, at the top of the page and again in its place in the record.
 */
.dgroup--medical .dgroup__card {
  background: #fffdf8;
  border-color: var(--warn-line);
  border-left: 3px solid var(--warn-line);
}

@media (min-width: 900px) {
  /* Two across — Contact beside About, Medical beside Notes — so the tab stops
     being one long ribbon of white down the middle of a wide screen.
     `start`, or a short group stretches to the height of the tall one next to
     it and renders as a card mostly full of nothing. */
  .dgroups { align-items: start; grid-template-columns: 1fr 1fr; }
}

.chain { display: flex; flex-wrap: wrap; gap: 10px; margin-top: 12px; }

/*
 * Chains are trimmed on a phone — the "what next" row after a save, and the
 * five actions on a patient's profile.
 *
 * Three buttons at 16px with 18px of side padding do not share a 380px line,
 * so "Ask for a review / Back to Today / Another payment" stacked into three
 * full rows of chrome under a receipt. Narrower type and tighter sides fit two
 * per line and stop the row shouting.
 *
 * `min-height` is untouched, deliberately: 44px is docs/03's tap target and
 * the reason it exists — a receptionist tapping one-handed between patients —
 * is at its strongest on exactly this screen size. What shrinks is the bulk
 * around the target, never the target.
 */
@media (max-width: 719px) {
  /*
   * A grid, not a wrapped row.
   *
   * Wrapping left the chain ragged — WhatsApp and Book on one line, Payment
   * and Review on the next, Instructions alone on a third, each a different
   * width because each label is a different length. Two equal columns give the
   * card a straight edge on both sides, and an odd button simply takes the
   * left half of the last row instead of floating.
   */
  .chain { display: grid; gap: 8px; grid-template-columns: repeat(2, minmax(0, 1fr)); }
  .chain .btn { font-size: 0.9375rem; padding-left: 12px; padding-right: 12px; width: 100%; }

  /*
   * Edit is the one control in the profile head that is not part of the action
   * chain, and wrapping put it alone on the left under the patient's name,
   * where it read as abandoned rather than placed. Pushed to the right margin
   * it lines up with the card's edge and belongs to the identity above it.
   */
  .profilehead > .btn {
    font-size: 0.875rem;
    margin-left: auto;
    min-height: 36px;
    padding: 6px 14px;
  }
  .profilehead__who { gap: 12px; }
  .avatar--lg { font-size: 1rem; height: 48px; width: 48px; }
}

/*
 * A card that folds — the patients filters, and anything else that is five
 * fields of refinement sitting above the thing being refined.
 *
 * `list-style: none` plus the WebKit pseudo-element: a <summary> draws a
 * disclosure triangle by default and the two engines disagree about which
 * property removes it, so both are needed. The caret is drawn here instead,
 * which also lets it rotate.
 */
.foldup > summary {
  align-items: center;
  cursor: pointer;
  display: flex;
  font-weight: 650;
  gap: 8px;
  list-style: none;
  min-height: 28px;
}
.foldup > summary::-webkit-details-marker { display: none; }
.foldup > summary::after {
  color: var(--n-600);
  content: '▾';
  font-size: 1rem;
  line-height: 1;
  margin-left: auto;
}
.foldup[open] > summary::after { content: '▴'; }
.foldup[open] > summary { margin-bottom: 14px; }
.foldup > summary:focus-visible { border-radius: 6px; box-shadow: var(--focus); outline: none; }

/*
 * The add-something variant — §73.
 *
 * Laboratories, Medicines and Aftercare notes each had their add form standing
 * open in a second `.card--narrow` below the list: a narrow centred card under
 * a full-width one, permanently showing eight empty fields for something you
 * do once a month. The form is the rare errand on those screens; the list is
 * the reason you opened them.
 *
 * So the summary is a button at the top of the same card and the form unfolds
 * under it, in place. `<details>` rather than a script for the same reason the
 * user menu is one: it opens with no JavaScript and needs no CSP exception.
 *
 * A leading + that becomes ×, and no trailing caret — this reads as a control
 * that makes something new, not as a section that expands. Sized off .btn
 * rather than composed with it, because a <summary> carrying .btn inherits a
 * flex display that fights `list-style: none` in WebKit.
 */
/*
 * The heading line it sits on: title left, button hard right.
 *
 * `flex-wrap` and not a two-column grid, because the open form is what decides
 * the button's width and a grid track would have to be told that number twice.
 * On a phone the form is wider than what is left beside the heading, so the
 * whole disclosure wraps to its own line — which is the right answer there
 * anyway, and needs no media query to reach.
 */
.cardhead {
  align-items: flex-start;
  display: flex;
  flex-wrap: wrap;
  gap: 8px 16px;
  justify-content: space-between;
  margin-bottom: 8px;
}
.cardhead > h2 { margin: 0; }
/*
 * A column, so the button keeps its place at the right edge while the form
 * opens underneath it rather than shoving it back to the left — the summary
 * must not move when it is pressed.
 */
.cardhead > .foldup--add {
  align-items: flex-end;
  display: flex;
  flex-direction: column;
  max-width: 100%;
}

/*
 * On a desktop the form opens under the heading, on the left, where reading
 * starts — the owner's call. A panel hanging off the button left two thirds
 * of a wide card empty. The pill is lifted out of the flow and pinned to the
 * card head's corner, so it holds exactly one spot open, closed, and while
 * the form unfolds beneath it. Phones keep the stacked flow above: there the
 * panel already spans the card and an absolute pill could collide with a
 * long heading.
 */
@media (min-width: 720px) {
  .cardhead { position: relative; }
  .cardhead > .foldup--add {
    /*
     * stretch, not flex-start: the browser lays a details' content inside an
     * internal wrapper, and flex-start let that wrapper shrink to fit — a
     * short form (the aftercare note's two fields) came out 582px in a 943px
     * card while its width: 100% resolved against the shrunken wrapper.
     * The pill is absolute, so it does not care either way.
     */
    align-items: stretch;
    flex: 1 0 100%;
    order: 2;
  }
  .cardhead > .foldup--add > summary {
    position: absolute;
    right: 0;
    top: 0;
  }
}

.foldup--add { margin-bottom: 4px; }
/*
 * flex-end, so the pill sits at the card's right edge open and closed alike.
 * It was flex-start, which pinned it to the *form's* left edge instead — on a
 * desktop card that meant the button leapt left the moment it was pressed,
 * the exact movement the .cardhead comment above promises never happens.
 */
.foldup--add > summary {
  align-self: flex-end;
  background: linear-gradient(180deg, var(--b-500), var(--b-600));
  border: 1px solid var(--b-600);
  border-radius: var(--radius-pill);
  color: var(--white);
  display: inline-flex;
  font-size: 0.875rem;
  font-weight: 600;
  gap: 6px;
  min-height: 38px;
  padding: 8px 18px;
  width: auto;
}
.foldup--add > summary:hover {
  background: linear-gradient(180deg, var(--b-600), var(--b-700));
  border-color: var(--b-700);
}
.foldup--add > summary::before { content: '+'; font-size: 1.0625rem; line-height: 1; }
.foldup--add[open] > summary::before { content: '×'; }
/* No ▾: the + already says what this does, and two marks is one too many. */
.foldup--add > summary::after { content: none; }
.foldup--add[open] > summary { margin-bottom: 16px; }
/*
 * The form itself, once open: a bordered panel, the same dress
 * .foldup--inline[open] wears — the border is what says these fields belong
 * to that button. On the form, not on the details, so the pill itself gains
 * no padding and never shifts.
 */
.foldup--add__form {
  background: var(--white);
  border: 1px solid var(--line);
  border-radius: var(--radius);
  max-width: 620px;
  padding: 14px 16px;
  width: 100%;
}

/*
 * On a desktop the panel spans the card and the fields flow as one line,
 * the way the .filters bar and the list rows below it already do — a 620px
 * column left the rest of a wide card empty, whichever side it sat on.
 * Name leads and gets the widest share; the note form's paragraph box gets
 * the widest of all; the submit takes the line below, where every one of
 * these forms already ends. The .row pairs dissolve into the same line.
 */
@media (min-width: 720px) {
  .foldup--add__form {
    align-items: flex-start;
    display: flex;
    flex-wrap: wrap;
    gap: 0 16px;
    max-width: none;
  }
  .foldup--add__form .field { flex: 1 1 150px; }
  .foldup--add__form > .field:first-of-type { flex: 2 1 240px; }
  .foldup--add__form .field:has(textarea) { flex: 3 1 320px; }
  .foldup--add__form > .row { display: contents; }
  .foldup--add__form > .btn { flex: 1 1 100%; max-width: max-content; }
}
/*
 * The full-size pair, same argument as .act--wa above: the mark is the colour.
 *
 * These are the ones that carry a whole card — "Send on WhatsApp" under an
 * instruction preview — so they keep more presence than the row pills, but
 * still as a bordered button rather than a green slab.
 */
/*
 * The hairline is `.act--wa`'s, to the value.
 *
 * It was two shades darker here — #8fd4b4 resting and #5cc294 on hover — which
 * on a profile beside the same button on Today read as a different control
 * with a heavier edge, a green outline competing with the mark inside it. The
 * mark is the green on this button; the border only has to make it findable.
 */
.btn--wa { background: var(--white); border-color: #b7e3ce; color: var(--n-700); justify-content: center; }
.btn--wa:hover { background: #f2fbf6; border-color: #8fd4b4; color: var(--ink); }
.btn--sms { background: var(--white); border-color: var(--n-300); color: var(--n-800); justify-content: center; }
.btn--sms:hover { background: var(--n-25); border-color: var(--n-400); }

.pager { align-items: center; display: flex; gap: 16px; justify-content: space-between; padding-top: 10px; }

/* --------------------------------------------------------- appointments */

.appt { display: flex; gap: 12px; }
/*
 * Done rows dim by their pieces, never by the row.
 *
 * This was `.appt--done { opacity: 0.62 }`, and opacity on the row is a trap:
 * it turns the whole row into one translucent atomic layer, so a status sheet
 * opened from inside it rendered see-through with the next row bleeding
 * through — and the panel's z-index was imprisoned inside the row's layer,
 * letting the chips of later rows paint on top of the open panel. Dimming the
 * leaves (and only the sheet's own chip, never the sheet) keeps an open panel
 * a solid surface that sits above everything, which is the whole job of a
 * popover.
 */
.appt--done > .appt__time,
.appt--done > .appt__body,
.appt--done .appt__side > :not(.sheet),
.appt--done .appt__side > .sheet > .chip { opacity: 0.62; }

/* Fixed width so the times form a clean column the eye can scan down. */
.appt__time { flex: 0 0 74px; font-variant-numeric: tabular-nums; font-weight: 600; }
.appt__body { flex: 1 1 auto; min-width: 0; overflow-wrap: break-word; }

/*
 * A rule between the time and the patient, on rows that have a time.
 *
 * The adjacent-sibling selector is doing real work: it is what keeps the bar
 * off the rows in this same list that carry no time — a lab case, a birthday —
 * where a bar with nothing to its left would be a stray mark rather than a
 * divider. On a column of times it gives the eye something to run down.
 */
.appt__time + .appt__body {
  border-left: 3px solid var(--b-500);
  border-radius: 2px;
  padding-left: 12px;
}
/* Done rows already dim; the bar goes with them rather than staying bright. */
.appt--done > .appt__time + .appt__body { border-left-color: var(--n-300); }
.appt__side { align-items: flex-end; display: flex; flex-direction: column; gap: 6px; }

/*
 * On a phone the row stops being three columns and becomes two lines.
 *
 * Three columns do not fit in 380px. The time takes 74, the controls about
 * 150, and what was left for the patient was ~130px — so a name broke one word
 * per line ("Arjun" / "Pillai"), a treatment took four lines, and a surname
 * with no break point overflowed its box and ran underneath the WhatsApp
 * button. A row six lines tall that also collided with itself.
 *
 * The actions drop to their own full-width line, indented to sit under the
 * patient rather than under the clock, and the body gets the card's width
 * minus the time. `overflow-wrap: break-word` above is the belt to this
 * braces: Indian surnames are long, and one that cannot be hyphenated has to
 * wrap rather than escape its column.
 *
 * **Placed after the base rule deliberately.** A media query adds no
 * specificity, so when this block sat above `.appt__side`'s own declaration
 * the later one won and the direction stayed `column` — while `flex-wrap` and
 * `padding-left`, which the base rule does not set, applied. Half a layout,
 * which is harder to spot than none of it.
 */
@media (max-width: 719px) {
  .appt { flex-wrap: wrap; }
  .appt__time { flex: 0 0 68px; }
  .appt__body { flex: 1 1 160px; }
  .appt__side {
    align-items: center;
    flex: 1 0 100%;
    flex-direction: row;
    flex-wrap: wrap;
    justify-content: flex-start;
  }
  /*
   * No indent, and it was worth more as width than as alignment.
   *
   * Lining the controls up under the patient's name cost 80px, and the three
   * a completed visit carries — status, WhatsApp, Instructions — need 326 of
   * the row's 338. So the indent was the whole reason Instructions dropped to
   * a second line. Aligned-but-wrapped is worse than flush-and-single: one
   * line per appointment is what lets a morning be read without scrolling,
   * which is the entire argument for this screen.
   *
   * The pills are trimmed here too, from 13px of side padding to 10 and the
   * gap from 8 to 6. That is not for this 412px phone, where they already fit
   * — it is the ~30px of slack that keeps the same three on one line at 380.
   * Below that they wrap, which is the correct thing to do rather than
   * shrinking text past reading size.
   */
  .appt__side { gap: 4px; }
  /*
   * The pills drop to the chip's own 12px here, and the chip's sides come in
   * with them.
   *
   * The target is **360px**, not the 412 of the phone this was reported on.
   * A first pass fitted the three controls at 380 with two pixels to spare,
   * which still wrapped on the reporter's device — most Android phones report
   * a 360px CSS viewport whatever their hardware, and 360 leaves the row 288px
   * against the 304 the buttons wanted. Fitting the width actually in the
   * field beats fitting the one in the emulator.
   */
  .appt__side .act { font-size: 0.75rem; padding-left: 10px; padding-right: 10px; }
  .appt__side .chip { padding-left: 8px; padding-right: 8px; }
}

/*
 * Cards give back 8px of their own padding on a phone.
 *
 * 20px a side is generous at 360px — it is 11% of the screen spent on the gap
 * between a card's edge and its text, on the device where content is scarcest.
 * 16 is still comfortable and it is the difference between an appointment's
 * three controls sharing a line and not.
 */
@media (max-width: 719px) {
  .card { padding: 16px; }
}

/*
 * At desk width the row's controls go inline.
 *
 * Stacked, a completed appointment is about 120px tall — a status chip, a
 * WhatsApp button and an Instructions button each on their own line — so four
 * appointments fill the card and a normal morning scrolls. This is the screen
 * whose entire job is showing the day without scrolling, and the space to fix
 * it was already there: the text uses about half the card's width and the rest
 * was empty.
 *
 * **Status first, then the actions** — the row reads state, then what can be
 * done about that state, which is also the order the sheet reads in on a
 * phone. An earlier cut pinned the chip last so the chips formed a straight
 * column against the right edge; that scanned well and read backwards, and
 * reading order won. The cost is real and accepted: a completed visit carries
 * three controls and a scheduled one carries two, so the chips no longer line
 * up with each other.
 *
 * `min-width` on the chip claws back some of that — every status is the same
 * width, so the WhatsApp buttons align with each other even when the chips
 * beside them do not.
 *
 * `align-items: center` rather than flex-start: the body is two lines on some
 * rows and three on others, and a chip pinned to the top of a three-line row
 * reads as belonging to the first line rather than to the row.
 */
@media (min-width: 1024px) {
  .appt__side {
    align-items: center;
    flex-direction: row;
    gap: 8px;
    justify-content: flex-end;
  }
  .appt__side .chip { min-width: 96px; text-align: center; }
}

.chip {
  background: var(--bg);
  border: 1px solid var(--line);
  border-radius: var(--radius-pill);
  cursor: pointer;
  display: inline-block;
  font-size: 0.75rem;
  font-weight: 650;
  letter-spacing: 0.005em;
  list-style: none;
  padding: 6px 12px;
  white-space: nowrap;
}
.chip--fixed { cursor: default; }
.chip::-webkit-details-marker { display: none; }
.chip:focus-visible { box-shadow: var(--focus); outline: none; }

/*
 * The six appointment states, deliberately walking cool → warm → resolved:
 * scheduled and confirmed are neutral blues, arrived is amber because someone
 * is physically waiting, and the three terminal states are green, red and grey.
 * Retuned onto the palette; the semantics are unchanged.
 */
.chip--scheduled { background: var(--n-100); border-color: var(--n-300); color: var(--n-600); }
.chip--confirmed { background: var(--b-50); border-color: var(--b-200); color: var(--b-600); }
.chip--arrived   { background: var(--warn-bg); border-color: var(--warn-line); color: var(--warn); }
/*
 * §157: not a seventh stored state — the arrived chip's face while the chair
 * runs (forDisplay derives it from started_at, today only). The one live state
 * on the screen, so the one solid chip: every waiting-or-done state is a pale
 * wash, and the filled brand green reads as "happening right now" from across
 * the room.
 */
.chip--in_treatment { background: var(--b-600); border-color: var(--b-700); color: var(--n-0); }
/*
 * §159: the third derived face — chair freed, visit not yet settled. Solid
 * like the running chair (both are live states), but the warm one: this is
 * the person standing at the desk owing money.
 */
.chip--at_desk { background: var(--warn); border-color: var(--warn); color: var(--n-0); }
.chip--completed { background: var(--good-bg); border-color: var(--good-line); color: var(--good); }
.chip--no_show   { background: var(--bad-bg); border-color: var(--bad-line); color: var(--bad); }
.chip--cancelled { background: var(--n-100); border-color: var(--n-200); color: var(--n-400); }

/*
 * §158/§159: the visit flow's sections. One card ruled into one-row sections
 * — state dot, name, one-line summary, door — so the whole visit reads at a
 * glance and the doors sit in thumb's reach on a phone. The dot vocabulary:
 * amber has something open, green was done this visit, grey holds older
 * records, hollow was never needed.
 */
.vsec { padding: 12px 0; border-top: 1px solid var(--line); }
.vsec:first-of-type { border-top: 0; padding-top: 4px; }
/*
 * The head wraps, and that is the fix for the crushed rows.
 *
 * It was a no-wrap flex with the body free to shrink to nothing, which held
 * on rows with one door and failed on the rows with two: "Perio and endo"
 * beside its pair of buttons read one word per line, and on a phone with a
 * boosted font size the Chair free button was drawn over its own label. Now
 * the body claims a readable minimum, and doors that cannot fit beside it
 * drop to their own line instead of squeezing it.
 *
 * `justify-content: flex-end` is invisible while everything shares one line
 * — the body's flex-grow eats the free space first — and right-aligns the
 * doors once they wrap, which keeps them in thumb's reach where §158 put
 * them rather than dangling under the dot.
 */
.vsec__head { display: flex; align-items: center; flex-wrap: wrap; gap: 10px; justify-content: flex-end; }
.vsec__body { flex: 1 1 8rem; min-width: 0; }
/* Doors never shrink: a shrunk form lets the button inside overflow it and
   paint over the text beside it, which is the screenshot this fixes. */
.vsec__head > .btn, .vsec__head > form { flex: none; margin: 0; }
/*
 * The doors wear a smaller face than the base button — twelve of them stack
 * on one phone card, and at full size the card is mostly button. The tap
 * target does NOT shrink with them: min-height stays at --tap (docs/03's
 * 44px floor for one-handed phone use), and inline-flex keeps the smaller
 * label centred inside the full-height target instead of floating at the
 * top of it. Narrower is the part that matters — width is what forces a
 * door off its row.
 */
.vsec__head .btn {
  align-items: center;
  display: inline-flex;
  font-size: 0.875rem;
  justify-content: center;
  padding: 6px 13px;
}
/*
 * A row with two doors groups them, so they wrap as a pair. Ungrouped, a
 * narrow screen seats the first beside the title and drops the second under
 * it alone — a staircase where a row was meant.
 */
.vsec__do {
  align-items: center;
  display: flex;
  flex: none;
  flex-wrap: wrap;
  gap: 8px;
  justify-content: flex-end;
}
.vsec__do form { margin: 0; }
.vsec__head h3 { margin: 0; font-size: 1rem; }
.vsec__sum {
  margin: 2px 0 0; font-size: 0.85rem; color: var(--muted);
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
.vdot { flex: none; width: 9px; height: 9px; border-radius: 50%; border: 1.5px solid var(--n-300); }
/* §160: the profile's bridge to a visit running right now. */
.livevisit { display: flex; align-items: center; gap: 12px; flex-wrap: wrap; }
.livevisit .btn { margin-left: auto; }
/*
 * §174: the record card is read, not glanced. §159's one-line summary
 * ellipses away whatever did not fit, which is right for a glance at a
 * running chair and wrong for the half of a finished visit somebody came
 * here to read, so its lines wrap instead.
 */
.vsec__sum--wrap { white-space: normal; overflow: visible; text-overflow: clip; }

.vdot--warn { background: var(--warn); border-color: var(--warn); }
.vdot--good { background: var(--good); border-color: var(--good); }
.vdot--has  { background: var(--n-400); border-color: var(--n-400); }

/*
 * A details/summary bottom sheet. No JavaScript required — the status chips
 * have to work on a cheap Android browser with a flaky connection, which is
 * exactly where a JS-only control fails.
 */
.sheet { position: relative; }
.sheet__panel {
  background: var(--white);
  border: 1px solid var(--line);
  border-radius: var(--radius);
  box-shadow: var(--shadow-pop);
  min-width: 190px;
  padding: 12px;
  position: absolute;
  right: 0;
  top: calc(100% + 6px);
  z-index: 10;
}
.sheet__panel form { margin: 0 0 6px; }
.sheet__panel p { margin: 0 0 8px; font-size: 0.8125rem; }

/*
 * On a phone it opens under the row, not off the side of it — §115.
 *
 * The panel hangs from the chip's right edge, which is right on a desktop
 * where the chip ends the row, and wrong on a phone where the row wraps and
 * the chip moves to the left: at 375px nine of the twelve sheets on Today
 * opened partly off the left of the screen, the worst by 97px.
 *
 * Pinning it to the viewport fixed that and broke something better. A panel
 * fixed to the screen stays where it is while the page scrolls, so it drifts
 * away from the appointment it belongs to and ends up sitting beside a row
 * somebody has scrolled past. A menu headed "Change to" has to stay with the
 * thing it is changing.
 *
 * So the row becomes what it hangs from. Stretched between the row's own
 * edges it cannot reach either side of the screen, whatever the chip does
 * inside it, and being absolute it travels with the content like any other
 * dropdown. The chip keeps its own size, so the row's layout is untouched.
 */
@media (max-width: 719px) {
  .appt { position: relative; }
  /* Static, so the panel measures itself against the row, not the chip. */
  .appt .sheet { position: static; }
  .appt .sheet__panel {
    left: 0;
    min-width: 0;
    right: 0;
    top: calc(100% + 2px);
    /* Over the row beneath it, under the dock at 20. */
    z-index: 15;
  }
  .appt .sheet__panel .btn { padding: 12px; }
}
.btn--quiet { background: var(--bg); border-color: var(--line); color: var(--ink); font-weight: 500; }
.btn--quiet:hover { background: #e9edf3; }

.chosen {
  background: var(--bg);
  border: 1px solid var(--line);
  border-radius: 8px;
  display: flex;
  gap: 10px;
  justify-content: space-between;
  padding: 11px 12px;
}

.filters { display: flex; flex-wrap: wrap; gap: 12px; align-items: flex-end; }
.filters .field { flex: 1 1 130px; margin: 0; }
.filters .btn { flex: 0 0 auto; }
/*
 * Search takes the whole first line and the page size takes as little as it
 * can. Left to the 130px basis above, the search box came out the same width
 * as a date field — the thing most likely to be typed into, sized like the
 * thing least likely to be.
 */
.filters .field--wide { flex: 1 1 100%; }
.filters .field--narrow { flex: 0 0 auto; }
.filters .field--narrow select { width: auto; }

.check { align-items: flex-start; display: flex; gap: 10px; }
.check input { margin-top: 3px; width: auto; min-height: 0; }

/*
 * The medical conditions picker — migration 055.
 *
 * Two columns, because nine conditions stacked in one make a long form longer
 * and these labels are two words at most. Below 380px it folds to one: at that
 * width two columns start wrapping "Bleeding disorder" onto a second line and
 * a ragged grid is harder to scan than a tall one.
 */
.checkgrid { display: grid; gap: 8px 14px; grid-template-columns: repeat(2, 1fr); margin-top: 6px; }
@media (max-width: 380px) { .checkgrid { grid-template-columns: 1fr; } }

/* ------------------------------------------------------------- payments */

/* Tabular numerals so a column of amounts lines up on the decimal. */
.amt { font-variant-numeric: tabular-nums; font-weight: 600; text-align: right; white-space: nowrap; }
.amt--due { color: var(--bad); }
.amt--neg { color: var(--muted); }
.amt__cap { font-size: 0.6875rem; font-weight: 400; }

.total { font-size: 1.125rem; margin: 0 0 12px; }

.bal { text-align: right; }
.bal .row__actions { justify-content: flex-end; margin-top: 4px; }

/* The rupee sign sits inside the field so the amount reads as money. */
.amount { align-items: center; display: flex; gap: 8px; }
.amount__sym { color: var(--muted); font-size: 1.125rem; }
.amount input { flex: 1; }

.planopt {
  align-items: flex-start;
  border: 1px solid var(--line);
  border-radius: 8px;
  display: flex;
  gap: 10px;
  margin-bottom: 8px;
  padding: 12px;
}
.planopt input { margin-top: 3px; width: auto; min-height: 0; }
.planopt:has(input:checked) { border-color: var(--navy); box-shadow: inset 0 0 0 1px var(--navy); }

.subfield { border-left: 2px solid var(--line); margin: 0 0 8px 14px; padding-left: 14px; }

.pay-note { margin-top: 8px; text-align: center; }

.strip { display: flex; gap: 28px; }
.strip__k { color: var(--muted); font-size: 0.8125rem; }
.strip__v { font-size: 1.375rem; font-weight: 700; font-variant-numeric: tabular-nums; }
.strip__v--quiet { color: var(--muted); font-size: 1.125rem; font-weight: 600; }

/*
 * ------------------------------------------------ the patient's own timeline
 *
 * A rail, replacing a flat list in which every row restated its own date. Five
 * rows reading "Yesterday" is the date said five times and the day boundary
 * said never; here it is said once, at the head of the day, and the spine
 * carries the eye from one entry to the next.
 *
 * The node is a tinted disc, not the old 55%-opacity dingbat. Kind is the
 * question this list is scanned for — "when did we last take money", "was
 * anything prescribed" — and a tint answers it before a word is read. Colour
 * is never the only statement: the title beside it always says the same thing.
 *
 * The rail is drawn as a border on the items list rather than a pseudo-element
 * so it starts and stops with the entries and cannot outrun the last one.
 */
.tline { display: grid; gap: 20px; }
.tlday__date {
  color: var(--muted);
  font-size: 0.6875rem;
  font-weight: 700;
  letter-spacing: 0.07em;
  margin: 0 0 8px 4px;
  text-transform: uppercase;
}
.tlday__items {
  border-left: 2px solid var(--line);
  display: grid;
  gap: 10px;
  list-style: none;
  margin: 0 0 0 15px;
  padding: 2px 0 2px 20px;
}
.tlitem { position: relative; }
.tlitem__card { margin: 0; padding: 12px 14px; }
.tlitem__what { min-width: 0; }
.tlitem__detail { font-size: 0.8125rem; margin-top: 2px; }
/*
 * The disc straddles the rail, centred on it: 20px of the list's padding, plus
 * 1px for half the 2px rail, plus 14px for half the 28px disc — 35px left of
 * the card's edge. Any other number and the rail runs visibly off-centre
 * through every node down the page.
 */
.tlnode {
  align-items: center;
  background: var(--n-100);
  border: 1px solid var(--n-200);
  border-radius: 50%;
  color: var(--n-600);
  display: flex;
  font-size: 0.8125rem;
  font-weight: 700;
  height: 28px;
  justify-content: center;
  left: -35px;
  line-height: 1;
  position: absolute;
  top: 10px;
  width: 28px;
}
.tlnode--appointment { background: var(--b-50); border-color: var(--b-200); color: var(--b-700); }
.tlnode--payment { background: var(--good-bg); border-color: var(--good-line); color: var(--good); }
.tlnode--instruction { background: #eef4fd; border-color: #cddff7; color: #2b5fa8; }
.tlnode--prescription { background: #f4eefc; border-color: #ddd0f2; color: #6b3fa0; }
/* §180: consent, warm and paper-like — no other kind wears this tint. */
.tlnode--consent { background: #fdf3e7; border-color: #f2ddc0; color: #8a5a1f; }
/* A visit that did not happen is not the brand's green. */
.tlnode--off { background: var(--bad-bg); border-color: var(--bad-line); color: var(--bad); }

/*
 * The codes under a collapsed row — "4 prescriptions written" and then RX013
 * to RX016, each still the link to its own record. The count is a summary and
 * never a dead end: nothing is reachable before the collapse that is not
 * reachable after it.
 */
.tlcodes { display: flex; flex-wrap: wrap; gap: 6px; margin-top: 10px; }
.tlcode {
  background: var(--n-25);
  border: 1px solid var(--line);
  border-radius: var(--radius-pill);
  color: var(--n-600);
  font-size: 0.75rem;
  font-variant-numeric: tabular-nums;
  font-weight: 600;
  padding: 4px 10px;
  text-decoration: none;
}
a.tlcode:hover { background: var(--white); border-color: var(--n-400); color: var(--ink); }

/* The filter strip above the rail, and the line that speaks when it empties. */
.tlfilters { display: flex; flex-wrap: wrap; gap: 8px; margin-bottom: 16px; }
.tline__none { margin: 0; }
/* Written for the timeline's <button> filters and reused by §148 for the
   chase tabs' status links, so it has to sit right on both: inline-flex and
   no underline do nothing to a button and everything to an anchor. */
.chipbtn {
  align-items: center;
  background: var(--white);
  border: 1px solid var(--line);
  border-radius: var(--radius-pill);
  color: var(--n-700);
  cursor: pointer;
  display: inline-flex;
  font: inherit;
  font-size: 0.8125rem;
  font-weight: 600;
  min-height: 34px;
  padding: 5px 14px;
  text-decoration: none;
}
.chipbtn:hover { background: var(--n-25); border-color: var(--n-400); }
.chipbtn:focus-visible { box-shadow: var(--focus); outline: none; }
.chipbtn--on { background: var(--b-600); border-color: var(--b-600); color: var(--white); }
.chipbtn--on:hover { background: var(--b-700); border-color: var(--b-700); color: var(--white); }

/*
 * ------------------------------------------------------- §148: the chase row
 *
 * Both chase tabs are queues, and both used to render a flat list in which
 * overdue was one red date inside a grey sentence. These are the parts that
 * say otherwise.
 *
 * The status strip first: three or four links where a card held one labelled
 * select. That card cost about 90px between the tabs and the work, to offer a
 * choice that is one tap here — and the select needed script to submit, which
 * the links do not.
 */
.statusbar { align-items: center; display: flex; flex-wrap: wrap; gap: 8px; margin-bottom: 16px; }
/* §149: the lab tab has four filter fields as well as its statuses, so the
   fold rides on the same line and claims the width only once opened — the
   arrangement §71 built for the Patients tabs. */
.statusbar .foldup--inline { margin: 0; }

/* Money on a quotes row, ahead of the buttons rather than in a column of its
   own: it is what the row is worth, and the tab is sorted by age not amount. */
.chaserow__amt { font-size: 0.9375rem; }

/* What is waiting, rather than how big the archive is. */
.duesum { font-size: 0.875rem; margin: 0 0 16px; }
.duesum__late { color: var(--bad); font-weight: 700; }

/*
 * A group of rows falling due in the same stretch of time. The heading is the
 * same small-caps label the Details groups and the timeline days use, so the
 * three screens that group things group them the same way.
 */
.duegroup { margin-bottom: 20px; }
.duegroup__label {
  color: var(--muted);
  font-size: 0.6875rem;
  font-weight: 700;
  letter-spacing: 0.07em;
  margin: 0 0 6px 4px;
  text-transform: uppercase;
}
.duegroup__label--bad { color: var(--bad); }
.duegroup__card { margin: 0; }
/* Overdue wears the tone, on the card's edge — the same device the Medical
   group uses on a profile. Never the only statement: the heading says it, and
   every row inside carries how late it is in words. */
.duegroup__card--bad { border-color: var(--bad-line); border-left: 3px solid var(--bad-line); }

/*
 * The row: who, why, when, and what to do about it.
 *
 * Two columns on a phone — the disc, and everything else — with the buttons
 * on their own line underneath. The "how late" chip rides on the name line
 * rather than taking a column of its own: as a third column it cost the
 * reason 94px of a 375px screen, which wrapped the one sentence the row
 * exists to carry and pushed the buttons onto a second line, measured at
 * 228px a row.
 */
.chaserow {
  align-items: start;
  display: grid;
  gap: 4px 12px;
  grid-template-columns: auto minmax(0, 1fr);
}
.chaserow__what { grid-column: 2; min-width: 0; }
.chaserow__who { align-items: center; display: flex; flex-wrap: wrap; gap: 8px; }
.chaserow__code { font-size: 0.8125rem; }
/* Why this person is being rung, in the same ink as their name. It used to sit
   mid-sentence in grey between the phone number and the date. */
.chaserow__line { font-size: 0.9375rem; margin-top: 1px; }
.chaserow__meta { font-size: 0.8125rem; margin-top: 2px; }
.chaserow__do {
  align-items: center;
  display: flex;
  flex-wrap: wrap;
  gap: 8px;
  grid-column: 2;
  margin-top: 8px;
}
.chaserow__do form { margin: 0; }

@media (min-width: 900px) {
  /* At a desk they come up beside the row and centre on it, which keeps the
     right edge straight down a list of twenty. */
  .chaserow { grid-template-columns: auto minmax(0, 1fr) auto; }
  .chaserow__do {
    align-self: center;
    grid-column: 3;
    grid-row: 1;
    justify-content: flex-end;
    margin-top: 0;
  }
}
.tabs__link {
  border-radius: var(--radius-pill);
  color: var(--n-600);
  padding: 6px 14px;
  text-decoration: none;
  white-space: nowrap;
}
.tabs__link:hover { background: rgb(255 255 255 / 60%); color: var(--ink); }

/* ------------------------------------------------------ actions & misc */

.grid { display: grid; gap: 12px; grid-template-columns: 1fr; }
@media (min-width: 520px) { .grid { grid-template-columns: 1fr 1fr; } }

.tile {
  background: var(--white);
  border: 1px solid var(--line);
  border-radius: var(--radius);
  box-shadow: var(--shadow-1);
  color: inherit;
  display: block;
  padding: 18px;
  text-decoration: none;
  transition: border-color 0.12s, box-shadow 0.12s, transform 0.1s;
}
.tile:hover { border-color: var(--b-200); box-shadow: var(--shadow-2); transform: translateY(-1px); }
.tile:focus-visible { box-shadow: var(--focus); outline: none; }
/* The icon was 0.6 opacity grey; brand-tinted, it reads as part of the tile. */
.tile__icon { color: var(--b-500); display: block; font-size: 1.5rem; margin-bottom: 6px; }
.tile strong { display: block; letter-spacing: -0.008em; margin-bottom: 2px; }

.preview {
  background: var(--bg);
  border: 1px solid var(--line);
  border-radius: 8px;
  margin-top: 10px;
  padding: 12px 14px;
}
.preview__label { color: var(--muted); font-size: 0.75rem; margin-bottom: 4px; text-transform: uppercase; }
.preview__body { margin: 0; white-space: pre-wrap; }

.overdue { color: var(--bad); font-weight: 600; }

/*
 * A whole list row that is the problem, rather than one word inside it.
 *
 * The lab list has marked its late cases `.row--bad` since §42 and the class
 * had no rule anywhere, so the one row on the screen that needs a phone call
 * looked exactly like the three that do not — the late-ness was four small red
 * words at the far right. This screen exists to say who to ring.
 *
 * A left edge and a tint, the same shape as `.alert--bad`, rather than red
 * text throughout: the patient's name and the lab's are still ordinary
 * information and turning them red says the wrong thing about them. Negative
 * margins so the tint reaches past the list's own padding and reads as a
 * band across the row.
 */
.plain > li.row--bad {
  background: var(--bad-bg);
  border-left: 3px solid var(--bad);
  margin-left: -12px;
  margin-right: -12px;
  padding-left: 9px;
  padding-right: 12px;
}

.inline-form { margin-top: 10px; }
.inline-form .field { margin-bottom: 8px; }

.reset { margin-top: 10px; }
.reset summary { cursor: pointer; }

/* The service-account address, which nobody should ever hand-type. */
.sacopy {
  align-items: center;
  display: flex;
  flex-wrap: wrap;
  gap: 10px;
  margin: 8px 0;
}
.sacopy code {
  background: var(--white);
  border: 1px solid var(--line);
  border-radius: 6px;
  flex: 1 1 220px;
  font-size: 0.8125rem;
  overflow-wrap: anywhere;
  padding: 8px 10px;
}

.steps { margin: 0 0 16px; padding-left: 20px; }
.steps li { margin-bottom: 10px; }

/* The three export layers, in docs/05's order of prominence. */
.layer {
  border-bottom: 1px solid var(--line);
  font-size: 1rem;
  margin: 26px 0 12px;
  padding-bottom: 6px;
}
.layer:first-of-type { margin-top: 8px; }

/* ------------------------------------------------------------- dashboard */

.rangebar { display: flex; flex-wrap: wrap; gap: 8px; margin-bottom: 14px; }
.rangebar__opt {
  background: var(--bg);
  border: 1px solid var(--line);
  border-radius: 999px;
  color: var(--ink);
  font-size: 0.875rem;
  min-height: 38px;
  line-height: 22px;
  padding: 7px 14px;
  text-decoration: none;
}
.rangebar__opt:hover { background: var(--n-100); }
.rangebar__opt.is-on { background: var(--b-600); border-color: var(--b-600); color: var(--white); }

/* The headline number first: an owner scans these, they do not read them. */
.tile__n {
  display: block;
  font-size: 1.5rem;
  font-variant-numeric: tabular-nums;
  font-weight: 700;
  line-height: 1.2;
  margin-bottom: 4px;
}
.tile--warn .tile__n { color: var(--bad); }

/*
 * §89: which way the number moved against the window immediately before.
 * Coloured by meaning, not by sign — the class is chosen by the service's
 * `good` flag, so collections up and no-shows down both read green.
 */
.tile__delta {
  color: var(--muted);
  font-size: 0.75rem;
  font-weight: 600;
  margin-left: 6px;
  vertical-align: 3px;
  white-space: nowrap;
}
.tile__delta--good { color: var(--good); }
.tile__delta--bad { color: var(--bad); }

/*
 * §93: the Overview leads with the answer. Collected gets the size and a
 * sparkline; "Right now" sits beside it in the brand tint, holding the two
 * facts that are about today rather than the range.
 */
.grid--hero { margin-bottom: 4px; }
.tile--hero .tile__n { font-size: 2rem; }
.tile--now {
  background: var(--b-50);
  border-color: var(--b-100);
}
.tile--now strong { color: var(--b-700); }
.tile--now__line { color: var(--b-700); font-size: 0.9375rem; margin: 6px 0 0; }
.tile--now__links { font-size: 0.875rem; margin: 8px 0 0; }

/* A quiet label over each tile group: two words, not a heading. */
.gridlabel {
  color: var(--muted);
  font-size: 0.6875rem;
  font-weight: 700;
  letter-spacing: 0.08em;
  margin: 18px 0 8px;
  text-transform: uppercase;
}

/*
 * §93: the shape of the range, drawn server-side as a bare polyline — no
 * axes, no library, nothing for a 360px phone to struggle with.
 * `non-scaling-stroke` keeps the line 1.5px however the box stretches.
 */
.spark { display: block; height: 24px; margin-top: 10px; width: 100%; }
.spark--hero { height: 40px; }
.spark polyline {
  fill: none;
  stroke: var(--b-500);
  stroke-linejoin: round;
  stroke-width: 1.5;
  vector-effect: non-scaling-stroke;
}
.tile--warn .spark polyline { stroke: var(--bad); opacity: 0.5; }

/* Bars rather than a chart library — one dependency and a canvas for six rows
   is not a trade worth making, and these read fine at 380px. */
.bars > li { border-bottom: 0; padding: 8px 0; }
.bars__row { display: flex; justify-content: space-between; margin-bottom: 4px; }
.bars__label { font-size: 0.9375rem; }
.bars__n { font-variant-numeric: tabular-nums; font-weight: 600; }
.bars__track { background: var(--n-100); border-radius: var(--radius-pill); height: 8px; overflow: hidden; }
.bars__fill { background: linear-gradient(90deg, var(--b-500), var(--b-700)); height: 100%; }

/*
 * Report tables — the enquiry analytics.
 *
 * A table, not the card-and-row pattern the rest of the app uses, because
 * these are read as columns: an owner scans down "Rate" looking for the low
 * number. Rows would put every figure on its own line and destroy that.
 *
 * Wrapped in .scrollx so a four-column table scrolls inside its card on a
 * phone rather than widening the page — a horizontal scrollbar on the whole
 * document is how a layout breaks on a 360px screen.
 */
.scrollx { -webkit-overflow-scrolling: touch; overflow-x: auto; }

.rep { border-collapse: collapse; font-size: 0.9375rem; min-width: 100%; }
.rep th, .rep td { padding: 9px 10px; text-align: left; white-space: nowrap; }
.rep thead th {
  border-bottom: 1px solid var(--line);
  color: var(--muted);
  font-size: 0.8125rem;
  font-weight: 600;
}
.rep tbody tr { border-bottom: 1px solid var(--line); }
.rep tbody tr:last-child { border-bottom: 0; }
/* First column carries the name and may be long; the rest are figures. */
.rep td:first-child, .rep th:first-child { padding-left: 0; white-space: normal; }
.rep .num { font-variant-numeric: tabular-nums; text-align: right; }
/* The number the eye is hunting for. */
.rep__low { color: var(--bad); font-weight: 600; }

.advanced summary { cursor: pointer; }
.advanced[open] summary { margin-bottom: 14px; }
.advanced .sacopy code { font-size: 0.75rem; }

/*
 * The headline inside a fold's summary line, so the folded Reports page
 * still reads as a report rather than a table of contents. Quiet on
 * purpose: the title is the question, this is the short answer.
 */
.sumhint {
  color: var(--muted);
  font-size: 0.8125rem;
  font-weight: 400;
  margin-left: 8px;
  white-space: nowrap;
}
/* Long clinic list labels can make the hint overflow a phone; let it wrap
   onto its own line under the title instead. */
@media (max-width: 480px) {
  .sumhint { display: block; margin: 2px 0 0 0; white-space: normal; }
}

/*
 * A hint that is a warning — §114.
 *
 * Only the modifier is styled. Plain `.hint` has never had a rule of its own
 * and is used on a dozen screens, so giving it one here would restyle all of
 * them to make one paragraph louder.
 */
.hint--warn {
  background: var(--warn-bg);
  border-left: 3px solid var(--warn-line);
  border-radius: 6px;
  color: var(--warn);
  padding: 8px 10px;
}

/*
 * "Worth your attention" — §109. Above the Overview tiles, so it carries
 * the brand tint the insight blocks use rather than competing with the
 * hero: it is the app talking, and the tiles below are the numbers.
 */
.acts {
  background: var(--b-50);
  border: 1px solid var(--b-100);
  border-radius: 12px;
  margin-bottom: 14px;
  padding: 12px 14px;
}
.acts__k {
  color: var(--b-700);
  font-size: 0.75rem;
  font-weight: 700;
  letter-spacing: 0.04em;
  margin: 0 0 8px;
  text-transform: uppercase;
}
.acts__list { margin: 0; }
.acts__row { border-top: 1px solid var(--b-100); padding: 8px 0; }
.acts__row:first-child { border-top: 0; padding-top: 0; }
.acts__row:last-child { padding-bottom: 0; }
/* Title and money on one line at any width, the amount pushed to the end. */
.acts__head {
  align-items: baseline;
  display: flex;
  gap: 10px;
  justify-content: space-between;
}
.acts__n {
  color: var(--b-700);
  font-variant-numeric: tabular-nums;
  font-weight: 700;
  white-space: nowrap;
}
.acts__line { font-size: 0.9125rem; margin: 2px 0 0; }

/*
 * The §89 arrows on report cards: which way the card's headline moved
 * against the window immediately before. Colour is meaning, not sign —
 * same rule as .tile__delta.
 */
.cmp { color: var(--muted); font-size: 0.8125rem; margin: 0 0 10px; }
.cmp__arrow { font-weight: 600; }
.cmp__arrow--good { color: var(--good); }
.cmp__arrow--bad { color: var(--bad); }

/*
 * The 12-month table's shape column: each month's collections against the
 * best month, so a run of good or bad months is visible at a glance.
 */
.trend__barhead { min-width: 72px; }
.trend__bar { min-width: 72px; padding-left: 12px; }
.trend__fill {
  background: var(--b-500);
  border-radius: 2px;
  height: 8px;
  min-width: 1px;
  opacity: 0.75;
}

/*
 * "What this means" — §101. The sentences a card derives from its own
 * numbers. Tinted with the brand ramp so it reads as the app speaking,
 * distinct from the grey .muted caveats around it.
 */
.insight {
  background: var(--b-50);
  border-left: 3px solid var(--b-500);
  border-radius: 6px;
  margin-top: 14px;
  padding: 10px 12px;
}
.insight__k {
  color: var(--b-700);
  font-size: 0.75rem;
  font-weight: 700;
  letter-spacing: 0.04em;
  margin: 0 0 6px;
  text-transform: uppercase;
}
.insight__line { font-size: 0.9125rem; margin: 0 0 8px; }
.insight__line:last-child { margin-bottom: 0; }

/*
 * §184: the same block standing on the patient page rather than at a card's
 * foot — the card-foot top margin becomes the page rhythm's bottom one.
 */
.insight--page { margin: 0 0 16px; }

/* The checkout reference gets read aloud and retyped — make it unmissable. */
.shopref { margin: 8px 0 16px; }
.shopref code {
  background: var(--bg);
  border: 1px dashed var(--line);
  border-radius: 8px;
  display: inline-block;
  font-size: 1.375rem;
  font-weight: 700;
  letter-spacing: 0.08em;
  padding: 10px 16px;
}

/* ---------------------------------------------------------------- nav */

/*
 * Fixed to the bottom on a phone — the front desk uses this one-handed, and
 * the bottom of the screen is the only part a thumb reaches comfortably.
 */
.nav {
  background: var(--white);
  border-top: 1px solid var(--line);
  bottom: 0;
  display: flex;
  left: 0;
  padding-bottom: env(safe-area-inset-bottom);
  position: fixed;
  right: 0;
  z-index: 20;
}
.nav__item {
  align-items: center;
  color: var(--muted);
  display: flex;
  flex: 1;
  flex-direction: column;
  font-size: 0.6875rem;
  gap: 2px;
  justify-content: center;
  min-height: 56px;
  text-decoration: none;
}
.nav__icon { font-size: 1.05rem; line-height: 1; }

/*
 * How many people are waiting to be contacted — §58. On the dock it perches
 * beside the icon; the desktop block below lets it flow inline after the
 * label instead.
 */
.nav__item { position: relative; }
.nav__badge {
  background: var(--b-600);
  border-radius: var(--radius-pill);
  color: var(--white);
  font-size: 0.6875rem;
  font-weight: 650;
  left: calc(50% + 6px);
  line-height: 1;
  padding: 3px 6px;
  position: absolute;
  top: 3px;
}
.nav__item.is-on { color: var(--b-700); font-weight: 650; }
/* A bar along the top edge of the active item — on a bottom-docked nav that is
   the edge nearest the content, so it points at the screen it belongs to. */
.nav__item.is-on { box-shadow: inset 0 2px 0 var(--b-700); }
.nav__item.is-off { opacity: 0.38; }
/* The desktop's grouping element. On the dock it dissolves entirely, so the
   items stay direct flex children of the bar — same trick as .deck. */
.nav__track { display: contents; }

/* Room for the fixed bar, so the last row is never trapped underneath it. */
.foot { padding-bottom: 88px; }

@media (min-width: 720px) {
  /*
   * Desktop: one segmented control, centred on a white bar — the five
   * sections as segments of a single grouped switch, the active one raised
   * white out of the grey track. The glyphs ride inline, same shapes as the
   * dock, so the sections keep one face across both layouts.
   */
  .nav {
    border-top: 0;
    border-bottom: 1px solid var(--line);
    bottom: auto;
    justify-content: center;
    padding: 8px 16px;
    position: static;
  }
  .nav__track {
    background: var(--n-100);
    border-radius: var(--radius-pill);
    display: flex;
    gap: 2px;
    padding: 3px;
  }
  .nav__item {
    border-radius: var(--radius-pill);
    flex: 0 0 auto;
    flex-direction: row;
    font-size: 0.9375rem;
    gap: 8px;
    min-height: 34px;
    padding: 4px 16px;
  }
  /* A touch smaller inline than over-label, so the glyph seasons the word
     rather than competing with it. */
  .nav__icon { font-size: 0.9375rem; opacity: 0.8; }
  .nav__badge { left: auto; position: static; top: auto; }
  .nav__item:hover { color: var(--n-700); }
  .nav__item:focus-visible { box-shadow: var(--focus); outline: none; }
  /* The raised white segment IS the marker — ring plus a whisper of lift. */
  .nav__item.is-on {
    background: var(--white);
    box-shadow: 0 0 0 1px var(--n-200), var(--shadow-1);
    color: var(--b-700);
  }
  .nav__item.is-on .nav__icon { opacity: 1; }
  .foot { padding-bottom: 32px; }
}

/*
 * ------------------------------------------------------- the desktop sidebar
 *
 * At 1024px the nav stops being a bar and becomes a rail down the left edge.
 *
 * Why here and not at 720px: a tablet in a clinic is held like a phone, and
 * the segmented control above is the right shape for it. The rail is for a
 * desk, where there is horizontal room to spare and vertical room to list
 * more than five destinations.
 *
 * That extra room is the whole point. The dock can hold five items because a
 * phone divides its width evenly between them; the rail can hold eleven, so
 * the things that were buried in the ☰ menu — the clinic's own lists, its
 * settings, its billing — get a permanent door instead of a hidden one.
 * Items that only appear here carry `--desk`, so the dock keeps its five.
 */
@media (min-width: 1024px) {
  .nav {
    background: var(--side-bg);
    border: 0;
    bottom: 0;
    display: flex;
    flex-direction: column;
    left: 0;
    overflow-y: auto;
    padding: 0 0 16px;
    position: fixed;
    right: auto;
    top: 0;
    width: var(--side-w);
    /*
     * Reset, and not a redundant one. The 720px block above sets
     * `justify-content: center` to centre the segmented control on its
     * horizontal bar — and this block turns the same element into a *column*,
     * where that property stops meaning "centre horizontally" and starts
     * meaning "centre vertically". The whole rail floated in the middle of the
     * viewport with 168px of empty green above the logo.
     *
     * The axis a flex property acts on changes with `flex-direction`, so any
     * rule inherited across a direction change has to be re-stated rather than
     * assumed harmless.
     */
    justify-content: flex-start;
  }

  /* The product's mark, at the head of its own chrome. */
  .nav__brand {
    align-items: center;
    color: var(--white);
    display: flex;
    font-size: 1.5rem;
    font-weight: 700;
    letter-spacing: -0.02em;
    min-height: 64px;
    padding: 0 20px;
    text-decoration: none;
  }
  .nav__brand img { display: block; height: 26px; width: auto; }

  .nav__track {
    background: none;
    border-radius: 0;
    display: flex;
    flex-direction: column;
    gap: 2px;
    padding: 8px 12px 0;
  }

  .nav__item {
    border-radius: var(--radius-sm);
    color: var(--side-ink);
    flex: 0 0 auto;
    flex-direction: row;
    font-size: 0.9375rem;
    gap: 12px;
    justify-content: flex-start;
    min-height: 40px;
    padding: 0 12px;
    position: relative;
  }
  .nav__icon { color: var(--side-ink-dim); font-size: 0.9375rem; opacity: 1; width: 16px; }
  .nav__item:hover { background: rgb(255 255 255 / 6%); color: var(--white); }
  .nav__item:focus-visible { box-shadow: 0 0 0 2px var(--b-200); outline: none; }

  /*
   * The active item: a filled block with a bright edge on the left. The edge
   * matters more than the fill — on a dark rail a fill alone is a slightly
   * lighter rectangle, which at a glance reads as a hover rather than a
   * location.
   */
  .nav__item.is-on {
    background: var(--side-on);
    /* The logo's accent green, doing the one job it is bright enough for. */
    box-shadow: inset 3px 0 0 var(--b-accent);
    color: var(--white);
    font-weight: 600;
  }
  .nav__item.is-on .nav__icon { color: var(--b-accent); }

  /* Pushed to the far edge: on a full-width row it belongs at the end. */
  .nav__badge {
    background: var(--bad);
    left: auto;
    margin-left: auto;
    position: static;
    top: auto;
  }

  /*
   * The setup group — Settings, the clinic's lists, billing, export.
   *
   * Deliberately quieter than the work above it. These are visited when
   * something needs changing, not as part of the day, and a permanent door
   * should not compete with the screens the front desk lives in.
   */
  .nav__group {
    border-top: 1px solid var(--side-rule);
    display: flex;
    flex-direction: column;
    gap: 2px;
    margin: 12px 12px 0;
    padding-top: 12px;
  }
  .nav__grouplabel {
    color: var(--side-ink-dim);
    font-size: 0.6875rem;
    font-weight: 650;
    letter-spacing: 0.06em;
    margin: 0 0 4px;
    padding: 0 12px;
    text-transform: uppercase;
  }
  .nav__group .nav__item { font-size: 0.875rem; min-height: 34px; }

  /*
   * Everything else clears the rail — by padding the body, not by shifting
   * each block.
   *
   * The topbar and the page already centre themselves with
   * `max(24px, calc((100% - 1080px) / 2))`, and a margin on each would leave
   * that arithmetic measuring the whole window while the content lived in
   * what was left of it: the header's contents would drift out of line with
   * the cards under them. Padding the body narrows what `100%` means, so
   * every one of those rules keeps working untouched. The rail is
   * `position: fixed` and so is measured against the viewport, which is what
   * makes this safe.
   */
  body { padding-left: var(--side-w); }

  /*
   * The topbar turns white here: with a dark rail already holding the
   * chrome, a dark bar above it boxed the content into a corner.
   */
  .topbar {
    background: var(--white);
    border-bottom: 1px solid var(--line);
    box-shadow: none;
    color: var(--ink);
  }
  .topbar__name { font-size: 1rem; }
  .topbar select { background: var(--n-50); border-color: var(--line); color: var(--ink); }
  .usermenu__button { color: var(--n-600); }
  .usermenu__button:focus-visible { outline-color: var(--b-500); }
}

/*
 * Items the dock does not have room for. Hidden by default rather than shown,
 * so a phone can never end up with a seven-item bar at 53px each.
 */
.nav__item--desk, .nav__group { display: none; }
@media (min-width: 1024px) {
  .nav__item--desk { display: flex; }
  .nav__group { display: flex; }
}
/* The brand only exists inside the rail; the phone has the topbar for that. */
.nav__brand { display: none; }
@media (min-width: 1024px) { .nav__brand { display: flex; } }

/* ------------------------------------------------- actions, where they belong */

/*
 * The seven actions, above the Today heading — DECISIONS §43.
 *
 * Four across on a phone rather than three, so seven wraps to two rows instead
 * of three. Today is the screen that has to show who to chase without
 * scrolling, and every row of buttons is a row of chase work pushed down.
 *
 * They are equal-weight targets rather than a row of small links because a
 * front desk taps these one-handed, mid-call, on a mid-range phone.
 */
/*
 * Flex rather than grid, for the second row.
 *
 * Seven items in four columns leaves the last cell of the second row empty, and
 * a grid pins the three that are there to the left — so the block ends in a
 * hole at the bottom right that reads as a missing button. Wrapping flex items
 * with justify-content centres the short row instead, which reads as the shape
 * the row is meant to be.
 *
 * The basis is written out rather than left to the content, so the four in the
 * first row stay identical widths — the thing minmax(0, 1fr) was doing before.
 */
/* ------------------------------------------------ Today: greeting and stats */

/*
 * The greeting and the date, on one line once there is room for them.
 *
 * On a phone the date drops under the greeting rather than shrinking it: a
 * heading that has to compete with a chip for 360px loses, and the date is
 * the thing that can afford a second line.
 */
.hello {
  align-items: baseline;
  display: flex;
  flex-wrap: wrap;
  gap: 4px 16px;
  justify-content: space-between;
  margin-bottom: 18px;
}
.hello > div { min-width: 0; }
.hello__hi { margin: 0 0 2px; }
.hello .sub { margin: 0; }
.hello__date {
  color: var(--muted);
  font-size: 0.875rem;
  margin: 0;
  white-space: nowrap;
}

/*
 * The four counts.
 *
 * Two columns on a phone, because one per row turns the top of the screen
 * into four bands of whitespace and pushes the day's work below the fold —
 * which on this screen is the whole game. They go to a single row of four
 * once the desk layout has the width for it.
 */
.stats {
  display: grid;
  gap: 10px;
  grid-template-columns: repeat(2, minmax(0, 1fr));
  margin-bottom: 20px;
}
.stat {
  align-items: center;
  background: var(--white);
  border: 1px solid var(--line);
  border-radius: var(--radius);
  color: inherit;
  display: flex;
  gap: 12px;
  padding: 14px;
  text-decoration: none;
}
.stat:hover { border-color: var(--b-200); box-shadow: var(--shadow-2); }
.stat:focus-visible { box-shadow: var(--focus); outline: none; }
.stat__icon {
  align-items: center;
  border-radius: 10px;
  display: flex;
  flex: 0 0 auto;
  font-size: 1.05rem;
  height: 40px;
  justify-content: center;
  width: 40px;
}
/*
 * A tint per card, so the row reads as four different things at a glance
 * rather than four identical tiles with different numbers in them. Each pairs
 * a wash with a text tone dark enough to sit on it.
 */
.stat__icon--appt { background: #e8f0fe; color: #2b5fb8; }
.stat__icon--req { background: var(--good-bg); color: var(--good); }
.stat__icon--chase { background: var(--warn-bg); color: var(--warn); }
.stat__icon--money { background: var(--b-50); color: var(--b-700); }
.stat__body { min-width: 0; }
.stat__n {
  display: block;
  font-size: 1.375rem;
  font-weight: 700;
  letter-spacing: -0.02em;
  line-height: 1.15;
}
/*
 * The label wraps on a phone rather than truncating.
 *
 * Two cards to a 380px row leaves about 105px beside the icon, and every one
 * of these labels is longer than that — so ellipsis turned all four into
 * "Appointment…", "Booking requ…", which is worse than a second line. Two
 * lines are allowed and a third is not: the cards stay the same height as
 * each other, which is what makes the row read as a row.
 */
.stat__k {
  color: var(--muted);
  display: -webkit-box;
  -webkit-box-orient: vertical;
  -webkit-line-clamp: 2;
  font-size: 0.8125rem;
  line-height: 1.3;
  overflow: hidden;
}

@media (min-width: 720px) {
  .stats { gap: 14px; grid-template-columns: repeat(4, minmax(0, 1fr)); }
  .stat { padding: 16px; }
  /* Room for one line here, so anything longer is a label that needs cutting. */
  .stat__k { -webkit-line-clamp: 1; }
}

/* A label on a toolbar, not a section heading — see the note in today.ejs. */
.quickrow__label {
  color: var(--muted);
  font-size: 0.8125rem;
  font-weight: 550;
  letter-spacing: 0.01em;
  margin: 0 0 8px;
}
.quickrow {
  display: flex;
  flex-wrap: wrap;
  gap: 8px;
  justify-content: center;
  margin: 0 0 20px;
}
.quick {
  align-items: center;
  background: var(--white);
  border: 1px solid var(--line);
  border-radius: var(--radius);
  color: var(--ink);
  display: flex;
  /* Four per row: the container less its three gaps, quartered. min-width: 0
     with it, or "appointment" widens its own button past the basis and pushes
     the fourth onto the next line. */
  flex: 0 0 calc((100% - 24px) / 4);
  flex-direction: column;
  min-width: 0;
  font-size: 0.75rem;
  font-weight: 550;
  gap: 5px;
  justify-content: center;
  /* Two lines of label, because every one of these is a verb and a noun. */
  line-height: 1.25;
  min-height: 68px;
  padding: 8px 4px;
  text-align: center;
  text-decoration: none;
}
.quick:hover { border-color: var(--b-200); box-shadow: var(--shadow-2); }
.quick:focus-visible { box-shadow: var(--focus); outline: none; }
.quick__icon { color: var(--b-500); font-size: 1.125rem; line-height: 1; }
.quick--primary { background: var(--b-50); border-color: var(--b-200); color: var(--b-700); }
.quick--primary .quick__icon { color: var(--b-700); }

/* Wide enough for all seven on one line, which is the whole point of the row.
   They share the width equally, so there is no short row left to centre. */
@media (min-width: 720px) {
  .quick { flex: 1 1 0; font-size: 0.8125rem; min-height: 64px; }
  .quick__icon { font-size: 1.25rem; }
}

/*
 * Each way a lab case can move on: one form per button, so a remake can carry
 * a date field that a cancellation does not. A rule between them, because two
 * submit buttons with nothing between them read as one choice with two halves.
 */
.moveform + .moveform {
  border-top: 1px solid var(--line);
  margin-top: 16px;
  padding-top: 16px;
}

/*
 * The stack trace on the development error page, and nothing else.
 *
 * It was called `.stack` and a form on the lab detail borrowed the name for
 * what its author meant by it — a vertical stack of fields — and inherited a
 * near-black panel, pale text, and `white-space: pre-wrap`, which rendered the
 * template's own newlines as great columns of empty space. `.trace` says what
 * it is and cannot be borrowed by accident. See §47.
 */
.trace {
  background: #0e1420;
  border-radius: 8px;
  color: #d6e0ee;
  font-size: 0.75rem;
  overflow-x: auto;
  padding: 12px;
  white-space: pre-wrap;
  word-break: break-word;
}

a { color: var(--navy-soft); }

/* ======================================================== desktop layer ==
 *
 * Everything above is the phone, and the phone is still the priority — the
 * front desk works this one-handed between patients. But until now that was
 * the *only* layout: `.wrap` stayed 560px at every width, so on a laptop the
 * app was a phone-shaped column stranded in the middle of the screen with
 * nothing either side of it.
 *
 * Three rules govern what widens and what does not:
 *
 *   1. Lists, dashboards and the Today screen take the space. They are
 *      scanned, and more of them visible at once is straightforwardly better.
 *   2. Forms do not. A 1100px-wide text input is harder to use than a 500px
 *      one — the eye loses the line, and a label sits miles from its field.
 *      Anything on a `.card--narrow` stays narrow on purpose.
 *   3. Nothing here changes below 1024px. A tablet in a clinic is closer to a
 *      phone than to a desk.
 */

.deck__main, .deck__side { display: contents; }

@media (min-width: 720px) {
  :root { --page-max: 760px; }
  .wrap { padding-top: 28px; }
  h1 { font-size: 1.625rem; }
}

@media (min-width: 1024px) {
  :root { --page-max: 1080px; }
  .wrap { padding: 30px 24px 48px; }

  /*
   * The header aligns with the content instead of hugging the screen edges.
   * Done with padding rather than an inner wrapper so the bar still runs the
   * full width of the window, which is what makes it read as chrome.
   *
   * The `+ 24px` is the page's own padding, and leaving it out is why the
   * clinic name used to sit 24px to the left of the cards underneath it. The
   * old formula lined the bar up with the *outer* edge of `.wrap`, not with
   * the content inside it. Invisible while the gutter happened to be smaller
   * than 24px — which it was at 1440 — and obvious on a wide monitor.
   *
   * `max(0px, …)` rather than `max(24px, …)` for the same reason: the 24px is
   * added once, at the end, instead of being the floor.
   */
  .topbar { padding-left: calc(max(0px, (100% - var(--page-max)) / 2) + 24px);
            padding-right: calc(max(0px, (100% - var(--page-max)) / 2) + 24px); }

  h1 { font-size: 1.75rem; letter-spacing: -0.01em; }
  h2 { font-size: 1.175rem; }

  /*
   * Cards lift slightly off the page rather than being outlined. At phone
   * size a border is cheaper to render and reads better against a small
   * screen; with room around them a soft shadow gives the depth that stops a
   * long page looking like a stack of boxes.
   */
  .card {
    border-color: #e4e9f0;
    box-shadow: 0 1px 2px rgb(15 37 71 / 4%), 0 6px 16px rgb(15 37 71 / 5%);
    padding: 24px;
  }

  /* Rule 2. Forms, and the pages built around one, stay readable. */
  .card--narrow { margin-left: auto; margin-right: auto; max-width: 620px; }

  /* Today: the day's schedule beside the chase list. */
  .deck {
    align-items: start;
    display: grid;
    gap: 20px;
    grid-template-columns: minmax(0, 1.4fr) minmax(0, 1fr);
  }
  .deck__main, .deck__side { display: block; }

  /* Six actions in three columns rather than a tall two-column strip. */
  .grid { gap: 16px; grid-template-columns: repeat(3, minmax(0, 1fr)); }
  .tile { padding: 22px; }

  /* A row of headline numbers reads as a dashboard; a column of them does not. */
  .grid--dash { grid-template-columns: repeat(4, minmax(0, 1fr)); }

  /*
   * List rows get a hover, which only makes sense once there is a pointer.
   * The padding is pulled out to the card edge so the highlight looks like a
   * row rather than a floating band.
   */
  .plain > li { border-radius: 6px; padding-left: 8px; padding-right: 8px; }
  .card .plain { margin-left: -8px; margin-right: -8px; }
  .plain > li:hover { background: #f8fafc; }
  .bars > li:hover, .seg .plain > li:hover { background: none; }

  /* Filters sit on one line with room to spare instead of wrapping. */
  .filters .field { flex: 0 1 200px; }
  /*
   * Search stops taking a whole line once there is room for it beside the
   * dates, but stays the widest thing in the row — it is the one field here
   * with an unbounded amount to hold. Per page keeps to its two digits.
   */
  .filters .field--wide { flex: 1 1 260px; }
  .filters .field--narrow { flex: 0 0 auto; }

  .nav__item { font-size: 0.9375rem; }

  .detail { grid-template-columns: 180px 1fr; }
  .strip { gap: 48px; }
}

/*
 * Beyond a laptop, keep growing — but only because of what is on this page.
 *
 * The old cap was 1200px flat, on the rule that a line of text 1400px wide is
 * unreadable. That rule is still right and still enforced, by `.card--narrow`:
 * anything built around a form stays at 620px however wide the window gets.
 * What the cap was also doing, wrongly, was pinning a two-column dashboard of
 * cards to 1200px on a 1900px monitor — 240px of empty page down each side,
 * while the appointment rows it was squeezing are exactly the kind of content
 * that reads better with room (the note above this block says so).
 *
 * The steps account for the sidebar: these query the viewport, and 224px of
 * every one of them is rail.
 */
@media (min-width: 1400px) { :root { --page-max: 1200px; } }
@media (min-width: 1600px) { :root { --page-max: 1360px; } }
@media (min-width: 1800px) { :root { --page-max: 1520px; } }

/* ---------------------------------------------------------------- calendar

   The day arranged by doctor. A grid rather than a list because the question
   it answers — is anyone doubled up — is spatial: two things side by side in
   one row is the answer, and no amount of sorting a list makes it as quick.

   Scrolls horizontally rather than wrapping. Columns that reflow stop being
   columns, and a clinic with four dentists on a 380px phone needs to swipe
   across them, not read them stacked (docs/03: 380px first).
*/
.cal__nav { display: flex; gap: 8px; margin: 0 0 12px; flex-wrap: wrap; }

.cal__jump { display: flex; gap: 8px; align-items: flex-end; margin: 0 0 16px; }
.cal__jump .field { margin: 0; }

.cal__scroll { overflow-x: auto; -webkit-overflow-scrolling: touch; }

.cal {
  border-collapse: collapse;
  width: 100%;
  min-width: 320px;
  font-size: 0.9rem;
}

.cal th, .cal td {
  border: 1px solid var(--line);
  vertical-align: top;
  padding: 6px;
}

.cal thead th {
  background: var(--n-50);
  text-align: left;
  font-size: 0.78rem;
  letter-spacing: 0.02em;
  text-transform: uppercase;
  color: var(--muted);
  position: sticky;
  top: 0;
}

/* The time gutter stays put while the doctors scroll past it — without it the
   rows lose their meaning the moment you swipe. */
.cal__time, .cal__timehead {
  position: sticky;
  left: 0;
  background: var(--bg);
  white-space: nowrap;
  width: 1%;
  font-variant-numeric: tabular-nums;
}
.cal__timehead { background: var(--n-50); z-index: 1; }

.cal__appt {
  display: block;
  padding: 6px 8px;
  margin-bottom: 4px;
  border-radius: 6px;
  border-left: 3px solid var(--b-500);
  background: var(--n-50);
  color: inherit;
  text-decoration: none;
}
.cal__appt:last-child { margin-bottom: 0; }

.cal__treat, .cal__meta { display: block; font-size: 0.8rem; color: var(--muted); }

/* Nobody is in the chair, so it holds no slot and never reads as a clash. */
.cal__appt--cancelled, .cal__appt--no_show { opacity: 0.55; border-left-color: var(--muted); }
.cal__appt--completed { border-left-color: var(--ok-600, #157347); }

/* Marked, never hidden — §39 leaves the judgement with the front desk. */
.cal__appt--clash {
  border-left-color: var(--bad-600, #b02a37);
  background: var(--bad-50, #fdf1f2);
}

@media (prefers-reduced-motion: no-preference) {
  .cal__appt { transition: background-color 120ms ease; }
}

/* ------------------------------------------------------- SMS card (§60)

   The desktop answer to an `sms:` link, which a desk machine cannot open.
   Never seen on a phone: ui.js only builds this where the pointer is fine,
   and there the link goes straight to the messaging app.

   z-index above the bottom nav (20) and the sticky header (30), because it
   is modal and half-covered by a nav bar it would look like a rendering bug.
*/
.smscard {
  align-items: center;
  background: rgba(10, 26, 53, 0.55);
  bottom: 0;
  display: flex;
  justify-content: center;
  left: 0;
  padding: 20px;
  position: fixed;
  right: 0;
  top: 0;
  z-index: 40;
}
.smscard__panel {
  background: var(--white);
  border-radius: 14px;
  box-shadow: var(--shadow-pop);
  max-height: 90vh;
  max-width: 380px;
  overflow-y: auto;
  padding: 20px;
  text-align: center;
  width: 100%;
}
.smscard__title { font-size: 1.125rem; margin: 0 0 6px; }
.smscard__hint { color: var(--muted); font-size: 0.9375rem; margin: 0 0 14px; }

/* The number is the headline now that the QR has gone (§60) — it is the thing
   being copied onto the phone by hand, so it is the largest thing on the card. */
.smscard__num { font-size: 1.375rem; font-weight: 700; letter-spacing: 0.01em; margin-top: 4px; }

/* The message, shown whole — it is what gets copied, so it must be readable
   and it must not be truncated by an ellipsis the clipboard would not carry. */
.smscard__body {
  background: var(--n-50);
  border: 1px solid var(--line);
  border-radius: 8px;
  font-size: 0.9375rem;
  margin: 10px 0 14px;
  padding: 10px;
  text-align: left;
  white-space: pre-wrap;
  word-break: break-word;
}
.smscard__actions { display: flex; gap: 10px; justify-content: center; }

/* Says what the Sent button does, under it rather than above the QR — by the
   time it matters the message has already gone. */
.smscard__hint--foot { font-size: 0.875rem; margin: 12px 0 0; }

/* ------------------------------------------- treatment plan items (§61)

   The tooth picker is a checkbox grid arranged as a dental chart. The
   arrangement is the whole point: read as a mouth it needs no legend, read as
   a list of numbers it needs one. lib/teeth owns the row order.
*/
.teeth { display: flex; flex-direction: column; gap: 4px; margin: 4px 0 8px; }
.teeth__jaw { display: flex; flex-direction: column; gap: 3px; }

/* The two halves of a jaw sit side by side with a midline gap between them. */
.teeth__row { display: flex; gap: 3px; justify-content: center; }
.teeth__row--right { margin-bottom: 1px; }
.teeth__jaw .teeth__row { flex-wrap: nowrap; }

@media (min-width: 420px) {
  .teeth__jaw {
    align-items: center;
    display: grid;
    gap: 3px 14px;
    grid-template-columns: 1fr 1fr;
  }
  .teeth__row--right { justify-content: flex-end; margin-bottom: 0; }
  .teeth__jaw .teeth__row:not(.teeth__row--right) { justify-content: flex-start; }
}

/*
 * A tooth is the label, not the checkbox: the input is hidden across the
 * whole cell so the crown and the number are one tap target a thumb cannot
 * miss. Hidden with opacity rather than display:none so it still takes
 * keyboard focus and :focus-visible can be styled on the crown.
 *
 * §117: the cell is a crown drawing from lib/toothShapes with the FDI number
 * beside its biting edge — number under an upper tooth, above a lower one,
 * so the numbers form the band between the arches the way a paper chart
 * prints them. The SVG scales with the cell, which is what keeps the row
 * shrinkable: a jaw is 16 teeth plus a midline gap, and the lab form puts
 * the chart inside `.card--narrow`, so the crowns must give up a few pixels
 * each rather than run out through the card's edge. `flex-grow` stays 0 so
 * nothing stretches on the wide plan screen.
 */
.tooth {
  align-items: center;
  cursor: pointer;
  display: flex;
  flex: 0 1 34px;
  flex-direction: column;
  gap: 2px;
  min-width: 0;
  position: relative;
}
.tooth--lower { flex-direction: column-reverse; }

/* Natural crown widths per shape — lib/toothShapes owns the numbers. */
.tooth--incisor { flex-basis: 23px; }
.tooth--canine { flex-basis: 25px; }
.tooth--premolar { flex-basis: 27px; }
.tooth--molar { flex-basis: 34px; }
.tooth--milkslot.tooth--incisor { flex-basis: 19px; }
.tooth--milkslot.tooth--canine { flex-basis: 21px; }
.tooth--milkslot.tooth--molar { flex-basis: 28px; }

.tooth input {
  height: 100%;
  left: 0;
  margin: 0;
  min-height: 0;
  opacity: 0;
  position: absolute;
  top: 0;
  width: 100%;
}
.tooth__crown { display: block; height: auto; width: 100%; }
.tooth__crown .crown { fill: var(--white); stroke: var(--line); stroke-width: 1.4; }
.tooth__num {
  color: var(--ink);
  font-size: 0.72rem;
  font-variant-numeric: tabular-nums;
  line-height: 1.3;
}
.tooth input:checked ~ .tooth__crown .crown {
  fill: var(--navy-soft);
  stroke: var(--navy-soft);
}
.tooth input:checked ~ .tooth__num { color: var(--navy); font-weight: 650; }
.tooth input:focus-visible ~ .tooth__crown {
  border-radius: var(--radius-sm);
  box-shadow: var(--focus);
  outline: none;
}

.teeth__milk { margin-bottom: 8px; }
.teeth__milk summary { color: var(--muted); cursor: pointer; font-size: 0.9125rem; }

/* ------------------------------------------- the read-only chart (§116)

   Same layout classes as the picker, different cells: a tooth with history
   is a link, one without is an inert crown. The colours are the product's
   own semantics — amber is work still to come, green is work finished — so
   the chart needs the same one-line legend everywhere or none at all.
*/
.tooth--read { cursor: default; }
a.tooth--read { color: inherit; cursor: pointer; text-decoration: none; }
.tooth--quiet .tooth__num { color: var(--muted); }
.tooth--planned .crown { fill: var(--warn-bg); stroke: var(--warn-line); }
.tooth--planned .tooth__num { color: var(--warn); font-weight: 650; }
.tooth--done .crown { fill: var(--good-bg); stroke: var(--good-line); }
.tooth--done .tooth__num { color: var(--good); font-weight: 650; }
.tooth--sel .tooth__crown {
  border-radius: var(--radius-sm);
  box-shadow: 0 0 0 2px var(--navy);
}
a.tooth--read:focus-visible .tooth__crown {
  border-radius: var(--radius-sm);
  box-shadow: var(--focus);
  outline: none;
}

.toothkey { align-items: center; display: flex; flex-wrap: wrap; gap: 6px; margin: 0 0 12px; }
.toothkey__swatch {
  border: 1px solid var(--line);
  border-radius: 4px;
  display: inline-block;
  height: 12px;
  width: 12px;
}
.toothkey__swatch--planned { background: var(--warn-bg); border-color: var(--warn-line); }
.toothkey__swatch--done { background: var(--good-bg); border-color: var(--good-line); }
.toothkey__swatch + .toothkey__swatch { margin-left: 10px; }

.toothhead { font-size: 1rem; margin: 16px 0 8px; }

/* Planned tooth work on the history lists. Its own name rather than reusing
   an appointment status class: "arrived" and "planned" only share a colour
   today, not a meaning. */
.chip--planned { background: var(--warn-bg); border-color: var(--warn-line); color: var(--warn); }

.plan-items { display: flex; flex-direction: column; gap: 10px; }
.plan-item {
  background: var(--white);
  border: 1px solid var(--line);
  border-radius: var(--radius);
  padding: 12px;
}
/* Done reads as settled, not as disabled — it is progress, not a dead row. */
.plan-item--done .amt { color: var(--muted); }
.plan-item--done strong { text-decoration: line-through; text-decoration-color: var(--n-300); }
.plan-item__edit { margin-top: 10px; }

/* An amount that is not a number yet — §62. Unpriced, not zero: it must not
   read with the weight of a figure somebody agreed to. */
.amt--none { color: var(--muted); font-weight: 500; }

.plan-edit { margin-top: 14px; }
.plan-edit summary { color: var(--muted); cursor: pointer; font-size: 0.9125rem; }
.plan-edit form { margin-top: 10px; }

/* Where a treatment's lab work physically is — §66. Sits under the line that
   quoted it, so "is the crown back" is answered where the money is. */
.plan-item__lab { font-size: 0.9125rem; margin-top: 6px; }

/* ------------------------------- the printed documents — §69, redrawn §140

   A patient's after-visit instructions on paper. Its own document rather than
   the app shell hidden by a print rule, so it can never print with a nav bar
   on it — but it still loads this stylesheet, because `style-src 'self'`
   means an inline <style> in that view would be dropped and the sheet would
   come out unstyled.

   Sized for A5 in the middle of A4: a clinic prints these in ones and twos on
   whatever paper is in the tray, and a full-width block of 11pt text is
   harder to read than a narrow one.

   Named `.paper` and not `.sheet`, which is what it was called for an hour.
   `.sheet` is already the status popover on Today (line 773), so redefining it
   here gave every one of those a 640px max-width and 24px of padding — §47's
   `.stack` collision repeated exactly, by somebody who had read §47. A word
   that describes what a thing looks like is a word another feature has already
   used.
*/

.paper {
  color: #111;
  font-size: 15px;
  line-height: 1.5;
  margin: 0 auto;
  max-width: 640px;
  padding: 24px 20px 40px;
}

/* ---------------------------------------------------------------- §140

   The letterhead. The clinic's own name leads and the person responsible for
   the page sits opposite it, because that is the pair somebody checks first:
   whose clinic, whose signature. The heavy rule closes it.

   What used to sit here was `.paper__kind` — a full-width bordered bar with
   centred uppercase text, ABOVE the clinic's name. It read as a form field
   rather than as stationery, and it pushed the letterhead a centimetre down
   every page. The document type now lives in the band below (.paper__band). */
.paper__head {
  align-items: flex-start;
  border-bottom: 2px solid #111;
  display: flex;
  gap: 16px;
  justify-content: space-between;
  padding-bottom: 12px;
}
.paper__head h1 { font-size: 1.375rem; letter-spacing: -0.01em; margin: 0; }
.paper__meta { color: var(--muted); font-size: 0.8125rem; margin: 3px 0 0; }

/* Who signs. Only ever a name and a registration number — the clinic records
   no qualifications, and a letterhead must not invent any. */
.paper__by { flex-shrink: 0; text-align: right; }
.paper__by strong { display: block; font-size: 0.9375rem; }

/* The band under the rule: which document this is, and its number and date.
   The reference reads first at the left because that is what gets quoted back
   over the phone; the badge holds the right-hand end. */
.paper__band {
  align-items: center;
  border-bottom: 1px solid var(--line);
  display: flex;
  gap: 12px;
  justify-content: space-between;
  margin-bottom: 20px;
  padding: 11px 0;
}
.paper__ref { color: var(--muted); font-size: 0.8125rem; }

/* Outlined, never filled. A solid block is the easiest thing to find in a
   stack of printouts and also the most toner on a page a clinic prints forty
   of a week; the outline is very nearly as findable and costs nothing. */
.paper__badge {
  border: 1px solid #111;
  border-radius: 3px;
  flex-shrink: 0;
  font-size: 0.6875rem;
  font-weight: 700;
  letter-spacing: 0.14em;
  padding: 4px 10px;
  text-transform: uppercase;
  white-space: nowrap;
}

/* Who the document is about, in a panel so the eye lands on it in one jump
   rather than reading down a list of labels to find the name. */
.paper__party {
  background: #f7f5f1;
  border: 1px solid var(--line);
  border-radius: var(--radius-sm);
  display: flex;
  gap: 16px;
  justify-content: space-between;
  margin-bottom: 22px;
  padding: 12px 14px;
}
.paper__party strong { display: block; font-size: 0.9375rem; }
.paper__party-r { text-align: right; }
.paper__label {
  color: var(--muted);
  display: block;
  font-size: 0.6875rem;
  font-weight: 700;
  letter-spacing: 0.12em;
  margin-bottom: 3px;
  text-transform: uppercase;
}

/* The instruction sheet's disclaimer — §69, kept out of the badge by §140.
   "Not a prescription" is a statement, not a type name, so it holds a full
   line of its own rather than shrinking into the corner of the band. Black
   and bold, where the old version was grey text in a grey box: a disclaimer
   that reads quieter than the medicines it disclaims is decoration.
   Pairs with .paper__band--tight, whose rule serves as this line's top. */
.paper__kind {
  border-bottom: 1px solid #111;
  font-size: 0.8125rem;
  font-weight: 700;
  letter-spacing: 0.06em;
  margin: 0 0 22px;
  padding: 9px 0;
  text-align: center;
  text-transform: uppercase;
}
.paper__band--tight { margin-bottom: 0; }

.paper__for { display: grid; gap: 4px 18px; grid-template-columns: max-content 1fr; margin: 0 0 20px; }
.paper__for dt { color: var(--muted); font-size: 0.8125rem; }
.paper__for dd { font-weight: 600; margin: 0; }

.paper h2 {
  border-bottom: 1px solid var(--line);
  font-size: 1rem;
  margin: 20px 0 8px;
  padding-bottom: 4px;
}

.paper__table { border-collapse: collapse; width: 100%; }
.paper__table th {
  color: var(--muted);
  font-size: 0.75rem;
  font-weight: 600;
  text-align: left;
  text-transform: uppercase;
}
.paper__table th, .paper__table td {
  border-bottom: 1px solid var(--line);
  padding: 8px 8px 8px 0;
  vertical-align: top;
}
/* The heavier rule under a column of headings, so the head of a table reads
   as a head rather than as another row. */
.paper__table thead th { border-bottom-color: #c9c5be; }
/* A second fact about the line above it — which tooth, which receipt paid it. */
.paper__sub { color: var(--muted); display: block; font-size: 0.8125rem; margin-top: 2px; }

/* The clinic's own paragraphs, with their newlines kept. */
.paper__body { margin: 0; white-space: pre-wrap; }

.paper__foot {
  border-top: 1px solid var(--line);
  color: var(--muted);
  font-size: 0.875rem;
  margin-top: 24px;
  padding-top: 10px;
}

/* The prescription's signature block — §137. The empty space above the line
   is the point of the whole document: room for ink. Right-aligned because
   that is where every pad in the country puts it. */
.paper__sign {
  margin: 48px 0 0 auto;
  max-width: 280px;
  text-align: center;
}
.paper__sign-line { border-bottom: 1px solid #111; display: block; height: 52px; }
.paper__sign-name { font-size: 0.9375rem; font-weight: 600; margin: 6px 0 0; }
.paper__sign-hint { color: var(--muted); font-size: 0.8125rem; margin: 2px 0 0; }

/* The consent form's pair of signatures — §180. The patient consents on the
   left, the doctor countersigns on the right where every pad puts them; each
   block keeps .paper__sign's room for ink. */
.paper__signs { display: flex; gap: 32px; justify-content: space-between; }
.paper__signs .paper__sign { flex: 1; margin: 48px 0 0; max-width: 280px; }

/* The medicines on a prescription — §140.

   Not a four-column table. A pharmacist reads down a list, so the drug leads
   its own line and the dosing sits under it as a sentence; only the duration
   is pulled right, because that is the fact that decides how much is handed
   over. The four-column version made the name compete with three cells of
   equal weight and wrapped badly on anything longer than "Amoxicillin". */
.paper__rx { display: flex; gap: 14px; }
/* The mark that says what kind of list this is, in the margin where a pad
   puts it. Pale: it is a landmark, not information. */
.paper__rx-mark {
  color: #b4ada3;
  font-size: 1.75rem;
  line-height: 1;
  padding-top: 2px;
}
.paper__meds { flex: 1; list-style: none; margin: 0; padding: 0; }
.paper__med {
  align-items: baseline;
  border-bottom: 1px solid var(--line);
  display: flex;
  gap: 10px;
  padding: 10px 0;
}
.paper__med-n { color: #b4ada3; font-size: 0.75rem; font-weight: 700; min-width: 12px; }
.paper__med-b { flex: 1; }
.paper__med-name { font-size: 0.9375rem; font-weight: 600; }
.paper__med-dose { color: var(--muted); font-size: 0.8125rem; margin-top: 2px; }
.paper__med-dur { font-size: 0.875rem; font-variant-numeric: tabular-nums; white-space: nowrap; }

/* The money documents — §138, restated by §140. Amount columns line up on one
   right margin with tabular figures, so the digits form a column a person can
   add up by eye; the closing row reads heavier than the lines above it. */
.paper__table td.num, .paper__table th.num {
  font-variant-numeric: tabular-nums;
  padding-right: 0;
  text-align: right;
  white-space: nowrap;
}
.paper__table tr.paper__total td { border-bottom: none; font-weight: 700; padding-top: 10px; }

/* The number the patient came to the desk for. It gets a panel and a size
   nothing else on the page has, because "what do I owe" and "what did I pay"
   are the one line each document is actually read for. */
.paper__due {
  align-items: center;
  background: #f7f5f1;
  border: 1px solid var(--line);
  border-radius: var(--radius-sm);
  display: flex;
  gap: 16px;
  justify-content: space-between;
  margin-top: 22px;
  padding: 14px 16px;
}
.paper__due-amt { font-size: 1.5rem; font-weight: 700; font-variant-numeric: tabular-nums; }
/* The receipt's opening figure — the first thing on the page. */
.paper__hero { font-size: 2rem; font-weight: 700; font-variant-numeric: tabular-nums; margin: 4px 0 0; }

@media print {
  /* The browser puts its own header and footer on unless the person turns
     them off in the dialog; nothing here can control that. What it can do is
     drop the app's own furniture and stop a table splitting mid-row. */
  .noprint { display: none; }
  .paper { max-width: none; padding: 0; }
  .paper__table tr { break-inside: avoid; }
  .paper h2 { break-after: avoid; }
  /* A medicine must never be split from its dose across a page break, and the
     signature must not start a page of its own. */
  .paper__med, .paper__due, .paper__party { break-inside: avoid; }
  .paper__sign, .paper__signs { break-inside: avoid; }
  /* The tint survives only if the person ticks "background graphics"; the
     border is what carries these panels on a default print. */
  .paper__party, .paper__due { background: transparent; }
}

/* ------------------------------------------------- an editable table — §69

   The Pharmacy medicine list: the same seven facts about twenty things, each
   row its own form so Save means that row and nothing else. A CSS grid rather
   than a <table>, because a <form> cannot wrap a <tr> and the alternatives —
   the HTML5 `form` attribute, or a form per cell — are more exotic than the
   layout is worth.

   The column headings do the labelling; every input carries an aria-label
   because a heading two rows up is not a label to a screen reader.

   It lives inside .scrollx: seven columns do not fit a phone, and the answer
   already established here (the Reports tables) is to scroll rather than
   reflow into something that has stopped being a table.
*/

.gtable { display: grid; min-width: 900px; }
.gtable__head, .gtable__row { display: contents; }

.gtable__head > * {
  border-bottom: 1px solid var(--line);
  color: var(--muted);
  font-size: 0.75rem;
  font-weight: 600;
  padding: 0 6px 6px 0;
  text-transform: uppercase;
}

.gtable__row > * { margin: 0 6px 6px 0; }
.gtable__row input[type="text"], .gtable__row input:not([type]) { width: 100%; }

/* The serial number. Left-aligned, because the # heading is: the digit sits
   under its own symbol, with the column's spare width as breathing room
   before the next column instead of the digit pressed against it. Tabular
   figures so a 9 above a 10 keeps one width. The margin lives here, not only
   on `.gtable__row > *`: the inventory serial sits outside its two row forms,
   and without its own margin the digit touched the count box while the
   pharmacy one kept its gap. */
.gtable__n {
  align-self: center;
  color: var(--muted);
  font-variant-numeric: tabular-nums;
  font-size: 0.875rem;
  margin: 0 6px 6px 0;
}

.gtable__tick { align-self: center; text-align: center; }
.gtable__row button { align-self: center; }

/* A retired row is still editable — it is dimmed, not disabled, because the
   way back is to tick the box and Save. */
.gtable__row--off input:not([type="checkbox"]) { color: var(--muted); }

/* #  name  form  dose  often  long  note  tick  save */
.gtable--med {
  grid-template-columns:
    2rem minmax(160px, 1.4fr) 90px 110px minmax(140px, 1.2fr)
    100px minmax(120px, 1fr) 5rem max-content;
}

/* #  count-form  name  unit  reorder  tick  save  history — §118. Narrower
   than the medicine table, so it needs less runway before it scrolls. */
.gtable--inv {
  grid-template-columns:
    2rem max-content minmax(160px, 1.6fr) 100px 90px 5rem max-content max-content;
  min-width: 640px;
}

/* The stocktake box: input, button and the low flag on one line. Not
   display:contents — this form is one cell, the row's other cells belong to
   the edit form beside it (forms cannot nest). It carries the same margin as
   the cells inside `.gtable__row`: without it this cell centres 3px lower
   than the rest of the row, and the serial number reads as misaligned
   against the very box it numbers. */
.invcount { align-items: center; display: flex; gap: 6px; margin: 0 6px 6px 0; }
.invcount input { width: 5rem; }

.pill--low { background: var(--warn-bg); border-color: var(--warn-line); color: var(--warn); }

/* The same margin as every other cell, for the same reason as .invcount. */
.gtable__peek { align-self: center; font-size: 0.8125rem; margin: 0 6px 6px 0; }

/* A one-line disclosure under a checkbox — the aftercare picker's "What it
   says" (§69). Checked against the stylesheet before naming it, which is the
   whole lesson of .sheet and .row__meta: a class that exists nowhere is one
   collision or one unstyled block waiting. */
.peek { margin: -6px 0 12px 28px; }
.peek summary { color: var(--muted); cursor: pointer; font-size: 0.8125rem; }
.peek p { margin: 4px 0 0; }

/* ---------------------------------- the treatment plan screen, redrawn §145

   What was wrong with the old one, in order of how much it cost:

   1. The five things a plan is *for* — book a visit, send to a lab, write a
      prescription, send instructions, print the bill — sat in a chain at the
      very bottom, under the treatments, the lab cases, the visits, the
      instructions, the prescriptions and the X-rays. On any plan with a
      history they were below the fold, on the screen whose whole job is to
      say what happens next.
   2. The page grows to 1520px (see --page-max) and spent all of it on one
      column, so three small numbers had 1450px of white beside them.
   3. Every treatment row carried three pill buttons and a closed disclosure.
      Ten treatments meant forty controls between the reader and the total.
   4. Six `h2` sections, five of them usually empty, each rendering its own
      "nothing here yet" paragraph.

   The rail fixes 1 and 2, `.rowmenu` fixes 3, `.fold` fixes 4. */

.planwrap { display: grid; }
.planwrap__rail, .planwrap__main { min-width: 0; }

/*
 * The rail is FIRST in the DOM and second on screen.
 *
 * On a phone the order that matters is balance, then the buttons, then the
 * work — which is source order, so the narrow layout needs no rules at all.
 * On a wide screen the same two blocks are placed explicitly into row 1,
 * column 2, which is the only way to move them right without reordering the
 * markup and making the phone read buttons-before-balance.
 */
@media (min-width: 1080px) {
  .planwrap {
    align-items: start;
    column-gap: 20px;
    grid-template-columns: minmax(0, 1fr) 340px;
  }
  .planwrap__main { grid-column: 1; grid-row: 1; }
  .planwrap__rail { grid-column: 2; grid-row: 1; position: sticky; top: 16px; }
}

/* The cards in this layout carry their own headings, so the page-level h2
   size is too loud inside them. Scoped to .planwrap: h2 is used on thirty
   other screens and none of them are asking for this. */
.planwrap .card > h2 { font-size: 1.0625rem; margin: 0 0 10px; }
.planwrap .card > h2 + .muted { margin-top: -6px; }

/* -------------------------------------------------------------- the money */

.plan-money__line {
  align-items: baseline;
  display: flex;
  gap: 12px;
  justify-content: space-between;
}
.plan-money__line .amt { font-size: 1.0625rem; }
.plan-money__cap { color: var(--n-400); font-size: 0.75rem; margin: 2px 0 12px; }

/*
 * The balance, in a block of its own and wearing its state's colour.
 *
 * It was the third of three equal columns, which made "settled" and "₹13,800
 * outstanding" look identical at a glance — and this is the number the front
 * desk opens the screen to read. Four states, because "not priced yet" is a
 * real one and must not borrow the red of a debt.
 */
.plan-bal {
  align-items: baseline;
  border: 1px solid var(--line);
  border-radius: var(--radius-sm);
  display: flex;
  gap: 12px;
  justify-content: space-between;
  margin: 0 0 12px;
  padding: 10px 14px;
}
.plan-bal__cap {
  font-size: 0.6875rem;
  font-weight: 700;
  letter-spacing: 0.04em;
  text-transform: uppercase;
}
.plan-bal__amt {
  font-size: 1.5rem;
  font-variant-numeric: tabular-nums;
  font-weight: 700;
  white-space: nowrap;
}
.plan-bal--outstanding { background: var(--bad-bg); border-color: var(--bad-line); color: var(--bad); }
.plan-bal--settled { background: var(--good-bg); border-color: var(--good-line); color: var(--good); }
.plan-bal--overpaid { background: var(--warn-bg); border-color: var(--warn-line); color: var(--warn); }
.plan-bal--unquoted { background: var(--n-25); color: var(--muted); }

/* ------------------------------------------------------------- what next */

.plan-next__list { margin: 0; }
.plan-next__list > li { border-bottom: 0; padding: 0; }

/* A full-width row rather than a .btn: five buttons stacked in a 340px rail
   read as five competing decisions, and only one of them is ever the one. */
.planact {
  align-items: center;
  border-radius: var(--radius-sm);
  color: var(--ink);
  display: flex;
  font-size: 0.9375rem;
  font-weight: 600;
  min-height: var(--tap);
  padding: 0 32px 0 10px;
  position: relative;
  text-decoration: none;
}
.planact::after {
  border-right: 2px solid var(--n-400);
  border-top: 2px solid var(--n-400);
  content: '';
  height: 7px;
  position: absolute;
  right: 14px;
  transform: rotate(45deg);
  width: 7px;
}
.planact:hover { background: var(--b-50); color: var(--b-700); }
.planact:focus-visible { box-shadow: var(--focus); outline: none; }

/* ----------------------------------------------------- the treatment list */

.plan-prog__cap { margin: 0 0 6px; }
.plan-prog {
  background: var(--n-100);
  border-radius: 4px;
  height: 8px;
  margin: 0 0 12px;
  overflow: hidden;
}
.plan-prog__fill { background: var(--b-500); display: block; height: 100%; }

/* Said once, while nothing has been ticked. `.hint` has never had a rule of
   its own (see .hint--warn) so this cannot borrow one. */
.plan-tip { color: var(--muted); font-size: 0.8125rem; margin: 0 0 8px; }

/* The rows moved inside the treatments card, so they stop being white boxes
   on white and go back to being list rows: .plain already rules them off. */
.plan-items { gap: 0; }
.plan-item { background: none; border: 0; border-radius: 0; padding: 12px 0; }

.plan-item__row { align-items: flex-start; display: flex; gap: 10px; }
.plan-item__tickform { flex: 0 0 auto; margin: 0; }
.plan-item__body { flex: 1 1 auto; min-width: 0; }
.plan-item__row > .amt { flex: 0 0 auto; padding-top: 2px; }

/*
 * The tick.
 *
 * A 26px circle with a 44px hit area behind it (`::before`, inset -9px), so
 * it meets --tap without putting a 44px hole in every row. The mark is always
 * in the DOM and transparent until done — one that appeared only on hover
 * would be invisible on the phones this is built for, and one that appeared
 * only when ticked would make the button change size.
 */
.tick {
  align-items: center;
  background: var(--white);
  border: 1.5px solid var(--n-300);
  border-radius: 50%;
  color: transparent;
  cursor: pointer;
  display: flex;
  font-size: 0.875rem;
  font-weight: 700;
  height: 26px;
  justify-content: center;
  line-height: 1;
  margin: 0;
  padding: 0;
  position: relative;
  width: 26px;
}
.tick::before { content: ''; inset: -9px; position: absolute; }
.tick:hover { border-color: var(--b-500); color: var(--b-200); }
.tick:focus-visible { box-shadow: var(--focus); outline: none; }
.tick--on { background: var(--good-bg); border-color: var(--good); color: var(--good); }

/* Where the crown is, on the row that quoted it (§66) — a link now, because
   it always led somewhere and never looked like it did. */
.plan-item__lab { color: var(--muted); display: inline-block; text-decoration: none; }
.plan-item__lab:hover { text-decoration: underline; }
.plan-item__lab--late { color: var(--bad); font-weight: 600; }

/*
 * The row's dot menu.
 *
 * A `details`, not a script menu: §21 forbids inline script and this has to
 * work on a phone that failed to load ui.js — where a second tap on the dots
 * closes it, which is the whole interaction. With the script running it is
 * registered in ui.js's POPOVERS list, so it also closes on an outside click
 * and on Escape, like every other dropdown in the product.
 */
.rowmenu { flex: 0 0 auto; position: relative; }
.rowmenu__dots {
  align-items: center;
  border-radius: 50%;
  color: var(--n-600);
  cursor: pointer;
  display: flex;
  /* Three periods sitting on the baseline, so the glyph needs both the size
     and a nudge upward to look centred in the circle it lives in. */
  font-size: 1.25rem;
  font-weight: 700;
  height: 30px;
  justify-content: center;
  letter-spacing: 1px;
  line-height: 0.55;
  list-style: none;
  padding-bottom: 7px;
  width: 30px;
}
.rowmenu__dots::-webkit-details-marker { display: none; }
.rowmenu__dots:hover { background: var(--n-50); color: var(--ink); }
.rowmenu[open] .rowmenu__dots { background: var(--n-100); color: var(--ink); }
.rowmenu__dots:focus-visible { box-shadow: var(--focus); outline: none; }
.rowmenu__panel {
  background: var(--white);
  border: 1px solid var(--line);
  border-radius: var(--radius-sm);
  box-shadow: var(--shadow-pop);
  min-width: 190px;
  padding: 6px;
  position: absolute;
  right: 0;
  top: 34px;
  z-index: 20;
}
.rowmenu__panel form { margin: 0; }
.rowmenu__item {
  background: none;
  border: 0;
  border-radius: 6px;
  color: var(--ink);
  cursor: pointer;
  display: block;
  font: inherit;
  padding: 9px 10px;
  text-align: left;
  text-decoration: none;
  width: 100%;
}
.rowmenu__item:hover { background: var(--n-50); }
.rowmenu__item:focus-visible { box-shadow: var(--focus); outline: none; }
.rowmenu__item--bad { color: var(--bad); }
.rowmenu__item--bad:hover { background: var(--bad-bg); }

/* The add form, folded away once the plan has a treatment in it. Dashed,
   because it is the one control on the card that adds rather than acts on
   what is already there. */
.plan-add { margin-top: 4px; }
.plan-add__open {
  border: 1px dashed var(--n-300);
  border-radius: var(--radius-sm);
  color: var(--b-600);
  cursor: pointer;
  font-weight: 600;
  list-style: none;
  padding: 12px;
  text-align: center;
}
.plan-add__open::-webkit-details-marker { display: none; }
.plan-add__open:hover { background: var(--b-50); border-color: var(--b-200); }
.plan-add__open:focus-visible { box-shadow: var(--focus); outline: none; }
.plan-add[open] .plan-add__open { margin-bottom: 14px; }

/* ------------------------------------------------------------- the record

   Five sections became five folds in one card. The summary carries the count
   and, where there is one, the fact worth knowing while it is shut — an
   overdue lab case, a prescription filed nowhere — because a fold that hides
   news is worse than the six headings it replaced. */

.fold { border-top: 1px solid var(--line); }
.fold:first-of-type { border-top: 0; }
.fold__head {
  align-items: center;
  cursor: pointer;
  display: flex;
  gap: 8px;
  list-style: none;
  min-height: var(--tap);
  padding: 4px 26px 4px 0;
  position: relative;
}
.fold__head::-webkit-details-marker { display: none; }
.fold__head::after {
  border-right: 2px solid var(--n-400);
  border-top: 2px solid var(--n-400);
  content: '';
  height: 7px;
  position: absolute;
  right: 8px;
  top: 50%;
  transform: translateY(-70%) rotate(135deg);
  width: 7px;
}
.fold[open] > .fold__head::after { transform: translateY(-25%) rotate(-45deg); }
.fold__head:hover .fold__name { color: var(--b-700); }
.fold__head:focus-visible { border-radius: 6px; box-shadow: var(--focus); outline: none; }
.fold__name { font-weight: 600; }
.fold__count {
  background: var(--n-100);
  border-radius: var(--radius-pill);
  color: var(--n-600);
  font-size: 0.75rem;
  font-weight: 700;
  min-width: 22px;
  padding: 2px 7px;
  text-align: center;
}
.fold__count--bad { background: var(--bad-bg); color: var(--bad); }
.fold__note {
  color: var(--muted);
  font-size: 0.8125rem;
  margin-left: auto;
  text-align: right;
}
.fold__note--bad { color: var(--bad); font-weight: 600; }
.fold__body { padding: 0 0 14px; }
.fold__body > :last-child { margin-bottom: 0; }

/* ---- Gum check (§151) --------------------------------------------------- */
/*
 * The six sextant boxes on the record page, coloured by the same three tones
 * the rest of the app uses. A two-by-three grid that collapses on the
 * narrowest phones by simply wrapping.
 */
.perigrid {
  display: grid;
  gap: 8px;
  grid-template-columns: repeat(3, 1fr);
  margin: 12px 0;
}
@media (max-width: 430px) {
  .perigrid { grid-template-columns: repeat(2, 1fr); }
}
.perigrid__box {
  border: 1px solid var(--line);
  border-radius: var(--radius-sm);
  display: flex;
  flex-direction: column;
  gap: 2px;
  padding: 10px 12px;
}
.perigrid__box--good { background: var(--good-bg); border-color: var(--good-line); color: var(--good); }
.perigrid__box--warn { background: var(--warn-bg); border-color: var(--warn-line); color: var(--warn); }
.perigrid__box--bad { background: var(--bad-bg); border-color: var(--bad-line); color: var(--bad); }
.perigrid__box--quiet { color: var(--muted); }
.perigrid__label { font-size: 0.75rem; font-weight: 600; }
.perigrid__score { font-size: 1.5rem; font-variant-numeric: tabular-nums; font-weight: 700; }
.perigrid__word { font-size: 0.75rem; }

/* The entry form's sextant rows: score select beside the furcation star. */
.perisext .row { align-items: center; display: flex; gap: 12px; }
.perisext select { flex: 1; min-width: 0; }
.perisext__star {
  align-items: center;
  color: var(--muted);
  display: flex;
  flex: none;
  font-size: 0.8125rem;
  gap: 6px;
  min-height: var(--tap);
}

/* ---- Full gum chart (§151) ---------------------------------------------- */
/* The sextant entry page: six pocket boxes per tooth with a bleed tick under
   each, and the rarely-needed rows folded behind a disclosure. */
.periotooth { border-top: 1px solid var(--line); padding-top: 10px; }
.periotooth__missing {
  color: var(--muted);
  font-size: 0.8125rem;
  font-weight: 400;
  margin-left: 10px;
}
.periotooth__missing input { width: auto; min-height: 0; }
.periosites {
  align-items: flex-start;
  display: flex;
  flex-wrap: wrap;
  gap: 6px;
  margin: 6px 0;
}
.periosites__side {
  color: var(--muted);
  flex-basis: 100%;
  font-size: 0.72rem;
}
.periosite { display: flex; flex-direction: column; gap: 2px; width: 44px; }
.periosite input[inputmode] {
  min-height: 40px;
  padding: 4px;
  text-align: center;
  width: 44px;
}
.periosite__bleed { display: flex; justify-content: center; }
.periosite__bleed input { margin: 0; min-height: 0; width: auto; }
.periomore { margin: 6px 0 2px; }
.periomore summary { color: var(--navy-soft); cursor: pointer; font-size: 0.8125rem; }
.periomore__pick { flex: 1; }

/* The chart itself: a column per tooth, scrolling sideways in its own box. */
.tablewrap { overflow-x: auto; }
.periochart {
  border-collapse: collapse;
  font-size: 0.78rem;
  font-variant-numeric: tabular-nums;
  margin: 10px 0;
  white-space: nowrap;
}
.periochart th, .periochart td {
  border: 1px solid var(--line);
  padding: 3px 6px;
  text-align: center;
}
.periochart__label {
  background: var(--bg);
  font-weight: 600;
  text-align: right;
}
.periochart__loss td { color: var(--navy); font-weight: 600; }
.periochart__tooth { font-weight: 700; }
.periochart__tooth--good { background: var(--good-bg); color: var(--good); }
.periochart__tooth--warn { background: var(--warn-bg); color: var(--warn); }
.periochart__tooth--bad { background: var(--bad-bg); color: var(--bad); }
.periochart__tooth--quiet { color: var(--muted); }

/* §151: the gums layer's crown tones, same grammar as planned/done. */
.tooth--good .crown { fill: var(--good-bg); stroke: var(--good-line); }
.tooth--good .tooth__num { color: var(--good); font-weight: 650; }
.tooth--warn .crown { fill: var(--warn-bg); stroke: var(--warn-line); }
.tooth--warn .tooth__num { color: var(--warn); font-weight: 650; }
.tooth--bad .crown { fill: var(--bad-bg); stroke: var(--bad-line); }
.tooth--bad .tooth__num { color: var(--bad); font-weight: 650; }

/* §151: the layer strip and the gums legend swatches. */
.toothlayers { display: flex; gap: 8px; margin: 0 0 12px; }
.toothkey__swatch--gumgood { background: var(--good-bg); border-color: var(--good-line); }
.toothkey__swatch--gumwarn { background: var(--warn-bg); border-color: var(--warn-line); }
.toothkey__swatch--gumbad { background: var(--bad-bg); border-color: var(--bad-line); }

/* §151: the chart drawn as a mouth — the SVG rows on the exam page. All
   colour lives here (the CSP allows no style attributes), and every class
   maps onto the same tones the rest of the product speaks. */
.periovis { overflow-x: auto; }
.periovis svg { display: block; margin: 8px 0; }
.pv-graph { fill: var(--n-100); }
.pv-guide { stroke: var(--n-300); stroke-width: 1; }
.pv-trace { fill: none; stroke: var(--bad); stroke-width: 1.8; stroke-linejoin: round; }
.pv-dot { fill: var(--n-300); }
.pv-dot--bled { fill: var(--bad); }
.pv-num { fill: var(--ink); font-size: 11px; font-variant-numeric: tabular-nums; }
.pv-num--bad { fill: var(--bad); font-weight: 700; }
.pv-loss { fill: var(--navy-soft); font-size: 11px; font-weight: 600; font-variant-numeric: tabular-nums; }
.pv-loss--bad { fill: var(--bad); font-weight: 700; }
.pv-band { fill: var(--muted); font-size: 10.5px; font-weight: 600; font-variant-numeric: tabular-nums; }
.pv-label { fill: var(--muted); font-size: 10.5px; }
.pv-crown { stroke-width: 1.4; }
.pv-crown--quiet { fill: var(--white); stroke: var(--line); }
.pv-crown--missing { fill: var(--bg); stroke: var(--line); stroke-dasharray: 3 3; }
.pv-crown--good { fill: var(--good-bg); stroke: var(--good-line); }
.pv-crown--warn { fill: var(--warn-bg); stroke: var(--warn-line); }
.pv-crown--bad { fill: var(--bad-bg); stroke: var(--bad-line); }
.pv-furc--warn { fill: var(--warn); }
.pv-furc--bad { fill: var(--bad); }
.pv-furcnum { font-size: 9.5px; font-weight: 700; }
.pv-mobbox { fill: var(--warn-bg); stroke: var(--warn-line); }
.pv-mob { fill: var(--warn); font-size: 10.5px; font-weight: 700; }

/* §151: the endo record's tooth diagram. */
.rcvis { text-align: center; }
.rcvis svg { display: inline-block; }
.rcvis__key { font-size: 0.8125rem; margin-top: 0; }
.rc-outline { fill: var(--white); stroke: var(--n-300); stroke-width: 1.6; }
.rc-chamber { fill: var(--b-100); stroke: var(--b-200); }
.rc-canal { fill: none; stroke-linecap: round; stroke-width: 3.2; }
.rc-canal--filled { stroke: var(--navy); }
.rc-canal--open { stroke: var(--warn); stroke-dasharray: 5 4; }
.rc-tip--filled { fill: var(--b-accent); }
.rc-tip--open { fill: var(--warn); }
.rc-label { font-size: 10.5px; font-weight: 700; }
.rc-label--filled { fill: var(--navy); }
.rc-label--open { fill: var(--warn); }
.rc-empty { fill: var(--n-400); font-size: 11px; }

/* §154: the picker's resting tint — an open finding on that tooth. The
   checked rule below it in this file keeps priority, so a ticked tooth is
   navy whatever was found there. */
.tooth--hint .crown { fill: var(--warn-bg); stroke: var(--warn-line); }
.tooth--hint .tooth__num { color: var(--warn); font-weight: 650; }
.tooth--hint input:checked ~ .tooth__crown .crown {
  fill: var(--navy-soft);
  stroke: var(--navy-soft);
}

/* The dictation button on the visit note — DECISIONS §199.

   Sits at the right-hand end of a field's label rather than under the box:
   the label row is dead space, and a control below the textarea pushes the
   next field down on the phone where this is actually used. Small and quiet
   on purpose — it is an alternative to typing, not the way in. */
.mic {
  align-items: center;
  background: var(--white);
  border: 1px solid var(--line);
  border-radius: var(--radius-pill);
  color: var(--n-700);
  cursor: pointer;
  display: inline-flex;
  float: right;
  font: inherit;
  font-size: 0.75rem;
  font-weight: 600;
  gap: 6px;
  /* Comfortably tappable on a phone without growing the label row. */
  min-height: 30px;
  padding: 3px 12px;
  /* A fraction of a second of pressing selects the label on Android, which
     swallows the tap and offers Copy and Share on the word Dictate — seen on
     a real phone. A button's caption is a control, not content. */
  -webkit-user-select: none;
  user-select: none;
  /* And a tap should be a tap: no double-tap zoom delay on this control. */
  touch-action: manipulation;
}
.mic:hover { background: var(--n-25); border-color: var(--n-400); }
.mic:focus-visible { box-shadow: var(--focus); outline: none; }

/* The dot carries the state, so "is it listening" survives the label text
   being read at a glance from arm's length over a patient. */
.mic__dot {
  background: var(--n-400);
  border-radius: 50%;
  height: 8px;
  width: 8px;
}
.mic--on { background: var(--bad-bg); border-color: var(--bad-line); color: var(--bad); }
.mic--on .mic__dot { background: var(--bad); animation: mic-pulse 1.4s ease-in-out infinite; }

@keyframes mic-pulse { 50% { opacity: 0.25; } }

/* Somebody who has switched motion off still gets the colour change. */
@media (prefers-reduced-motion: reduce) {
  .mic--on .mic__dot { animation: none; }
}

.mic__note { margin-top: -4px; }

/* Where dictation says why it stopped (§199). Sits under the three boxes and
   above the save button, so the sentence is between the thing that failed and
   the thing they were about to press. */
.mic__status { margin-top: 8px; }

/* Voice entry on the gum chart — DECISIONS §200. */

/* The toggle holds the microphone for the whole sextant, so it sits full
   width above the teeth rather than floating in a label like the note's
   per-field buttons. */
.mic--wide { display: flex; float: none; justify-content: center; margin-bottom: 10px; min-height: 38px; width: 100%; }

/* Where the next spoken number will land. The dictating doctor reads the
   chart from arm's length over a patient, so the mark is a heavy ring, not
   a subtle tint. */
.periosite input.periosite__cursor {
  border-color: var(--b-600);
  box-shadow: 0 0 0 2px var(--b-600);
}

/* What was heard, and what was refused. The read-back is the safety, so it
   stays visible under the toggle rather than flashing. */
.periovoice__status { margin: 0 0 10px; }
.periovoice__status--bad { color: var(--bad); }

/*
 * Tooth notation — DECISIONS §212.
 *
 * Every tooth number is rendered three times (lib/viewGlobals toothRef) and
 * <body data-notation> decides which one is visible. No attribute at all —
 * a page rendered outside the clinic app — reads as FDI, the stored form.
 */
.tn { display: none; }
body[data-notation="fdi"] .tn--fdi,
body[data-notation="universal"] .tn--universal,
body[data-notation="palmer"] .tn--palmer,
body:not([data-notation]) .tn--fdi { display: inline; }

.notation {
  align-items: center;
  display: flex;
  flex-wrap: wrap;
  gap: 6px;
  margin: 0 0 12px;
}
.notation__legend { color: var(--muted); font-size: 0.85rem; margin-right: 2px; }
.notation .seg__opt { margin: 0; position: relative; }
.notation .seg__opt span {
  font-size: 0.85rem;
  line-height: 18px;
  min-height: 0;
  padding: 5px 10px;
}

/* ------------------------------------------- the Teeth tab (§213)

   One chart, centred, with the tooth's panel and the whole-mouth panel
   under it. The chart rows themselves are the §61/§117 picker and the §116
   read chart, unchanged; this is the frame around them.
*/
.mouth__head {
  align-items: flex-start;
  display: flex;
  flex-wrap: wrap;
  gap: 8px 16px;
  justify-content: space-between;
  margin-bottom: 8px;
}
.mouth__title { font-size: 1.125rem; margin: 0; }
.mouth__sub { font-size: 0.875rem; margin: 2px 0 0; }
.mouth__head .notation { margin: 0; }
.mouth__tools {
  align-items: center;
  display: flex;
  flex-wrap: wrap;
  gap: 8px;
  justify-content: space-between;
  margin: 4px 0 8px;
}
.mouth__tools .toothlayers { margin: 0; }
/* The chart never pushes the card wider than the screen: a narrow phone
   shrinks the crowns (they are flex-basis, not width), and if something
   still will not fit the chart scrolls inside its own box rather than the
   page scrolling sideways. */
.mouth__chart {
  align-items: center;
  display: flex;
  flex-direction: column;
  min-width: 0;
  overflow-x: auto;
  padding: 4px 0;
}
.mouth__chart .teeth { margin: 4px auto 8px; max-width: 100%; }
.mouth__chart .teeth__milk { align-self: stretch; text-align: center; }
.mouth__chart .toothkey { justify-content: center; margin-bottom: 4px; }
.mouthcols { display: grid; gap: 12px; margin-top: 12px; }
@media (min-width: 900px) {
  .mouthcols { grid-template-columns: minmax(0, 1fr) minmax(0, 1fr); align-items: start; }
}
.mouthpanel {
  background: var(--n-25);
  border: 1px solid var(--line);
  border-radius: var(--radius);
  min-width: 0;
  padding: 14px 16px 16px;
}
.mouthpanel > .toothhead:first-child { margin-top: 0; }
.mouthpanel .toothhead + .toothhead { margin-top: 14px; }
.mouthpanel__sub { color: var(--muted); font-size: 0.8125rem; margin: -4px 0 10px; }
/* The tooth's doors: the note-a-finding fold takes the full width (it opens
   into a form), the root canal button sits under it. Not .chain, which
   turns into a two-column grid on a phone and would squeeze the fold. */
.mouthpanel__doors { display: flex; flex-wrap: wrap; gap: 10px; margin-top: 10px; }
.mouthpanel__doors details.act { flex: 1 1 100%; }
.mouthpanel__doors > .btn { flex: 1 1 auto; }
.mouthpanel .chain { margin-top: 10px; }
/* One thumb-wide door on a phone rather than a button floating at the right. */
@media (max-width: 719px) {
  .mouth__mark { flex: 1 1 100%; text-align: center; }
}

/* Marking findings: the strip, the chart as tick boxes, one row of fields. */
.exam__strip {
  align-items: center;
  display: flex;
  gap: 8px;
  justify-content: space-between;
  margin: 0 0 8px;
}
.exam__fields { display: grid; gap: 10px; margin-top: 8px; }
.exam__fields .field { margin: 0; }
@media (min-width: 720px) {
  .exam__fields { align-items: end; grid-template-columns: minmax(0, 1fr) minmax(0, 1.4fr) auto; }
}
/* On a phone the save button rides at the foot of the screen while the
   chart scrolls, so a long chart never hides the one button that matters. */
@media (max-width: 719px) {
  .exam__save { bottom: 12px; box-shadow: var(--shadow-1); position: sticky; z-index: 2; }
}

/* ------------------------------------------- what got done (§214)

   One line, three answers. The legend is the line as the plan screen
   prints it; the segmented radios under it read left to right from "no
   change" to "finished", and the first is checked so a review visit costs
   no taps. */
.donerow { border: 0; margin: 0 0 14px; padding: 0; }
.donerow__what { display: block; margin-bottom: 6px; }
.donerow__seg { margin: 0; }
.donerow__seg .seg__opt { margin: 0 6px 6px 0; position: relative; }
.field--short input { max-width: 120px; }

/* ------------------------------------------- instalments (§215)

   The schedule in the plan's money rail: one line per instalment, the
   state at the right in the money block's own colours. */
.instal { margin: 12px 0 0; }
.instal__rows { margin: 4px 0 8px; }
.instal__row {
  align-items: baseline;
  display: grid;
  gap: 8px;
  grid-template-columns: minmax(0, 1fr) auto auto;
  padding: 4px 0;
  border-top: 1px solid var(--line);
}
.instal__row:first-child { border-top: 0; }
.instal__due { color: var(--muted); }
.instal__amt { font-variant-numeric: tabular-nums; }
.instal__state { color: var(--muted); font-size: 0.8125rem; text-align: right; }
.instal__row--paid .instal__state { color: var(--good); }
.instal__row--due .instal__state { color: var(--warn); font-weight: 650; }
.instal__row--overdue .instal__state { color: var(--bad); font-weight: 650; }
.instal .plan-edit form + form { margin-top: 8px; }
