/* Dentista web client.
   The palette is the one the marketing site and the apps already use — teal
   for action, clay for anything clinical that needs attention — so the browser
   client reads as the same product rather than a lookalike. */

/* Themes are resolved in JavaScript (js/theme.js), which stamps
   data-theme="light" or "dark" on <html> — including for the "system" setting,
   which it resolves via matchMedia and re-stamps when the OS flips. So the
   palette is written exactly once per theme instead of once per theme per
   selector, which is what stops the two from drifting apart as tokens are
   added. An inline script in index.html does the first stamp before paint, so
   there is no flash of the wrong theme. The app is JavaScript-driven
   throughout, so this adds no failure mode that did not already exist — and
   the bare :root block below is a complete, correct light theme regardless. */

:root, :root[data-theme="light"] {
  --teal:        #008080;
  --teal-bright: #0F9C9C;
  --teal-deep:   #04595A;
  --teal-wash:   rgba(0, 128, 128, 0.07);
  --teal-edge:   rgba(0, 128, 128, 0.18);

  --paper:      #FFFFFF;
  --slate:      #F8FAFC;
  --raised:     #FFFFFF;
  --line:       #E2E8F0;
  --line-soft:  #F1F5F9;

  --ink:        #1D1D1F;
  --ink-soft:   #55585F;
  --ink-faint:  #8A8F98;

  --clay:       #9C4A2F;
  --clay-wash:  #FDF3F0;
  --clay-edge:  rgba(156, 74, 47, 0.22);

  --good:       #1F7A4D;
  --good-wash:  #F0F9F4;

  --info:       #2C5FA8;
  --info-wash:  #EFF4FC;
  --warn:       #8A5A16;
  --warn-wash:  #FBF4E7;

  --sans: "Instrument Sans", -apple-system, BlinkMacSystemFont, system-ui, sans-serif;
  --mono: "IBM Plex Mono", ui-monospace, "SF Mono", Menlo, monospace;


  /* Odontogram. These are the exact values in Dentista/Views/ToothPalette.swift,
     which is the point of writing them out rather than deriving them from the
     tokens above: they are hand-tuned clinical codes, not theme colours, and a
     chart must not change meaning because somebody picked a nicer accent.
     Grouped by what a condition means — clay is the one warm hue and belongs to
     caries alone, everything restorative is teal varied by weight (sealant
     lightest, crown deepest), and an absent tooth is neutral grey. */
  --tooth-caries:     #9C4A2F;
  --tooth-filling:    #008080;
  --tooth-sealant:    #5FB0A8;
  --tooth-crown:      #04595A;
  --tooth-bridge:     #2C6E7F;
  --tooth-implant:    #3F7D6B;
  --tooth-root_canal: #6B7A99;
  --tooth-missing:    #8A8F98;
  --tooth-unerupted:  #AFB4BC;
  --tooth-exfoliated: #AFB4BC;
  --tooth-unknown:    #8A8F98;

  /* The tooth number under the cell. Runs darker than the matching zone fill:
     a colour tuned to read as a filled trapezoid does not carry at caption
     size. The two greys are ordered against each other and both stay out of
     the way — faintest of all is unexamined, the only state making no claim. */
  --tooth-num-sound:      #2F6B3F;
  --tooth-num-treatment:  #854F0B;
  --tooth-num-absent:     #A5AAB2;
  --tooth-num-unexamined: #C3C7CD;

  --tooth-line: rgba(138, 143, 152, 0.45);
  --tooth-edge: rgba(138, 143, 152, 0.35);
  --tooth-haze: rgba(138, 143, 152, 0.16);
  --shadow: 0 1px 2px rgba(15, 23, 42, 0.04), 0 4px 16px rgba(15, 23, 42, 0.05);
  --radius: 12px;

  /* The date picker, the sort menu and the scrollbars are drawn by the
     browser, not by this stylesheet. Without this they stay light on a dark
     page — which is how a themed app gives itself away. */
  color-scheme: light;
}

:root[data-theme="dark"] {
  --teal:        #3AC4C4;
  --teal-bright: #5BD9D9;
  --teal-deep:   #0B3B3C;
  --teal-wash:   rgba(58, 196, 196, 0.10);
  --teal-edge:   rgba(58, 196, 196, 0.24);

  --paper:      #101215;
  --slate:      #16191E;
  --raised:     #1A1E24;
  --line:       #2A2F37;
  --line-soft:  #21252C;

  --ink:        #ECEEF1;
  --ink-soft:   #A8AEB8;
  --ink-faint:  #757C87;

  --clay:       #E08A6B;
  --clay-wash:  rgba(224, 138, 107, 0.10);
  --clay-edge:  rgba(224, 138, 107, 0.28);

  --good:       #56C98C;
  --good-wash:  rgba(86, 201, 140, 0.10);

  --info:       #7FB0F0;
  --info-wash:  rgba(127, 176, 240, 0.12);
  --warn:       #E0B36B;
  --warn-wash:  rgba(224, 179, 107, 0.12);


  --tooth-caries:     #D2795A;
  --tooth-filling:    #2FB3B3;
  --tooth-sealant:    #8ACFC6;
  --tooth-crown:      #4E9EA0;
  --tooth-bridge:     #6FB2C4;
  --tooth-implant:    #6FBBA2;
  --tooth-root_canal: #9BA8C4;
  --tooth-missing:    #9AA0A8;
  --tooth-unerupted:  #767C85;
  --tooth-exfoliated: #767C85;
  --tooth-unknown:    #9AA0A8;

  --tooth-num-sound:      #7FBF8F;
  --tooth-num-treatment:  #E0A64A;
  --tooth-num-absent:     #7E848C;
  --tooth-num-unexamined: #62676E;

  --tooth-line: rgba(168, 174, 184, 0.45);
  --tooth-edge: rgba(168, 174, 184, 0.35);
  --tooth-haze: rgba(168, 174, 184, 0.16);
  --shadow: 0 1px 2px rgba(0, 0, 0, 0.4), 0 4px 16px rgba(0, 0, 0, 0.3);

  color-scheme: dark;
}

