/* ---------- Design tokens ----------
   Apple-grounded system, and the rules behind it are worth stating because
   almost every past colour problem came from breaking one of them:

   1. ONE tint. The accent is the only chromatic colour in the chrome. Neutrals
      carry structure; the tint carries meaning (selected, primary, linked).
      There is deliberately no second decorative accent — a pink alongside the
      purple is what made everything read as equally important.
   2. Elevation is material, not glow. Surfaces separate by lightness and a
      hairline, the way Apple stacks grouped content. No coloured halos.
   3. Fills, not gradients. A gradient under white text has two contrast ratios
      and only one of them gets checked; the pink end of the old primary button
      measured 2.65:1.
   4. Every tint is written against a channel triplet -- the --*-rgb tokens --
      with the alpha applied at the point of use, so one channel definition
      drives the whole surface. Shadows included, via --shadow-rgb, so dark
      mode can deepen them without a second set of literals.

   Light is the default and dark follows the system, which is what "Apple
   design philosophy" actually means at the palette level — the OS decides.
   base.html injects --accent and its derivatives for BOTH schemes; everything
   below is scheme-owned and not user-editable, so a chosen colour can no
   longer strand the twenty tokens around it. */
:root {
  color-scheme: light dark;

  /* --- Accent. Overridden per-scheme by base.html from the saved theme. --- */
  --accent: #8b35b6;
  --accent-rgb: 139 53 182;
  --accent-hover: #77299e;
  --on-accent: #ffffff;

  /* --- Neutrals: Apple's grouped-background stack --- */
  --bg: #f2f2f7;            /* systemGroupedBackground — the page ground */
  --surface: #ffffff;       /* secondarySystemGroupedBackground — cards */
  --fill: #f0f0f4;          /* secondarySystemFill — inset controls */
  --fill-strong: #e4e4ea;   /* the same, one step firmer (hover, tracks) */
  --border: #d3d3d8;        /* opaque separator */
  --border-soft: #e5e5ea;   /* the hairline between rows */

  /* Label hierarchy. Every one of these clears 4.5:1 on both --surface and
     --bg; the old --text-faint sat at 4.13:1 and carried every subtitle,
     hint and card description in the app. */
  --text-strong: #000000;
  --text: #1c1c1e;
  --text-muted: #55555c;
  --text-faint: #6a6a72;

  --bg-rgb: 242 242 247;
  --surface-rgb: 255 255 255;
  --muted-rgb: 85 85 92;
  --shadow-rgb: 0 0 0;

  /* --- Semantic colour. Text tones are darkened for AA on light; the fills
         are the same hue at low alpha, so one triplet drives both. --- */
  --success: #1a7f43;
  --success-rgb: 26 127 67;
  --warning: #8a5300;
  --warning-rgb: 138 83 0;
  --error: #b3261e;
  --error-rgb: 179 38 30;

  --success-text: var(--success);
  --success-bg: rgb(var(--success-rgb) / .12);
  --warning-text: var(--warning);
  --warning-bg: rgb(var(--warning-rgb) / .12);
  --error-text: var(--error);
  --error-bg: rgb(var(--error-rgb) / .12);

  --sidebar-w: 260px;
  --topbar-h: 52px;
  --focus-ring: 0 0 0 3.5px rgb(var(--accent-rgb) / .35);

  /* --- Type ramp, Apple's steps. Sizes were previously ad hoc: eighteen
         distinct values between .68rem and 1.65rem, which is why nothing
         lined up vertically. --- */
  --fs-caption2: .6875rem;   /* 11 */
  --fs-caption: .75rem;      /* 12 */
  --fs-footnote: .8125rem;   /* 13 */
  --fs-subhead: .9375rem;    /* 15 */
  --fs-callout: 1rem;        /* 16 */
  --fs-title3: 1.25rem;      /* 20 */
  --fs-title2: 1.375rem;     /* 22 */
  --fs-title1: 1.75rem;      /* 28 */

  /* --- Radii, tied to control size the way continuous corners are. --- */
  --radius-sm: .5rem;
  --radius-md: .75rem;
  --radius-lg: 1rem;
  --radius-xl: 1.25rem;
  --radius-pill: 9999px;

  /* Light mode separates by lightness, so shadows stay nearly invisible —
     a card is white on grey, not white with a drop shadow. */
  --shadow-sm: 0 1px 2px rgb(var(--shadow-rgb) / .05);
  --shadow-md: 0 1px 3px rgb(var(--shadow-rgb) / .06), 0 6px 16px -6px rgb(var(--shadow-rgb) / .08);
  --shadow-lg: 0 12px 32px -8px rgb(var(--shadow-rgb) / .14);

  /* Apple's ease-out: quick departure, long settle. No overshoot anywhere —
     interface chrome that bounces reads as a toy, and the old --ease-bounce
     was rotating card icons on hover. */
  --ease: cubic-bezier(.32, .72, 0, 1);
  --dur: .22s;
}

@media (prefers-color-scheme: dark) {
  :root {
    --bg: #000000;
    --surface: #1c1c1e;
    --fill: #2c2c2e;
    --fill-strong: #3a3a3c;
    --border: #38383a;
    --border-soft: #2c2c2e;

    --text-strong: #ffffff;
    --text: #f2f2f7;
    --text-muted: #b0b0b6;
    --text-faint: #98989f;

    --bg-rgb: 0 0 0;
    --surface-rgb: 28 28 30;
    --muted-rgb: 176 176 182;

    /* Apple's dark system colours: they brighten rather than darken, because
       a dark ground needs luminance from the colour, not saturation. */
    --success: #30d158;
    --success-rgb: 48 209 88;
    --warning: #ff9f0a;
    --warning-rgb: 255 159 10;
    --error: #ff453a;
    --error-rgb: 255 69 58;

    --success-bg: rgb(var(--success-rgb) / .16);
    --warning-bg: rgb(var(--warning-rgb) / .16);
    --error-bg: rgb(var(--error-rgb) / .16);

    --shadow-sm: 0 1px 2px rgb(var(--shadow-rgb) / .4);
    --shadow-md: 0 2px 6px rgb(var(--shadow-rgb) / .45);
    --shadow-lg: 0 16px 40px -8px rgb(var(--shadow-rgb) / .6);
  }
}

*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }

html {
  scrollbar-color: var(--border) transparent;
  scrollbar-width: thin;
  -webkit-text-size-adjust: 100%;
}

/* San Francisco first — this is a Mac and iPhone app in practice, and SF is
   what makes the type feel native rather than merely sans-serif. */
html, body {
  font-family: -apple-system, BlinkMacSystemFont, 'SF Pro Text', 'Segoe UI', system-ui, sans-serif;
  background: var(--bg);
  color: var(--text);
  font-size: var(--fs-subhead);
  line-height: 1.47;
  -webkit-font-smoothing: antialiased;
  -moz-osx-font-smoothing: grayscale;
  text-rendering: optimizeLegibility;
}

/* Tight tracking on headings only, the way SF Display differs from SF Text. */
h1, h2, h3 { letter-spacing: -.021em; line-height: 1.2; }

a { color: inherit; }
button, input, select, textarea { font-family: inherit; font-size: inherit; }

::selection { background: rgb(var(--accent-rgb) / .28); }

::-webkit-scrollbar { width: 9px; height: 9px; }
::-webkit-scrollbar-track { background: transparent; }
::-webkit-scrollbar-thumb { background: var(--border); border-radius: var(--radius-pill); border: 2px solid transparent; background-clip: content-box; }
::-webkit-scrollbar-thumb:hover { background: var(--text-faint); background-clip: content-box; }

:focus-visible {
  outline: none;
  box-shadow: var(--focus-ring);
  border-radius: var(--radius-sm);
}

.visually-hidden {
  position: absolute;
  width: 1px; height: 1px;
  padding: 0;
  margin: -1px;
  border: 0;
  overflow: hidden;
  clip: rect(0 0 0 0);
  clip-path: inset(50%);
  white-space: nowrap;
}

.skip-link {
  position: absolute;
  left: -9999px;
  top: 0;
  background: var(--accent);
  color: var(--on-accent);
  padding: .5rem 1rem;
  z-index: 200;
  border-radius: 0 0 var(--radius-sm) 0;
  font-weight: 600;
}
.skip-link:focus { left: 0; }

/* ---------- App shell ---------- */
.app-shell { display: flex; min-height: 100vh; }

/* The sidebar is a material, not a panel: translucent surface over the page
   ground with a hairline edge, so it belongs to the window rather than
   sitting on top of it. */
.sidebar {
  width: var(--sidebar-w);
  flex-shrink: 0;
  background: rgb(var(--surface-rgb) / .72);
  backdrop-filter: blur(24px) saturate(180%);
  -webkit-backdrop-filter: blur(24px) saturate(180%);
  border-right: 1px solid var(--border-soft);
  display: flex;
  flex-direction: column;
  position: fixed;
  top: 0; left: 0; bottom: 0;
  z-index: 100;
  transition: transform var(--dur) var(--ease);
}
@supports not (backdrop-filter: blur(1px)) {
  .sidebar { background: var(--surface); }
}

/* Moving between pages keeps the sidebar still and changes only the page
   beside it -- a cross-document view transition. Chrome/Edge 126+ and Safari
   18.2+ run it; every other browser just loads the page as before. Its scroll
   position is put back by the script straight after the sidebar in base.html,
   which runs before the new page is drawn. Off for anyone who asked for less
   motion. */
@media (prefers-reduced-motion: no-preference) {
  @view-transition { navigation: auto; }
}
.sidebar { view-transition-name: sidebar; }
::view-transition-group(*) { animation-duration: .18s; }

.sidebar-brand {
  display: flex;
  align-items: center;
  gap: .6rem;
  padding: 1rem 1.1rem;
  font-weight: 600;
  font-size: var(--fs-callout);
  letter-spacing: -.01em;
  color: var(--text-strong);
}
.sidebar-brand .logo-mark {
  line-height: 1;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 1.85rem;
  height: 1.85rem;
  border-radius: var(--radius-sm);
  background: var(--accent);
  color: var(--on-accent);
  flex-shrink: 0;
}
.sidebar-brand .logo-mark svg { width: 1.15rem; height: 1.15rem; }

.sidebar-nav {
  flex: 1;
  overflow-y: auto;
  padding: .25rem .6rem 1rem;
}

.nav-section-label {
  font-size: var(--fs-caption2);
  font-weight: 600;
  letter-spacing: .05em;
  text-transform: uppercase;
  color: var(--text-faint);
  padding: 1rem .55rem .3rem;
}

/* macOS sidebar selection: a solid accent-filled row. It is the one place the
   tint is used at full strength, which is what makes "where am I" legible at
   a glance without a glowing bar or a gradient. */
.nav-link {
  display: flex;
  align-items: center;
  gap: .6rem;
  padding: .45rem .55rem;
  border-radius: var(--radius-sm);
  color: var(--text);
  text-decoration: none;
  font-size: var(--fs-subhead);
  font-weight: 450;
  margin-bottom: .1rem;
  transition: background var(--dur) var(--ease), color var(--dur) var(--ease);
}
.nav-link:hover { background: rgb(var(--muted-rgb) / .1); }
.nav-link.active {
  background: var(--accent);
  color: var(--on-accent);
  font-weight: 590;
}
.nav-link.active:hover { background: var(--accent-hover); }
.nav-link .icon { flex-shrink: 0; display: inline-flex; width: 1.1rem; height: 1.1rem; opacity: .9; }
.nav-link .icon svg { width: 100%; height: 100%; }
.nav-link.active .icon { opacity: 1; }

.sidebar-footer {
  padding: .8rem 1.15rem;
  border-top: 1px solid var(--border-soft);
  font-size: var(--fs-caption2);
  color: var(--text-faint);
}

.main {
  flex: 1;
  margin-left: var(--sidebar-w);
  display: flex;
  flex-direction: column;
  min-height: 100vh;
  min-width: 0;
}

.sticky-header { position: sticky; top: 0; z-index: 50; }

