/*
  The whole app's stylesheet. `templates/base.html` is the only thing that
  loads it and every page extends that template, so every rule here is on
  every page. A rule needed by one page goes in its own file loaded through
  `base.html`'s `{% block extra_css %}` instead (issue #137) --
  `account_list.css` first among them; `static/css/` is the current list. A page whose only
  unique styling is a rule or two keeps it here as a named component class
  instead; a stylesheet request per page is not worth three lines.

  It lived inline in base.html until issue #134. Out here WhiteNoise serves it
  precompressed and under a content-hashed filename, so it is fetched once and
  read from cache after that; inline, the identical bytes were re-sent with
  every single page load. They were uncompressed too until issue #141 added
  `GZipMiddleware`, which is no longer a difference between the two -- the
  caching is.

  Which is why the comments below are ordinary CSS comments again. While this
  was inline, Django's template engine had no idea what one was and shipped it
  straight through into the HTML -- so the house rule was `{% comment %}`,
  enforced by a test, and writing down *why* a rule exists cost bytes on every
  request. Out here it costs nothing on any request. Comment freely.

  One hazard the old inline form did not have, found by reading computed styles
  in the browser rather than by any test: a comment that quotes CSS's own
  closing delimiter ends there, and every rule after it is parsed as garbage
  until the next brace. This header did exactly that on its first draft, which
  silently emptied `:root` below and left the app bar transparent while the
  file still returned 200 and the whole suite still passed. Do not write the
  delimiter inside a comment -- say "a CSS comment" instead.
*/
/*
  The "Refined Clinical" palette at Balanced intensity -- issue #257 is the
  spec these values come from, decided against three rounds of mockups.

  Naming, because the restyle added three kinds of token:

  * plain colour (--accent, --mine, --excluded): the thing's own hue -- fills,
    borders, rings.
  * `-ink` (--accent-ink, --mine-ink, --excluded-ink): the text variant,
    wherever hue-coloured text sits -- tint, surface, or page bg. The plain
    hue is never a text colour: a hue tuned to carry --on-accent on its
    fills cannot also clear text contrast, most sharply on the dark
    palettes (issue #281). Non-text marks keep the plain hue -- the subtab
    underline, the app bar's aria-hidden alert glyph -- since a mark needs
    only 3:1 and its meaning lives in adjacent text. A dark theme's -ink
    is *lighter* than its plain hue.
  * --on-accent: text sitting on a solid accent/mine/excluded fill. One token
    rather than #fff at each site so the dark palette can retune it in one
    place. The pairing is a two-sided constraint (issues #281/#285): a
    fill must be dark enough that --on-accent clears 4.5:1 on it --
    enforced for every theme block by ComposedTextPairsStayReadable
    (shifts/tests/test_static_assets.py), with the app's other composed
    text pairs -- while staying light enough to mark at 3:1 against its
    own tint. Only Light's and Dark's mine/excluded are measured to that
    second half; Ember's fills and the dark accents sit below it, which
    is why the guard has no mark side -- one to widen when they retune.
    On the dark side the window is narrow: retune by measuring, not by
    eye.

  --radius / --radius-sm / --radius-ctl is the 12/10/8 scale: cards / grid
  cells and small panels / controls (buttons, inputs, selects).

  --shadow is the resting elevation every raised surface wears; --shadow-hi
  is the raised state a *clickable* element lifts to on hover, plus the one
  at-rest exception: a panel that genuinely floats over the page body
  (account_list's row-menu dropdown). Labels (status pills) stay flat --
  elevation marks controls, not labels.

  --bar-*: the app bar's own colours. In this palette the bar is the accent,
  but the dark palette will split them (its bar is deeper than its accent),
  so nothing but these tokens may colour the bar. Two literals outside CSS
  restate them and cannot read tokens -- base.html's theme-color meta and
  static/manifest.json's theme_color/background_color -- so a palette change
  edits those two files as well.
*/
:root {
  --bg: #f2f5f6;
  --surface: #ffffff;
  --surface-2: #fbfcfc;
  --text: #16262b;
  --muted: #617277;
  --accent: #14505f;
  --accent-ink: #0f4250;
  --accent-light: #e6f0f2;
  --on-accent: #ffffff;
  --mine: #1e8650;
  --mine-light: #e8f6ee;
  --mine-ink: #12703f;
  --excluded: #bd5346;
  --excluded-bg: #fbebe8;
  --excluded-ink: #a8422f;
  --warning: #d9a02c;
  --warning-bg: #fdf4e5;
  --border: #e3e9ea;
  --shift-a: #3f86cf;
  --shift-b: #bd7f2f;
  /* The category left bar, spelled once: every surface that draws it in a
     box-shadow list says var(--cat-bar-a), never the inset literal, so a
     bar retune is one edit (per category) instead of ten. */
  --cat-bar-a: inset 4px 0 0 0 var(--shift-a);
  --cat-bar-b: inset 4px 0 0 0 var(--shift-b);
  --radius: 12px;
  --radius-sm: 10px;
  --radius-ctl: 8px;
  --shadow: 0 1px 2px rgba(16,40,48,.05), 0 1px 1px rgba(16,40,48,.04);
  --shadow-hi: 0 4px 10px rgba(16,40,48,.09), 0 1px 2px rgba(16,40,48,.06);
  /* The confirm dialog's ::backdrop wash: the theme's text tone at .45,
     deepened to black in the dark palettes the way the shadows are. A
     token because ::backdrop is still a page colour a theme must retune,
     even though only the <dialog> ever wears it. */
  --backdrop: rgba(22,38,43,.45);
  /* Hover feedback strength, tokens so each theme sets how strong and in
     which direction: brightness() is multiplicative, so the darken that
     reads on these light fills barely moves an already-dark one, and a
     dark-side theme restates both tokens above 1. -lift is every
     brightness hover's tier (the link chips gave up their own deeper
     dial in #293's retune -- mint a -deep token at the first chip-only
     retune, not before). -solid is the exception tier: a fill that
     wears --on-accent ink is dark in every palette (the contrast suite
     guarantees the white ink reads on it at rest), so no theme's darken
     has headroom there -- those controls lighten in every theme. The
     lighten spends label contrast while the pointer rests: measured at
     ~3.6:1 on the light side and ~3.4:1 on the dark side against the
     4.5:1 rest floor, accepted in PR #294 -- motion and shadow carry
     the state alongside, and a lighten small enough to hold the floor
     would be invisible. Wearers are .btn-primary:hover, the
     assigned/excluded badge hovers and the pressed .toggle-chip's hover --
     that is the full roster; a new solid-fill control joins by adding a
     rule, nothing inherits in. */
  --hover-lift: brightness(0.93);
  --hover-solid: brightness(1.15);
  --bar-bg: var(--accent);
  --bar-line: rgba(255,255,255,0.14);
  --bar-shadow: 0 1px 0 rgba(0,0,0,0.06);
  /* The white-alpha family the bar's nav controls are drawn with -- part of
     the same "nothing but these tokens may colour the bar" boundary, since
     the dark palette's deeper bar will want them retuned together. */
  --bar-outline: rgba(255,255,255,0.32);
  --bar-wash: rgba(255,255,255,0.12);
  --bar-wash-press: rgba(255,255,255,0.18);
}
/* --warning has no -ink variant, deliberately, in any palette: its one
   text use is the flash-warning glyph, which sits on --warning-bg with
   enough contrast as-is in every palette block -- sandstone's pair
   measures no worse than light's own. Mint the token when a warning
   text-on-tint use first appears, not before. */