* { box-sizing: border-box; }

html { -webkit-text-size-adjust: 100%; }

body {
  margin: 0;
  background: var(--slate);
  color: var(--ink);
  font-family: var(--sans);
  line-height: 1.55;
  -webkit-font-smoothing: antialiased;
  overflow-x: hidden;
}

a { color: var(--teal); text-underline-offset: 3px; }
:focus-visible { outline: 2px solid var(--teal); outline-offset: 2px; border-radius: 4px; }

h1, h2, h3 { text-wrap: balance; margin: 0; line-height: 1.2; }

.eyebrow {
  font-family: var(--mono);
  font-size: 0.66rem;
  letter-spacing: 0.14em;
  text-transform: uppercase;
  color: var(--ink-faint);
}

.num { font-variant-numeric: tabular-nums; }

/* ------------------------------------------------------------------ shell */

/* A rail rather than a row of tabs. Five destinations in three groups need
   vertical room and a heading each, and the groups carry information a flat
   row could not: two screens answer a question with somebody in the chair,
   one is money, the last two are the practice itself. Below 960px there is no
   room for a rail, so the same markup lies down into the bar it used to be. */

.app { min-height: 100dvh; display: grid; grid-template-columns: 248px minmax(0, 1fr); }

.sidebar {
  position: sticky; top: 0; z-index: 20;
  height: 100dvh; align-self: start;
  display: flex; flex-direction: column; gap: 1.15rem;
  padding: 1.05rem 0.85rem 0.9rem;
  background: var(--paper);
  border-right: 1px solid var(--line);
}

.brand {
  display: flex; align-items: center; gap: 0.6rem;
  padding: 0.1rem 0.55rem;
  text-decoration: none; color: inherit;
}
.brand img { width: 30px; height: 30px; border-radius: 50%; flex: none; }
.brand-name { display: flex; flex-direction: column; min-width: 0; line-height: 1.25; }
.brand strong { font-size: 0.98rem; letter-spacing: -0.01em; }
.brand-sub {
  font-size: 0.72rem; color: var(--ink-faint);
  white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
}

.nav {
  flex: 1; min-height: 0; overflow-y: auto;
  display: flex; flex-direction: column; gap: 1.2rem;
}
.nav-group { display: flex; flex-direction: column; gap: 0.1rem; }
.nav-head { padding: 0 0.6rem 0.25rem; }

.nav a {
  display: flex; align-items: center; gap: 0.65rem;
  padding: 0.46rem 0.6rem;
  border-radius: 9px;
  font-size: 0.9rem; font-weight: 500;
  color: var(--ink-soft); text-decoration: none;
}
.nav a:hover { background: var(--line-soft); color: var(--ink); }
/* A link out is marked as one. The arrow is a glyph rather than another icon
   because it belongs to the label, not beside it — it should wrap and fade with
   the words it qualifies. */
.nav a.nav-ext > span::after {
  content: '↗';
  margin-left: 0.3rem;
  font-size: 0.85em;
  color: var(--ink-faint);
}
.nav a.nav-ext:hover > span::after { color: inherit; }
.nav a[aria-current="page"] { background: var(--teal-wash); color: var(--teal); }

.ico { flex: none; line-height: 0; color: var(--ink-faint); }
.ico svg { width: 18px; height: 18px; display: block; }
.nav a:hover .ico, .nav a[aria-current="page"] .ico { color: inherit; }

.side-foot {
  display: flex; align-items: center; gap: 0.4rem;
  padding: 0.8rem 0.15rem 0;
  border-top: 1px solid var(--line-soft);
}
.who { display: flex; flex-direction: column; min-width: 0; line-height: 1.25; flex: 1; }
.who-name {
  font-size: 0.84rem;
  white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
}
.who-role { font-size: 0.7rem; }
.side-foot button.small { padding: 0.3rem 0.55rem; }

main { width: 100%; max-width: 1180px; margin: 0 auto; padding: 1.9rem 1.75rem 4rem; }

.page-head {
  display: flex; align-items: flex-end; gap: 1rem;
  flex-wrap: wrap; margin-bottom: 1.35rem;
}
.page-head h1 { font-size: 1.55rem; letter-spacing: -0.02em; }
.page-head .spacer { flex: 1; }

/* No room for a rail. Same markup, laid down: the groups collapse away with
   `display: contents` so their links become items of the one horizontal strip.

   The strip is left-aligned on purpose. A flex row that overflows its own
   `overflow-x` container can only be scrolled towards its end, so pushing it
   right with `flex-end` or `margin-left: auto` would put the first tabs
   somewhere no gesture reaches. */