.topbar {
  height: var(--topbar-h);
  border-bottom: 1px solid var(--border-soft);
  display: flex;
  align-items: center;
  gap: .75rem;
  padding: 0 1.25rem;
  background: rgb(var(--bg-rgb) / .8);
  backdrop-filter: blur(24px) saturate(180%);
  -webkit-backdrop-filter: blur(24px) saturate(180%);
}
@supports not (backdrop-filter: blur(1px)) {
  .topbar { background: var(--bg); }
}

.topbar-title {
  font-size: var(--fs-subhead);
  font-weight: 590;
  color: var(--text-strong);
  letter-spacing: -.01em;
  /* Truncate rather than push: a long page title must never be what shoves the
     account controls off the right edge of a phone. */
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
  min-width: 0;
}
.topbar-spacer { flex: 1; }
.topbar-user { display: flex; align-items: center; gap: .5rem; }

/* ---------- Module tabs ---------- */
/* One tab per module this account can reach (Staff, Practice, …). base.html
   renders the strip only when there is more than one, so a single-module
   owner never gets a switcher with nothing to switch to.

   flex-shrink: 0 is load-bearing. The topbar's own overflow rule is that the
   page title truncates rather than pushing the account controls off a 390px
   phone, so the strip must hold its width and let .topbar-title ellipsis down
   into whatever is left. */
.module-tabs {
  display: flex;
  gap: .125rem;
  padding: .1875rem;
  background: var(--fill);
  border-radius: var(--radius-sm);
  flex-shrink: 0;
}
.module-tab {
  display: inline-flex;
  align-items: center;
  padding: .3rem .7rem;
  border-radius: calc(var(--radius-sm) - .1875rem);
  font-size: var(--fs-footnote);
  font-weight: 510;
  color: var(--text-muted);
  text-decoration: none;
  white-space: nowrap;
  transition: color .15s ease, background .15s ease;
}
.module-tab:hover { color: var(--text-strong); }
/* The menu's copy of the strip: hidden until a phone, where it replaces the
   top bar's (see the 600px block). Tabs share the width, so three fit. */
.module-tabs-drawer { display: none; margin: 0 0 .75rem; }
.module-tabs-drawer .module-tab { flex: 1; justify-content: center; }
.module-tab.active {
  background: var(--surface);
  color: var(--text-strong);
  font-weight: 590;
  box-shadow: 0 1px 2px rgb(var(--shadow-rgb) / .12);
}

/* ---------- Back to a routine ----------
   A routine step opened this page (routines.go). The bar is the way back, and
   coming back ticks the step, so it is the loudest thing on the page: a full
   accent bar at the very top, sticky with the rest of the header. */
.routine-return { margin: 0; }
.routine-return-btn {
  display: flex;
  align-items: center;
  gap: .55rem;
  width: 100%;
  min-height: 3rem;
  padding: .55rem 1.25rem;
  border: none;
  background: var(--accent);
  color: var(--on-accent);
  font: inherit;
  font-size: var(--fs-callout);
  font-weight: 600;
  text-align: left;
  cursor: pointer;
}
.routine-return-btn:hover { background: var(--accent-hover); }
.routine-return-btn .icon svg { width: 1.25rem; height: 1.25rem; display: block; }
.routine-return-step { font-weight: 500; opacity: .85; }

/* ---------- Announcements ---------- */
.announcement-stack { display: flex; flex-direction: column; }

.announcement-banner {
  display: flex;
  align-items: center;
  gap: .75rem;
  padding: .55rem 1.25rem;
  background: var(--warning-bg);
  color: var(--warning-text);
  border-bottom: 1px solid rgb(var(--warning-rgb) / .3);
  font-size: var(--fs-footnote);
  font-weight: 500;
}
.announcement-banner-text { flex: 1; }
.announcement-banner-close {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 1.6rem;
  height: 1.6rem;
  flex-shrink: 0;
  border-radius: var(--radius-pill);
  border: none;
  background: transparent;
  color: inherit;
  cursor: pointer;
  transition: background var(--dur) var(--ease);
}
.announcement-banner-close:hover { background: rgb(var(--warning-rgb) / .18); }

/* Birthdays and work anniversaries share the banner, not its colour: an
   announcement is something to act on, a celebration is not. So this one wears
   the accent tint instead of the warning one -- still the single house tint,
   written over the channel triplet like every other tint. */
.announcement-banner.is-celebration {
  background: rgb(var(--accent-rgb) / .14);
  color: var(--text-strong);
  border-bottom-color: rgb(var(--accent-rgb) / .28);
}
.announcement-banner.is-celebration .announcement-banner-close:hover {
  background: rgb(var(--accent-rgb) / .2);
}
.announcement-banner-icon {
  display: inline-flex;
  flex-shrink: 0;
  color: var(--accent);
}
.announcement-banner-icon svg { width: 1.15rem; height: 1.15rem; }

/* A due reminder is the one banner that asks for an action rather than just
   saying something, so it carries two controls of its own. They are outlined
   in the banner's own ink instead of being filled: a solid button inside a
   tinted strip puts two competing tints in one bar. */
.announcement-banner-text a { color: inherit; font-weight: 590; }
.reminder-banner-note { display: block; font-size: var(--fs-caption); opacity: .85; }
.reminder-banner-btn {
  flex-shrink: 0;
  padding: .2rem .65rem;
  border: 1px solid rgb(var(--warning-rgb) / .45);
  border-radius: var(--radius-pill);
  background: transparent;
  color: var(--warning-text);
  font: inherit;
  font-size: var(--fs-caption);
  font-weight: 510;
  cursor: pointer;
}
.reminder-banner-btn:hover { background: rgb(var(--warning-rgb) / .18); }

.sidebar-toggle {
  display: none;
  background: none;
  border: none;
  color: var(--accent);
  border-radius: var(--radius-sm);
  width: 2rem;
  height: 2rem;
  align-items: center;
  justify-content: center;
  cursor: pointer;
}
.sidebar-toggle:hover { background: rgb(var(--muted-rgb) / .1); }

.admin-badge {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 1.9rem;
  height: 1.9rem;
  border-radius: var(--radius-pill);
  background: transparent;
  border: none;
  color: var(--accent);
  text-decoration: none;
  transition: background var(--dur) var(--ease);
}
.admin-badge:hover { background: rgb(var(--accent-rgb) / .12); }
.admin-badge svg { width: 1.05rem; height: 1.05rem; }

.username-chip {
  display: flex;
  align-items: center;
  gap: .5rem;
  font-size: var(--fs-footnote);
  font-weight: 500;
  color: var(--text-strong);
  padding: .2rem .35rem;
  border-radius: var(--radius-pill);
  line-height: 1.25;
}
.username-chip .avatar {
  width: 1.75rem;
  height: 1.75rem;
  border-radius: var(--radius-pill);
  background: var(--accent);
  color: var(--on-accent);
  display: flex;
  align-items: center;
  justify-content: center;
  font-size: var(--fs-caption);
  font-weight: 600;
  text-transform: uppercase;
  flex-shrink: 0;
}
.role-pill {
  font-size: var(--fs-caption2);
  color: var(--text-faint);
  font-weight: 400;
}

.logout-form { display: inline; }
.icon-btn {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 1.9rem;
  height: 1.9rem;
  border-radius: var(--radius-pill);
  border: none;
  background: transparent;
  color: var(--text-muted);
  cursor: pointer;
  transition: background var(--dur) var(--ease), color var(--dur) var(--ease);
}
.icon-btn:hover { color: var(--text-strong); background: rgb(var(--muted-rgb) / .12); }
.icon-btn svg { width: 1.05rem; height: 1.05rem; }
/* "Install the app" starts hidden until Chrome says it can install
   (app_install.js). .icon-btn and .btn both set a display, which beats the
   bare hidden attribute, so the pair has to say it again. */
[data-install-app][hidden] { display: none; }

.content { padding: 1.75rem 2rem 3rem; max-width: 1100px; width: 100%; }
/* 1100px is a reading measure, right for every page that is mostly prose, cards
   and a narrow table. A console page is not one of those: the Users table is
   eight columns wide and two of them hold edit forms, so it needs around
   1050px before something is hidden. Opt in with {% block content_class %}. */
.content-wide { max-width: 1500px; padding-left: 1.5rem; padding-right: 1.5rem; }

.sidebar-backdrop {
  display: none;
  position: fixed;
  inset: 0;
  background: rgb(var(--shadow-rgb) / .35);
  z-index: 90;
}

/* ---------- Branch mark ---------- */
.branch-mark {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  flex: none;
  width: 1.9rem;
  height: 1.9rem;
  border-radius: var(--radius-sm);
  overflow: hidden;
  background: rgb(var(--accent-rgb) / .12);
  color: var(--accent);
  font-size: var(--fs-footnote);
  font-weight: 600;
  line-height: 1;
  text-transform: uppercase;
}
/* contain, not cover: a logo is a mark to be read whole, and cropping one to
   fill a square is how you lose half a wordmark. */
.branch-mark img { width: 100%; height: 100%; object-fit: contain; display: block; }
.branch-mark-lg {
  width: 3rem;
  height: 3rem;
  border-radius: var(--radius-md);
  font-size: var(--fs-title3);
}

.branch-chip-mark { display: inline-flex; align-items: center; }

/* The branch / organisation switcher. Only rendered for people who have more
   than one of either, so it is never in the way of the single-shop owner it
   would only confuse. A <details> rather than a scripted menu: it opens,
   closes and keyboard-navigates on its own, and each row is a real form post
   so the pick survives without JavaScript. */
.scope-switcher { position: relative; }
.scope-switcher > summary {
  display: inline-flex;
  align-items: center;
  gap: .4rem;
  padding: .25rem .5rem .25rem .3rem;
  border-radius: var(--radius-sm);
  cursor: pointer;
  list-style: none;
  color: var(--text);
  max-width: 13rem;
}
.scope-switcher > summary::-webkit-details-marker { display: none; }
.scope-switcher > summary:hover { background: var(--surface-hover, rgb(var(--accent-rgb) / .08)); }
.scope-switcher > summary:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }
.scope-switcher-label {
  font-size: var(--fs-footnote);
  font-weight: 600;
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}
.scope-switcher-caret { width: .85rem; height: .85rem; flex: none; opacity: .6; }
.scope-switcher[open] .scope-switcher-caret { transform: rotate(180deg); }

.scope-switcher-menu {
  position: absolute;
  right: 0;
  top: calc(100% + .35rem);
  z-index: 40;
  min-width: 13rem;
  max-height: 60vh;
  overflow-y: auto;
  padding: .35rem;
  background: var(--surface);
  border: 1px solid var(--border);
  border-radius: var(--radius-md);
  box-shadow: 0 8px 24px rgb(0 0 0 / .14);
}
.scope-switcher-heading {
  margin: .35rem .5rem .2rem;
  font-size: var(--fs-caption);
  font-weight: 600;
  color: var(--text-faint);
  text-transform: uppercase;
  letter-spacing: .05em;
}
.scope-switcher-item {
  display: block;
  width: 100%;
  text-align: left;
  padding: .4rem .5rem;
  border: 0;
  border-radius: var(--radius-sm);
  background: none;
  color: var(--text);
  font: inherit;
  font-size: var(--fs-footnote);
  cursor: pointer;
}
.scope-switcher-item:hover { background: rgb(var(--accent-rgb) / .1); }
.scope-switcher-item.is-current { font-weight: 700; color: var(--accent); }
.scope-switcher-item.is-current::after { content: ' \2713'; }

@media (max-width: 640px) {
  .scope-switcher-label { display: none; }
  .scope-switcher > summary { max-width: none; }
}

.branch-header { display: flex; align-items: center; gap: .85rem; }
.branch-header-name {
  font-size: var(--fs-caption);
  font-weight: 600;
  color: var(--text-faint);
  text-transform: uppercase;
  letter-spacing: .05em;
  margin-bottom: .1rem;
}