/*
  Theme plumbing (issue #257). The :root block above is the standard light
  palette -- the default, the forced Light theme, and the light side of
  System. base.html stamps User.theme's name on <html> as data-theme.
  Themes are a flat list: a future palette is one more
  :root[data-theme="..."] block of these same custom properties (with its
  color-scheme line), plus a choice in users/models.py -- the picker
  derives from the choices, and CONVENTIONS.md ("Themes are a flat list")
  carries the full recipe, including the theme-color meta branch a forced
  theme adds in base.html. No crossing with dark.

  color-scheme is which side the browser draws its own widgets on
  (scrollbars, form controls, date pickers): System declares both and the
  device picks; the forced themes pin one side. Every future light-side
  theme declares `light`.

  The dark token block appears twice below -- once as the forced Dark theme,
  once as System on a device set to dark -- because a selector and a media
  query cannot share one rule, and generating one from the other needs a
  build step this app does not have. The one-block alternative, light-dark()
  at every token, was rejected: an engine that predates that function drops
  every colour in the app at computed-value time -- the same silent failure
  class as the color-mix note further down, at total instead of local scale.
  DarkTokenTwinsStayIdentical (shifts/tests/test_static_assets.py) pins the
  twins identical, so an edit to one fails the suite rather than quietly
  forking the palette. The twin is Dark's alone: System resolves only to
  :root's light values or Dark's block, so a second dark-side theme
  (Ember) has no twin and needs none.
*/
:root { color-scheme: light dark; }
:root[data-theme="light"] { color-scheme: light; }
/*
  The forced-dark rules are @media screen so printing falls back to the
  light palette. Print drops backgrounds by default but honours text
  colour, so an unscoped dark block would put near-white ink on white
  paper. System needs no such scoping: print evaluates
  prefers-color-scheme as light, which skips the twin below the same way.
*/
@media screen {
  :root[data-theme="dark"] { color-scheme: dark; }
}
/*
  The dark palette (issue #257, tuned at Balanced). Colours, shadows and
  the hover filters: the radius scale is theme-neutral, and --cat-bar-* needs no
  restatement because the bars reference var(--shift-a/b) at their point of
  use and pick the dark hues up from here. Dark shadows are tuned, not
  scaled -- dark needs disproportionate opacity for a shadow to register at
  all. Three families the issue-257 spec left open are derived here: the
  warning pair, --on-accent (white clears 4.5:1 on the dark accent; the
  dark text tone does not), and the bar's white-alpha family, dialled down
  a step because white reads louder on the deeper bar.
*/
@media screen {
  :root[data-theme="dark"] {
    --bg: #0e1416;
    --surface: #161f22;
    --surface-2: #1a2427;
    --text: #e8eff0;
    --muted: #93a6ab;
    --accent: #2b7d8f;
    --accent-ink: #8ed3e2;
    --accent-light: #1b353d;
    --on-accent: #ffffff;
    --mine: #298553;
    --mine-light: #14301f;
    --mine-ink: #79dfa4;
    --excluded: #b35c50;
    --excluded-bg: #361d19;
    --excluded-ink: #f0a294;
    --warning: #e2ae4e;
    --warning-bg: #322511;
    --border: #222f33;
    --shift-a: #5b9ede;
    --shift-b: #cf9a4e;
    --shadow: 0 1px 2px rgba(0,0,0,.4);
    --shadow-hi: 0 6px 18px rgba(0,0,0,.55);
    --backdrop: rgba(0,0,0,.55);
    --hover-lift: brightness(1.25);
    --hover-solid: brightness(1.15);
    --bar-bg: #112a31;
    --bar-line: rgba(255,255,255,0.10);
    --bar-shadow: 0 1px 0 rgba(0,0,0,0.35);
    --bar-outline: rgba(255,255,255,0.30);
    --bar-wash: rgba(255,255,255,0.10);
    --bar-wash-press: rgba(255,255,255,0.16);
  }
}
@media (prefers-color-scheme: dark) {
  :root[data-theme="system"] {
    --bg: #0e1416;
    --surface: #161f22;
    --surface-2: #1a2427;
    --text: #e8eff0;
    --muted: #93a6ab;
    --accent: #2b7d8f;
    --accent-ink: #8ed3e2;
    --accent-light: #1b353d;
    --on-accent: #ffffff;
    --mine: #298553;
    --mine-light: #14301f;
    --mine-ink: #79dfa4;
    --excluded: #b35c50;
    --excluded-bg: #361d19;
    --excluded-ink: #f0a294;
    --warning: #e2ae4e;
    --warning-bg: #322511;
    --border: #222f33;
    --shift-a: #5b9ede;
    --shift-b: #cf9a4e;
    --shadow: 0 1px 2px rgba(0,0,0,.4);
    --shadow-hi: 0 6px 18px rgba(0,0,0,.55);
    --backdrop: rgba(0,0,0,.55);
    --hover-lift: brightness(1.25);
    --hover-solid: brightness(1.15);
    --bar-bg: #112a31;
    --bar-line: rgba(255,255,255,0.10);
    --bar-shadow: 0 1px 0 rgba(0,0,0,0.35);
    --bar-outline: rgba(255,255,255,0.30);
    --bar-wash: rgba(255,255,255,0.10);
    --bar-wash-press: rgba(255,255,255,0.16);
  }
}
/*
  Sandstone (issue #273): the warm light theme, tuned in the round-two
  mockups. Light-side, so it retunes rather than restates. The bar's
  white-alpha family inherits :root's values because this bar (#5d3f26)
  sits at the same depth as the teal bar they were tuned on -- checked,
  not assumed; a paler bar would need them retuned, not inherited.
  --bar-bg is a literal, not the :root alias, because the bar sits a step
  darker than the accent here; base.html's sandstone theme-color meta
  restates it, pinned by PaletteMirrorsStayInSync.
*/
:root[data-theme="sandstone"] {
  color-scheme: light;
  --bg: #efe7da;
  --surface: #faf5ec;
  --surface-2: #f4eddf;
  --text: #2b2318;
  --muted: #726658;
  --accent: #6f4d31;
  --accent-ink: #573b24;
  --accent-light: #eeddc6;
  --mine: #55803a;
  --mine-light: #ebf1de;
  --mine-ink: #3f6127;
  --excluded: #bf4b26;
  --excluded-bg: #f9e3d8;
  --excluded-ink: #96351a;
  --warning: #c79433;
  --warning-bg: #f7edd6;
  --border: #e0d4bf;
  --shift-a: #4a6f8e;
  --shift-b: #9a5b68;
  --shadow: 0 1px 2px rgba(70,50,20,.07), 0 1px 1px rgba(70,50,20,.05);
  --shadow-hi: 0 4px 10px rgba(70,50,20,.12), 0 1px 2px rgba(70,50,20,.08);
  --backdrop: rgba(43,35,24,.45);
  --bar-bg: #5d3f26;
}
/*
  Ember (issue #273): the warm dark theme -- charcoal-brown where Dark is
  cool blue-grey. Dark-side, so it restates the whole palette, inside
  @media screen like the forced-dark block above. The bar's white-alpha
  family repeats Dark's dialled-down values: this bar (#2e1f14) sits at
  the same depth as Dark's -- checked, not assumed. --accent runs a step
  deeper than the mockup's amber on purpose -- --on-accent has to read on
  both the accent fills and the bar, and warm white clears 4.5:1 on this
  accent where it could not on the brighter one.
*/
@media screen {
  :root[data-theme="ember"] {
    color-scheme: dark;
    --bg: #171210;
    --surface: #221a15;
    --surface-2: #2a201a;
    --text: #f0e6da;
    --muted: #a8988a;
    --accent: #8a5f2a;
    --accent-ink: #e8bd81;
    --accent-light: #3a2c1c;
    --on-accent: #f5ead9;
    --mine: #3c7546;
    --mine-light: #22321f;
    --mine-ink: #a4d8a4;
    --excluded: #b4482c;
    --excluded-bg: #3a1e14;
    --excluded-ink: #f0a488;
    --warning: #d9a850;
    --warning-bg: #33260f;
    --border: #35291f;
    --shift-a: #6f93b8;
    --shift-b: #b06d89;
    --shadow: 0 1px 2px rgba(0,0,0,.5);
    --shadow-hi: 0 6px 18px rgba(0,0,0,.65);
    --backdrop: rgba(0,0,0,.55);
    --hover-lift: brightness(1.25);
    --hover-solid: brightness(1.15);
    --bar-bg: #2e1f14;
    --bar-line: rgba(255,255,255,0.10);
    --bar-shadow: 0 1px 0 rgba(0,0,0,0.35);
    --bar-outline: rgba(255,255,255,0.30);
    --bar-wash: rgba(255,255,255,0.10);
    --bar-wash-press: rgba(255,255,255,0.16);
  }
}
/*
  Harbor (issue #273): the reassigned light theme -- the sanctioned
  families exception (CONVENTIONS.md, "Themes are a flat list"), keyed by
  .legend-state in the signup legend. Light-side, so it retunes rather
  than restates; --surface stays :root's white on purpose. --excluded runs
  a step deeper than the mockup's amber -- --on-accent's white clears
  4.5:1 on it where the amber held it to 3.5:1 -- and because that lands
  needs-coverage in the warning family, the warning pair is retuned
  yellower here rather than inherited, or error and warning flashes
  would share one cream. The bar-alpha family is retuned too, not
  inherited: this bar sits half a stop paler than the teal bar :root's
  values were tuned on, so the white alphas step up to keep the nav
  outline and hairline reading. --bar-bg restates the accent's value as
  a literal so the chrome pin has a value to hold.
*/
:root[data-theme="harbor"] {
  color-scheme: light;
  --bg: #f3f5f8;
  --surface-2: #fafbfd;
  --text: #1b2430;
  --muted: #657080;
  --accent: #7c4a70;
  --accent-ink: #63385a;
  --accent-light: #f0e2ee;
  --mine: #2e66b8;
  --mine-light: #e8f0fb;
  --mine-ink: #224f92;
  --excluded: #a85a0a;
  --excluded-bg: #fbf1de;
  --excluded-ink: #8f5a06;
  --warning: #b99a1d;
  --warning-bg: #faf5cf;
  --border: #dee4ec;
  --shift-a: #2a8f8a;
  --shift-b: #a85275;
  --shadow: 0 1px 2px rgba(20,30,48,.06), 0 1px 1px rgba(20,30,48,.05);
  --shadow-hi: 0 4px 10px rgba(20,30,48,.10), 0 1px 2px rgba(20,30,48,.07);
  --backdrop: rgba(27,36,48,.45);
  --bar-bg: #7c4a70;
  --bar-line: rgba(255,255,255,0.18);
  --bar-outline: rgba(255,255,255,0.42);
  --bar-wash: rgba(255,255,255,0.16);
  --bar-wash-press: rgba(255,255,255,0.22);
}
* { box-sizing: border-box; }
html {
  scrollbar-gutter: stable;
}
body {
  margin: 0;
  font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
  background: var(--bg);
  color: var(--text);
  padding-bottom: env(safe-area-inset-bottom);
}
header.app-bar {
  background: var(--bar-bg);
  color: var(--on-accent);
  padding: 14px 16px;
  padding-top: max(14px, env(safe-area-inset-top));
  position: sticky;
  top: 0;
  z-index: 10;
  display: flex;
  align-items: center;
  justify-content: space-between;
  /* The hairline is drawn inside the bar's own dark field, so it reads as a
     bevel rather than a border; the 1px drop below is what separates the bar
     from the page once both are this close in value. */
  border-bottom: 1px solid var(--bar-line);
  box-shadow: var(--bar-shadow);
}
header.app-bar h1 {
  font-size: 1.1rem;
  margin: 0;
  font-weight: 650;
  letter-spacing: -0.01em;
}
header.app-bar a {
  color: var(--on-accent);
  text-decoration: none;
  font-size: 0.85rem;
  opacity: 0.88;
}
/*
  The outlined pill every nav destination wears (issue #136). Eight anchors in
  `base.html` carried this as a `style=` attribute -- seven of them 164 bytes
  and the twice-nested My Signups one 166 -- re-sent with every render. Same
  cost issue #134 moved this whole file out of `base.html` to avoid, still being
  paid in the file #134 had just emptied.

  The byte figures below predate issue #141, which now compresses every
  template response: on the wire the duplication cost roughly a twenty-seventh
  of what is quoted here. The de-duplication is still worth having for the
  reason it always was -- one rule to change instead of eight.

  **Eight in the template, but at most six on any page**, so don't recompute the
  saving as 8 x 164. Two separate pairs are mutually exclusive, and only the
  first is visible from this template:

    * the two Dashboard links, an `{% if is_admin_user %}` /
      `{% elif is_readonly_viewer %}` pair, and
    * My Signups and Schedule -- `shifts/context_processors.py:36,49` gives the
      first to a physician and the second to a non-physician, so no account can
      hold both.

  Brute-forcing every role x flag combination puts the ceiling at six.

  What that saves depends on who is looking, which is worth stating because the
  obvious measurement is the unrepresentative one: a *superuser* sees six and
  saves 925 bytes per render, but the Overtime Admins on production are group
  members rather than superusers, get five, and save ~778. A plain physician
  sees three.

  Only three declarations, because the four in the rule above are not repeated:
  it already gives every app-bar anchor its colour, font-size, opacity and
  `text-decoration: none`, and half of each inline copy was restating them.
  Neither rule sets `font-weight` -- these render at the UA default, and there
  is nothing in the app bar to inherit one from.

  Scoped to `header.app-bar a` rather than being a free-floating `.nav-link`
  because the class is not self-sufficient and should not look as though it is.
*/
header.app-bar a.nav-link {
  border: 1px solid var(--bar-outline);
  border-radius: var(--radius-ctl);
  padding: 5px 11px;
  transition: opacity 0.1s ease, background 0.1s ease, transform 0.1s ease;
}
/*
  The nav half of issue #188: every button had feedback, no nav element did.
  The nav pills are transparent fills on the dark bar -- a brightness
  filter would move only text and border, which is why this family can
  never join the hover tokens' tiers (--on-accent ink notwithstanding);
  the lift is a white wash plus full opacity instead, the
  failed-email-alert's press scale for touch. Hover gated like every hover in this file -- see
  the lift group's comment; :active stays ungated and later, so press wins.
*/
@media (hover: hover) {
  header.app-bar a.nav-link:hover {
    opacity: 1;
    background: var(--bar-wash);
  }
}
header.app-bar a.nav-link:active {
  transform: scale(0.96);
  background: var(--bar-wash-press);
}

/*
  The undelivered-email indicator (issue #185). A solid light pill among the
  outlined transparent ones, which is what makes it read as an alert rather than
  a sixth destination -- and it is the reason the fill is `--surface` and the
  text `--text` rather than a red fill with white on it: dark text on white is
  about 15:1, and the red is spent on the glyph, where it reinforces a signal
  the glyph and the number already carry. That ordering is
  the flash rules' convention applied here: colour may support the meaning, never
  be it, because red and green are the pair colourblind viewers cannot separate.

  `margin-left: auto` rather than a `gap` on `header.app-bar`. The bar is
  `justify-content: space-between` with the title at one end, so a third in-flow
  child would otherwise be spread into the middle at every width. An auto margin
  consumes the free space before `justify-content` sees it, pinning this to the
  right beside whatever follows -- the nav row on desktop, the hamburger below
  768px, where `#nav-links` leaves the flow entirely. Adding `gap` to the bar
  would have restyled the existing title/nav spacing on the way past, which is
  the issue #136 mistake.
*/
header.app-bar a.failed-email-alert {
  display: inline-flex;
  align-items: baseline;
  gap: 5px;
  margin-left: auto;
  margin-right: 14px;
  padding: 4px 10px;
  /*
    Control radius, and bordered, because in this bar that pair *is* the
    "I am a link" marker -- every nav destination wears it. The border is
    `--excluded` rather than the nav's `rgba(255,255,255,0.32)` for the plain
    reason that a white outline on a white fill cannot be seen; spending the
    red here also keeps it off the text, which stays dark-on-white at roughly
    15:1.

    Not to be confused with the status pills in `_status_pills.html`
    ("Signups open", "Not published"), which are genuinely non-clickable and are
    `border-radius: 999px`. Fully round reads as a label in this app; the 8px
    rounded rectangle reads as a control. Keep that distinction if either
    changes.
  */
  border: 1px solid var(--excluded);
  border-radius: var(--radius-ctl);
  background: var(--surface);
  color: var(--text);
  opacity: 1;
  font-weight: 600;
  white-space: nowrap;
  transition: transform 0.1s ease, background 0.1s ease;
}
/* Plain hue, sanctioned: the glyph is a mark, not text (token charter) --
   aria-hidden, with the count and label beside it carrying the meaning. */
header.app-bar a.failed-email-alert span[aria-hidden] {
  color: var(--excluded);
}
@media (hover: hover) {
  header.app-bar a.failed-email-alert:hover {
    background: var(--excluded-bg);
  }
}
/*
  Press feedback, matching `.btn-secondary-sm`. Hover does not exist on a
  phone, and the mobile bar is exactly where this renders as a bare glyph and
  number -- so the tap response is the only confirmation a touch user gets that
  they hit a control rather than a label.
*/
header.app-bar a.failed-email-alert:active {
  transform: scale(0.96);
  background: var(--excluded-bg);
}

@media (max-width: 767px) {
  /*
    The count and glyph alone on a phone. The label is what makes this
    self-explanatory on a desktop bar that has room for it; below 768px the bar
    holds a wrapping page title and the hamburger, and a five-word label there
    pushes the title into a third line. `title` on the anchor keeps the full
    wording reachable either way.

    Only the label changes here. A first draft also dropped the right margin to
    12px, which `/simplify` asked the awkward question about: 14px is principled
    above, matching `#nav-links`' own `gap` so the pill sits on the same rhythm as
    the links beside it, and the 12px was a value nobody had evaluated. That is
    the shape of the issue #136 mistake -- a number that had never rendered
    quietly restyling a surface -- so the no-change option wins until someone
    deliberately decides otherwise.
  */
  header.app-bar a.failed-email-alert .failed-email-alert-label {
    display: none;
  }
}
main {
  max-width: 480px;
  margin: 0 auto;
  padding: 12px;
}

.month-heading {
  font-size: 1.6rem;
  font-weight: 700;
  letter-spacing: -0.015em;
  margin: 4px 0 12px;
  color: var(--text);
}