@media (max-width: 959px) {
  .app { display: block; }

  .sidebar {
    height: auto;
    flex-direction: row; align-items: center; gap: 0.6rem;
    padding: 0.5rem 0.9rem;
    border-right: none; border-bottom: 1px solid var(--line);
  }

  .brand { padding: 0; }
  .brand-sub { display: none; }

  .nav { flex: 1; flex-direction: row; gap: 0.1rem; overflow-x: auto; overflow-y: hidden; }
  .nav::-webkit-scrollbar { display: none; }
  .nav { scrollbar-width: none; }
  .nav-group { display: contents; }
  .nav-head { display: none; }
  .nav a { padding: 0.35rem 0.6rem; white-space: nowrap; }

  .side-foot { padding: 0; border-top: none; }
  .who { display: none; }

  main { padding: 1.25rem 0.9rem 3rem; }
  .page-head { gap: 0.75rem; }
  .page-head h1 { font-size: 1.35rem; }
  th, td { padding-left: 0.75rem; padding-right: 0.75rem; }
}

/* Narrower still (small phones): the strip takes its own row, and the icons
   give way to the words. An icon-only rail would be five guesses. */
@media (max-width: 560px) {
  .sidebar { flex-wrap: wrap; }
  .nav { order: 3; flex: 1 0 100%; }
  .nav .ico { display: none; }
  .side-foot { margin-left: auto; }
  .brand strong { font-size: 0.92rem; }
}

/* ------------------------------------------------------------------ glance */

/* 150px, not 168: at 375px the four figures have to come down to two columns
   rather than one, or the day's appointments start below a screen and a half
   of summary — which inverts what the page is for. */
.stats {
  display: grid; gap: 0.9rem; margin-bottom: 1.3rem;
  grid-template-columns: repeat(auto-fit, minmax(150px, 1fr));
}

.stat {
  min-width: 0;
  display: flex; flex-direction: column; gap: 0.25rem;
  padding: 0.9rem 1.05rem 1rem;
  background: var(--raised);
  border: 1px solid var(--line);
  border-radius: var(--radius);
  box-shadow: var(--shadow);
  color: inherit; text-decoration: none;
}
a.stat:hover { border-color: var(--teal-edge); }

.stat-value {
  font-size: 1.85rem; font-weight: 600;
  letter-spacing: -0.03em; line-height: 1.15;
  font-variant-numeric: tabular-nums;
}
.stat-value.money   { font-size: 1.3rem; letter-spacing: -0.02em; }
.stat-value.owing   { color: var(--clay); }
.stat-value.settled { color: var(--good); }
.stat-value.pending { color: var(--line); }

.stat-sub { line-height: 1.35; }

.stat-parts {
  display: flex; flex-wrap: wrap; gap: 0.1rem 0.8rem;
  margin-top: 0.2rem; font-size: 0.78rem;
}
.stat-part { display: inline-flex; align-items: center; gap: 0.32rem; }
.stat-part .num { font-weight: 600; }
.dot { width: 7px; height: 7px; border-radius: 50%; }
.dot.good    { background: var(--good); }
.dot.teal    { background: var(--teal); }
.dot.neutral { background: var(--ink-faint); }

@media (max-width: 560px) {
  .stats { gap: 0.7rem; }
  .stat { padding: 0.7rem 0.8rem 0.75rem; }
  .stat-value { font-size: 1.5rem; }
  .stat-value.money { font-size: 1.02rem; }
  .stat-sub { font-size: 0.76rem; }
  .stat-parts { gap: 0.05rem 0.55rem; font-size: 0.72rem; }
}

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

.card {
  min-width: 0;
  background: var(--raised);
  border: 1px solid var(--line);
  border-radius: var(--radius);
  box-shadow: var(--shadow);
  overflow: hidden;
}

.card + .card { margin-top: 1.1rem; }

/* The chart card is full width and the two columns follow it, so its next
   sibling is the grid rather than another card — which left the one card on
   the page sitting flush against what comes below it. */
.card + .grid-2 { margin-top: 1.1rem; }

.card > header {
  display: flex; align-items: center; gap: 0.75rem;
  padding: 0.85rem 1.1rem;
  border-bottom: 1px solid var(--line-soft);
  /* The patients header is a count, a search term of any length, and a
     control. On a phone that does not fit, and a control clipped by the card's
     own rounding is a control she cannot reach. Let it drop to a second row. */
  flex-wrap: wrap;
}
.card > header h2 { font-size: 0.95rem; font-weight: 600; }
.card > header .spacer { flex: 1; }

.card-body { padding: 1.1rem; }

/* `min-width: 0` on the columns: a grid item defaults to `min-width: auto`,
   which means it refuses to shrink below its widest content — and the widest
   content here is a table. Without this the tables push the column past the
   viewport on a phone instead of scrolling inside their own `.scroller`. */
.grid-2 { display: grid; gap: 1.1rem; grid-template-columns: 1fr; }
.grid-2 > * { min-width: 0; }
@media (min-width: 860px) { .grid-2 { grid-template-columns: 1.55fr 1fr; align-items: start; } }

/* ----------------------------------------------------------------- tables */

.scroller { overflow-x: auto; }

table { width: 100%; border-collapse: collapse; font-size: 0.9rem; }

th {
  text-align: left;
  font-family: var(--mono);
  font-size: 0.64rem;
  letter-spacing: 0.12em;
  text-transform: uppercase;
  color: var(--ink-faint);
  font-weight: 400;
  padding: 0.55rem 1.1rem;
  border-bottom: 1px solid var(--line-soft);
  white-space: nowrap;
}