/* ---------- Page header ---------- */
.page-header {
  display: flex;
  align-items: flex-start;
  justify-content: space-between;
  gap: 1rem;
  flex-wrap: wrap;
  margin-bottom: 1.5rem;
}
.page-header h1 {
  font-size: var(--fs-title1);
  font-weight: 680;
  color: var(--text-strong);
  letter-spacing: -.024em;
}
.page-header .subtitle { color: var(--text-faint); font-size: var(--fs-subhead); margin-top: .25rem; }
.header-actions { display: flex; gap: .5rem; flex-wrap: wrap; }

/* Super Admin's organisation picker (_org_picker.html), on the pages that are
   about one organisation's people and roles. A segmented row of chips, the
   selected one filled with the accent, like the Settings branch picker. */
.org-picker {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: .5rem;
  margin-bottom: 1.25rem;
}
.org-picker-label {
  font-size: var(--fs-footnote);
  font-weight: 590;
  color: var(--text-muted);
  margin-right: .25rem;
}
.org-chip {
  display: inline-flex;
  align-items: center;
  padding: .35rem .8rem;
  border-radius: var(--radius-pill);
  background: var(--fill);
  color: var(--text-muted);
  font-size: var(--fs-footnote);
  font-weight: 510;
  text-decoration: none;
  transition: background var(--dur) var(--ease), color var(--dur) var(--ease);
}
.org-chip:hover { color: var(--text-strong); background: var(--fill-strong); }
.org-chip.on { background: var(--accent); color: var(--on-accent); }
.org-chip.on:hover { background: var(--accent-hover); color: var(--on-accent); }
.org-chip-platform { font-style: italic; }

/* ---------- Cards / grid ---------- */
.card {
  background: var(--surface);
  border: 1px solid var(--border-soft);
  border-radius: var(--radius-lg);
  padding: 1.25rem;
  box-shadow: var(--shadow-sm);
}
/* The heading at the top of a card. Shared rather than page-scoped because a
   dozen pages want it and they had drifted into two versions of the same
   thing: this rule used to live inside admin_settings.html's own head block,
   which left the Order Board's `class="card-heading"` styling nothing at all,
   while eleven other pages hand-wrote the same three declarations inline.
   One definition here is what keeps a card heading the same size on every
   page it appears on. */
.card h2.card-heading,
h2.card-heading,
.card h3.card-heading {
  font-size: var(--fs-callout);
  font-weight: 620;
  color: var(--text-strong);
  margin-bottom: 1rem;
}
/* The line of explanation under one. Pairs with .card-heading so the gap
   between a heading and its hint is set once. */
.card-heading + .hint { margin-top: -.6rem; margin-bottom: 1rem; }

/* ---------- Stat tiles ---------- */
/* A row of single numbers above a table — how many check-ins, how many
   branches. Shared because the pages that want one had each written the label
   and the figure as inline styles, at three different sizes. Auto-fit rather
   than a fixed column count so three tiles and six both fill the row and both
   stack on a phone. */
.stat-row {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(9rem, 1fr));
  gap: 1rem;
  margin-bottom: 1.5rem;
}
.stat-row > * { min-width: 0; }
.stat-tile .stat-label {
  font-size: var(--fs-caption);
  color: var(--text-faint);
  margin-bottom: .2rem;
}
.stat-tile .stat-value {
  font-size: var(--fs-title2);
  font-weight: 660;
  color: var(--text-strong);
  letter-spacing: -.02em;
  line-height: 1.15;
}
/* A figure that is only worth reading when it isn't zero — an unowned branch,
   an owner running nothing yet. Tinted so it is findable in a row of neutral
   numbers, and left neutral at zero so a clean install isn't covered in
   warnings. */
.stat-tile .stat-value.is-flagged { color: var(--warning); }

/* ---------- A person's record ---------- */
/* Label-over-value pairs: the Employee Status record and the employee's own
   My Profile page draw the same facts, so they share one grid. */
.record-grid {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(11rem, 1fr));
  gap: 1rem 1.5rem;
  margin: 0;
}
.record-grid > div { min-width: 0; }
.record-label {
  font-size: var(--fs-caption); color: var(--text-faint);
  text-transform: uppercase; letter-spacing: .04em; margin-bottom: .15rem;
}
.record-value { color: var(--text-strong); font-weight: 590; word-break: break-word; }
.record-value .muted { font-weight: 400; }

/* ---------- Page tabs ---------- */
/* A strip of links between views of one record -- the Practice entity page.
   Links, not buttons: each tab is its own URL, so a tab survives a refresh
   and a save lands back on it. */
.page-tabs { display: flex; gap: .25rem; border-bottom: 1px solid var(--border); margin-bottom: 1.25rem; overflow-x: auto; }
.page-tab {
  padding: .55rem .9rem; font-size: var(--fs-subhead); color: var(--text-muted);
  text-decoration: none; border-bottom: 2px solid transparent; margin-bottom: -1px; white-space: nowrap;
}
.page-tab:hover { color: var(--text-strong); }
.page-tab.on { color: var(--accent); border-bottom-color: var(--accent); font-weight: 590; }

/* A figure that should not wrap or jiggle as it changes: money, hours. */
.num { font-variant-numeric: tabular-nums; white-space: nowrap; }
.num-strong { font-variant-numeric: tabular-nums; white-space: nowrap; font-weight: 590; color: var(--text-strong); }

/* ---------- Leave ---------- */
/* One request drawn by _leaves.html on three pages: My Leaves, Leave Requests
   and the Leave section of a person's record. */
.leave-when { font-weight: 590; color: var(--text-strong); white-space: nowrap; }
.leave-sub { font-size: var(--fs-caption); }
.leave-decide { display: flex; gap: .4rem; align-items: center; flex-wrap: wrap; }
.leave-decide input[type=text] { min-width: 8rem; flex: 1 1 8rem; }

.grid { display: grid; gap: 1rem; }
/* Grid items default to min-width:auto, which lets a wide table push its own
   column past the layout instead of scrolling inside it — the Users page
   table used to run off the right edge of the window for exactly this reason.
   overflow-x on .table-wrap only works once the track is allowed to shrink. */
.grid > * { min-width: 0; }
.grid-cols-3 { grid-template-columns: repeat(3, 1fr); }
.grid-cols-2 { grid-template-columns: repeat(2, 1fr); }
.grid-sidebar { grid-template-columns: 320px 1fr; }
.grid-sidebar-wide { grid-template-columns: 380px 1fr; }
@media (max-width: 760px) {
  .grid-cols-3, .grid-cols-2, .grid-sidebar, .grid-sidebar-wide {
    grid-template-columns: 1fr;
  }
}

/* ---------- Users page layout ---------- */
/* The Users table is eight columns wide and two of them hold edit forms, so it
   needs the better part of 1050px before something is hidden. It used to sit in
   the middle track of `300px 1fr 280px` inside a 1100px page, which left it
   about 430px on a 1440px laptop: six of the eight columns were reachable only
   by scrolling a box inside the page, while the right-hand third of the window
   stood empty. The fix is that nothing else takes a column beside it — the form
   is a disclosure and the dashboard a strip, both full width. */
.users-layout { display: flex; flex-direction: column; gap: 1rem; }
.add-user-panel { padding: 1rem 1.25rem; }
.add-user-panel[open] { padding-bottom: 1.25rem; }
.add-user-summary {
  cursor: pointer;
  font-size: 1.05rem;
  font-weight: 700;
  color: var(--text-strong);
  list-style: none;
  display: flex;
  align-items: center;
  gap: .5rem;
}
.add-user-summary::-webkit-details-marker { display: none; }
/* A chevron that turns, rather than the platform triangle, so the panel reads
   as one of the app's own controls. */
.add-user-summary::before {
  content: "";
  width: .5rem;
  height: .5rem;
  border-right: 2px solid var(--text-faint);
  border-bottom: 2px solid var(--text-faint);
  transform: rotate(-45deg);
  transition: transform var(--dur) var(--ease);
}
.add-user-panel[open] .add-user-summary::before { transform: rotate(45deg); }
.add-user-summary:focus-visible { outline: none; box-shadow: var(--focus-ring); border-radius: var(--radius-sm); }
.add-user-grid {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(14rem, 1fr));
  gap: 0 1.5rem;
  align-items: start;
  margin-top: 1rem;
}
/* Same reason as `.grid > *`: without this the weekday chips inside the
   fieldset set their own track's min-content and push the page sideways on a
   390px phone instead of reflowing. */
.add-user-grid > *, .dashboard-panel > * { min-width: 0; }
/* The weekday row is a set of seven chips, not one field: give it a whole row
   rather than letting it squeeze into a 14rem track. */
.add-user-grid-wide { grid-column: 1 / -1; }

/* Once everything is a full-width block, five stat cards and a creation form
   stand between a phone and the list it came for. The list goes first there. */
@media (max-width: 860px) {
  .users-layout > .users-table-col { order: -1; }
  .users-layout > .dashboard-col { order: 1; }
}

.nav-card {
  display: flex;
  gap: .9rem;
  align-items: center;
  text-decoration: none;
  color: inherit;
  transition: background var(--dur) var(--ease), border-color var(--dur) var(--ease);
}
.nav-card:hover { background: var(--fill); border-color: var(--border); }
.nav-card:active { background: var(--fill-strong); }
.nav-card .nav-card-icon {
  width: 2.4rem;
  height: 2.4rem;
  border-radius: var(--radius-md);
  background: rgb(var(--accent-rgb) / .12);
  color: var(--accent);
  display: flex;
  align-items: center;
  justify-content: center;
  flex-shrink: 0;
}
.nav-card .nav-card-icon svg { width: 1.3rem; height: 1.3rem; }
.nav-card h2 {
  font-size: var(--fs-subhead);
  font-weight: 590;
  color: var(--text-strong);
  margin-bottom: .1rem;
}
.nav-card p { font-size: var(--fs-footnote); color: var(--text-faint); line-height: 1.4; }

/* ---------- Buttons ---------- */
.btn {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: .4rem;
  text-decoration: none;
  padding: .5rem 1rem;
  border-radius: var(--radius-md);
  font-size: var(--fs-subhead);
  font-weight: 510;
  cursor: pointer;
  border: 1px solid transparent;
  white-space: nowrap;
  transition: background var(--dur) var(--ease), opacity var(--dur) var(--ease), border-color var(--dur) var(--ease);
}
/* Apple presses fade rather than move — a button that translates on :active
   drags the text with it and reads as a wobble at small sizes. */
.btn:active:not(:disabled) { opacity: .72; }
.btn:disabled { opacity: .4; cursor: not-allowed; }

.btn-primary { background: var(--accent); color: var(--on-accent); }
.btn-primary:hover:not(:disabled) { background: var(--accent-hover); }

.btn-secondary { background: var(--fill); color: var(--text-strong); border-color: transparent; }
.btn-secondary:hover:not(:disabled) { background: var(--fill-strong); }

.btn-danger { background: transparent; color: var(--error-text); border-color: rgb(var(--error-rgb) / .4); }
.btn-danger:hover:not(:disabled) { background: var(--error-bg); border-color: rgb(var(--error-rgb) / .6); }

.btn-sm { padding: .35rem .75rem; font-size: var(--fs-footnote); border-radius: var(--radius-sm); }

/* ---------- Attendance corrections ---------- */
/* The form that corrects a day lives in the row it corrects, so the day being
   changed is never more than a glance from the fields changing it. A <details>
   rather than a dialog: the row simply grows, which needs no positioning and
   behaves the same on a phone as on a desk. */
.correct-cell > summary { list-style: none; display: inline-flex; }
.correct-cell > summary::-webkit-details-marker { display: none; }
.correct-form {
  margin-top: .6rem;
  padding: .8rem;
  min-width: 15rem;
  text-align: left;
  background: var(--fill);
  border: 1px solid var(--border);
  border-radius: var(--radius-md);
}
.correct-form .field { margin-bottom: .6rem; }
.correct-form .hint { margin: 0 0 .6rem; }

/* ---------- Pay ---------- */
/* The Pay form opens in the row whose money it records, the same way a
   correction does: Employee Status, Still to pay, a person's paid history and
   the month page all draw it from _salary.html. */