.btn-primary, .btn-secondary, .btn-outline, .btn-danger {
  padding: 10px 18px;
  border-radius: var(--radius-ctl);
  font-weight: 600;
  cursor: pointer;
  box-shadow: var(--shadow);
  /* No background channel: since the hover amendment nothing swaps these
     buttons' background in place -- publish-ready/-not-ready arrive by node
     replacement, which never transitions. */
  transition: transform 0.1s ease, filter 0.1s ease, box-shadow 0.1s ease;
}
.btn-primary {
  border: none;
  background: var(--accent);
  color: var(--on-accent);
}
.btn-secondary {
  border: 1px solid var(--border);
  background: var(--surface);
  color: var(--text);
}
.btn-outline {
  border: 1px solid var(--accent);
  background: var(--surface);
  color: var(--accent-ink);
}
.btn-danger {
  border: 1px solid var(--excluded);
  background: var(--surface);
  color: var(--excluded-ink);
}
/*
  THE lift -- issue #257's hover amendment, one rule for every surface that
  carries the identical body: rise 1px, deepen the shadow, shift the
  element's brightness in its own colour -- var(--hover-lift), so the
  theme sets the strength and direction. brightness() replaces the old
  per-variant background swaps -- white darkens to warm grey, a
  red-tinted button to deeper red -- so the feedback is uniform without
  a hover colour per variant. One grouped rule rather than a copy per
  component, so the next tune is one edit; each selector keeps its own
  specificity, and every member's :active sits later in source, so
  holding a control still wins the transform tie.

  Gated on (hover: hover) -- every :hover in this file is -- because touch
  browsers emulate hover on tap and make it *stick*: a badge tap whose
  confirm is cancelled sends no request, swaps nothing, and would
  otherwise sit lifted until the next touch. Press feedback (:active) is
  the touch channel and is deliberately outside the gate.

  Two selectors are here despite living "elsewhere": the grid one is safe
  outside the width media block (below 768px the grid is display:none, so
  it is inert exactly where the block would exclude it), and the accounts
  row-menu summary is safe outside its page file (the element exists on one
  page; the selector is inert everywhere else, and joining the group is
  what keeps the lift one edit). The shift compounds restate the category
  bar into the raised shadow -- see the bar comment at "Shift-type accent".
*/
@media (hover: hover) {
  .btn-primary:hover, .btn-secondary:hover, .btn-outline:hover, .btn-danger:hover,
  .btn-secondary-sm:hover,
  .day-row.tappable:hover,
  .calendar-grid .grid-cell.tappable:hover,
  .month-card:hover,
  /* Only the live arrows: the disabled one is a <span>, so it cannot match
     this and needs no :not() to keep it out. */
  a.month-nav-arrow:hover,
  .row-menu summary:hover {
    transform: translateY(-1px);
    box-shadow: var(--shadow-hi);
    filter: var(--hover-lift);
  }
  .day-card.tappable.shift-tue-thu:hover {
    box-shadow: var(--cat-bar-a), var(--shadow-hi);
  }
  .day-card.tappable.shift-fri-mon:hover {
    box-shadow: var(--cat-bar-b), var(--shadow-hi);
  }
  .calendar-grid .grid-cell.tappable.shift-tue-thu:hover {
    box-shadow: var(--cat-bar-a), var(--shadow-hi);
  }
  .calendar-grid .grid-cell.tappable.shift-fri-mon:hover {
    box-shadow: var(--cat-bar-b), var(--shadow-hi);
  }
  /* Solid tier (the -solid token's comment in :root carries the why and
     the roster). Same lift motion and shadow -- only the filter tier
     differs. This rule beats the group's identical selector by SOURCE
     ORDER alone: keep it after the group inside this media block, and
     never merge it into the group -- merged, the primary silently drops
     to the lift tier and no test notices. */
  .btn-primary:hover { filter: var(--hover-solid); }
}
.btn-primary:active, .btn-secondary:active, .btn-outline:active, .btn-danger:active {
  transform: scale(0.97);
  filter: brightness(0.91);
}
/* Keyboard focus in the theme's own accent, for the whole button family --
   the browser default ring ignores the palette. Grouped like the hover rule
   above and for the same reason: one edit tunes every wearer. Born scoped to
   the confirm dialog's buttons; widened app-wide because the justification
   never was dialog-shaped (/code-review on PR #307). :focus-visible, not
   :focus, so a mouse click does not paint a ring.

   The two day-cell tap surfaces join the family (issue #258): they carry
   role="button" and tabindex, so keyboard focus lands on them and has to
   show. The 2px *outward* offset is load-bearing there, not inherited
   styling: the state ring (further down) is an outline at -2px, and outline
   is one property -- an inset focus ring would be invisible exactly on the
   mine/needs-coverage days. While a state day holds focus the accent ring
   outside replaces its state ring; the state's fill and border stay, so the
   day still reads. The grid selector is safe outside the width media block:
   below 768px the grid subtree is display:none. `.tappable` scopes it and
   outranks the state rules; only tappable cells are focusable at all. */
.btn-primary:focus-visible, .btn-secondary:focus-visible, .btn-outline:focus-visible,
.btn-danger:focus-visible, .btn-secondary-sm:focus-visible, .btn-ghost-sm:focus-visible,
.day-row.tappable:focus-visible,
.calendar-grid .grid-cell.tappable:focus-visible {
  outline: 2px solid var(--accent);
  outline-offset: 2px;
}
.btn-secondary-sm, .btn-ghost-sm {
  /* background stays a live channel here, unlike the full-size buttons:
     ghost hover and both :active states still swap it. */
  transition: transform 0.1s ease, filter 0.1s ease, background 0.1s ease,
              box-shadow 0.1s ease;
  font-size: 0.8rem;
  padding: 4px 10px;
  border-radius: 6px;
  cursor: pointer;
}
/* Elevation on the solid small button but not the ghost one -- the ghost
   variant is the deliberately quiet member of the pair. */
.btn-secondary-sm {
  border: 1px solid var(--border);
  background: var(--surface);
  color: var(--text);
  box-shadow: var(--shadow);
}
/* Hover: the shared lift group above. */
.btn-secondary-sm:active {
  transform: scale(0.96);
  background: var(--accent-light);
}
.btn-ghost-sm {
  border: 1px solid var(--border);
  background: none;
  color: var(--muted);
}
/* The ghost stays shadowless on hover too -- it rises and shifts
   brightness like every control, but never gains the elevation it
   deliberately lacks at rest. Its own rule, not a lift-group member:
   different body. Known exception on the dark themes (#293, accepted):
   the fill feedback here is the background swap, brightness on a
   background of none moves only text and border, and --bg sits darker
   than the surface-2 panel this button lives on -- so this one fill
   dims where its siblings lighten. A redesign needs a
   "lighter-than-panel" token that does not exist; file it if the dim
   bothers anyone in use. */
@media (hover: hover) {
  .btn-ghost-sm:hover {
    transform: translateY(-1px);
    filter: var(--hover-lift);
    background: var(--bg);
  }
}
.btn-ghost-sm:active {
  transform: scale(0.96);
  background: var(--accent-light);
}
.day-row.tappable {
  cursor: pointer;
  transition: transform 0.1s ease, filter 0.1s ease, box-shadow 0.1s ease;
}
/* Hover: the shared lift group above (with the bar-restating compounds).
   Press tiers, since the values look scattered: full-size cards press at
   0.98, buttons at 0.97, small controls (badges, small buttons, nav, grid
   cells, row menu) at 0.96 -- the bigger the surface, the shallower the
   press. Brightness rides alongside at 0.91 for solid fills, 0.93 for
   tinted cards and the link chips: the tinted press matches the hover
   darken and the solid press digs past it, the same relation the
   pre-#293 values held, moved
   with the hover retune -- a press may never sit BRIGHTER than the hover
   it interrupts, or the click reads as a release. Literals, not the
   hover tokens, on purpose (#293): a press darken barely shows on a
   charter-dark fill in any theme (the solid tier's wearers included),
   so scale motion is what carries press there, judged sufficient. Two
   exceptions to the isolation: .btn-secondary-sm:active and
   .btn-ghost-sm:active swap background and set no filter, so their held
   fill keeps whatever the hover token painted. Revisit if presses feel
   dead in real use. */
.day-row.tappable:active,
.month-card:active {
  transform: scale(0.98);
  filter: brightness(0.93);
}
.form-field {
  display: block;
  width: 100%;
  margin-top: 4px;
  padding: 8px 10px;
  border-radius: var(--radius-ctl);
  border: 1px solid var(--border);
}

/*
  Flash messages, colour-coded by level. The same banner carries both
  "Schedule published" and "Not cleared: ... reload and try again", so a
  single success-green style made every refusal read as a confirmation.
  Colour alone is not enough to carry that: success/error is exactly the
  green/red pair red-green colourblind viewers cannot separate, and the
  tinted backgrounds are pale enough to be near-identical to them. Each
  level therefore also gets a distinct leading glyph and a thicker left
  border. An unrecognised level keeps .flash's neutral surface styling
  rather than silently inheriting some other level's colour.
*/
.flash-list { margin-bottom: 14px; }
.flash {
  display: flex;
  gap: 8px;
  align-items: baseline;
  background: var(--surface);
  border: 1px solid var(--border);
  border-left-width: 4px;
  border-radius: var(--radius);
  color: var(--text);
  padding: 10px 14px;
  margin-bottom: 8px;
  font-size: 0.9rem;
  box-shadow: var(--shadow);
}
.flash::before { font-weight: 700; }
/* Each glyph is text on its level's tint, so it wears the -ink form
   (issue #281). Warning is the sanctioned exception: it has no -ink,
   and the token block's --warning note carries why. */
.flash-success { background: var(--mine-light); border-color: var(--mine); }
.flash-success::before { content: "\2713"; color: var(--mine-ink); }
.flash-error { background: var(--excluded-bg); border-color: var(--excluded); }
.flash-error::before { content: "\2715"; color: var(--excluded-ink); }
.flash-warning { background: var(--warning-bg); border-color: var(--warning); }
.flash-warning::before { content: "!"; color: var(--warning); }
.flash-info { background: var(--accent-light); border-color: var(--accent); }
.flash-info::before { content: "i"; color: var(--accent-ink); }

/*
  The shared confirm dialog (issue #306) -- base.html's one <dialog>, filled
  per call site by app.js. The buttons wear the app's own .btn-* classes, so
  they get the grouped hover lift, the grouped focus ring below, and the
  theme retunes for free; nothing here restyles them.

  max-width is min()'d against the viewport because a bare 420px would
  *replace* the UA stylesheet's own viewport clamp on <dialog> rather than
  add to it -- on a 375px phone a long confirm rendered exactly edge to
  edge (/code-review on PR #307, reproduced at 375px).

  The dim behind the dialog is the second box-shadow, not ::backdrop:
  ::backdrop only learned to read custom properties from the originating
  element in 2023-24 engines, and on the older ones inside this app's own
  baseline (Safari 15.4-17.3 has showModal but not the inheritance) a
  var() there computes to *transparent* while still suppressing the UA's
  default dim. The dialog element inherits tokens everywhere it exists at
  all, so a viewport-sized shadow spread carries the same token to every
  engine; the UA's faint backdrop dim underneath it is unstyled and adds
  imperceptibly.

  white-space: pre-line because two confirm messages carry a real blank line
  (the at-risk warnings on Run Master and Clear) that a normal <p> would
  collapse -- the native confirm() honoured those newlines and the message
  text is unchanged in this move.
*/
dialog.confirm-dialog {
  max-width: min(420px, calc(100vw - 32px));
  background: var(--surface);
  color: var(--text);
  border: 1px solid var(--border);
  border-radius: var(--radius);
  box-shadow: var(--shadow-hi), 0 0 0 100vmax var(--backdrop);
  padding: 20px;
}
.confirm-dialog-message {
  margin: 0 0 18px;
  font-size: 0.95rem;
  line-height: 1.5;
  white-space: pre-line;
}
.confirm-dialog-buttons {
  display: flex;
  justify-content: flex-end;
  gap: 10px;
}

/*
  Equal-width tabs (issue #257): grid columns rather than content-hugging
  inline-flex, so every tab in a bar is the same size -- and so the columns
  can compress equally on a phone via minmax(0, 1fr) plus max-width instead
  of overflowing the viewport (the four-tab config nav was ~11px wider than
  a 375px screen with content-width tabs). The narrow-viewport padding rule
  below keeps the longest label inside its compressed column down to ~350px
  viewports; narrower than that, the ellipsis on the link is the bound, not
  the padding. The rule's 480px trigger is main's own mobile max-width cap
  -- the point below which the container starts tracking the viewport -- so
  if that cap moves, this breakpoint moves with it.

  A sliding active pill was tried here and deliberately dropped -- the rule
  is CONVENTIONS.md "The tab pill does not slide"; the decision and the
  sanctioned same-document route are #264.
*/
/* The pill the row sits in, shared with the picker that replaces the config
   nav below 768px (.admin-picker, further down). One rule naming both rather
   than a second copy of six declarations: they are peers wearing one chrome,
   the arrangement .month-nav-arrow already uses for its live and inert
   spellings. Only the grid below is the bar's own. */
.admin-tabs, .admin-picker {
  max-width: 100%;
  border: 1px solid var(--accent);
  border-radius: 999px;
  padding: 3px;
  margin-bottom: 20px;
  background: var(--surface);
  box-shadow: var(--shadow);
}
.admin-tabs {
  display: inline-grid;
  grid-auto-flow: column;
  grid-auto-columns: minmax(0, 1fr);
}
/* The type and pill shape a tab wears, shared with the picker's control so
   the collapsed form cannot drift from the row it stands in for -- they are
   required to look like the same object, and below 768px the picker is the
   only one of the two anybody sees, so a drift would show on a phone and
   never on a desktop.

   `padding` is deliberately NOT in this group. The 480px override below
   narrows it to compress a four-column grid, which is a bar problem; the
   picker is one inline-block control and wants its own asymmetric padding
   for the divider and arrow. */
.admin-tabs a, .admin-tabs .tab-dead, .admin-picker-control select {
  font-size: 0.85rem;
  font-weight: 600;
  border-radius: 999px;
}
.admin-tabs a, .admin-tabs .tab-dead {
  text-align: center;
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
  text-decoration: none;
  padding: 6px 15px;
  background: transparent;
  color: var(--accent-ink);
  transition: background 0.1s ease;
}
/* The active tab's fill, worn by the collapsed picker for the same reason as
   the group above: it *is* the active tab, drawn on its own. */
.admin-tabs a.active, .admin-picker-control {
  background: var(--accent);
  color: var(--on-accent);
}
/* A tab for a view this month does not have yet -- signup.html's "Assigned"
   before publish (#267). Same idea as .month-nav-arrow-dead: it keeps its
   grid slot so the bar does not change width when the state changes, and it
   is a <span>, so none of the interactive rules above or below can reach it
   without a selector naming it.

   Deliberately NOT the arrows' 0.38. That value dims a chevron, which only
   has to read as "absent"; this is a word somebody has to read. Effective
   contrast against the bar, measured in both palettes: 0.38 gives 2.08:1
   light / 2.60:1 dark, and 0.55 gives 3.10:1 / 3.98:1 -- light is the weaker
   side, which is the opposite of the guess. Disabled controls are exempt
   from the WCAG minimum, so this is a legibility choice, not a compliance
   one; if the two dim treatments are ever unified, unify upward. */