td { padding: 0.7rem 1.1rem; border-bottom: 1px solid var(--line-soft); vertical-align: top; }
tbody tr:last-child td { border-bottom: none; }
tbody tr.tappable:hover { background: var(--teal-wash); cursor: pointer; }

td.right, th.right { text-align: right; }
td.money { font-variant-numeric: tabular-nums; white-space: nowrap; }

/* ------------------------------------------------------------------ bits  */

.pill {
  display: inline-block;
  padding: 0.1rem 0.55rem;
  border-radius: 999px;
  font-size: 0.72rem;
  font-weight: 500;
  border: 1px solid transparent;
  white-space: nowrap;
}
.pill.neutral { background: var(--line-soft); color: var(--ink-soft); }
.pill.teal    { background: var(--teal-wash); color: var(--teal); border-color: var(--teal-edge); }
.pill.good    { background: var(--good-wash); color: var(--good); }
.pill.alert   { background: var(--clay-wash); color: var(--clay); border-color: var(--clay-edge); }
.pill.info    { background: var(--info-wash); color: var(--info); }
.pill.warn    { background: var(--warn-wash); color: var(--warn); }

button, .button {
  font: inherit;
  font-size: 0.88rem;
  font-weight: 500;
  padding: 0.45rem 0.95rem;
  border-radius: 8px;
  border: 1px solid var(--line);
  background: var(--raised);
  color: var(--ink);
  cursor: pointer;
  text-decoration: none;
  display: inline-flex; align-items: center; gap: 0.45rem;
}
button:hover, .button:hover { border-color: var(--teal-edge); color: var(--teal); }