.pay-cell > summary { list-style: none; display: inline-flex; margin-top: .35rem; }
.pay-cell > summary::-webkit-details-marker { display: none; }
.pay-form {
  margin-top: .6rem;
  padding: .8rem;
  min-width: 14rem;
  max-width: 18rem;
  text-align: left;
  background: var(--fill);
  border: 1px solid var(--border);
  border-radius: var(--radius-md);
}
.pay-form .field { margin-bottom: .6rem; }
.pay-form input, .pay-form select { width: 100%; }
.pay-amount { font-variant-numeric: tabular-nums; white-space: nowrap; }
.pay-amount-strong { font-weight: 590; color: var(--text-strong); }
.pay-detail { font-size: var(--fs-caption); color: var(--text-faint); }
.pay-lines td.num { text-align: right; font-variant-numeric: tabular-nums; white-space: nowrap; }
.pay-lines .pay-total td { border-top: 1px solid var(--border); font-weight: 700; color: var(--text-strong); }

/* A setting whose second question only exists because of the first: the steps
   are numbered and the second is indented under the answer that reveals it, so
   an owner can see that leaving step 1 alone leaves nothing undone. */
.settings-step-label {
  font-size: var(--fs-subhead);
  font-weight: 590;
  color: var(--text-strong);
  margin: 0 0 .3rem;
}
.settings-step {
  margin: 1.1rem 0 0;
  padding: 1rem 0 0 1rem;
  border-top: 1px solid var(--border);
  border-left: 2px solid rgb(var(--accent-rgb) / .35);
}
@media (max-width: 30rem) { .settings-step { padding-left: .7rem; } }
.btn-block { width: 100%; }

/* ---------- Forms ---------- */
.field { margin-bottom: 1rem; }
.field label {
  display: block;
  font-size: var(--fs-footnote);
  font-weight: 510;
  color: var(--text-muted);
  margin-bottom: .35rem;
}
.field .hint { font-size: var(--fs-caption); color: var(--text-faint); margin-top: .3rem; line-height: 1.4; }
.field .error-text { font-size: var(--fs-caption); color: var(--error-text); margin-top: .3rem; }

/* Every text-like input the app actually uses. tel, time and email used to
   fall through to the browser's native control, which is why the Users table
   had three differently-shaped boxes in one row. */
input[type=text], input[type=password], input[type=file],
input[type=date], input[type=number], input[type=month],
input[type=time], input[type=tel], input[type=email],
input[type=search],
select, textarea {
  width: 100%;
  background: var(--fill);
  border: 1px solid transparent;
  border-radius: var(--radius-md);
  padding: .55rem .75rem;
  color: var(--text-strong);
  font-size: var(--fs-subhead);
  outline: none;
  transition: background var(--dur) var(--ease), border-color var(--dur) var(--ease), box-shadow var(--dur) var(--ease);
}
input[type=text]:hover, input[type=password]:hover, input[type=date]:hover,
input[type=number]:hover, input[type=month]:hover, input[type=time]:hover,
input[type=tel]:hover, input[type=email]:hover, input[type=search]:hover,
textarea:hover, select:hover { background: var(--fill-strong); }
input[type=text]:focus, input[type=password]:focus, input[type=date]:focus,
input[type=number]:focus, input[type=month]:focus, input[type=time]:focus,
input[type=tel]:focus, input[type=email]:focus, input[type=search]:focus,
textarea:focus, select:focus {
  background: var(--surface);
  border-color: var(--accent);
  box-shadow: var(--focus-ring);
}
textarea { resize: vertical; min-height: 5rem; line-height: 1.5; }

/* A select styled like a text input but with no indicator reads as an input
   you cannot type in. The chevron is an inline SVG data URI because the CSP is
   default-src 'self' — no external asset, and it recolours per scheme. */
select {
  appearance: none;
  -webkit-appearance: none;
  padding-right: 2rem;
  background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16' fill='none' stroke='%236a6a72' stroke-width='1.6' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='m4 6.5 4 4 4-4'/%3E%3C/svg%3E");
  background-repeat: no-repeat;
  background-position: right .65rem center;
  background-size: 1rem;
}
@media (prefers-color-scheme: dark) {
  select {
    background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16' fill='none' stroke='%2398989f' stroke-width='1.6' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='m4 6.5 4 4 4-4'/%3E%3C/svg%3E");
  }
}

/* The native date/month glyph is drawn dark. It only needs inverting on a dark
   ground — inverting it unconditionally, as the offer form used to, left a
   white-on-white picker button in light mode. */
@media (prefers-color-scheme: dark) {
  input[type=date]::-webkit-calendar-picker-indicator,
  input[type=month]::-webkit-calendar-picker-indicator,
  input[type=time]::-webkit-calendar-picker-indicator { filter: invert(1) opacity(.7); }
}

/* A row of controls ending in a button — six forms were hand-rolling this as
   an inline style, with the gap and margin drifting between them. */
.form-row {
  display: flex;
  gap: .75rem;
  align-items: flex-end;
  flex-wrap: wrap;
}
.form-row .field { margin-bottom: 0; }

/* Field widths. These were inline styles picking 9rem, 12rem, 14rem and 320px
   with no relationship to each other; three steps cover every case. */
.field-xs { max-width: 8rem; }
.field-sm { max-width: 13rem; }
.field-md { max-width: 20rem; }
.field-xs input, .field-sm input, .field-md input { width: 100%; }

input[type=search] {
  -webkit-appearance: none;
  appearance: none;
}
input[type=search]::-webkit-search-decoration,
input[type=search]::-webkit-search-cancel-button { -webkit-appearance: none; }

input[type=color] {
  width: 3rem;
  height: 2.25rem;
  padding: .2rem;
  border: 1px solid var(--border);
  border-radius: var(--radius-sm);
  background: var(--fill);
  cursor: pointer;
}
input[type=color]:hover { background: var(--fill-strong); }
input[type=color]:focus-visible { outline: none; box-shadow: var(--focus-ring); }

/* min-width:0 is not tidiness — every browser's UA stylesheet gives fieldset
   `min-inline-size: min-content`, which no amount of `width` or `max-width` on
   it overrides. So a fieldset wrapping anything with a wide min-content (the
   Users page's seven weekday chips) refuses to shrink below that intrinsic
   width and pushes the whole page sideways: /manage/users scrolled to 538px
   on a 390px phone from exactly this. Same class of bug as the `.grid > *`
   rule further down, but a fieldset needs it stated even when it is not a
   grid child. */
fieldset { border: none; min-width: 0; }
legend {
  font-size: var(--fs-footnote);
  font-weight: 510;
  color: var(--text-muted);
  margin-bottom: .45rem;
  padding: 0;
}
.checkbox-grid { display: grid; grid-template-columns: repeat(2, 1fr); gap: .4rem; }
@media (max-width: 520px) { .checkbox-grid { grid-template-columns: 1fr; } }

.checkbox-row {
  display: flex;
  align-items: center;
  gap: .55rem;
  background: transparent;
  border: 1px solid transparent;
  border-radius: var(--radius-sm);
  padding: .45rem .55rem;
  transition: background var(--dur) var(--ease);
}
.checkbox-row:hover { background: var(--fill); }
.checkbox-row:has(input:checked) { background: rgb(var(--accent-rgb) / .1); }

/* Apple checkboxes fill with the accent and stamp a white glyph — the tick is
   never a second colour, and a checked box never turns green. Green means
   "succeeded", not "selected", and conflating the two was why the old
   permission grids read like a status report. */
.checkbox-row input[type=checkbox], .checkbox-row input[type=radio] {
  appearance: none;
  -webkit-appearance: none;
  position: relative;
  width: 1.1rem;
  height: 1.1rem;
  flex-shrink: 0;
  margin: 0;
  border: 1.5px solid var(--border);
  border-radius: .3rem;
  background: var(--surface);
  cursor: pointer;
}
.checkbox-row input[type=radio] { border-radius: var(--radius-pill); }
.checkbox-row input[type=checkbox]:hover:not(:disabled),
.checkbox-row input[type=radio]:hover:not(:disabled) { border-color: var(--text-faint); }
.checkbox-row input[type=checkbox]:checked, .checkbox-row input[type=radio]:checked {
  background-color: var(--accent);
  border-color: var(--accent);
}
.checkbox-row input[type=checkbox]::after {
  content: '';
  position: absolute;
  left: 50%;
  top: 46%;
  width: .26rem;
  height: .52rem;
  border: solid var(--on-accent);
  border-width: 0 .13rem .13rem 0;
  transform: translate(-50%, -58%) rotate(45deg) scale(.4);
  opacity: 0;
}
.checkbox-row input[type=radio]::after {
  content: '';
  position: absolute;
  left: 50%;
  top: 50%;
  width: .42rem;
  height: .42rem;
  border-radius: var(--radius-pill);
  background: var(--on-accent);
  transform: translate(-50%, -50%) scale(.4);
  opacity: 0;
}
.checkbox-row input[type=checkbox]:checked::after {
  opacity: 1;
  transform: translate(-50%, -58%) rotate(45deg) scale(1);
}
.checkbox-row input[type=radio]:checked::after {
  opacity: 1;
  transform: translate(-50%, -50%) scale(1);
}
.checkbox-row input[type=checkbox]:focus-visible, .checkbox-row input[type=radio]:focus-visible {
  outline: none;
  box-shadow: var(--focus-ring);
}
.checkbox-row input[type=checkbox]:disabled, .checkbox-row input[type=radio]:disabled {
  opacity: .4;
  cursor: not-allowed;
}
.checkbox-row label { margin: 0; font-size: var(--fs-subhead); color: var(--text); font-weight: 400; cursor: pointer; }

@media (prefers-reduced-motion: no-preference) {
  .checkbox-row input[type=checkbox], .checkbox-row input[type=radio] {
    transition: background-color var(--dur) var(--ease), border-color var(--dur) var(--ease);
  }
  .checkbox-row input[type=checkbox]::after, .checkbox-row input[type=radio]::after {
    transition: transform var(--dur) var(--ease), opacity var(--dur) var(--ease);
  }
}

/* ---------- Info row ----------
   An icon beside a line of explanation. checkin.html used to build this out of
   .checkbox-row, which meant a static notice inherited the hover and
   :has(:checked) states of a control and looked clickable. */
.info-row {
  display: flex;
  align-items: flex-start;
  gap: .6rem;
  padding: .7rem .8rem;
  border-radius: var(--radius-md);
  background: var(--fill);
  color: var(--text-muted);
  font-size: var(--fs-footnote);
  line-height: 1.45;
}
.info-row > svg, .info-row .info-row-icon svg {
  width: 1.15rem;
  height: 1.15rem;
  flex: none;
  margin-top: .05rem;
}
.info-row-title { font-weight: 510; color: var(--text-strong); }
.info-row-success { background: var(--success-bg); color: var(--success-text); }
.info-row-success .info-row-title { color: var(--success-text); }
.info-row-warning { background: var(--warning-bg); color: var(--warning-text); }
.info-row-warning .info-row-title { color: var(--warning-text); }

/* ---------- Permission groups (parent + gated sub-permissions) ---------- */
.permission-group {
  border: 1px solid var(--border-soft);
  border-radius: var(--radius-md);
  padding: .7rem .75rem;
  margin-bottom: .6rem;
  background: transparent;
}
.permission-group-parent { margin-bottom: .35rem; }
.permission-group-children {
  display: grid;
  grid-template-columns: repeat(2, 1fr);
  gap: .3rem;
  padding-left: 1.35rem;
  border-left: 1.5px solid var(--border-soft);
  margin-left: .55rem;
}
@media (max-width: 520px) { .permission-group-children { grid-template-columns: 1fr; } }
.permission-group-children .checkbox-row.is-locked { opacity: .45; }
.permission-loose-grid { margin-top: .4rem; }

/* ---------- Weekday checkbox row (Weekoffs) ---------- */
.weekday-grid { grid-template-columns: repeat(auto-fit, minmax(4.5rem, 1fr)); }
.weekday-grid .checkbox-row { justify-content: center; padding: .5rem .4rem; }