.admin-tabs .tab-dead { opacity: 0.55; }
@media (max-width: 480px) {
  .admin-tabs a, .admin-tabs .tab-dead { padding: 6px 4px; }
}

/*
  ---- The config nav's picker (issue #361) ----

  Below 768px the config nav's four equal-width tabs no longer fit the column,
  and a fifth cannot fit at all: `main` caps at 480px until that same
  breakpoint, so the bar gets 456px and five tabs need 551. The row is
  therefore replaced by a select naming the current section -- not truncated,
  not wrapped, not scrolled. `_admin_tabs.html` renders both and owns why.

  **Keyed on `admin-tabs-nav`, never on `.admin-tabs`.** That base class is a
  generic pill-tab row worn by five other bars -- the shift-category rows on
  the dashboard and the signup page, the Signups/Assigned toggle, and the two
  month lists. All five are needed on a phone, and a rule written against
  `.admin-tabs` here would collapse every one of them.

  768px rather than the 465px where the four tabs still technically fit: it is
  the breakpoint that already exists, and it keeps this independent of #362.
*/
.admin-picker { display: none; }
@media (max-width: 767px) {
  .admin-tabs.admin-tabs-nav { display: none; }
  /* inline-block, so the pill is as wide as the section name rather than the
     column -- the bar it replaces is `inline-grid` for the same reason. */
  .admin-picker { display: inline-block; }
}
/*
  The select wears the active tab's fill, so the collapsed control reads as the
  bar showing only where you are. `appearance: none` is what lets it: without
  it the platform draws its own chrome over the pill.
*/
.admin-picker-control {
  position: relative;
  display: block;
  border-radius: 999px;
}
.admin-picker-control select {
  appearance: none;
  /* The one vendor prefix in this file, and it is for Safari before 15.4 --
     without it the platform draws its own chrome over the pill, which is the
     whole illusion. Delete it when those devices are gone. */
  -webkit-appearance: none;
  width: 100%;
  border: 0;
  background: transparent;
  color: inherit;
  /* `font-family`/`line-height` rather than `font: inherit`, which is what
     this was: the shorthand also resets size and weight, and it sits after
     the shared type group above, so it would have quietly undone the two
     declarations that keep the picker matching the active tab. A form
     control's own default font is the only thing that needs overriding
     here. */
  font-family: inherit;
  line-height: inherit;
  /* Right padding clears the divider and the triangle below; the left matches
     `.admin-tabs a` so the label sits where the active tab's did. Type and
     radius come from the shared group up beside `.admin-tabs a`. */
  padding: 6px 44px 6px 15px;
  cursor: pointer;
}
.admin-picker-control select:focus-visible {
  outline: 2px solid var(--accent);
  outline-offset: 3px;
}
/*
  **The options do not inherit the closed control's ink**, and both
  declarations are load-bearing rather than tidy.

  The select above is white-on-accent so the collapsed pill reads as the
  active tab. The dropdown the browser opens from it inherits that `color`
  and none of that `background`, so without this the list is white text on
  the popup's own pale surface -- every row invisible but the highlighted
  one, which is legible only because the selection band supplies the
  contrast behind it. It shipped that way and was caught by eye, not here:
  the popup is drawn by the platform, so it is outside the page, outside a
  screenshot, and outside every computed-style read of the control itself.

  `background` alone is not the fix and neither is `color`. In dark mode the
  popup also stayed the browser's near-white while the page around it was
  dark, so the list was wrong in both palettes for two different reasons and
  one declaration would have fixed one theme's symptom.

  `--text` on `--surface` is a registered pair in
  `ComposedTextPairsStayReadable`, so naming tokens here is also what makes
  the list's contrast measured in every theme rather than assumed.

  **If Firefox turns out to be in use, check the closed control here first.**
  It is reported to paint a closed `<select>` with the selected option's
  colour rather than the select's own, which would put `--text` on the
  `--accent` fill -- 1.74:1 in the light palette, and unreadable. Untested:
  only Chromium has been looked at, and nobody is known to use Firefox. Noted
  rather than fixed because the obvious remedy is not one: scoping this to
  `option:checked` names the very option a closed control would be painted
  from, so it would change nothing.
*/
.admin-picker-control option {
  color: var(--text);
  background: var(--surface);
}
/*
  Divider and arrow, both `currentColor` off the fill above, so a new theme
  retunes them with everything else.

  **The arrow is drawn with borders rather than set as a glyph, and that is
  not a style choice.** An SVG would have to arrive as a `data:` URI, which
  `default-src 'self'` blocks -- `img-src` inherits from it and `data:` does
  not satisfy `'self'`. Measured, not assumed: an injected `data:` image
  raises a violation naming `img-src` while a same-origin control loads.
  A text glyph was the other candidate and is worse here: U+25BE is drawn
  small inside its em box, so it stays small however the font-size is raised,
  and how small depends on the font the device happens to have. Borders
  depend on nothing and size exactly as asked.
*/
.admin-picker-control::before,
.admin-picker-control::after {
  content: "";
  position: absolute;
  pointer-events: none;
}
.admin-picker-control::before {
  right: 32px;
  top: 6px;
  bottom: 6px;
  width: 1px;
  background: currentColor;
  opacity: 0.35;
}
.admin-picker-control::after {
  right: 10px;
  top: 50%;
  width: 0;
  height: 0;
  transform: translateY(-40%);
  border-left: 6px solid transparent;
  border-right: 6px solid transparent;
  border-top: 7px solid currentColor;
}
/* Inactive tabs answer the cursor (issue #188); the active tab is where you
   already are, so it stays put. */
@media (hover: hover) {
  .admin-tabs a:not(.active):hover {
    background: var(--accent-light);
  }
}
.admin-subtabs {
  display: flex;
  gap: 4px;
  border-bottom: 1px solid var(--border);
  margin-bottom: 20px;
}
.admin-subtabs a {
  text-decoration: none;
  font-size: 0.8rem;
  font-weight: 600;
  padding: 6px 12px 8px;
  color: var(--muted);
  border-bottom: 2px solid transparent;
  margin-bottom: -1px;
}
.admin-subtabs a.active {
  /* -ink (issue #281): as plain accent the selected tab was dimmer on the
     dark themes than the inactive tabs' --muted -- it read as the disabled
     one. The underline keeps the plain hue; a mark, not text. */
  color: var(--accent-ink);
  border-bottom-color: var(--accent);
}
/* Same issue #188 gap as .admin-tabs, quieter shape to match the underline
   style: the text firms up and the underline sketches itself in. */
@media (hover: hover) {
  .admin-subtabs a:not(.active):hover {
    color: var(--text);
    border-bottom-color: var(--border);
  }
}

/*
  Shift-type accent: the category left bar, worn by grid cells, signup day
  cards and admin day rows. `box-shadow` is one property, so the moment a
  surface also carries `--shadow` the two spellings fight and one silently
  vanishes -- which is why every bar-wearing surface *restates* the bar as a
  compound (`.day-card.shift-tue-thu { box-shadow: inset ..., var(--shadow) }`)
  next to its own elevation rule, and again for hover in the lift group.
  There is no bare `.shift-tue-thu` rule: all three wearers are elevated, so
  a bare rule could never win and would only misdirect the next reader. A
  new bar-wearing surface adds its own compound pair here or goes without.
*/

.grid-legend {
  display: flex;
  /* Wraps since the Harbor key (issue #273) grew the row to four entries,
     which overflows a 375px phone unwrapped. */
  flex-wrap: wrap;
  gap: 6px 16px;
  font-size: 0.8rem;
  color: var(--muted);
  margin-bottom: 10px;
}
.grid-legend span { display: inline-flex; align-items: center; gap: 6px; }

/*
  Per-physician Tue-Thu/Fri-Mon breakdown: hidden by default (opt-in
  detail, not everyone wants it); toggled via a class on the stable
  wrapper rather than the table itself, since the table is replaced
  wholesale by htmx oob swaps on badge clicks and would otherwise
  lose the toggle state on every click.
*/
.breakdown-detail {
  display: none;
  /* Typography arrived with the inline-style extraction (issue #175). It
     lives here rather than with the other stats-table rules so the whole
     component is one block -- two `.breakdown-detail` rules 500 lines apart
     is one an editor finds and one they miss. */
  color: var(--muted);
  font-size: 0.8rem;
}
#physician-stats-section.show-breakdown .breakdown-detail { display: inline; }

/*
  Column widths come from here, never from the data (issue #123). The
  table is re-sent whole by an oob swap on every badge click, and under
  auto layout the browser re-measures each time -- so a count crossing
  9 -> 10, or a physician's first assignment giving them a row, shifted
  every column downstream of it. `fixed` reads widths off the header row
  only and never measures a cell, so nothing moves.

  The max-width is the point of the change, not a detail: `main` caps at
  900px while this table's content needs about 470, and auto layout hands
  the ~400px of slack out *in proportion* to intrinsic width. That is what
  made "Assignments" fatter than "Signups" (longer word, bigger share) and
  what pushed each number far from the name it belongs to.

  It sits on the *section* rather than on the table because the breakdown
  toggle is a sibling of the table inside that wrapper, in a flex row that
  right-justifies it. Capping only the table left the button tracking the
  900px edge of `main` while the table stopped at 640 -- so one cap, on
  the element that owns both, rather than the same number written twice
  and free to drift apart. The table therefore depends on rendering inside
  that wrapper; `_physician_stats_table.html` records that, since all three
  of its include sites are what make it true.

  The numeric columns are percentages rather than px so they shrink with
  the viewport instead of overflowing a phone, and they are sized for the
  breakdown-*on* state. Sizing them per toggle state would cost two lines
  and reintroduce the reshuffle: the widths are identical breakdown on or
  off, and on the three-column Daytime tab, where the first column simply
  takes the larger remainder.
*/
#physician-stats-section { max-width: 640px; }
#physician-stats-table { table-layout: fixed; width: 100%; }
#physician-stats-table th:not(:first-child) { width: 20%; }

/*
  The name column is the one that can be handed a token with nowhere to
  break. A physician with no first or last name renders their email
  instead, and an email has no space in it -- so where auto layout used to
  widen the column to fit one, a fixed column lets it run straight over
  the numbers beside it. Measured before this line existed: a 45-character
  address rendered 337px of content into a 256px cell, and at 375px, where
  the column is 140px, even a short address overflowed.

  `break-word` rather than `anywhere`: the two are indistinguishable under
  a fixed layout, which ignores intrinsic widths, and `break-word` is
  understood by every browser this app is likely to meet.
*/
#physician-stats-table td:first-child { overflow-wrap: break-word; }

/*
  Two spellings for two of those headers. At 375px each numeric column is
  about 70px, which "Assignments" overflows into the column beside it, and
  that is what made the phone view read as crowded. Desktop has the room
  and keeps the full word rather than being abbreviated to match.

  Mobile-first like `.calendar-grid` below: the short spelling is the
  default and the full one is restored in the `min-width: 768px` block, so
  every responsive rule in this file reads the same direction.

  Named for the table rather than scoped to it by id. `.hdr-abbr` would
  have been a general name whose generality was a lie -- the rule hiding
  it would still have been id-scoped, so the same class used on any other
  header would silently render both spellings at every width.
*/
.stats-hdr-full { display: none; }

.grid-legend i {
  display: inline-block;
  width: 12px; height: 12px;
  border-radius: 4px;
  /* The swatch is the entry's whole point; the label flexes, not it. */
  flex-shrink: 0;
}

/*
  Doubles/gap-claim name badges (inline variant): mobile card list and
  the admin schedule view both render at every width, unlike the block
  variant below which only exists inside the desktop calendar grid.
*/
.name-badge-inline {
  font-size: 0.65rem;
  vertical-align: super;
  opacity: 0.8;
  margin-left: 4px;
}

/* Desktop calendar grid: hidden by default (mobile-first), shown on wide viewports. */
.calendar-grid { display: none; }