button.primary {
  background: var(--teal); border-color: var(--teal); color: #fff;
}
button.primary:hover { background: var(--teal-bright); border-color: var(--teal-bright); color: #fff; }

input[type="search"], input[type="date"] {
  font: inherit;
  font-size: 0.9rem;
  padding: 0.45rem 0.75rem;
  border: 1px solid var(--line);
  border-radius: 8px;
  background: var(--raised);
  color: var(--ink);
  min-width: 0;
}
input[type="search"] { width: min(340px, 100%); }

.muted { color: var(--ink-faint); }
.soft  { color: var(--ink-soft); }
.small { font-size: 0.82rem; }

.empty { padding: 2.25rem 1.1rem; text-align: center; color: var(--ink-faint); font-size: 0.9rem; }

select.sorter {
  font: inherit; font-size: 0.84rem;
  padding: 0.24rem 0.4rem;
  border: 1px solid var(--line);
  border-radius: 8px;
  background: var(--raised);
  color: var(--ink);
  cursor: pointer;
}

/* ----------------------------------------------------------------- people */

/* Patients have no photograph — the record has never held one — so a name
   gets initials on a hue derived from it. Twelve hues, well apart, at a
   saturation that stays behind the teal rather than competing with it. */
.person { display: flex; align-items: center; gap: 0.65rem; min-width: 0; }
.person-lines { min-width: 0; }
.person-name { font-weight: 500; }

.avatar {
  --hue: 200;
  flex: none;
  width: 32px; height: 32px;
  border-radius: 50%;
  display: inline-flex; align-items: center; justify-content: center;
  font-size: 0.71rem; font-weight: 600; letter-spacing: 0.02em;
  background: hsl(var(--hue) 40% 90%);
  color: hsl(var(--hue) 48% 27%);
  user-select: none;
}
:root[data-theme="dark"] .avatar {
  background: hsl(var(--hue) 20% 25%);
  color: hsl(var(--hue) 45% 76%);
}

/* Rows built around a disc read better centred on it; rows of stacked notes
   (treatments, the audit trail) still hang from the top. */
table.people td { vertical-align: middle; }

/* On a phone the name cell is narrow enough to wrap, and a disc centred
   against three wrapped lines floats away from the name it belongs to. Smaller,
   and hung from the first line. */
/* A phone number that wraps stops being a phone number. */
td.phone { white-space: nowrap; }

@media (max-width: 560px) {
  .person { align-items: flex-start; gap: 0.45rem; }
  .avatar { width: 24px; height: 24px; font-size: 0.62rem; margin-top: 1px; }

  /* Three columns do not fit in 375px without shredding the names. The number
     is one tap away in the record; finding the person is what the list is
     for, so the column that goes is the phone. */
  table.roster .phone { display: none; }

  /* And the word "Sort" goes with it: every option names what it sorts by,
     so the label was only ever saying it twice. */
  .sort-label { display: none; }
}

/* --------------------------------------------------------------- patient  */

.identity { display: flex; align-items: baseline; gap: 0.75rem; flex-wrap: wrap; }
.identity h1 { font-size: 1.6rem; letter-spacing: -0.02em; }

.facts {
  display: grid; gap: 0.85rem 1.75rem;
  grid-template-columns: repeat(auto-fit, minmax(140px, 1fr));
}
.fact .eyebrow { display: block; margin-bottom: 0.15rem; }
.fact .value { font-size: 0.94rem; }

.alerts {
  border: 1px solid var(--clay-edge);
  background: var(--clay-wash);
  border-radius: var(--radius);
  padding: 0.9rem 1.1rem;
  margin-bottom: 1.1rem;
}
.alerts h2 { font-size: 0.8rem; color: var(--clay); letter-spacing: 0.04em; text-transform: uppercase; font-family: var(--mono); font-weight: 400; }
.alerts p { margin: 0.4rem 0 0; font-size: 0.92rem; }

.totals { display: grid; gap: 0.9rem; }
.total-row { display: flex; align-items: baseline; justify-content: space-between; gap: 1rem; }
.total-row .label { font-size: 0.86rem; color: var(--ink-soft); }
.total-row .value { font-variant-numeric: tabular-nums; font-size: 1rem; }
.total-row.headline { padding-top: 0.85rem; border-top: 1px solid var(--line-soft); }
.total-row.headline .label { color: var(--ink); font-weight: 600; font-size: 0.92rem; }
.total-row.headline .value { font-size: 1.35rem; font-weight: 600; letter-spacing: -0.02em; }
.total-row.headline.owing .value { color: var(--clay); }
.total-row.headline.settled .value { color: var(--good); }

.note {
  font-size: 0.8rem; color: var(--ink-faint);
  padding: 0.75rem 1.1rem; border-top: 1px solid var(--line-soft);
  background: var(--slate);
}

/* ------------------------------------------------------------ theme knob -- */

.theme-toggle {
  padding: 0.4rem;
  line-height: 0;
  border-radius: 8px;
}
.theme-toggle svg { width: 17px; height: 17px; display: block; }

/* The device setting. Full width because the two options are sentences, not
   words, and a select that truncates the sentence explaining what it does is
   worse than no explanation. */
.device-choice { margin-top: 0.75rem; }
/* Past the global `select { max-width: 210px }`, which is there to stop the
   audit filters growing to fill a card. These two options are sentences. */
.device-choice select {
  width: 100%; max-width: none;
  padding: 0.4rem 0.5rem; font-size: 0.88rem;
}

/* ---------------------------------------------------------------- audit --- */

.filters {
  display: flex; flex-wrap: wrap; gap: 0.6rem; align-items: center;
  padding: 0.85rem 1.1rem;
  border-bottom: 1px solid var(--line-soft);
  background: var(--slate);
}
.filters label {
  display: inline-flex; align-items: center; gap: 0.4rem;
  font-size: 0.8rem; color: var(--ink-soft);
}

select {
  font: inherit;
  font-size: 0.85rem;
  padding: 0.35rem 0.6rem;
  border: 1px solid var(--line);
  border-radius: 8px;
  background: var(--raised);
  color: var(--ink);
  max-width: 210px;
}

.when { white-space: nowrap; font-variant-numeric: tabular-nums; }

/* ------------------------------------------------------- prescriptions --- */

.rx + .rx { border-top: 1px solid var(--line-soft); }
.rx { padding: 0.95rem 1.1rem; }
.rx-head {
  display: flex; align-items: baseline; gap: 0.6rem;
  flex-wrap: wrap; margin-bottom: 0.6rem;
}
.rx-head .who { font-size: 0.82rem; color: var(--ink-faint); }
.rx-line { display: flex; gap: 0.6rem; padding: 0.3rem 0; align-items: baseline; }
.rx-line .no {
  font-family: var(--mono); font-size: 0.7rem; color: var(--ink-faint);
  min-width: 1.4rem;
}
.rx-line .what { flex: 1; min-width: 0; }
.rx-line .sig { font-size: 0.84rem; color: var(--ink-soft); }
.rx-line .qty {
  font-variant-numeric: tabular-nums; font-size: 0.84rem;
  color: var(--ink-soft); white-space: nowrap;
}

/* ----------------------------------------------------------------- states */

/* The gate is teal in BOTH themes, and every colour on it is a literal rather
   than a token. That is deliberate. This is the screen the installed app's
   splash resolves into, and Android paints that splash from a single
   `background_color` in manifest.webmanifest — #008080, which is also the exact
   colour of the disc in the icon. A gate that followed the theme would meet the
   splash cleanly in one theme and flash against it in the other, so it does not
   follow the theme. Anything added here must be written for a teal ground; the
   light/dark tokens do not apply inside this block. */
.gate {
  min-height: 100dvh;
  display: grid; place-items: center; padding: 1.5rem;
  background: #008080;
  color: #FFFFFF;
}
.gate .panel {
  width: min(420px, 100%);
  padding: 2.25rem;
  text-align: center;
}
/* The mark is a teal disc carrying a cream tooth, so on this ground the disc
   *is* the ground and what remains is the tooth — which is precisely what the
   splash showed a moment earlier. The card it used to sit on is gone for the
   same reason: a white panel is the seam this change exists to remove.
   The white ring gives the mark back an edge now that the disc has none. It is
   sized 80px rather than 72 because `box-sizing: border-box` is global here, so
   a border would otherwise eat 8px out of the tooth instead of adding to it.

   The gates use the SQUARE artwork (icon-512-maskable.png, teal to its edges);
   the round one stays on the rail, which is the same split the marketing site
   made. Swapping them needs no resizing — both frame the tooth at 64% of width
   and 66% of height, measured rather than assumed. The 22% corner is the
   app-icon proportion: this artwork is an app icon, and a hard-cornered square
   reads as a cropping mistake. */
.gate img.mark {
  width: 80px; height: 80px;
  border: 4px solid #FFFFFF;
  border-radius: 22%;
  margin-bottom: 1.25rem;
}
/* Every line of text here is solid white, and the hierarchy comes from size
   and weight rather than from opacity. That is forced by the ground: white on
   #008080 is 4.8:1, which clears AA with almost nothing to spare, so *any*
   dimmed white — the usual way to quieten a fineprint — drops under 4.5:1.
   Tinted text fails for the same reason. Do not reintroduce rgba() text here;
   make it smaller instead. */
.gate h1 { font-size: 1.3rem; letter-spacing: -0.02em; margin-bottom: 0.4rem; color: #FFFFFF; }
.gate p { color: #FFFFFF; font-size: 0.9rem; margin: 0 0 1.5rem; }
.gate strong { color: #FFFFFF; }
.gate .fineprint { margin: 1.5rem 0 0; font-size: 0.74rem; line-height: 1.5; color: #FFFFFF; }
/* A sign-in failure. This was an inline `style="color: var(--clay)"` — clay is
   unreadable on teal, and an inline style would have won the cascade whatever
   this file said. It cannot be re-tinted (see above), so it is marked by a
   quiet dark chip instead of by hue, which also lifts its own contrast. */
.gate p.error {
  color: #FFFFFF;
  background: rgba(0, 0, 0, 0.18);
  border-radius: 8px;
  padding: 0.55rem 0.8rem;
  margin-bottom: 1.25rem;
}

/* The button colours invert: cream on teal, rather than teal on cream. */
.gate button {
  width: 100%; justify-content: center; padding: 0.6rem 1rem;
  background: transparent;
  border-color: rgba(255, 255, 255, 0.45);
  color: #FFFFFF;
}
.gate button:hover {
  background: rgba(255, 255, 255, 0.12);
  border-color: rgba(255, 255, 255, 0.72);
  color: #FFFFFF;
}
.gate button.primary { background: #FFFFFF; border-color: #FFFFFF; color: #04595A; }
.gate button.primary:hover { background: #E8F1F1; border-color: #E8F1F1; color: #04595A; }
/* Google's sign-in button specifies Roboto Medium, so the label — and only the
   label — uses it. Self-hosted rather than linked from fonts.googleapis.com:
   this page is shown before anyone has chosen to sign in, and a webfont pulled
   from Google would tell Google that the clinic opened it. Roboto is Apache
   2.0, so serving our own copy is allowed. Latin subset only, 16 KB; the label
   is three English words and will never need more.

   No font-size here on purpose. Google's spec is 14px and `button` is already
   0.88rem/500, which lands on 14px at the default root size *and* still scales
   if the reader has set a larger one. A hard 14px would take that away. */
@font-face {
  font-family: 'Roboto';
  font-style: normal;
  font-weight: 500;
  font-display: swap;
  src: url('fonts/roboto-medium-latin.woff2') format('woff2');
  unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA,
    U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193,
    U+2212, U+2215, U+FEFF, U+FFFD;
}
.gate button.google span {
  font-family: 'Roboto', var(--sans);
  letter-spacing: 0.25px;
}

/* Google's mark keeps its own colours and its own size. It is their asset,
   not part of this palette: it must not pick up the button's `currentColor`,
   and `flex: none` stops the button squeezing it when the label wraps on a
   narrow phone. */
.gate .gmark { flex: none; }
.gate button:disabled { opacity: 0.55; cursor: default; }
/* The global focus ring is `var(--teal)`, which is invisible against this. */
.gate :focus-visible { outline-color: #FFFFFF; }

.banner {
  padding: 0.6rem 1.25rem;
  font-size: 0.85rem;
  border-bottom: 1px solid var(--clay-edge);
  background: var(--clay-wash);
  color: var(--clay);
  display: flex; gap: 0.6rem; align-items: center;
}
.banner .spacer { flex: 1; }

/* Fixed rather than placed in the layout: the warning has to be seen from
   whatever is on screen, and the shell is a grid whose cells it would
   otherwise have to belong to. Sixty seconds of covering the top of the rail
   is the intended trade. */
.idle-warning {
  position: fixed; inset: 0 0 auto 0; z-index: 60;
  box-shadow: var(--shadow);
}
.idle-warning .idle-count { font-variant-numeric: tabular-nums; }
.idle-warning button { border-color: var(--clay-edge); color: var(--clay); }
.idle-warning button:hover { border-color: var(--clay); color: var(--clay); }

.skeleton { padding: 2.5rem 1.1rem; text-align: center; color: var(--ink-faint); font-size: 0.88rem; }

/* A search still in flight. The results that are already on screen stay there
   and fade a little, rather than being replaced by a spinner: what she typed
   two letters ago is nearly always still the right thing to be looking at, and
   emptying the card under her fingers makes the page jump. */
.card[aria-busy="true"] .scroller,
.card[aria-busy="true"] .empty,
.card[aria-busy="true"] .note { opacity: 0.5; }
.card .scroller, .card .empty, .card .note { transition: opacity 0.15s ease; }

@media (prefers-reduced-motion: reduce) {
  .card .scroller, .card .empty, .card .note { transition: none; }
}

/* ------------------------------------------------------------- odontogram */

/* The chart is drawn in SVG by js/chart.js, a port of ToothCell.swift. Sizes
   live here rather than there so the cell scales with the viewport; the strokes
   are non-scaling, so they stay the widths the Swift cell uses at any size. */

.odontogram {
  --tooth: 34px;
  --slot: calc(var(--tooth) + 4px);
  display: flex;
  flex-direction: column;
  gap: 20px;
  padding: 1.4rem 0 0.4rem;
  overflow-x: auto;
}

/* Centred with auto margins rather than `align-items: center`, and the
   difference is not cosmetic. A flex item centred by the container and wider
   than it overflows equally on both sides, and the left overflow cannot be
   scrolled to — at phone width that silently hid teeth 18, 17 and 16 with no
   way to reach them. Auto margins collapse to zero when the row does not fit,
   so the arch starts at the left edge and the whole of it scrolls. */
.odontogram > * {
  width: max-content;
  margin-inline: auto;
}

/* Nested rows sit closer to their own arch than the two jaws sit to each
   other, which is what makes them read as one group rather than four. */
.odontogram.mixed { gap: 8px; }

@media (max-width: 720px) { .odontogram { --tooth: 28px; } }

.arch {
  position: relative;
  display: flex;
  align-items: center;
  gap: 4px;
}

.arch-side { display: flex; gap: 4px; }

/* The label sits in a lane the row reserves for it, rather than floating above
   the row where it lands on the first tooth's hidden-marking badge. */
.arch.labelled { padding-top: 1.35em; }

.arch-label {
  position: absolute;
  left: 0;
  top: 0;
  font-size: 0.7rem;
  line-height: 1.35;
  color: var(--ink-faint);
}

.arch-midline {
  width: 1px;
  align-self: stretch;
  margin: 0 4px;
  background: var(--tooth-line);
}

/* Exactly as wide as `--teeth` teeth would be, so a nested primary arch lines
   each tooth up under the successor that replaces it. */
.arch-indent { width: calc(var(--teeth) * var(--slot) - 4px); }

.arch-divider {
  width: min(520px, 100%);
  height: 1px;
  background: var(--line);
  flex-shrink: 0;
}

.odontogram.mixed .arch-divider { margin: 6px 0; }

.tooth {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 3px;
  padding: 0;
  border: 0;
  background: none;
  cursor: pointer;
  font: inherit;
  border-radius: 5px;
}

.tooth-art { position: relative; display: block; line-height: 0; }

.tooth-cell { width: var(--tooth); height: var(--tooth); display: block; }

.tooth-zone {
  stroke: var(--tooth-edge);
  stroke-width: 0.5;
  vector-effect: non-scaling-stroke;
}

/* An unmarked surface on an examined tooth is drawn clean — the absence of a
   fill rather than a literal white, so it stays clean in both themes instead
   of glowing on a dark chart. An unexamined one keeps a faint haze, like a
   pane nobody has wiped. */
.tooth-zone.clean { fill: none; }
.tooth-zone.haze  { fill: var(--tooth-haze); }

.tooth-outline {
  fill: none;
  stroke: var(--tooth-line);
  vector-effect: non-scaling-stroke;
}

.tooth-cross {
  stroke: var(--tooth-missing);
  stroke-width: 1.5;
  vector-effect: non-scaling-stroke;
}

.tooth-more {
  position: absolute;
  top: -5px;
  right: -6px;
  padding: 0 3px;
  border-radius: 999px;
  background: var(--clay);
  color: #fff;
  font-size: 0.55rem;
  font-weight: 700;
  line-height: 1.5;
}

.tooth-number {
  font-size: 0.68rem;
  font-variant-numeric: tabular-nums;
  line-height: 1;
}

.tooth-number.sound          { color: var(--tooth-num-sound); }
.tooth-number.needsTreatment { color: var(--tooth-num-treatment); }
.tooth-number.absent         { color: var(--tooth-num-absent); }
.tooth-number.unexamined     { color: var(--tooth-num-unexamined); }

.tooth:hover .tooth-art { filter: brightness(1.06); }
.tooth.selected { background: var(--teal-wash); box-shadow: 0 0 0 2px var(--teal-edge); }

/* The key. Without it the restorative teals are eight shades of one colour. */
.chart-legend {
  display: flex;
  flex-direction: column;
  gap: 0.5rem;
  padding: 0.9rem 0 0;
  border-top: 1px solid var(--line-soft);
  margin-top: 1rem;
}

.legend-group { display: flex; flex-wrap: wrap; gap: 0.35rem 0.9rem; }

.legend-item {
  display: inline-flex;
  align-items: center;
  gap: 0.35rem;
  font-size: 0.75rem;
  color: var(--ink-soft);
}

.swatch {
  width: 0.72rem;
  height: 0.72rem;
  border-radius: 3px;
  opacity: 0.85;
}

.legend-number {
  font-size: 0.7rem;
  font-weight: 600;
  font-variant-numeric: tabular-nums;
}

.legend-number.sound          { color: var(--tooth-num-sound); }
.legend-number.needsTreatment { color: var(--tooth-num-treatment); }
.legend-number.absent         { color: var(--tooth-num-absent); }
.legend-number.unexamined     { color: var(--tooth-num-unexamined); }

.chart-filter {
  margin: 0;
  padding: 0.5rem 1.1rem;
  border-top: 1px solid var(--line-soft);
  font-size: 0.78rem;
  color: var(--ink-soft);
  background: var(--teal-wash);
}

/* --------------------------------------------------------------- controls */

.segmented {
  display: inline-flex;
  border: 1px solid var(--line);
  border-radius: 8px;
  overflow: hidden;
}

.segmented button {
  padding: 0.3rem 0.8rem;
  border: 0;
  background: none;
  color: var(--ink-soft);
  font: inherit;
  font-size: 0.82rem;
  cursor: pointer;
}

.segmented button + button { border-left: 1px solid var(--line); }
.segmented button.on { background: var(--teal-wash); color: var(--teal); font-weight: 600; }

/* ---------------------------------------------------- notes and consents */

.notes { margin: 0; padding: 0; list-style: none; display: grid; gap: 1rem; }

.notes li { padding-left: 0.9rem; border-left: 2px solid var(--teal-edge); }

.note-head { display: flex; flex-wrap: wrap; align-items: baseline; gap: 0.5rem; }
.note-date { font-size: 0.75rem; color: var(--ink-faint); font-variant-numeric: tabular-nums; }
.note-what { font-size: 0.88rem; font-weight: 600; }
.note-tooth { font-size: 0.72rem; color: var(--ink-faint); font-variant-numeric: tabular-nums; }
.note-body { margin: 0.2rem 0 0; font-size: 0.86rem; color: var(--ink-soft); }

.signed + .signed { margin-top: 1.1rem; padding-top: 1.1rem; border-top: 1px solid var(--line-soft); }
.signed-head { display: flex; flex-wrap: wrap; align-items: center; gap: 0.5rem; margin-bottom: 0.5rem; }
.signed-date { font-size: 0.75rem; color: var(--ink-faint); font-variant-numeric: tabular-nums; }
.signed .facts { margin-bottom: 0.5rem; }
.signed p { margin: 0; }

.chips { display: flex; flex-wrap: wrap; gap: 0.3rem; }

.chip {
  padding: 0.1rem 0.5rem;
  border: 1px solid var(--teal-edge);
  border-radius: 999px;
  background: var(--teal-wash);
  color: var(--teal);
  font-size: 0.72rem;
}

.mono { font-family: var(--mono); }

/* A day heading inside the coming-up table. A separator row rather than a
   table per day: one table keeps the columns aligned down the whole fortnight,
   which is the thing that makes it scannable. */
tr.day-break td {
  background: var(--slate);
  padding-top: 0.5rem; padding-bottom: 0.5rem;
  border-bottom: 1px solid var(--line);
}
tr.day-break .day-name { font-weight: 600; margin-right: 0.6rem; }
tbody tr.day-break:hover { background: var(--slate); cursor: default; }

/* Retired catalogue entries: kept and dimmed rather than hidden, because a
   withdrawn price is still the price a treatment already in the record was
   charged at. */
tbody tr.faded td { opacity: 0.55; }

/* The row under a paged table. Empty when everything is on screen, which is
   why it is a container the view refills rather than a node it adds. */
.more-row {
  display: flex; align-items: center; gap: 0.75rem;
  padding: 0 1.1rem;
}
.more-row:not(:empty) { padding: 0.9rem 1.1rem; border-top: 1px solid var(--line-soft); }

/* ------------------------------------------------------------------ print */

@page { margin: 14mm; }

@media print {
  .sidebar, .stats, .banner, .no-print { display: none !important; }
  body { background: #fff; }
  .app { display: block; }
  main { max-width: none; padding: 0; }
  .card { border: none; box-shadow: none; break-inside: avoid; }
  .card + .card { margin-top: 1.5rem; }
  .card + .grid-2 { margin-top: 1.5rem; }
  a { color: inherit; text-decoration: none; }

  /* Controls do nothing on paper — except the teeth, which are buttons
     because a tooth is selectable, and are the chart itself. Blanket-hiding
     `button` printed a patient record with two empty arches. */
  button:not(.tooth), .button, input, select, .theme-toggle, .filters, .more-row {
    display: none !important;
  }

  /* Except the one that says what you are looking at: the chart is drawn for
     one dentition and the printed page has to name which, or a mixed-dentition
     chart and a permanent one are indistinguishable once they leave the
     screen. Keep the pressed button, drop the rest of the control. */
  .segmented { border: none; }
  .segmented button { display: none !important; }
  .segmented button.on {
    display: inline-block !important;
    border: none; padding: 0; background: none; font-weight: 600;
  }
  .segmented button.on::before { content: 'Dentition: '; font-weight: 400; }

  /* A record is read as one document, not as two columns of cards. */
  .grid-2 { display: block; }
  .grid-2 > * + * { margin-top: 1.5rem; }

  /* Keep a heading with what it heads, repeat column headers over a break,
     and never split a row down the middle. */
  .card > header { break-after: avoid; }
  thead { display: table-header-group; }
  tr, .fact, .total-row { break-inside: avoid; }

  /* A mono laser turns a tinted pill into a grey smudge, so give the shape
     back its meaning in ink that always prints. */
  .pill {
    background: none !important; color: #000 !important;
    border: 1px solid #767676 !important;
  }
  .alerts { border: 1.5pt solid #000; background: none; }
  .avatar { display: none; }

  /* The chart is usually the reason to print at all. Its colours ARE the
     content — a caries wedge and a filling wedge differ by nothing else — so
     they are exempted from the browser's habit of dropping backgrounds, and
     the arches are kept whole and unscrolled. */
  .odontogram {
    overflow: visible;
    break-inside: avoid;
    print-color-adjust: exact;
    -webkit-print-color-adjust: exact;
  }
  .scroller { overflow: visible; }
  .note { background: none; }
}