/* ---------- Sortable table headers ---------- */
th.sortable { padding: 0; }
th.sortable a {
  display: flex;
  align-items: center;
  gap: .25rem;
  padding: .6rem .9rem;
  color: inherit;
  text-decoration: none;
}
th.sortable a:hover { color: var(--text-strong); }
th.sortable .sort-arrow { font-size: .6rem; color: var(--accent); }

/* ---------- Device badges ---------- */
.device-list { display: flex; flex-direction: column; gap: .3rem; }
.device-chip {
  display: inline-flex;
  align-items: center;
  gap: .4rem;
  font-size: var(--fs-caption2);
  font-family: ui-monospace, SFMono-Regular, 'SF Mono', Menlo, monospace;
  color: var(--text-muted);
  background: var(--fill);
  border: none;
  border-radius: var(--radius-sm);
  padding: .25rem .5rem;
}
.device-chip form { display: inline-flex; }
.device-chip .link-btn { font-size: var(--fs-caption2); }

/* An edit form living inside a table cell: an input or two and its Save. It
   deliberately does not wrap — a table whose min-content exceeds the space it
   is given scrolls inside .table-wrap, which is the answer on a phone, and
   wrapping instead would break the control across lines at every width. */
.cell-form { display: flex; gap: .4rem; align-items: center; }
/* A flex item shrinks to nothing before its row does, and .field-xs sets only a
   maximum, so the phone input was ending up 30px wide — one digit of the number
   it is showing. Give the column something to hold. */
.cell-form input { min-width: 6rem; }

/* ---------- Dashboard widgets (Users section) ---------- */
/* A strip across the page rather than a column beside the table — five stat
   cards stacked in a 280px rail were the reason the table had no room. */
.dashboard-panel {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(13rem, 1fr));
  gap: .85rem;
  align-items: start;
}
.widget-toggle-bar { display: flex; flex-wrap: wrap; gap: .4rem; margin-bottom: .2rem; grid-column: 1 / -1; }
.widget-toggle-chip {
  display: inline-flex;
  align-items: center;
  gap: .35rem;
  font-size: var(--fs-caption);
  color: var(--text-muted);
  background: var(--fill);
  border: none;
  border-radius: var(--radius-pill);
  padding: .28rem .7rem .28rem .5rem;
  cursor: pointer;
  user-select: none;
}
.widget-toggle-chip:hover { background: var(--fill-strong); }
.widget-toggle-chip input { width: .9rem; height: .9rem; margin: 0; cursor: pointer; accent-color: var(--accent); }
.widget-card {
  border: 1px solid var(--border-soft);
  border-radius: var(--radius-md);
  padding: .85rem 1rem;
  background: var(--surface);
}
.widget-card h3 {
  font-size: var(--fs-caption);
  font-weight: 590;
  color: var(--text-faint);
  margin-bottom: .4rem;
  letter-spacing: 0;
  text-transform: none;
}
/* Tabular figures so a column of numbers lines up digit for digit. */
.widget-stat {
  font-size: var(--fs-title2);
  font-weight: 660;
  color: var(--text-strong);
  font-variant-numeric: tabular-nums;
  letter-spacing: -.02em;
}
.widget-list { display: flex; flex-direction: column; gap: .3rem; font-size: var(--fs-footnote); }
.widget-list-row { display: flex; justify-content: space-between; gap: .5rem; color: var(--text-muted); }
.widget-list-row strong { color: var(--text); font-weight: 510; }
.widget-empty { color: var(--text-faint); font-size: var(--fs-footnote); }

/* ---------- Tables ----------
   Hairline separators, no zebra. Alternating row fills fight the hover state
   and make a wide table read as a barcode; Apple's lists separate rows with a
   single inset rule and let the hover do the pointing. */
/* position: relative is load-bearing, not decoration. Every table carries a
   .visually-hidden <caption>, which is position:absolute; with a static
   wrapper its containing block was the page, so it escaped this scroll
   container and stretched the document to the full width of the table — the
   Users page scrolled sideways to 917px on a 390px phone while the table
   itself was correctly clipped. */
.table-wrap {
  position: relative;
  overflow-x: auto;
  border-radius: var(--radius-lg);
  border: 1px solid var(--border-soft);
  background: var(--surface);
}
table { width: 100%; border-collapse: collapse; font-size: var(--fs-footnote); }
caption { text-align: left; padding: .7rem .9rem; color: var(--text-faint); font-size: var(--fs-footnote); }
thead { background: transparent; }
thead th {
  padding: .6rem .9rem;
  text-align: left;
  font-weight: 510;
  color: var(--text-faint);
  text-transform: none;
  font-size: var(--fs-caption);
  letter-spacing: 0;
  white-space: nowrap;
  border-bottom: 1px solid var(--border-soft);
}
tbody tr { transition: background .12s var(--ease); }
tbody tr + tr { border-top: 1px solid var(--border-soft); }
tbody tr:hover { background: var(--fill); }
tbody td { padding: .6rem .9rem; color: var(--text); vertical-align: middle; font-variant-numeric: tabular-nums; }
tbody td.name, tbody td.strong { font-weight: 510; color: var(--text-strong); }
/* A table cut into groups: one <tbody> per group, each led by a full-width
   <th scope="rowgroup">. Client documents by month use it on the firm's side
   and in the portal, which is why it lives here and not in either page. */
tbody + tbody { border-top: 1px solid var(--border-soft); }
tr.group-row, tr.group-row:hover { background: var(--fill); }
tr.group-row th {
  padding: .45rem .9rem;
  text-align: left;
  font-size: var(--fs-caption);
  font-weight: 590;
  color: var(--text-strong);
}
tr.group-row th .muted { font-weight: 400; }
.muted { color: var(--text-faint); font-size: var(--fs-caption); }

.empty-state {
  text-align: center;
  padding: 3rem 1.5rem;
  color: var(--text-faint);
  font-size: var(--fs-subhead);
}
/* An empty state may lead with one of the shared icons. Sized here rather than
   in the page, because the Practice module has several of these -- no clients,
   no documents, nothing due -- and hand-sized copies would drift. The icon
   macro emits a bare 24x24 viewBox with no width/height, so without this it
   stretches to fill the card. */
.empty-state .icon { display: block; }
.empty-state .icon svg {
  width: 2.5rem;
  height: 2.5rem;
  margin: 0 auto .75rem;
  color: var(--text-faint);
}

/* ---------- Badges / alerts ---------- */
.badge {
  display: inline-block;
  border-radius: var(--radius-pill);
  font-size: var(--fs-caption2);
  font-weight: 590;
  padding: .18rem .55rem;
  letter-spacing: 0;
  text-transform: none;
  white-space: nowrap;
  border: none;
}
.badge-success { background: var(--success-bg); color: var(--success-text); }
.badge-warning { background: var(--warning-bg); color: var(--warning-text); }
.badge-neutral { background: var(--fill); color: var(--text-muted); }
.badge-danger { background: var(--error-bg); color: var(--error-text); }

/* A table cell holding more than one verdict — how the day started and how it
   ended. Badges are nowrap by design, so they need somewhere to wrap to. */
.status-stack { display: flex; flex-wrap: wrap; gap: .3rem; }

.alert {
  padding: .7rem .9rem;
  border-radius: var(--radius-md);
  font-size: var(--fs-footnote);
  font-weight: 450;
  margin-bottom: 1rem;
  border: none;
}
.alert-success { background: var(--success-bg); color: var(--success-text); }
.alert-error { background: var(--error-bg); color: var(--error-text); }
/* Saved, but worth a second look: leave past its allowance, a warning that
   reaches the limit. */
.alert-warning { background: var(--warning-bg); color: var(--warning-text); }

/* ---------- Attendance QR display ---------- */
.qr-card { max-width: 420px; text-align: center; margin: 0 auto; }
/* The QR stays on white in both schemes: scanners read contrast, and an
   inverted code is a support call, not a style choice. */
/* Block-level and self-centred, not inline-flex: as an inline box it shared a
   line with the status badge, which then sat in the QR's bottom-right corner. */
.qr-frame {
  display: flex;
  width: max-content;
  margin: 0 auto;
  padding: 14px;
  border-radius: var(--radius-lg);
  background: #ffffff;
  box-shadow: var(--shadow-md);
}
.qr-frame img { width: 240px; height: 240px; display: block; }
.qr-url-box {
  margin-top: 1.1rem;
  background: var(--fill);
  border: none;
  border-radius: var(--radius-sm);
  padding: .6rem .75rem;
  font-size: var(--fs-caption2);
  font-family: ui-monospace, SFMono-Regular, 'SF Mono', Menlo, monospace;
  color: var(--text-muted);
  word-break: break-all;
}
.qr-actions { display: flex; gap: .6rem; margin-top: 1.25rem; justify-content: center; }
.qr-note { max-width: 420px; margin: 1.25rem auto 0; }

/* ---------- Auth pages (no sidebar) ---------- */
.auth-page {
  min-height: 100vh;
  display: flex;
  align-items: center;
  justify-content: center;
  padding: 1.5rem;
  background: var(--bg);
}
.auth-card {
  background: var(--surface);
  border: 1px solid var(--border-soft);
  border-radius: var(--radius-xl);
  padding: 2rem 1.75rem;
  width: 100%;
  max-width: 360px;
  box-shadow: var(--shadow-md);
}
.auth-logo {
  color: var(--on-accent);
  margin: 0 auto .8rem;
  width: 3rem;
  height: 3rem;
  display: flex;
  align-items: center;
  justify-content: center;
  border-radius: var(--radius-lg);
  background: var(--accent);
}
.auth-logo svg { width: 1.7rem; height: 1.7rem; }
.auth-card h1 {
  text-align: center;
  font-size: var(--fs-title2);
  font-weight: 680;
  color: var(--text-strong);
  letter-spacing: -.022em;
}
.auth-card .sub { text-align: center; color: var(--text-faint); font-size: var(--fs-footnote); margin: .3rem 0 1.5rem; }
.auth-hint {
  margin-top: 1.1rem;
  text-align: center;
  font-size: var(--fs-caption);
  color: var(--text-faint);
  line-height: 1.45;
}
.auth-hint a { color: var(--accent); font-weight: 510; }

/* The "or" rule between the password form and the passkey button. A bare
   horizontal line reads as the end of the card; the word is what says the two
   are alternatives. */
.auth-or {
  display: flex;
  align-items: center;
  gap: .6rem;
  margin: 1.1rem 0;
  font-size: var(--fs-caption);
  color: var(--text-faint);
}
.auth-or::before,
.auth-or::after {
  content: "";
  flex: 1;
  height: 1px;
  background: var(--border-soft);
}

/* What the passkey ceremony has to say for itself, on the login page and on
   the passkeys page both -- which is why it is here rather than in either
   page's own head block. */
.passkey-status {
  font-size: var(--fs-caption);
  color: var(--text-faint);
  line-height: 1.45;
  margin-top: .6rem;
}
.passkey-status-error { color: var(--error-text); }
/* "This device is signed up as <name>" on the forgot-password page. */
.auth-confirm {
  background: var(--fill);
  border-radius: var(--radius-md);
  padding: .7rem .8rem;
  margin-bottom: .9rem;
  text-align: center;
  font-size: var(--fs-footnote);
  color: var(--text);
}
.auth-confirm strong { color: var(--text-strong); }
/* Rule with a word sitting on it, between the two ways into the same form. */
.auth-divider {
  display: flex;
  align-items: center;
  gap: .6rem;
  margin: 1.2rem 0 .9rem;
  font-size: var(--fs-caption);
  color: var(--text-faint);
}
.auth-divider::before,
.auth-divider::after {
  content: '';
  flex: 1;
  height: 1px;
  background: var(--border-soft);
}

/* ---------- Check-in identity strip ---------- */
.checkin-identity {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: .75rem;
  background: var(--fill);
  border: none;
  border-radius: var(--radius-md);
  padding: .6rem .75rem;
  margin-bottom: 1rem;
  font-size: var(--fs-footnote);
  color: var(--text-muted);
}
.checkin-identity strong { color: var(--text-strong); font-weight: 590; }