@media (min-width: 768px) {
  main { max-width: 900px; }
  main.main-wide { max-width: 1250px; }
  /*
    The full stats headers, restored where there is room for them --
    see `.stats-hdr-full` above for what the two spellings are.
  */
  .stats-hdr-full { display: inline; }
  .stats-hdr-abbr { display: none; }
  .calendar-grid {
    display: grid;
    grid-template-columns: repeat(7, 1fr);
    gap: 6px;
    margin-bottom: 20px;
  }
  .calendar-grid .grid-weekday {
    font-size: 0.72rem;
    font-weight: 700;
    color: var(--muted);
    text-align: center;
    padding-bottom: 4px;
    text-transform: uppercase;
    letter-spacing: 0.06em;
  }
  .calendar-grid .grid-cell {
    background: var(--surface);
    border: 1px solid var(--border);
    border-radius: var(--radius-sm);
    min-height: 91px;
    min-width: 0;
    padding: 8px;
    position: relative;
    box-shadow: var(--shadow);
  }
  /* The category bar restated with the elevation -- see the bar comment at
     "Shift-type accent". The compound (0,3,0) outranks the base cell rule
     (0,2,0), so the bar survives wherever these rules sit. */
  .calendar-grid .grid-cell.shift-tue-thu {
    box-shadow: var(--cat-bar-a), var(--shadow);
  }
  .calendar-grid .grid-cell.shift-fri-mon {
    box-shadow: var(--cat-bar-b), var(--shadow);
  }
  .calendar-grid .grid-cell .grid-names {
    margin-top: 4px;
    font-size: 0.7rem;
    line-height: 1.3;
    color: var(--text);
    display: flex;
    flex-direction: column;
    gap: 2px;
  }
  .calendar-grid .grid-cell .grid-name-row {
    display: flex;
    align-items: baseline;
    gap: 4px;
    min-width: 0;
  }
  .calendar-grid .grid-cell .grid-name-text {
    flex: 0 1 auto;
    min-width: 0;
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
  }
  .calendar-grid .grid-cell .grid-name-text.called-in {
    font-weight: 700;
  }
  .calendar-grid .grid-cell .grid-name-badge {
    flex-shrink: 0;
    font-weight: 700;
  }
  /* The Daytime posted order in the grid (issue #312): the same .rank-pill
     both card surfaces wear, at this surface's own scale -- the surface
     adopts the class and keeps only its own layout in its own rule (#291).

     The worked ✓ is mine-*family* on every surface, which is the invariant --
     not one token. `--mine-ink` here and on `.p-worked-note`, because both
     are text on a tint or surface; the admin badge is `--mine` as a fill
     under `--on-accent` ink, and stays that way: the plain hue is never a
     text colour (the token charter's -ink rule, #281). How this surface
     says the word rather than showing it is `_grid_cell.html`'s business,
     and its own comment carries the reason. */
  .calendar-grid .grid-cell .rank-pill {
    font-size: 0.55rem;
    padding: 0 5px;
    min-width: auto;
  }
  .calendar-grid .grid-cell .grid-name-badge-worked {
    color: var(--mine-ink);
  }
  .calendar-grid .grid-cell .grid-name-badge-gap-claim {
    margin-left: -5px;
  }
  .calendar-grid .grid-cell .grid-undo-note {
    margin-top: 4px;
    font-size: 0.65rem;
    font-weight: 600;
    color: var(--mine-ink);
  }
  .calendar-grid .grid-cell.empty {
    background: transparent; border-color: transparent; box-shadow: none;
  }
  /* State rings: grouped with the card rules outside this block -- search
     "state ring" -- so each state has one spelling for both surfaces. */
  .calendar-grid .grid-cell .grid-cell-top {
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: 6px;
  }
  .calendar-grid .grid-cell .grid-date {
    font-size: 0.85rem;
    font-weight: 650;
    font-variant-numeric: tabular-nums;
  }
  .calendar-grid .grid-cell .grid-count {
    background: var(--accent-light);
    border: 1px solid var(--border);
    color: var(--accent-ink);
    font-size: 0.7rem;
    font-weight: 700;
    font-variant-numeric: tabular-nums;
    border-radius: 999px;
    padding: 2px 7px;
  }
  .calendar-grid .grid-cell.tappable {
    cursor: pointer;
    transition: transform 0.1s ease, filter 0.1s ease, box-shadow 0.1s ease;
  }
  /* Hover: the shared lift group near the buttons (its comment covers why
     the grid selectors live outside this width block). The old accent
     border-swap hover is gone, so state cells no longer need a defensive
     hover rule for their border colour. */
  .calendar-grid .grid-cell.tappable:active { transform: scale(0.96); filter: brightness(0.93); }

  /*
    On desktop the grid is the input surface; the list below becomes
    read-only detail (matches v2's original grid-above/list-below pattern,
    avoids two surfaces both claiming to be clickable).
  */
  .day-row {
    pointer-events: none;
    cursor: default;
  }

  /*
    Desktop: the grid is the single surface for browsing and marking
    availability, showing names directly in its cells -- the day-row
    list below is mobile-only, where there's no grid and the list is
    the only surface.
  */
  .day-row-list { display: none; }
}

/*
  Nav: inline row on desktop (default). Below 768px it collapses into
  a hamburger-revealed dropdown panel instead -- wrapping the row was
  considered but rejected since it degrades every time another nav
  item gets added; a hamburger's footprint stays constant.
*/
.nav-toggle {
  display: none;
  background: none;
  border: none;
  color: var(--on-accent);
  font-size: 1.4rem;
  line-height: 1;
  cursor: pointer;
  padding: 0;
}

/*
  `align-items` and `gap` were a `style=` attribute on the div until issue #136,
  and moving them here is what let the `!important` below go. That flag was not
  guarding anything: it existed only because an inline declaration outranks any
  stylesheet rule however specific, so the mobile panel could not switch the row
  to `stretch` by ordinary means. With both declarations in this file the media
  query wins on source order at equal specificity, which is the mechanism doing
  the work rather than a flag suppressing one.

  **The `gap` beside it had no such flag, and that asymmetry is why the media
  query below now says 14px.** `align-items: stretch !important` won on main, so
  the panel always stretched; `gap: 10px` sat directly under it with no flag,
  lost to the inline `gap: 14px`, and so had never once applied since the day it
  was written. Hoisting the attribute would have switched it on and taken the
  sub-768px panel from 14px to 10px between links, 328px to 300px tall.

  It is 14px because that is what shipped and what anyone has ever seen. The
  10px was not a design decision that this change restores -- it was dead the
  moment it was written, so nobody ever evaluated it, and adopting it here would
  have meant a CSS de-duplication quietly restyling a surface on its way past.
  Change it deliberately or not at all.

  Measured in the browser against a *faithful* reconstruction of main, meaning
  both halves -- the inline attribute and the `!important` together. Reinstating
  only the attribute makes it look as though `align-items` changed too, and it
  did not: alignment and the 343px link width are identical either way.

  The general lesson, since the next person hoisting an inline declaration will
  hit it: enumerate every stylesheet declaration the attribute was outranking,
  not only the one wearing the `!important`. The flag marks where somebody
  noticed the conflict. The silent losers are the ones that move.
*/
#nav-links {
  display: flex;
  align-items: center;
  gap: 14px;
}

@media (max-width: 767px) {
  .nav-toggle { display: block; }
  #nav-links {
    display: none;
    position: absolute;
    top: 100%;
    left: 0;
    right: 0;
    background: var(--bar-bg);
    flex-direction: column;
    align-items: stretch;
    /* 14px, not the 10px written here originally -- see above. That value was
       outranked by an inline attribute from the day it was written and never
       once rendered, so matching what shipped is the no-change option. */
    gap: 14px;
    padding: 12px 16px;
    z-index: 20;
  }
  #nav-links.open { display: flex; }
  #nav-links select { width: 100%; }
}

/*
  ---- Extracted inline styles (issue #175) ----
  Everything below this line came out of template style="" attributes,
  verbatim -- same declarations, same values.

  **One class per component, not per declaration set.** Several components
  below carry the same body today (`font-size: 0.8rem; color: var(--muted)`
  is six of them). They keep their own names on purpose: a physician's name
  list and a Max-slots hint are different things that happen to agree right
  now, and the restyle will move some of them apart. What is not allowed is
  one component spelled twice, or a body duplicated across files -- those
  are unified here.

  Where two components differ by a single value they stay two classes, since
  merging them would be a visible change and this pass must render
  identically to what it replaced.
*/

/* base.html */
.nav-user { color: var(--on-accent); opacity: 0.7; font-size: 0.8rem; }
.logout-form { margin: 0; }
.logout-btn {
  background: none; border: none; color: var(--on-accent); text-decoration: none;
  font-size: 0.85rem; opacity: 0.85; cursor: pointer; padding: 0;
  font-family: inherit;
  transition: opacity 0.1s ease;
}
/* Text-only control on the dark bar: opacity is the one channel it has.
   The other half of issue #188's app-bar list, with .nav-link above. */
@media (hover: hover) {
  .logout-btn:hover { opacity: 1; }
}
/* Rendered by base.html and re-rendered out-of-band by
   _assignment_day_badges.html; one rule keeps the two spellings identical. */
#live-counts-data { display: none; }

/* _month_switcher.html -- the list and the prev/next arrows either side of
   it (issue #257) read as one row of controls, so the box they share is one
   rule rather than two that a comment promises are identical. */
.month-switcher, .month-nav-arrow {
  background: var(--surface);
  border: 1px solid var(--border);
  border-radius: var(--radius-ctl);
  box-shadow: var(--shadow);
}
.month-switcher {
  color: var(--text);
  padding: 6px 10px; font-size: 0.85rem;
}
.month-switcher-nav { display: inline-flex; align-items: center; gap: 6px; }
.month-nav-arrow {
  display: inline-flex; align-items: center; justify-content: center;
  min-width: 34px; min-height: 34px; padding: 0 9px;
  color: var(--accent-ink);
  font-size: 1rem; font-weight: 700; line-height: 1; text-decoration: none;
  transition: transform 0.1s ease, filter 0.1s ease, box-shadow 0.1s ease;
}
/* An end-of-list arrow: flat, because elevation in this app means "operable
   control". Why it is rendered at all rather than omitted is the template's
   business, and its comment holds that. */
.month-nav-arrow-dead {
  opacity: 0.38;
  box-shadow: none;
}
/* `a.` on both halves, not just hover: the anchor qualifier is what keeps
   the inert span out, and dropping it from one of the pair also breaks the
   equal-specificity tie the lift group's comment relies on -- hover would
   out-rank press and holding a live arrow would never scale. */
a.month-nav-arrow:active { transform: scale(0.97); filter: brightness(0.91); }

/* _signup_window.html */
.signup-window-dates {
  color: var(--text); font-weight: 600; font-variant-numeric: tabular-nums;
}
/* One bound, kept whole. Since issue #345 each bound carries its time, so
   "Jul 1, 7:00 AM – Jul 10, 5:00 PM" offers eight places to break and only the
   pair either side of the dash reads. This leaves that pair and closes the
   other six, the worst of which splits "5:00" from "PM". */
.signup-window-bound { white-space: nowrap; }

/* _status_pills.html + _publish_pill.html: one base, one class per colourway.
   pill-neutral is shared by "Not published", "Signups not yet open" and
   "Archived" because their inline declarations were already byte-identical. */
.status-pill {
  border-radius: 999px; padding: 3px 10px; font-size: 0.8rem; font-weight: 600;
}
/* Ink text on the tinted fills, and the border softened to a 45% mix of the
   state colour -- a solid state-colour border read heavier than the text it
   framed. Pills stay flat: labels get no elevation.

   The mix lives in an @supports block, not inline: color-mix() wrapping a
   var() is accepted at parse time everywhere and only fails at
   computed-value time, where it resets border-style to its initial `none`
   rather than falling back to an earlier declaration -- so a plain
   fallback line above it would be discarded too, and a pre-2023 engine
   would show no border at all. Gated, old engines keep the solid border
   they always had. */
.status-pill.pill-accent {
  border: 1px solid var(--accent);
  background: var(--accent-light); color: var(--accent-ink);
}
.status-pill.pill-neutral {
  border: 1px solid var(--border); background: var(--surface-2); color: var(--muted);
}
.status-pill.pill-open {
  border: 1px solid var(--mine);
  background: var(--mine-light); color: var(--mine-ink);
}
.status-pill.pill-closed {
  border: 1px solid var(--excluded);
  background: var(--excluded-bg); color: var(--excluded-ink);
}
@supports (color: color-mix(in srgb, red, red)) {
  .status-pill.pill-accent { border-color: color-mix(in srgb, var(--accent) 45%, transparent); }
  .status-pill.pill-open { border-color: color-mix(in srgb, var(--mine) 45%, transparent); }
  .status-pill.pill-closed { border-color: color-mix(in srgb, var(--excluded) 45%, transparent); }
}

/* _readiness_dot.html */
.readiness-dot {
  width: 8px; height: 8px; border-radius: 50%; flex-shrink: 0; display: inline-block;
}
.readiness-dot.dot-green { background: var(--mine); }
.readiness-dot.dot-yellow { background: var(--warning); }
.readiness-dot.dot-red { background: var(--excluded); }

/* The small muted note, on every surface that has one: "No signups for this
   day.", "Everyone available is already signed up.", "All days fully
   staffed.", the ranked-day empty state, the failed-email queue count, the
   release-schedule per-month note. `.flush` is for the <p> uses, which need
   the paragraph margin off; the <div> uses have none to begin with. */
.muted-note { font-size: 0.85rem; color: var(--muted); }
.muted-note.flush { margin: 0; }

/* Cross-page primitives (issue #291): looks that had grown a copy per
   stylesheet -- and page stylesheets cannot see each other, so a retune
   landed in one copy while the rest kept the old look. A surface adopts
   the class in its template and keeps only its own layout, spacing and
   type in its own rule. */
/* The raised card. Worn in templates -- grep class="card" for the
   roster rather than trusting a list here to stay complete. The
   near-quartets that stay out differ on purpose: .prepublish-checklist
   and the grid cells are the --radius-sm tier, .dash-details is the
   deliberately quieter --surface-2 panel, and the flash wears the
   quartet too but widens one border edge into its state bar and
   retints per level -- one render site in base.html, left standalone. */
.card {
  background: var(--surface); border: 1px solid var(--border);
  border-radius: var(--radius); box-shadow: var(--shadow);
}
/* The muted uppercase section head; .spaced is the mid-page variant.
   .pref-group-head below is not a wearer deliberately: it differs in
   size, tracking and margin, so the base would be all overrides there. */
.section-head {
  font-size: 0.85rem; font-weight: 700; text-transform: uppercase;
  letter-spacing: 0.04em; color: var(--muted); margin: 0 0 12px;
}
.section-head.spaced { margin: 24px 0 12px; }
/* The muted empty-state line ("No months...", "No upcoming assignments...").
   Sibling of .muted-note above, kept apart by size and role: a note
   annotates a surface that exists, this stands in for one that is empty.
   The intro lines (.faq-intro, .release-intro) share this body but not
   the role and deliberately stay their own spellings. */
.empty-note { color: var(--muted); font-size: 0.9rem; }
/* The accent chip: --accent-ink on --accent-light in the 5px corner
   family. .signup-chip (a control with a remove button, not a label)
   overrides the radius to a pill, as a compound so no file reorder can
   flip it. Chips that are links shift brightness on hover and keyboard
   focus -- var(--hover-lift), so the light themes darken and the dark
   themes lighten (the chips' own deeper dial was retired in #293; the
   :root token comment marks the re-mint point). The underline lives
   only under forced colors -- and not because filters stop running
   there (they do not): that mode repaints the fill with system colours,
   so a brightness shift has nothing left to act on, and the underline,
   drawn in the forced ink, is the feedback that survives. Nowhere
   else -- an underline is foreign to this app's hover language (#293).
   The hover is gated like every hover in this file, so a tap cannot
   stick a chip shifted; the ungated :focus-visible is the keyboard
   channel. A page rule that must restyle a link chip's text-decoration
   or transition spells its selector a.<class> -- this a. rule outranks
   a bare class. */