.link-btn {
  background: none;
  border: none;
  padding: 0;
  color: var(--accent);
  font-size: var(--fs-footnote);
  font-weight: 510;
  cursor: pointer;
  text-decoration: none;
  white-space: nowrap;
}
.link-btn:hover { text-decoration: underline; }

/* ---------- Check-in result panel ---------- */
.result-panel {
  margin-top: 1.25rem;
  padding: 1.5rem 1.25rem;
  border-radius: var(--radius-lg);
  text-align: center;
  border: none;
  background: var(--fill);
}
.result-panel-success { background: var(--success-bg); }
.result-panel-late { background: var(--warning-bg); }
.result-icon { display: flex; justify-content: center; margin-bottom: .7rem; }
.result-heading {
  font-size: var(--fs-title3);
  font-weight: 620;
  color: var(--text-strong);
  margin-bottom: .25rem;
  letter-spacing: -.02em;
}
.result-sub { font-size: var(--fs-footnote); color: var(--text-muted); }

/* Animated checkmark (SVG circle + check path draw-in) */
.checkmark { width: 64px; height: 64px; stroke: var(--success-text); }
.checkmark-circle {
  stroke: var(--success-text);
  stroke-width: 2.5;
  stroke-dasharray: 145;
  stroke-dashoffset: 0;
}
.checkmark-check {
  stroke: var(--success-text);
  stroke-width: 3;
  stroke-linecap: round;
  stroke-linejoin: round;
  stroke-dasharray: 32;
  stroke-dashoffset: 0;
}

@media (prefers-reduced-motion: no-preference) {
  @keyframes checkmark-circle-draw {
    from { stroke-dashoffset: 145; }
    to   { stroke-dashoffset: 0; }
  }
  @keyframes checkmark-check-draw {
    from { stroke-dashoffset: 32; }
    to   { stroke-dashoffset: 0; }
  }
  .checkmark-circle { stroke-dashoffset: 145; animation: checkmark-circle-draw .5s var(--ease) forwards; }
  .checkmark-check { stroke-dashoffset: 32; animation: checkmark-check-draw .35s var(--ease) .5s forwards; }
}

/* Amber "late" clock icon — distinct visual treatment, same success=true state */
.late-icon { width: 56px; height: 56px; color: var(--warning-text); }

/* ---------- Order board ----------
   Shared by the board page and the full-screen shop display, which is why it
   is here rather than in either template's head: one order card, drawn the
   same way in both, and the same materials list under it. */

/* A segmented picker for the branch and the day, same idea as the Settings
   branch chips: the current one is filled with the accent, the rest sit quiet
   on a neutral fill. */
.board-chips, .board-daynav {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: .4rem;
  margin-bottom: .9rem;
}
.board-chip {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  min-width: 2.2rem;
  padding: .35rem .8rem;
  border-radius: var(--radius-pill);
  background: var(--fill);
  color: var(--text-muted);
  font-size: var(--fs-footnote);
  font-weight: 510;
  text-decoration: none;
  transition: background var(--dur) var(--ease), color var(--dur) var(--ease);
}
.board-chip:hover { color: var(--text-strong); background: var(--fill-strong); }
.board-chip.on { background: var(--accent); color: var(--on-accent); }
.board-chip.on:hover { background: var(--accent-hover); color: var(--on-accent); }

.board-jump { display: flex; align-items: center; gap: .4rem; margin-left: auto; }
.board-jump input[type=date] { width: auto; }

.board-new { margin-bottom: 1.25rem; }

/* The split. --board-split is the left column's share of the row and the
   divider is absolutely positioned on the seam between the two tracks, so the
   panes always add up to exactly 100% however far the divider is dragged --
   a fixed divider track would push the row past the viewport at the extremes
   and scroll the whole page sideways. At 0% or 100% one pane is a zero-width
   track and clips its own content, which is the "drag it out to a full page"
   end state. */
.board-split {
  --board-split: 50%;
  position: relative;
  display: grid;
  grid-template-columns: minmax(0, var(--board-split)) minmax(0, 1fr);
  align-items: start;
  margin-bottom: 1.5rem;
}
.board-pane {
  min-width: 0;
  overflow: hidden;
  padding: 0 .6rem;
}
.board-pane:first-of-type { padding-left: 0; }
.board-pane:last-of-type { padding-right: 0; }

.board-pane-head {
  display: flex;
  align-items: center;
  gap: .5rem;
  padding-bottom: .5rem;
  margin-bottom: .75rem;
  border-bottom: 1px solid var(--border-soft);
}
.board-pane-head h2 {
  flex: 1;
  min-width: 0;
  font-size: var(--fs-callout);
  font-weight: 620;
  color: var(--text-strong);
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}

.board-divider {
  position: absolute;
  top: 0;
  bottom: 0;
  left: var(--board-split);
  width: 1.2rem;
  margin-left: -.6rem;
  display: flex;
  align-items: center;
  justify-content: center;
  cursor: col-resize;
  touch-action: none;
  border-radius: var(--radius-pill);
}
.board-divider-grip {
  width: 2px;
  height: 100%;
  border-radius: var(--radius-pill);
  background: var(--border-soft);
  transition: background var(--dur) var(--ease), width var(--dur) var(--ease);
}
.board-divider:hover .board-divider-grip,
.board-divider:focus-visible .board-divider-grip,
.board-split.is-dragging .board-divider-grip {
  background: var(--accent);
  width: 3px;
}
/* Nothing to grab hold of when a pane is collapsed to nothing, so the handle
   stays visible at the very edge instead of sitting half outside the page. */
.board-split.is-right-only .board-divider { margin-left: 0; }
.board-split.is-left-only .board-divider { margin-left: -1.2rem; }
/* A collapsed pane still renders its own padding -- border-box floors the box
   there even on a zero-width track -- which leaves a 10px sliver beside the
   column that is supposed to have the whole page. Take the padding with it. */
.board-split.is-left-only .board-pane:last-of-type,
.board-split.is-right-only .board-pane:first-of-type { padding: 0; }

.board-empty {
  padding: 1.5rem .5rem;
  color: var(--text-faint);
  font-size: var(--fs-footnote);
}

/* ---------- One order ---------- */
.order-card {
  background: var(--surface);
  border: 1px solid var(--border-soft);
  border-radius: var(--radius-lg);
  padding: .85rem;
  margin-bottom: .7rem;
  box-shadow: var(--shadow-sm);
}
.order-card-head {
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  gap: .5rem;
}
.order-no {
  font-size: var(--fs-callout);
  font-weight: 620;
  color: var(--text-strong);
  word-break: break-word;
}
/* The slot is what the counter scans for, so it is the one tinted thing on
   the card -- and tabular figures keep a column of times aligned. */
.order-time {
  flex-shrink: 0;
  padding: .15rem .5rem;
  border-radius: var(--radius-pill);
  background: rgb(var(--accent-rgb) / .12);
  color: var(--accent);
  font-size: var(--fs-caption);
  font-weight: 590;
  font-variant-numeric: tabular-nums;
  white-space: nowrap;
}
.order-time-unset { background: var(--fill); color: var(--text-faint); }
.order-customer { font-size: var(--fs-footnote); color: var(--text-muted); margin-top: .15rem; }
.order-customer a { color: var(--accent); text-decoration: none; }
.order-customer a:hover { text-decoration: underline; }

.order-photos { display: flex; flex-wrap: wrap; gap: .4rem; margin-top: .6rem; }
.order-photo {
  position: relative;
  flex: 1 1 8rem;
  min-width: 0;
  display: block;
  border-radius: var(--radius-md);
  overflow: hidden;
  background: var(--fill);
}
.order-photo img {
  display: block;
  width: 100%;
  height: 8.5rem;
  object-fit: cover;
}
.order-photo-tag {
  position: absolute;
  left: .35rem;
  bottom: .35rem;
  padding: .1rem .4rem;
  border-radius: var(--radius-sm);
  /* Its own ink, not a token: this label sits on a photograph, where the page
     palette has no idea what is underneath it. */
  background: rgb(0 0 0 / .58);
  color: #ffffff;
  font-size: var(--fs-caption2);
  font-weight: 510;
}

.order-notes {
  margin-top: .6rem;
  font-size: var(--fs-footnote);
  color: var(--text);
  white-space: pre-wrap;
  overflow-wrap: anywhere;
}
.order-materials {
  margin-top: .5rem;
  padding: .45rem .55rem;
  border-radius: var(--radius-sm);
  background: var(--fill);
  font-size: var(--fs-footnote);
  color: var(--text-muted);
  white-space: pre-wrap;
  overflow-wrap: anywhere;
}
.order-label {
  display: block;
  font-size: var(--fs-caption2);
  font-weight: 590;
  text-transform: uppercase;
  letter-spacing: .04em;
  color: var(--text-faint);
}
.order-card-foot { margin-top: .55rem; }

/* The current photo beside its replace field, on the edit form. */
.order-photo-current { margin-top: .5rem; }
.order-photo-current img {
  display: block;
  max-width: 10rem;
  border-radius: var(--radius-md);
  margin-bottom: .4rem;
}

/* ---------- Tomorrow's materials ---------- */
.materials-panel {
  border: 1px solid rgb(var(--accent-rgb) / .3);
  border-radius: var(--radius-lg);
  background: rgb(var(--accent-rgb) / .07);
  padding: .8rem;
  margin-bottom: .9rem;
}
.materials-panel h3 {
  font-size: var(--fs-caption);
  font-weight: 620;
  text-transform: uppercase;
  letter-spacing: .04em;
  color: var(--accent);
  margin-bottom: .5rem;
}
.materials-list { list-style: none; display: flex; flex-direction: column; gap: .45rem; }
.materials-list li { display: flex; flex-direction: column; gap: .1rem; }
.materials-list li + li { border-top: 1px solid var(--border-soft); padding-top: .45rem; }
.materials-ref {
  font-size: var(--fs-caption);
  font-weight: 590;
  color: var(--text-faint);
  font-variant-numeric: tabular-nums;
}
.materials-text { font-size: var(--fs-footnote); color: var(--text); white-space: pre-wrap; overflow-wrap: anywhere; }

.board-later { margin-bottom: 1.5rem; }

/* ---------- Full-screen shop display ---------- */
/* Read from across the room: bigger type, no chrome, and the two columns
   fixed rather than draggable -- nobody is standing at this screen. */
.board-display {
  min-height: 100vh;
  background: var(--bg);
  padding: 1rem 1.25rem 1.5rem;
}
.board-display-bar {
  display: flex;
  align-items: flex-end;
  justify-content: space-between;
  gap: 1rem;
  padding-bottom: .75rem;
  margin-bottom: 1rem;
  border-bottom: 1px solid var(--border-soft);
}
.board-display-bar h1 {
  font-size: var(--fs-title1);
  font-weight: 680;
  color: var(--text-strong);
}
.board-display-branch {
  font-size: var(--fs-footnote);
  font-weight: 590;
  color: var(--text-faint);
}
.board-display-cols {
  display: grid;
  grid-template-columns: minmax(0, 1.65fr) minmax(0, 1fr);
  gap: 1.5rem;
  align-items: start;
}
.board-display-col { min-width: 0; }
.board-display-col .board-pane-head h2 { font-size: var(--fs-title3); }
.board-display-col .order-no { font-size: var(--fs-title3); }
.board-display-col .order-time { font-size: var(--fs-subhead); }
.board-display-col .order-notes { font-size: var(--fs-subhead); }
.board-display-col .order-photo img { height: 11rem; }
.board-display-materials .materials-list-lg .materials-text { font-size: var(--fs-callout); }
.board-display-materials .materials-list-lg .materials-ref { font-size: var(--fs-footnote); }
.board-display-materials {
  border-radius: var(--radius-xl);
  background: var(--surface);
  border: 1px solid var(--border-soft);
  padding: 1rem;
}
.board-display-note {
  margin-top: .9rem;
  font-size: var(--fs-footnote);
  color: var(--warning-text);
  background: var(--warning-bg);
  border-radius: var(--radius-sm);
  padding: .5rem .6rem;
}