.accent-chip {
  color: var(--accent-ink); background: var(--accent-light);
  border-radius: 5px;
}
a.accent-chip { text-decoration: none; transition: filter 0.1s ease, transform 0.1s ease; }
@media (hover: hover) {
  a.accent-chip:hover { filter: var(--hover-lift); }
}
a.accent-chip:focus-visible { filter: var(--hover-lift); }
/* The touch channel, ungated like every press and later in source than
   hover and focus so holding wins the ties: small-control scale with
   the tinted-fill press dip. Chips were the one control-shaped link
   with no tap response (PR #294 review follow-up). */
a.accent-chip:active { transform: scale(0.96); filter: brightness(0.93); }
@media (forced-colors: active) and (hover: hover) {
  a.accent-chip:hover { text-decoration: underline; }
}
@media (forced-colors: active) {
  a.accent-chip:focus-visible { text-decoration: underline; }
}

/* no_months.html -- .empty-note plus the page margin. */
.no-months-note { margin: 20px 0; }

/* An <a> wearing a .btn-* class needs these two back; the button rules
   assume a <button>. Grep btn-link-reset for the wearers -- a hand-kept
   roster here went stale once already. */
.btn-link-reset { display: inline-block; text-decoration: none; }

/* password_reset_complete.html */
.auth-return-link { margin-top: 12px; }

/* _month_totals.html */
#month-totals { display: flex; gap: 24px; flex-wrap: wrap; margin-bottom: 20px; }
.total-tile { padding: 14px 20px; }
.total-tile-num { font-size: 1.6rem; font-weight: 700; font-variant-numeric: tabular-nums; }
.total-tile-label { font-size: 0.8rem; color: var(--muted); }

/* The full-width data table, shared by the coverage-gap table
   (_missing_assignments_table.html), the accounts table, the failed-emails
   table and the audit log. Each of those still names its own cells, which
   differ; only this base was identical in all four. */
.data-table { width: 100%; border-collapse: collapse; font-size: 0.85rem; }

/* _missing_assignments_table.html. Cell-level classes rather than element
   selectors on purpose: the empty-state td has no border-bottom, and a
   `.data-table td` rule would have added one. */
.missing-th { text-align: left; padding: 6px 8px; border-bottom: 1px solid var(--border); }
.missing-td { padding: 6px 8px; border-bottom: 1px solid var(--border); }
.missing-td-empty { padding: 6px 8px; color: var(--muted); }

/* _day_row.html and _daytime_physician_day.html -- the signup-surface day
   card. Their inline attributes were spelled differently (whitespace, and
   the daytime one has no gap_open branch) but resolved to the same
   declarations per state, so they share one family.
   `.day-row` stays what it was: the behaviour hook (.tappable rules, the
   desktop pointer-events gate); `.day-card` carries the extracted look,
   with the raised-card base worn as `.card` in the templates. */
.day-card {
  display: flex; flex-direction: column; gap: 4px;
  padding: 14px 16px; margin-bottom: 8px;
}
/* The category bar restated with the elevation -- see the bar comment at
   "Shift-type accent". */
.day-card.shift-tue-thu { box-shadow: var(--cat-bar-a), var(--shadow); }
.day-card.shift-fri-mon { box-shadow: var(--cat-bar-b), var(--shadow); }
/* The state ring: 2px, drawn with outline at -2px offset so it thickens the
   edge with zero layout shift and follows the corner radius. One rule per
   state, one class vocabulary, for BOTH surfaces -- phone day card and
   desktop grid cell -- which is the is_tappable consistency rule holding by
   construction. The grid selectors are safe outside the width media block:
   below 768px the grid subtree is display:none. No shift-* on a state day
   (CONVENTIONS.md, is_tappable section).

   These stay `outline`, not box-shadow like the category bar above: the
   keyboard focus ring (the :focus-visible group near the buttons) relies on
   outline being one property to *replace* the state ring while a state day
   holds focus (issue #258). Migrated to box-shadow, a focused state day
   would paint both rings, and no test would notice -- ring rendering is
   browser-only. */
.day-card.state-mine,
.calendar-grid .grid-cell.state-mine {
  background: var(--mine-light); border-color: var(--mine);
  outline: 2px solid var(--mine); outline-offset: -2px;
}
.day-card.state-gap,
.calendar-grid .grid-cell.state-gap {
  background: var(--excluded-bg); border-color: var(--excluded);
  outline: 2px solid var(--excluded); outline-offset: -2px;
}
.day-card-top {
  display: flex; align-items: center; justify-content: space-between; gap: 10px;
}
.day-card-date { font-weight: 600; font-size: 1rem; }
.day-card-right { display: flex; align-items: center; gap: 8px; flex-shrink: 0; }
.day-note { font-size: 0.8rem; font-weight: 650; }
.day-note.day-note-mine { color: var(--mine-ink); }
.day-note.day-note-gap { color: var(--excluded-ink); }
.day-count {
  background: var(--accent-light); border: 1px solid var(--border);
  color: var(--accent-ink); font-size: 0.8rem; font-weight: 700;
  font-variant-numeric: tabular-nums;
  border-radius: 999px; padding: 4px 10px;
}
/* The daytime count pill, one rule for both daytime surfaces (physician
   list and admin ranked list) so the pair cannot drift. Kept separate
   from .day-count above rather than unified, since adding its border
   would be a visible change. */
.day-count-plain, .ranked-count {
  background: var(--accent-light); color: var(--accent-ink);
  font-size: 0.8rem; font-weight: 700; font-variant-numeric: tabular-nums;
  border-radius: 999px; padding: 4px 10px;
}
.day-people { font-size: 0.8rem; color: var(--muted); }

/* The Daytime posted order (issue #312), on both of its surfaces -- the
   physician Assigned view's day cards (.p-ranked-*, the prefix that keeps
   these names off other pages' classes) and the admin ranked list, which
   puts .rank-pill inside its existing .ranked-row.

   The pill's 1px ink border is load-bearing, not decoration: the accent
   tint alone dissolves into --mine-light on a signed-up day's mine-tinted
   card, and the -ink tone is the one already tuned to read on tints in
   every theme (the mine family's hue is the theme's own business -- Harbor's
   is blue). The "new" variant is the unplaced late signup, dashed to say
   "not yet part of the order". */
.rank-pill {
  flex-shrink: 0; font-size: 0.65rem; font-weight: 700;
  background: var(--accent-light); color: var(--accent-ink);
  border: 1px solid var(--accent-ink); border-radius: 999px;
  padding: 2px 8px; min-width: 2rem; text-align: center;
}
.rank-pill-new {
  background: var(--warning-bg); color: var(--text);
  border-style: dashed; border-color: var(--warning);
}
.p-ranked-rows { display: flex; flex-direction: column; gap: 5px; margin-top: 8px; }
.p-ranked-row { display: flex; align-items: center; gap: 8px; font-size: 0.85rem; }
.p-name { font-weight: 600; }
.p-name-mine { font-weight: 700; color: var(--mine-ink); }
.p-name-muted { color: var(--muted); }
/* .p-, not bare .worked-note: release_schedule.css owns that name for its
   own page, and base.html loads page CSS after this file -- a bare twin
   here leaks any declaration the page rule doesn't restate (found as a
   font-weight leak by /code-review on PR #313). */
.p-worked-note { color: var(--mine-ink); font-weight: 700; font-size: 0.75rem; }
/* The admin list's reorder arrows, pushed to the row's far edge so the
   tap targets sit clear of the mark-as-worked badges. */
.rank-move { display: flex; gap: 4px; margin-left: auto; }
.rank-arrow { padding: 2px 9px; font-size: 0.8rem; }

/* The admin day surfaces: _assignment_day_badges.html and
   _daytime_ranked_day.html. (_signup_chip_row.html sits inside both but
   has its own block below.) `.chip-row` is also My Assignments' type-chip
   row, and `.add-select` the account roster dropdown on account_list.html
   -- same control, same declarations. */
.inline-form { display: inline; }
.chip-row { display: flex; flex-wrap: wrap; gap: 6px; }
.add-row { display: flex; gap: 6px; align-items: center; }
.add-select {
  font-size: 0.8rem; padding: 4px 6px; border-radius: 6px;
  border: 1px solid var(--border);
}
.signups-block {
  border-top: 1px solid var(--border); padding-top: 8px;
  display: flex; flex-direction: column; gap: 6px;
}
.signups-block-spaced { margin-top: 8px; }
.signups-label { font-size: 0.75rem; font-weight: 600; color: var(--muted); }

/* Filter controls (issue #340, My Assignments' panel). Here rather than in
   the page stylesheet because every other control in this app is here, and a
   page-local control would be the first one anybody had to go looking for.

   `.toggle-chip` is a multi-select toggle and deliberately NOT a widening of
   `.accent-chip`: every interactive rule on that one is `a.accent-chip`, a
   link with a single appearance, and a control that stays pressed is a
   different thing wearing a similar shape. The rounded rectangle is shared on purpose though
   -- app.css's tab pill means "switches view in place, one at a time", which
   is the one promise a multi-select must not make.

   The checkbox is the control; the label is its skin. Visually hidden rather
   than `display: none`, which would take it out of the tab order and leave a
   toggle no keyboard could reach. Focus is drawn on the label, since the
   input itself is a 1px speck. */
/* Present for screen readers, copy and CSS-off; absent from the rendered
   page. One recipe for every such element -- the chip's checkbox, and the
   "&" a My Assignments row reads between two chips. */
.visually-hidden, .toggle-chip input {
  position: absolute; width: 1px; height: 1px; overflow: hidden;
  clip-path: inset(50%); white-space: nowrap;
}
.toggle-chip {
  display: inline-flex; align-items: center; cursor: pointer;
  padding: 4px 11px; border-radius: var(--radius-ctl);
  border: 1px solid var(--border); background: var(--surface);
  color: var(--muted); font-size: 0.82rem; font-weight: 600;
  transition: background 0.1s ease, color 0.1s ease, border-color 0.1s ease;
}
.toggle-chip:has(input:checked) {
  background: var(--accent); border-color: var(--accent); color: var(--on-accent);
}
.toggle-chip:has(input:focus-visible) {
  outline: 2px solid var(--accent); outline-offset: 2px;
}
@media (hover: hover) {
  .toggle-chip:hover { border-color: var(--accent); }
  /* Solid tier (the -solid token's comment in :root carries the why and the
     roster): a pressed chip wears --on-accent ink on the accent fill. */
  .toggle-chip:has(input:checked):hover { filter: var(--hover-solid); }
}
.toggle-chip:active { transform: scale(0.96); }

/* The filter panel's selects. Sized between `.add-select` and the month
   switcher; a shared base for all three would have to reconcile three
   paddings and three radii, which is more coupling than three short rules. */
.ctl-select {
  font: inherit; font-size: 0.83rem; padding: 5px 7px;
  border-radius: var(--radius-ctl); border: 1px solid var(--border);
  background: var(--surface); color: var(--text);
}

/* The physician badge pill, clickable (badge grid, ranked day) and
   read-only (both surfaces' is_read_only branches). */
.pbadge { border-radius: 999px; padding: 6px 12px; font-size: 0.8rem; font-weight: 600; }
/* Elevation on the clickable badge only: the span variant is the read-only
   branch of the same surface, and a label does not get a control's shadow. */
button.pbadge {
  border: none; cursor: pointer; box-shadow: var(--shadow);
  transition: transform 0.1s ease, filter 0.1s ease, box-shadow 0.1s ease;
}
/* Not a lift-group member: no translateY. The count and doubles bubbles are
   absolutely-positioned siblings anchored to .badge-wrap, so a lifted badge
   would shear 1px out from under its own overlays. The brightness shift +
   deepen carry the hover alone here. */
@media (hover: hover) {
  button.pbadge:hover {
    filter: var(--hover-lift);
    box-shadow: var(--shadow-hi);
  }
  /* Solid tier (the -solid token's comment in :root carries the why and
     the roster): assigned and excluded wear --on-accent; avail keeps
     the lift tier with its light tint. This compound outranks
     button.pbadge's press rule too, which is why the press filter is
     restated at matching specificity below. */
  button.pbadge.pbadge-assigned:hover,
  button.pbadge.pbadge-excluded:hover { filter: var(--hover-solid); }
}
/* The touch channel -- badges are the dashboard's most-tapped control and
   press is the only feedback a finger gets. */
button.pbadge:active {
  transform: scale(0.96);
  filter: brightness(0.91);
}
/* The solid-tier states' press, restated at (0,3,1): without this,
   their (0,3,1) hover above outranks the (0,2,1) press rule and a held
   badge stays at the hover lighten -- press must win, as everywhere. */
button.pbadge.pbadge-assigned:active,
button.pbadge.pbadge-excluded:active { filter: brightness(0.91); }
span.pbadge { display: inline-block; }
/* Compounds, not bare .pbadge-* -- the bare spelling (0,1,0) sat *below*
   `button.pbadge`/`span.pbadge` (0,1,1), harmless only while those rules set
   no colour. The compound outranks them wherever it sits. */
.pbadge.pbadge-assigned { background: var(--mine); color: var(--on-accent); }
.pbadge.pbadge-excluded { background: var(--excluded); color: var(--on-accent); }
.pbadge.pbadge-avail { background: var(--accent-light); color: var(--accent-ink); }

/* _assignment_day_badges.html */
.admin-day {
  display: flex; flex-direction: column; gap: 8px;
  padding: 12px 16px; margin-bottom: 6px;
}
/* The category bar restated with the elevation -- see the bar comment at
   "Shift-type accent". */
.admin-day.shift-tue-thu { box-shadow: var(--cat-bar-a), var(--shadow); }
.admin-day.shift-fri-mon { box-shadow: var(--cat-bar-b), var(--shadow); }
/*
  Opting back into pointer events. Above 768px `.day-row` is deliberately
  inert (see the `pointer-events: none` rule in the desktop media block) so
  the calendar grid is the only place a signup can be toggled. The admin
  badge grid reuses `.day-row` for its container but *is* its own input
  surface, so it has to undo that -- the inline attribute this replaces did
  it by out-ranking every stylesheet rule.

  **Two classes, not one, and that is the point.** `.admin-day` alone ties
  with `.day-row` on specificity and would win only by sitting later in this
  file. A reorder would then make every badge, Max field and Add Signup
  select on the dashboard silently unclickable on a desktop -- no error, and
  no test covers pointer events. The compound wins wherever it sits.
*/
.day-row.admin-day { pointer-events: auto; cursor: default; }
.admin-day.admin-day-full { background: var(--bg); opacity: 0.65; }
.admin-day-date { font-weight: 600; font-size: 0.95rem; }
.max-note { font-size: 0.8rem; color: var(--muted); }
.max-form {
  display: flex; align-items: center; gap: 6px;
  font-size: 0.8rem; color: var(--muted);
}
.max-input { width: 48px; padding: 3px 6px; border-radius: 6px; border: 1px solid var(--border); }
.badge-wrap { position: relative; display: inline-block; }
.double-bubble {
  position: absolute; bottom: -8px; left: -8px; background: var(--excluded);
  color: var(--on-accent); border-radius: 999px; min-width: 18px; height: 18px;
  padding: 0 4px; display: flex; align-items: center; justify-content: center;
  font-size: 0.6rem; font-weight: 700; border: 2px solid var(--surface);
  pointer-events: none;
}
.assignment-count-badge {
  /* --accent, never --muted, under the --on-accent digits (issue #281):
     the accent fill is the one pairing every palette tunes --on-accent
     against; --muted is a text tone, far too light a fill for it on the
     dark themes. */
  position: absolute; top: -8px; right: -8px; background: var(--accent);
  color: var(--on-accent); border-radius: 50%; width: 18px; height: 18px;
  display: flex; align-items: center; justify-content: center;
  font-size: 0.65rem; font-weight: 700; border: 2px solid var(--surface);
  pointer-events: none;
}

/* _signup_chip_row.html -- colours from .accent-chip, radius overridden
   to the pill on purpose; the why, and why a compound, live at the
   primitive's comment. */
.accent-chip.signup-chip {
  display: inline-flex; align-items: center; gap: 4px;
  border-radius: 999px; padding: 3px 4px 3px 10px; font-size: 0.8rem;
}
.chip-byadmin { font-size: 0.65rem; opacity: 0.75; }
.chip-form { display: inline; margin: 0; }
.chip-x {
  border: none; background: none; cursor: pointer; color: var(--accent-ink);
  font-weight: 700; padding: 0 4px; line-height: 1;
}
.chip-none { font-size: 0.8rem; color: var(--muted); }

/* _daytime_ranked_day.html -- the raised-card base rides in as .card. */
.ranked-day {
  padding: 14px 16px; margin-bottom: 8px;
}
/* Three explicitly-placed columns rather than the flex space-between this
   was, so the handover chip sits in the card's true centre. Under
   space-between it only *looked* centred while the date and the count pill
   happened to be near-equal width -- a day carrying marks reads "3 signed
   up · 1 marked" and slid the chip visibly left. Each child names its own
   column, so a header missing the chip, the count, or both still lays out
   the same: an absent child leaves its column at zero width and the 1fr
   pair still splits the slack. */
.ranked-day-top {
  display: grid; grid-template-columns: 1fr auto 1fr;
  align-items: center; gap: 10px; margin-bottom: 0;
}
.ranked-day-top .day-card-date { grid-column: 1; justify-self: start; }
.ranked-day-top .handover-flag { grid-column: 2; justify-self: center; }
/* nowrap so the count's column is sized by what it says. Without it the
   equal 1fr columns hand the count half the slack whatever it needs, and
   "3 signed up · 1 marked" broke across two lines inside its own pill from
   560px down. Sized this way the wider column takes what it wants from the
   date's, which costs the chip a few pixels of centring in that band and
   nothing at the widths anyone reads this at. */
.ranked-day-top .ranked-count {
  grid-column: 3; justify-self: end; white-space: nowrap;
}
/* On a phone the three do not fit one line: the date, the chip and a count
   reading "3 signed up · 1 marked" want ~420px of viewport, and the equal
   1fr columns squeezed the count into two lines at 375px well before that.
   The chip takes its own centred row instead, which keeps the centring the
   card is asking for rather than trading it for a cramped line.

   480px here against the rows' 767px, deliberately: measured, this header
   holds one line down to 500px, so stacking it any earlier would cost the
   centred chip its place beside the date for nothing. */
@media (max-width: 480px) {
  .ranked-day-top { grid-template-columns: 1fr auto; }
  .ranked-day-top .day-card-date { grid-row: 1; }
  .ranked-day-top .ranked-count { grid-column: 2; grid-row: 1; }
  .ranked-day-top .handover-flag { grid-column: 1 / -1; grid-row: 2; }
}
.ranked-day-top.has-rows { margin-bottom: 10px; }
/* .ranked-count: grouped with .day-count-plain up in the day-card
   section -- the same pill on the sibling daytime surface. */
.ranked-rows { display: flex; flex-direction: column; gap: 6px; }
.ranked-row { display: flex; align-items: center; gap: 8px; }
.ranked-score { color: var(--muted); font-size: 0.8rem; }

/* The handover flag (issue #312): the days where somebody was overtaken in
   the posted order, which is where the admin has a decision to make.

   Amber, not red and not a ring: both are spoken for on this surface --
   red is the needs-coverage family app-wide, and a ring meant the
   consecutive run this PR retires -- and reusing either would say the wrong
   thing to anyone who has learnt them elsewhere. The words carry the
   meaning; the colour only draws the eye, per the token charter's rule that
   colour may reinforce a signal but never be it.

   **Only the glyph wears the plain hue.** --warning has no -ink variant in
   any palette, deliberately, and the one sanctioned plain-hue text use is
   .flash-warning's "!" -- a glyph, bold, on --warning-bg. Lettering is a
   different case, and the token's own charter says to mint the -ink form
   when it appears rather than borrow the glyph's tolerance: measured,
   --warning on --warning-bg is 2.14:1 in light, 2.34:1 in sandstone and
   2.47:1 in harbor, against AA's 4.5:1 for normal text. So the words are
   --text on --warning-bg -- a registered pair in
   ComposedTextPairsStayReadable, 12.8:1 or better everywhere -- and amber
   is carried by the glyph, the chip's border, the row tint and the card's
   edge, every one of them a mark rather than a word.

   The card's edge restates --shadow because it is a box-shadow list: the
   .card rule it overrides carries the elevation, and dropping it would
   flatten exactly the cards asking to be noticed. Same mechanism as
   --cat-bar-a on the assignment day cards, and 3px rather than that bar's
   4px so the two never read as the same mark. */
.ranked-day.handover {
  box-shadow: inset 3px 0 0 0 var(--warning), var(--shadow);
}
.handover-flag {
  display: inline-flex; align-items: center; gap: 4px;
  background: var(--warning-bg); color: var(--text);
  border: 1px solid var(--warning); border-radius: 999px;
  padding: 2px 9px;
  font-size: 0.7rem; font-weight: 700; white-space: nowrap;
}
/* Generated content, the way every .flash-* glyph is: it is the mark, and
   the chip's words carry the meaning without it. U+2691. */
.handover-flag::before { content: "\2691"; color: var(--warning); }
/* Every displaced row is tinted, not just the first: with 0-3 signups a day
   a three-row card is near the worst case, and the tint is what makes
   "which rows moved" scannable without reading. The side margins pull the
   tint out towards the card's own padding so it reads as a band across the
   row rather than a box around the badge. */
.ranked-row.handover-from {
  background: var(--warning-bg); border-radius: var(--radius-sm);
  margin: 0 -6px; padding: 3px 6px;
}
.handover-note { color: var(--muted); font-style: italic; font-size: 0.8rem; }
/* Below 768px the row's parts do not fit one line, and left to flex they
   each broke somewhere different: a long name pushed the score down, a
   short one let the score stay and pushed the arrows down alone, so no two
   rows in a card had the same shape -- and a name squeezed in place broke
   *inside itself*, "Liam / Brown". Named rows and columns instead, which
   pins the arrows to the top right of every row whatever the name beside
   them does. Each child places itself, so a row missing its pill (no
   posted order yet) or its arrows (a past day) lays out the same.

   767px, not the header's 480px below, and measured rather than picked:
   name badges on the September roster wrap from 750px down and are clean
   at 768px, so the two rules genuinely want different widths -- the header
   stays on one line to 500px. This was 480px first, which left every width
   from 481 to 767 on the flex row: caught by shrinking a desktop window
   rather than by phone emulation, which is why sampling three widths
   missed it.

   The roster's longest name clears it, which was worth measuring rather
   than reasoning about: "Venkataraghavan Renduchintala" is ~96px wider
   than the next longest, and the arithmetic said his cards would wrap up
   to ~864px. They do not -- the badge holds one line on a *displaced* row,
   the widest kind, at every width from 768px to 1000px. The score is the
   flexible item here, so it absorbs the squeeze first and the badge is
   only reached once the score has collapsed; below that point the grid has
   already taken over. Above 768px there is nothing left to protect. */
@media (max-width: 767px) {
  /* Only the axis that changes: the base rule's align-items and its
     `gap: 8px` both carry over, so this restates neither. */
  .ranked-row {
    display: grid; grid-template-columns: auto minmax(0, 1fr) auto;
    row-gap: 3px;
  }
  .ranked-row > .rank-pill { grid-column: 1; grid-row: 1; }
  .ranked-row > .inline-form,
  .ranked-row > .pbadge { grid-column: 2; grid-row: 1; justify-self: start; }
  .ranked-row > .rank-move { grid-column: 3; grid-row: 1; }
  .ranked-row > .ranked-score { grid-column: 1 / -1; grid-row: 2; }
  .ranked-row > .handover-note { grid-column: 1 / -1; grid-row: 3; }
}
/* The section's two state lines. Both sit above the day cards and describe
   the whole month: how many days change hands, and -- when the answer is
   "none because nothing was built" -- the call to action that fixes it. */
/* The muted lead-in is .section-hint's, worn as a second class -- it is
   the same thing in the same place, a section's lead-in on the admin
   dashboard, and a near-copy differing by 2px of margin is how that family
   grows a third member nobody retunes together. All that is local is the
   count, which lifts out of the muted body. */
.handover-summary strong { color: var(--text); }
.order-state-line {
  display: flex; flex-wrap: wrap; align-items: center; gap: 10px;
  font-size: 0.85rem; color: var(--muted); margin-bottom: 10px;
}

/* _physician_stats_table.html */
.stats-table { border-collapse: collapse; margin-bottom: 28px; font-size: 0.9rem; }
.stats-head-row { text-align: left; border-bottom: 2px solid var(--border); }
.stats-row { border-bottom: 1px solid var(--border); }
.stats-cell { padding: 8px; }
.stats-cell-strong { font-weight: 600; }
.stats-cell-muted { color: var(--muted); }
/* `.breakdown-detail`'s own rule is up with the show/hide toggle it belongs
   to -- search "Per-physician Tue-Thu/Fri-Mon breakdown". */
.phase-sq { display: inline-block; width: 8px; height: 8px; border-radius: 2px; }
.phase-sq.phase-sq-a { background: var(--shift-a); }
/* The margin is the gap between the two squares, so it rides on the second
   one rather than on a third class -- which is also how the single inline
   attribute this came from spelled it. */
.phase-sq.phase-sq-b { background: var(--shift-b); margin-left: 4px; }

/* _prepublish_controls.html */
#prepublish-controls { display: contents; }
.prepublish-checklist {
  display: flex; align-items: center; gap: 12px; flex-wrap: wrap;
  flex: 1 1 100%; padding: 8px 14px; border-radius: var(--radius-sm);
  border: 1px solid var(--border); background: var(--surface); font-size: 0.85rem;
  box-shadow: var(--shadow);
}
.checklist-title { font-weight: 600; color: var(--text); white-space: nowrap; }
.checklist-item { display: flex; align-items: center; gap: 6px; cursor: pointer; white-space: nowrap; }
.checklist-sep { width: 1px; height: 18px; background: var(--border); flex-shrink: 0; }
/* Compound with each state's base button class, so the override holds
   wherever these sit in the file. Bare `.publish-ready` would tie with
   `.btn-primary` and win only on source order -- and a reorder would light
   the Publish button up as ready over a month the server will refuse. */
.btn-primary.publish-ready {
  background: var(--mine);
  /* Transparent twin of not-ready's 1px border: the button is
     intrinsically sized, so without it the ready swap grows the box 2px
     and the row visibly jumps -- the OOB replacement never transitions. */
  border: 1px solid transparent;
}
/* Not-ready wears the .btn-secondary shape with --muted text (issue #284):
   inert but readable. The old --muted *fill* put --on-accent on a text
   tone -- under 2.6:1 on the dark themes -- and no theme's --text reads on
   that grey either. As text on --surface, --muted is the pairing it is
   tuned for: 4.5:1 or better in every palette. Inertness now comes from
   being the action bar's one all-grey control rather than from a fill. */
.btn-secondary.publish-not-ready { color: var(--muted); }