@media (max-width: 860px) {
  /* A phone gets one column after the other. The divider has nothing to
     resize once the panes stack, so it goes away with them. */
  .board-split { grid-template-columns: minmax(0, 1fr); }
  .board-pane { overflow: visible; padding: 0; }
  .board-pane + .board-pane { margin-top: 1.5rem; }
  .board-divider { display: none; }
  .board-jump { margin-left: 0; }
  .board-display-cols { grid-template-columns: minmax(0, 1fr); gap: 1rem; }
}

/* ---------- Responsive ---------- */
@media (max-width: 860px) {
  .sidebar { transform: translateX(-100%); width: min(var(--sidebar-w), 82vw); box-shadow: var(--shadow-lg); }
  body.sidebar-open .sidebar { transform: translateX(0); }
  body.sidebar-open .sidebar-backdrop { display: block; }
  .main { margin-left: 0; }
  .sidebar-toggle { display: flex; }
  .content { padding: 1.25rem 1rem 2.5rem; }
}

@media (max-width: 600px) {
  .topbar { padding: 0 .5rem; gap: .35rem; }
  /* The account cluster is five controls wide on desktop and does not fit a
     390px phone beside a title and a menu button — at 44px touch targets it
     overflowed the viewport. Keep identity (avatar) and the two actions that
     have no other home; the Owner shortcut is a link to a page the menu
     already lists. */
  .username-chip .role-pill { display: none; }
  .username-chip > span { display: none; }
  .username-chip { padding: 0; gap: 0; }
  .admin-badge { display: none; }
  .module-tabs { padding: .125rem; }
  .module-tab { padding: .3rem .55rem; }
  /* Menu button + three module tabs + the account cluster came to ~444px,
     so on a phone the switcher moves into the menu and the title gets the
     room back. */
  .topbar .module-tabs { display: none; }
  .module-tabs-drawer { display: flex; }
  .content { padding: 1rem .9rem 2.5rem; }
  .page-header { margin-bottom: 1.15rem; }
  .page-header h1 { font-size: var(--fs-title2); }
  .card { padding: 1rem; }
  .header-actions { width: 100%; }
  .header-actions .btn { flex: 1; }
}

/* Touch targets reach Apple's 44pt minimum on phones and any coarse pointer. */
@media (max-width: 600px), (pointer: coarse) {
  .btn { min-height: 2.75rem; padding: .65rem 1.1rem; }
  .btn-sm { min-height: 2.4rem; padding: .5rem .9rem; }
  .icon-btn, .sidebar-toggle, .admin-badge { width: 2.75rem; height: 2.75rem; }
  .checkbox-row { padding: .6rem .55rem; }
  .checkbox-row input[type=checkbox], .checkbox-row input[type=radio] { width: 1.35rem; height: 1.35rem; }
  .nav-link { padding: .65rem .55rem; }
  .link-btn { padding: .3rem 0; }
}

/* One switch turns off every transition and animation in the app. */
@media (prefers-reduced-motion: reduce) {
  *, *::before, *::after {
    animation-duration: .01ms !important;
    animation-iteration-count: 1 !important;
    transition-duration: .01ms !important;
    scroll-behavior: auto !important;
  }
}

/* ---------- Kanban: the vocabulary its five pages share ----------
   Priority, a due date and a card's metadata line appear on the board, the
   task, the ticket list, the ticket and the reminders page. Copying them into
   five head blocks is exactly how two pages' input styling drifted apart, so
   they are defined once here; the board's own column layout, which only the
   board has, stays in its template.

   Priority is the one place this section leans on the semantic tokens rather
   than the accent: urgent and high are a state to act on, which is what
   --error and --warning already mean everywhere else in the app. Low and
   normal are deliberately neutral -- if every card shouted, none would. */
.k-pill {
  display: inline-flex;
  align-items: center;
  gap: .25rem;
  flex-shrink: 0;
  padding: .12rem .5rem;
  border-radius: var(--radius-pill);
  font-size: var(--fs-caption2);
  font-weight: 590;
  white-space: nowrap;
}
.k-pill-urgent { background: var(--error-bg); color: var(--error-text); }
.k-pill-high { background: var(--warning-bg); color: var(--warning-text); }
.k-pill-normal { background: var(--fill); color: var(--text-muted); }
.k-pill-low { background: var(--fill); color: var(--text-faint); }
.k-pill-accent { background: rgb(var(--accent-rgb) / .12); color: var(--accent); }
.k-pill-done { background: var(--success-bg); color: var(--success-text); }

/* Ticket status, on the list and on the ticket itself. Open wears the warning
   tint because it is the one state that owes somebody an answer; in-progress
   takes the accent (something is happening); waiting is neutral because the
   ball is not in the shop's court; resolved and closed are the success tone. */
.k-status-open { background: var(--warning-bg); color: var(--warning-text); }
.k-status-in_progress { background: rgb(var(--accent-rgb) / .12); color: var(--accent); }
.k-status-waiting { background: var(--fill); color: var(--text-muted); }
.k-status-resolved, .k-status-closed { background: var(--success-bg); color: var(--success-text); }

.k-due {
  font-size: var(--fs-caption);
  color: var(--text-muted);
  font-variant-numeric: tabular-nums;
  white-space: nowrap;
}
.k-due-over { color: var(--error-text); font-weight: 590; }

.k-meta {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: .4rem;
  margin-top: .5rem;
  font-size: var(--fs-caption);
  color: var(--text-faint);
}
.k-who {
  display: inline-flex;
  align-items: center;
  gap: .3rem;
  padding: .12rem .5rem .12rem .2rem;
  border-radius: var(--radius-pill);
  background: var(--fill);
  color: var(--text-muted);
  font-size: var(--fs-caption2);
}
.k-who .avatar {
  width: 1.1rem;
  height: 1.1rem;
  border-radius: var(--radius-pill);
  background: var(--accent);
  color: var(--on-accent);
  display: inline-flex;
  align-items: center;
  justify-content: center;
  font-size: var(--fs-caption2);
  font-weight: 620;
  text-transform: uppercase;
}
.k-who-none { background: transparent; color: var(--text-faint); font-style: italic; }

/* The thread both the task and the ticket page carry. One shape, so a comment
   on a card and a reply on a ticket read as the same kind of thing. */
.k-thread { display: flex; flex-direction: column; gap: .6rem; }
.k-note {
  border: 1px solid var(--border-soft);
  border-radius: var(--radius-md);
  padding: .6rem .7rem;
  background: var(--surface);
}
.k-note-internal { background: var(--fill); border-style: dashed; }
.k-note-head {
  display: flex;
  flex-wrap: wrap;
  align-items: baseline;
  gap: .4rem;
  font-size: var(--fs-caption);
  color: var(--text-faint);
  margin-bottom: .25rem;
}
.k-note-head strong { color: var(--text-strong); font-weight: 590; font-size: var(--fs-footnote); }
.k-note-body { font-size: var(--fs-footnote); color: var(--text); white-space: pre-wrap; overflow-wrap: anywhere; }

/* One reminder, on the Reminders page and under a task or ticket. */
.k-reminder {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: .5rem;
  padding: .55rem .6rem;
  border-radius: var(--radius-md);
  border: 1px solid var(--border-soft);
}
.k-reminder + .k-reminder { margin-top: .4rem; }
.k-reminder-due { border-color: rgb(var(--warning-rgb) / .45); background: var(--warning-bg); }
.k-reminder-done { opacity: .6; }
.k-reminder-main { flex: 1; min-width: 10rem; }
.k-reminder-main strong { display: block; font-size: var(--fs-subhead); color: var(--text-strong); font-weight: 510; }
.k-reminder-main span { font-size: var(--fs-caption); color: var(--text-muted); }
.k-reminder-actions { display: flex; flex-wrap: wrap; gap: .3rem; align-items: center; }
.k-reminder-actions form { display: inline-flex; }

/* A row of filter controls that wraps rather than pushing the page sideways. */
.k-toolbar {
  display: flex;
  flex-wrap: wrap;
  align-items: flex-end;
  gap: .5rem;
  margin-bottom: 1rem;
}
.k-toolbar .field { margin-bottom: 0; }
.k-toolbar select, .k-toolbar input { width: auto; min-width: 8rem; }

/* The owner's cross-branch summary on Home. Deliberately plain: it is a
   read-only roll-up, and anything that looked like a control here would be
   promising an action this page does not have. */
.branch-rollup-head { margin-bottom: .85rem; }
.branch-rollup-head .hint { margin: .15rem 0 0; }
.branch-rollup tr.is-active-branch { background: rgb(var(--accent-rgb) / .06); }
.branch-rollup tr.is-active-branch .name { font-weight: 700; }

/* ---------- Cash counter ---------- */
/* One row per note: the label, how many, and what that comes to. A single
   column on purpose -- people count the whole drawer first and take notes out
   after, so the count and the take-out are two stacked steps rather than one
   wide grid whose tab order would run across them. */
.cash-step { margin-top: 1.25rem; }
.cash-step:first-child { margin-top: 0; }
.cash-step-title { font-size: var(--fs-subhead); font-weight: 620; color: var(--text-strong); margin: 0 0 .2rem; }
.cash-step-hint { font-size: var(--fs-caption); color: var(--text-faint); margin: 0 0 .6rem; }
.cash-rows { border-top: 1px solid var(--border-soft); }
.cash-row {
  display: grid;
  grid-template-columns: minmax(0, 1fr) 5.75rem minmax(4.5rem, auto);
  align-items: center;
  gap: .6rem;
  padding: .45rem 0;
  border-bottom: 1px solid var(--border-soft);
}
.cash-row-label { font-size: var(--fs-subhead); color: var(--text-strong); font-weight: 510; }
.cash-row-label small { display: block; font-size: var(--fs-caption); color: var(--text-faint); font-weight: 400; }
.cash-row input { text-align: right; font-variant-numeric: tabular-nums; }
.cash-row-amount { text-align: right; color: var(--text-muted); font-size: var(--fs-footnote); font-variant-numeric: tabular-nums; white-space: nowrap; }
.cash-row-of { display: flex; align-items: center; justify-content: flex-end; gap: .4rem; font-size: var(--fs-caption); color: var(--text-faint); white-space: nowrap; }
.cash-row.is-empty { display: none; }
.cash-total {
  display: flex;
  justify-content: space-between;
  align-items: baseline;
  gap: .75rem;
  flex-wrap: wrap;
  margin-top: .7rem;
  font-size: var(--fs-subhead);
  color: var(--text-muted);
}
.cash-total strong { font-size: var(--fs-title3); color: var(--text-strong); font-variant-numeric: tabular-nums; }

/* The result read top to bottom, like a cash book: from last night, plus
   sales, minus money out, equals what it should be -- then what was there.
   It reads on a phone without a table, and answers "why short?" by itself. */
.cash-book { margin: 0; }
.cash-book-row {
  display: flex;
  justify-content: space-between;
  align-items: baseline;
  gap: 1rem;
  padding: .45rem 0;
  border-bottom: 1px solid var(--border-soft);
  font-size: var(--fs-subhead);
}
.cash-book-row dt { color: var(--text-muted); min-width: 0; }
.cash-book-row dt small { display: block; font-size: var(--fs-caption); color: var(--text-faint); }
.cash-book-row dd { margin: 0; font-variant-numeric: tabular-nums; white-space: nowrap; color: var(--text-strong); }
.cash-book-row.is-total { border-bottom: 2px solid var(--border); }
.cash-book-row.is-total dt, .cash-book-row.is-total dd { font-weight: 620; color: var(--text-strong); }
.cash-book-row.is-sub dt { padding-left: 1rem; font-size: var(--fs-footnote); }
.cash-book-row.is-sub dd { font-size: var(--fs-footnote); color: var(--text-muted); }