/* Shared by signup.html and admin_dashboard.html -- the two month pages.
   (.tabs-mid also spaces signup's Assigned/Signups toggle, which is an
   .admin-tabs pill row wearing the same 14px.) */
.admin-tabs.tabs-tight { margin-bottom: 10px; }
.meta-row {
  display: flex; gap: 10px; flex-wrap: wrap; align-items: center;
  margin-bottom: 10px; font-size: 0.85rem; color: var(--muted);
}
.meta-row.spaced { margin-bottom: 16px; }
/* The status pills as one flex item, so the row breaks between the window line
   and the pills rather than between one pill and the next (issue #345). Keeps
   its own flex-wrap: three pills (an archived month) still break within the
   group rather than overflowing it.

   Not shared with .month-card-pills, which does the same job for this app's
   two other _status_pills.html callers: the gap matches the parent row's, and
   .meta-row's is 10px against .month-card's 8px. */
.meta-row-pills { display: flex; gap: 10px; flex-wrap: wrap; }
.page-headrow {
  display: flex; align-items: center; justify-content: space-between;
  gap: 10px; flex-wrap: wrap; margin: 4px 0 12px;
}
.month-heading.month-heading-tight { margin: 0; }
.signup-hint { color: var(--muted); font-size: 0.85rem; margin: 4px 0 14px; }
/* Scoped under .grid-legend so they outrank `.grid-legend i` (0,2,0 over
   0,1,1) -- bare .legend-a/b lost that contest and only looked fine because
   the i rule sets no colour. */
.grid-legend .legend-a { background: var(--shift-a); }
.grid-legend .legend-b { background: var(--shift-b); }
/*
  The key CONVENTIONS.md's families exception owes (#273), in
  signup.html's legend. The spans render for every theme -- the swatches
  draw from the state tokens, so they are correct anywhere -- and one
  CSS rule hides them where no key is owed; a future theme that earns a
  key joins the :not() list. Solid chips like .legend-a/b, not the
  state's tint-plus-ring: the tints are tuned against the card surface
  and go invisible on the page background the legend sits on.
*/
:root:not([data-theme="harbor"]) .grid-legend .legend-state { display: none; }
.grid-legend .legend-mine { background: var(--mine); }
.grid-legend .legend-gap { background: var(--excluded); }

/* admin_dashboard's badge key (the template comment carries why chips
   replaced colour words). The avail chip adds a border the badge does
   not have, so its pale tint reads as a chip on the page background.
   Fourth site-local swatch primitive after .readiness-dot, .phase-sq and
   .grid-legend i -- kept separate like them: this one sits in running
   prose, not a flex row. */
.key-dot {
  display: inline-block;
  width: 10px; height: 10px;
  border-radius: 3px;
}
.key-dot-assigned { background: var(--mine); }
.key-dot-avail { background: var(--accent-light); border: 1px solid var(--border); }
.key-dot-excluded { background: var(--excluded); }

/* The section heading and its lead-in, on the admin pages that stack
   several sections: admin_dashboard, admin_schedule, admin_settings,
   account_list, queued_email_failures. */
.section-h3 { font-size: 1.1rem; margin-bottom: 6px; }
.section-hint { color: var(--muted); font-size: 0.85rem; margin: 0 0 10px; }

/* Every labelled form field in the app -- seven templates today. */
.form-label { font-size: 0.85rem; font-weight: 600; }

/* Table and form odds and ends, each used by two or more pages. */
.cell-form { margin: 0; }
.audit-pager {
  display: flex; justify-content: space-between; align-items: center;
  margin-top: 10px; font-size: 0.85rem;
}
.pager-note { color: var(--muted); }
.pref-intro { color: var(--muted); font-size: 0.85rem; margin: 0 0 14px; }
.pref-help { font-weight: 400; color: var(--muted); font-size: 0.8rem; margin: 2px 0 4px; }
.pref-input { width: 100%; padding: 8px 10px; border-radius: var(--radius-ctl); border: 1px solid var(--border); }
.pref-divider {
  border-top: 1px solid var(--border); padding-top: 14px;
  display: flex; flex-direction: column; gap: 14px;
}
.pref-group-head {
  font-size: 0.75rem; font-weight: 700; text-transform: uppercase;
  letter-spacing: 0.05em; color: var(--muted);
}
.pref-check-label {
  font-size: 0.85rem; font-weight: 600;
  display: flex; align-items: center; gap: 10px; cursor: pointer;
}
.pref-checkbox { width: 16px; height: 16px; cursor: pointer; }
.pref-note { font-weight: 400; color: var(--muted); font-size: 0.8rem; }
.pref-h3 { font-size: 1rem; font-weight: 600; margin: 0 0 6px; }
/* The "Per shift type" grid on both Preferences pages (issue #330): a label
   column sized to its content, then Per week / Per month sharing the rest
   -- about 80px each on a 360px phone, measured. A row label is the
   category, with the pool beneath it on the two Evening rows beside the
   schedule's own Tue-Thu / Fri-Mon swatch (.phase-sq, the stats table's),
   so the split reads as the two halves of the week the grid and legend
   already colour that way. The column headers wear .pref-group-head a
   step smaller and the row labels wear .form-label (they are the inputs'
   labels, via aria-labelledby), so a retune of either reaches them. The
   swatch sheds .phase-sq-b's sibling margin: that gap is for the stats
   table, where the two squares sit side by side. */
/* .rule-grid is the Schedule tab's signup-window rule (issue #341): the same
   two-value-columns-per-row shape, so it shares the rules rather than
   restating them, the way .pref-form/.create-form/.settings-form share one
   body below. Its column heads wear .pref-group-head too, which is now the
   one muted uppercase group head across both pages -- the `pref-` prefix
   under-describes it, and renaming it would touch every Preferences
   template, so it stays. */
.limits-grid, .rule-grid {
  display: grid; grid-template-columns: max-content 1fr 1fr;
  gap: 8px 10px; align-items: center;
}
.limits-grid .pref-group-head, .rule-grid .pref-group-head { font-size: 0.72rem; }
/* .form-field's top margin is for a label stacked above it; in a grid the
   label is the row's first cell, so the input sits on the row's baseline. */
.rule-grid .form-field { margin-top: 0; }
.limits-row { line-height: 1.2; }
.limits-pool {
  display: flex; align-items: center; gap: 5px;
  font-size: 0.75rem; font-weight: 600; color: var(--muted); margin-top: 2px;
}
.limits-pool .phase-sq { margin-left: 0; }
.limits-grid .pref-input { font-variant-numeric: tabular-nums; }
/* The scheduling-limits form -- preferences.html (physician, with a
   section below it) and admin_preferences.html (admin, last on the page),
   hence the one difference, the bottom margin -- and the same card worn by
   the Appearance panel's theme picker (issue #257). Its body is the
   grouped form-card rule in the Promoted section below; only the
   margin modifier lives here. */
.pref-form.spaced-below { margin-bottom: 28px; }
.updated-note { color: var(--muted); font-size: 0.8rem; margin: 0; }

/* Shared by admin_months.html and archived_months.html -- the month lists. */
.admin-tabs.tabs-mid { margin-bottom: 14px; }
.month-row { display: flex; align-items: stretch; gap: 8px; margin-bottom: 10px; }
/* Raised-card base worn as .card in both month-list templates. */
.month-card {
  flex: 1; min-width: 0; text-decoration: none; color: inherit;
  padding: 14px 18px;
  display: flex; justify-content: space-between; align-items: center;
  flex-wrap: wrap; gap: 10px;
  transition: transform 0.1s ease, filter 0.1s ease, box-shadow 0.1s ease;
}
/* Hover: the shared lift group; press: grouped with .day-row.tappable's,
   which it matches. */
.month-card-title { font-weight: 700; font-size: 1.05rem; margin-bottom: 4px; }
.month-card-pills { display: flex; gap: 8px; flex-wrap: wrap; }
.month-card-stats {
  display: grid; grid-template-columns: repeat(4, max-content);
  column-gap: 8px; row-gap: 4px; font-size: 0.85rem; color: var(--muted);
  justify-items: end; align-items: baseline;
}
.stat-label { font-weight: 600; color: var(--text); }
.stat-num { color: var(--text); }
.flex-form { display: flex; }
.btn-secondary.archive-btn { padding: 0 14px; font-size: 0.8rem; white-space: nowrap; }

/* Promoted: shared by 2+ pages once the last templates were extracted
   (admin_preferences/release_schedule/account_edit; account_list/
   admin_schedule; admin_settings/admin_schedule/account_edit;
   preferences/admin_email_preferences). */
.btn-row { display: flex; gap: 10px; }
.physician-pick {
  font-size: 0.85rem; font-weight: 600; display: block;
  max-width: 360px; margin-bottom: 20px;
}
/* The form card, one body for the four of them -- shared values in the
   group, each form's own deltas below it. All four wear .card in their
   templates. */
.pref-form, .create-form, .settings-form, .pref-form-email {
  padding: 16px 20px;
  display: flex; flex-direction: column; gap: 12px; max-width: 360px;
}
.pref-form { gap: 14px; }
.pref-form, .pref-form-email { max-width: 400px; }
.create-form { margin-bottom: 28px; }
/* .theme-radio / .theme-choice-label: the Appearance panel's rows are the
   email panel's row shape with a radio instead of a checkbox, so they share
   the rules rather than restating them. */
.pref-checkbox-accent, .theme-radio { width: 16px; height: 16px; cursor: pointer; accent-color: var(--accent); }
.email-check-label, .theme-choice-label {
  display: flex; align-items: center; gap: 10px; font-size: 0.9rem; cursor: pointer;
}
.email-divider { border-top: 1px solid var(--border); padding-top: 12px; margin-top: 4px; }
.snooze-label { font-size: 0.85rem; font-weight: 600; display: block; }
.snooze-active-note { color: var(--excluded-ink); font-size: 0.8rem; margin: 6px 0 0; }

/* admin_schedule.html */
.latest-note { color: var(--muted); font-size: 0.85rem; margin: 0 0 4px; }


/* login.html + password_reset_confirm.html -- the auth pages. */
.auth-form { display: flex; flex-direction: column; gap: 12px; margin-top: 24px; }
.auth-error { color: var(--excluded-ink); }
.auth-note { color: var(--muted); }
.auth-input {
  width: 100%; padding: 12px; border-radius: var(--radius-ctl);
  border: 1px solid var(--border); font-size: 1rem; margin-top: 4px;
}
/* Compound, so the padding override does not depend on sitting below
   `.btn-primary` in this file. */
.btn-primary.auth-submit { padding: 14px; font-size: 1rem; }

/*
  Error pages (issues #263, #300, #302): templates/_error_page.html is the
  only wearer -- the five Django-named error templates and the axes lockout
  page all render through it. Standalone documents with no app bar, so the
  page has to read as the app at rest on tokens alone -- the status code is
  oversized muted type rather than a heading, and the global `main` rule
  provides the column.
*/
.error-page { text-align: center; padding-top: 18vh; }
.error-code {
  font-size: 2.6rem;
  font-weight: 700;
  color: var(--muted);
  letter-spacing: 0.04em;
  margin: 0;
}
.error-page h1 { font-size: 1.4rem; margin: 6px 0 10px; }
.error-note { color: var(--muted); margin: 0 0 24px; }

/*
  Reduced motion: the restyle made micro-motion systemic (hover lifts, press
  scales, shadow transitions), so it gets the one systemic escape. Zeroing
  duration rather than removing the states: the lift and press still land,
  instantly -- the preference is about movement over time, not about state
  feedback. 0s and not a near-zero: transition-property's initial value is
  `all`, so any non-zero duration on * makes every element in the document
  transition-eligible and fire transition events on every style change --
  and nothing in this app (htmx included: it settles on timers) listens for
  them. !important is the point, not a smell: this must outrank every
  per-component transition above without naming them.

  **The `!important` is the protection, not the position.** This used to be
  the last rule in the file and said so; issue #138 appended the htmx
  indicator rules below it. Nothing broke -- an important declaration
  outranks a later normal one whatever the order, and that was always what
  was doing the work here. But a later rule that is *also* `!important`
  would win on specificity, `.htmx-indicator` (0,1,0) beating `*` (0,0,0),
  and motion would come back for the people who asked for none. So: nothing
  below may carry `!important` on a transition property.
*/
@media (prefers-reduced-motion: reduce) {
  * {
    transition-duration: 0s !important;
  }
}

/*
  htmx's own indicator rules, moved here from htmx's own injected <style>.

  htmx writes these three into a <style> element at init unless told not to.
  `default-src 'self'` blocks that element (issue #138), which does not error
  anywhere -- htmx neither checks nor reports it -- and simply leaves
  `.htmx-indicator` at its default `opacity: 1`. An indicator that is meant to
  appear only during a request would sit visible on the page forever.

  Nothing in this app uses `hx-indicator` today, so this restores no broken
  screen. It is here so that the next thing that does works the way htmx
  documents, rather than discovering this. `includeIndicatorStyles` is turned
  off in `base.html`'s htmx-config meta, and the two go together: turning the
  injection off without these leaves the same silent default behind.

  Copied from `htmx.min.js` at the vendored version -- the declarations are
  its, the whitespace is ours, so do not diff them literally. Keep them in
  step with an htmx upgrade the way CONVENTIONS' vendoring rule already asks.

  The two `.htmx-request` selectors are not a duplicate pair: one is a
  descendant, the other a compound on a single element, and htmx ships both.

  Not tokenised, because there is no colour here -- opacity and a duration --
  and `PageColoursComeFromTokens` is about colour literals.
*/
.htmx-indicator { opacity: 0; }
.htmx-request .htmx-indicator { opacity: 1; transition: opacity 200ms ease-in; }
.htmx-request.htmx-indicator { opacity: 1; transition: opacity 200ms ease-in; }