.cash-actions { display: flex; flex-wrap: wrap; gap: .5rem; margin-top: 1rem; }
.cash-list { list-style: none; margin: 0; padding: 0; }
.cash-list li {
  display: flex;
  justify-content: space-between;
  align-items: flex-start;
  gap: .75rem;
  padding: .55rem 0;
  border-bottom: 1px solid var(--border-soft);
  font-size: var(--fs-footnote);
}
.cash-list li:last-child { border-bottom: none; }
.cash-list .cash-list-main { min-width: 0; }
.cash-list .cash-list-main strong { color: var(--text-strong); font-weight: 510; }
.cash-list .cash-list-sub { display: block; font-size: var(--fs-caption); color: var(--text-faint); }
.cash-list .cash-list-amount { font-variant-numeric: tabular-nums; white-space: nowrap; color: var(--text-strong); font-weight: 590; }
.cash-list li.is-cancelled .cash-list-main strong,
.cash-list li.is-cancelled .cash-list-amount { text-decoration: line-through; color: var(--text-faint); }
.cash-choices { display: flex; flex-wrap: wrap; gap: .4rem; margin: 0 0 .75rem; border: 0; padding: 0; }
.cash-choices label { display: inline-flex; }
.cash-choices input { position: absolute; opacity: 0; pointer-events: none; }
.cash-choices span {
  display: inline-block;
  padding: .45rem .8rem;
  border-radius: var(--radius-pill);
  background: var(--fill);
  color: var(--text-muted);
  font-size: var(--fs-footnote);
  cursor: pointer;
  border: 1px solid transparent;
}
.cash-choices input:checked + span { background: rgb(var(--accent-rgb) / .14); color: var(--accent); border-color: rgb(var(--accent-rgb) / .45); font-weight: 590; }
.cash-choices input:focus-visible + span { box-shadow: var(--focus-ring); }
.cash-denoms { font-size: var(--fs-caption); }
.cash-denoms td, .cash-denoms th { padding: .4rem .6rem; text-align: right; }
.cash-denoms td:first-child, .cash-denoms th:first-child { text-align: left; }
.cash-denoms tfoot th, .cash-denoms tfoot td { border-top: 1px solid var(--border); font-weight: 590; color: var(--text-strong); }
.cash-denoms .cash-denoms-out td { text-align: left; font-weight: 450; color: var(--text-muted); white-space: normal; }

/* Today, as the day runs: opening cash, morning count, money in and out, the
   close, the POS figure. A dot per step -- a tick when done, the step's
   number while it is to do, dashed for later -- so the page says what is
   next before anybody reads a figure. */
.cash-alert { text-decoration: none; margin-bottom: .75rem; }
.cash-day { list-style: none; margin: 0; padding: 0; }
.cash-day-step {
  display: grid;
  grid-template-columns: 1.6rem minmax(0, 1fr);
  gap: .7rem;
  padding: .8rem 0;
  border-bottom: 1px solid var(--border-soft);
}
.cash-day-step:first-child { padding-top: 0; }
.cash-day-step:last-child { border-bottom: none; padding-bottom: 0; }
.cash-day-dot {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 1.6rem;
  height: 1.6rem;
  border-radius: 50%;
  border: 1.5px solid var(--border);
  color: var(--text-faint);
  font-size: var(--fs-caption);
  font-weight: 620;
  font-variant-numeric: tabular-nums;
}
.cash-day-dot svg { width: 1rem; height: 1rem; }
.is-done > .cash-day-dot { background: var(--success-bg); border-color: transparent; color: var(--success-text); }
.is-todo > .cash-day-dot { border-color: var(--accent); color: var(--accent); }
.is-open > .cash-day-dot { background: rgb(var(--accent-rgb) / .14); border-color: transparent; color: var(--accent); }
.is-later > .cash-day-dot, .is-skipped > .cash-day-dot { border-style: dashed; }
.cash-day-body { min-width: 0; }
.cash-day-head {
  display: flex;
  justify-content: space-between;
  align-items: baseline;
  flex-wrap: wrap;
  gap: .2rem .75rem;
  margin: .15rem 0 0;
}
.cash-day-name { font-size: var(--fs-subhead); font-weight: 590; color: var(--text-strong); }
.is-later .cash-day-name, .is-skipped .cash-day-name { color: var(--text-muted); font-weight: 510; }
.cash-day-figure {
  display: inline-flex;
  align-items: baseline;
  gap: .4rem;
  font-variant-numeric: tabular-nums;
  white-space: nowrap;
  color: var(--text-strong);
  font-weight: 590;
}
.cash-day-sub { margin: .15rem 0 0; font-size: var(--fs-footnote); color: var(--text-muted); }
.card-heading + .cash-day-sub { margin: -.7rem 0 .9rem; }
.cash-day-warn { color: var(--warning-text); }
.cash-day-actions { display: flex; flex-wrap: wrap; gap: .5rem; margin-top: .6rem; }
.cash-day-total {
  display: flex;
  justify-content: space-between;
  align-items: baseline;
  gap: .75rem;
  margin: .7rem 0 0;
  padding-top: .55rem;
  border-top: 1px solid var(--border-soft);
  font-size: var(--fs-footnote);
  color: var(--text-muted);
}
.cash-day-total strong { color: var(--text-strong); font-variant-numeric: tabular-nums; white-space: nowrap; }
.cash-day-form { margin-top: .6rem; }
.cash-day-step .correct-cell { margin-top: .4rem; }
h2.cash-day-title { display: flex; flex-wrap: wrap; align-items: center; gap: .5rem; }

.cash-for { margin: 0 0 1rem; font-size: var(--fs-subhead); color: var(--text-muted); }
.cash-for strong { color: var(--text-strong); }

/* The close's two screens, marked above the heading: count and lock first,
   then the POS figure. */
.cash-progress { list-style: none; display: flex; flex-wrap: wrap; gap: .4rem; margin: 0 0 .6rem; padding: 0; font-size: var(--fs-caption); }
.cash-progress li {
  display: inline-flex;
  align-items: center;
  gap: .4rem;
  padding: .2rem .7rem .2rem .25rem;
  border-radius: var(--radius-pill);
  background: var(--fill);
  color: var(--text-muted);
}
.cash-progress li > span {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 1.2rem;
  height: 1.2rem;
  border-radius: 50%;
  background: var(--surface);
  font-weight: 620;
}
.cash-progress li.is-current { background: rgb(var(--accent-rgb) / .14); color: var(--accent); font-weight: 590; }
.cash-progress li.is-current > span { background: var(--accent); color: var(--on-accent); }
.cash-progress li.is-done > span { background: var(--success-bg); color: var(--success-text); }

/* A day's verdict, first on its record: the result in its badge's colour,
   the one line that says why, then the note if it needs one. */
.cash-result-verdict { margin: 0; font-size: var(--fs-title2); font-weight: 680; color: var(--text-strong); font-variant-numeric: tabular-nums; }
.cash-result.is-success .cash-result-verdict { color: var(--success-text); }
.cash-result.is-warning .cash-result-verdict { color: var(--warning-text); }
.cash-result.is-danger .cash-result-verdict { color: var(--error-text); }
.cash-result-line { margin: .25rem 0 0; font-size: var(--fs-subhead); color: var(--text-muted); }
.cash-result .status-stack { margin-top: .7rem; }
.cash-result-note { margin-top: 1rem; padding-top: 1rem; border-top: 1px solid var(--border-soft); }

/* The money form: the amount first and large, then the choices two-up on a
   phone rather than pills wrapping ragged across four lines. */
.cash-amount input { font-size: var(--fs-title3); font-weight: 590; font-variant-numeric: tabular-nums; }
.cash-choices-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(9.5rem, 1fr)); }
.cash-choices-grid label { display: flex; }
.cash-choices-grid span {
  flex: 1;
  display: flex;
  align-items: center;
  justify-content: center;
  border-radius: var(--radius-sm);
  text-align: center;
  padding: .6rem;
}
.cash-tip {
  margin: -.25rem 0 .9rem;
  padding: .55rem .7rem;
  border-radius: var(--radius-sm);
  background: var(--warning-bg);
  color: var(--warning-text);
  font-size: var(--fs-footnote);
}

/* ---------- One person's month ---------- */
/* The streak and the calendar (attendance_month.py, _month_calendar.html).
   Shared by Records, where staff read their own month, and Employee Status,
   where a manager reads somebody else's. Side by side where there is room,
   stacked on a phone. */
.am { display: grid; gap: 1.25rem 2.5rem; margin-bottom: 1.5rem; }
@media (min-width: 1000px) {
  .am { grid-template-columns: minmax(0, 1fr) minmax(0, 23rem); align-items: start; }
}
.am .card-heading { margin-bottom: .75rem; }
/* The streak is the one big number. The flame is lit in the accent while
   a streak runs and grey at nought, so the same figure says "keep it" or
   "start again" before a word is read. */
.am-score { display: flex; gap: 1.1rem; align-items: center; }
.am-flame {
  flex: none; width: 4.75rem; height: 4.75rem; border-radius: var(--radius-pill);
  display: grid; place-items: center;
  background: var(--fill); color: var(--text-faint);
}
.am-flame.is-lit { background: rgb(var(--accent-rgb) / .14); color: var(--accent); }
.am-flame svg { width: 2.6rem; height: 2.6rem; }
.am-hero-value { display: block; font-size: 3rem; font-weight: 700; line-height: 1; letter-spacing: -.03em; color: var(--text-strong); }
.am-hero-label { display: block; margin-top: .25rem; font-weight: 600; color: var(--text-strong); }
.am-line { margin-top: .3rem; font-size: var(--fs-footnote); color: var(--text-muted); }

.am-weekdays, .am-days {
  list-style: none; display: grid; gap: 4px;
  grid-template-columns: repeat(7, minmax(0, 1fr));
}
.am-weekdays { margin-bottom: .3rem; font-size: var(--fs-caption2); color: var(--text-faint); text-align: center; }
.am-day {
  position: relative; aspect-ratio: 1; border-radius: var(--radius-sm);
  display: flex; flex-direction: column; align-items: center; justify-content: center; gap: .15rem;
  font-size: var(--fs-caption); font-weight: 590; line-height: 1; color: var(--text);
  font-variant-numeric: tabular-nums;
}
.am-day svg { width: .9rem; height: .9rem; }
.am-day[data-detail] { cursor: pointer; }
.am-day[data-detail]:hover, .am-day.is-picked { outline: 2px solid var(--text-faint); outline-offset: 1px; }
/* Today wears the accent ring whatever kind of day it turns out to be.
   Days still to come, or before the first one that counts, are bare
   numbers: there is nothing to say about them yet. */
.am-day.is-now { box-shadow: inset 0 0 0 2px var(--accent); }
.am-day.k-off, .am-day.k-before, .am-day.k-future, .am-day.k-today { color: var(--text-faint); }
.am-readout { margin-top: .6rem; min-height: 1.4em; font-size: var(--fs-footnote); color: var(--text-muted); }

/* One colour per verdict, shared by the days and the legend's keys. Status
   colours, the same three the records table's badges use, and every one
   with its own glyph -- so the day reads by shape too, not colour alone. */
.am-swatch.k-on_time { background: rgb(var(--success-rgb) / .16); }
.am-swatch.k-late, .am-swatch.k-early { background: rgb(var(--warning-rgb) / .16); }
.am-swatch.k-half, .am-swatch.k-absent { background: rgb(var(--error-rgb) / .14); }
.am-swatch.k-off, .am-swatch.k-extra { background: var(--fill); }
.am-swatch.k-on_time svg, .am-swatch.k-extra svg { color: var(--success); }
.am-swatch.k-late svg, .am-swatch.k-early svg { color: var(--warning); }
.am-swatch.k-half svg, .am-swatch.k-absent svg { color: var(--error); }

.am-legend {
  list-style: none; display: flex; flex-wrap: wrap; gap: .45rem 1rem; margin-top: .75rem;
  font-size: var(--fs-footnote); color: var(--text-muted);
}
.am-legend li { display: inline-flex; align-items: center; gap: .35rem; }
.am-legend strong { color: var(--text-strong); font-weight: 620; }
.am-key { flex: none; width: 1.25rem; height: 1.25rem; border-radius: .35rem; display: grid; place-items: center; }
.am-key svg { width: .85rem; height: .85rem; }
