/* ─────────────────────────────────────────────────────────────────────────
   Shared design system — the avendesk.com theme.
   Loaded by every page in /public. Component classes below are the contract
   several pages' inline <script> blocks rely on (they set these classes,
   or read var(--accent)/var(--muted)/var(--bad)/var(--warn)/var(--line)/
   var(--panel) directly in generated markup) — names are load-bearing.

   Two shells share these tokens: `.app-shell` (agency side) and
   `.portal-shell` (business-owner side). Both are built the way the marketing
   site is: a white rail and a white bar carrying the navigation, hairline
   slate rules between the regions, and a very pale blue canvas under it all
   holding the work.

   PALETTE — Clear Sky, lifted from the marketing site's own :root so the
   product and the site it is sold on are the same object:
     white  #ffffff / #f7fafd / #f1f5fa / #e7edf6
     navy   #0d142b / #070d1e / #18224a
     text   #0d1526 / #55637a / #64748b
     azure  #2f5cf0 / #1e3fc4      iris #8b3ce8
     good   #0f8a62   danger #d1372c   caution #e0952b

   The accent is a COLOUR again. It was ink — a black primary button — under
   the paper palette, where black on bone was the most confident control the
   page could have. On white it is merely severe, and the reference this was
   rebuilt against spends its blue on exactly one thing: the action. So
   --accent is the azure, the primary button carries the azure-to-iris ramp,
   and the focus ring is the same azure at a wider offset. That does mean
   "interactive" and "focused" share a hue; they are told apart by the ring
   sitting OUTSIDE the control with a gap, which is a stronger signal than a
   second colour and does not cost the palette a third one.

   Light is the DEFAULT here — `:root` with no attribute — and navy is the
   opt-in dark theme further down. The token NAMES are unchanged through
   three palettes now, on purpose: every page script that reads var(--accent)
   or sets .card keeps working untouched.
   ────────────────────────────────────────────────────────────────────── */

:root {
  /* surfaces — warm paper page, lifted card stock, inset wells */
  --bg: #f7fafd;
  --bg-shell: #ffffff;      /* the rail and the bar are white on every shell */
  --panel: #ffffff;
  --panel-2: #f1f5fa;
  --panel-3: #e7edf6;
  --line: #dce4ee;
  --line-strong: #c6d2e2;

  /* text */
  --text: #0d1526;
  --muted: #55637a;
  --muted-2: #64748b;

  /* brand + semantics.
     "-dark" variants are the variant that stays readable as TEXT on the page
     surface. On paper that means genuinely darker; on the forest theme below
     they invert to lighter tints. The name is kept because page scripts
     reference it directly. */
  /* The accent has to be a COLOUR, not ink.
     The default button on this site is already ink (`--invert-bg`), and
     `.accent` is documented as "the one place the accent is spent as a fill —
     an action about spending money or going live, not just navigating". Point
     both at ink and that distinction silently disappears: every button on
     every page becomes the same dark pill. The azure carries white text at
     5.3:1, so the fill needs no second thought. */
  --accent: #2f5cf0;
  --accent-dark: #1e3fc4;
  --accent-soft: #eef2ff;
  /* The focus ring keeps its own token even though it currently matches
     --accent, because the two are answerable to different things: the accent
     is a brand decision and the ring has to clear 3:1 against whatever it
     lands on. They agree on the azure here; the dark theme and the rail
     override the ring further down, where the azure would be too dark to
     find against a navy surface and the rail is the first thing Tab
     reaches. */
  --focus: #2f5cf0;
  --good: #0f8a62;
  --good-dark: #0a6b4c;
  --good-soft: #e7f6f0;
  /* Caution is the one warm colour in a cool palette, which is most of why
     it works: nothing else on the page is amber, so a warning never has to
     compete with the accent for the same meaning. */
  --warn: #e0952b;
  --warn-soft: #fdf3e3;
  /* The same gold, dark enough to be READ. --warn is tuned as a fill and a
     border: on paper it is 3.5:1, which is fine behind a 7px meter bar and
     not fine as the 10px tracked-out caps it was also being used for. Every
     place the gold carries words points here instead. */
  --warn-text: #96560c;
  --bad: #d1372c;
  /* What stays readable ON --bad. White is 6.57:1 here and only 2.79:1 on the
     dark theme's lighter coral below, so every badge, live chip and danger
     button that paints white on red was failing in dark mode. The ink travels
     with the red instead of being assumed. */
  --bad-ink: #ffffff;
  --bad-soft: #fdecea;

  /* categorical chart palette — amber / ink / green / slate / light amber /
     red, in a fixed order so two charts on one page never disagree. Chosen
     for separability first: amber and light amber are two steps apart in
     lightness, not two neighbouring tints, so they stay distinct printed in
     grey or seen by a red-green colour-blind reader. */
  --c1: #2f5cf0;
  --c2: #8b3ce8;
  --c3: #0f8a62;
  --c4: #64748b;
  --c5: #22a7c4;
  --c6: #d1372c;
  /* The ink that stays readable ON each of those, for the one place a figure
     is written inside its own segment (`.split-bar`). No single value works
     across six: black on the violet is 3.4:1 and white on the gold is 4.05:1,
     so each colour names its own partner and every pair clears 4.5:1. */
  --c1-ink: #ffffff;   /* 6.53 : 1 — the old gold took black ink; this amber
                          is far darker and takes white */
  --c2-ink: #ffffff;   /* 17.9 : 1 */
  --c3-ink: #ffffff;   /* 5.30 : 1 */
  --c4-ink: #ffffff;   /* 5.68 : 1 */
  --c5-ink: #ffffff;   /* 4.52 : 1 */
  --c6-ink: #ffffff;   /* 6.57 : 1 */
  /* The ghost bar sitting behind a real one — "what the full day could hold". */
  --track: #e7edf6;
  --track-line: #d3dded;

  /* The one high-contrast lozenge on a page — a selected pill, tab or chip, a
     tooltip, the portal's round mark — which is simply the page's own colours
     inverted. Every one of those elements reads these, so the forest theme
     below flips all of them by swapping two values. */
  --invert-bg: #0d142b;
  --invert-fg: #f3f6fc;
  --invert-glow: rgba(13, 21, 38, .4);
  /* Dimmer behind a dialog or the portal's sign-in gate. */
  --overlay: rgba(13, 21, 38, .55);
  /* The fade at the bottom of a scrollable well. */
  --fade: rgba(13, 21, 38, .12);

  /* elevation + shape. The site's own --elev-page / --elev-lift: a hairline
     contact shadow plus one very soft, very far cast. */
  --shadow-xs: 0 1px 2px #0d15260a;
  --shadow-sm: 0 1px 2px #0d15260a, 0 8px 24px #0d152610;
  --shadow-md: 0 1px 2px #0d15260f, 0 16px 40px -12px #0d152633;
  --shadow-lg: 0 2px 6px #0d15260d, 0 28px 64px -22px #0d15263d;
  --radius-sm: 8px;
  --radius-md: 12px;
  --radius-lg: 16px;
  --radius-xl: 22px;
  --radius-full: 999px;

  /* Type. One face, self-hosted, doing both jobs — the same Figtree the
     marketing site sets its headlines in, so the product and the site it is
     sold on are one object rather than two that merely share a palette.

     Self-hosted rather than named-and-hoped. The previous set listed
     "Instrument Sans" and Fraunces as though they might be installed; on
     nearly every visitor machine they are not, so both fell back to Segoe UI
     and the portal had no typographic identity at all. The CSP sets
     font-src 'self', so a <link> to Google Fonts is not a style choice that
     degrades — it is a request the browser refuses. The one 20KB latin subset
     is served from /fonts and its @font-face sits directly below this block.

     --font-display is no longer a different family. It is the same face with
     tighter tracking applied at the call site, which is what the reference
     does and what the marketing site now does. */
  --font: Figtree, -apple-system, BlinkMacSystemFont, "Segoe UI", "Helvetica Neue", Arial, sans-serif;
  --font-display: Figtree, -apple-system, BlinkMacSystemFont, "Segoe UI", "Helvetica Neue", Arial, sans-serif;
  --font-mono: ui-monospace, SFMono-Regular, "SF Mono", Menlo, Consolas, monospace;

  /* ── Type scale ───────────────────────────────────────────────────────
     Eleven sizes, and nothing between them. The page used to carry thirty-one
     — six of them (12 / 12.5 / 13 / 13.5 / 14 / 14.5) inside a 2.5px band, so
     the differences were doing no work while every new rule had to guess which
     of the six it wanted. Names are the value, so a rule reading
     `var(--text-13)` is self-documenting and a wrong one is obvious on sight.

     Tight steps at the bottom (labels and captions are read, not compared),
     widening as they climb, so the display numbers separate at a glance.

     In `rem`, not px: 1rem is whatever the reader's browser is set to, so a
     reader who has turned text up gets the whole scale turned up with it. The
     comments name the px each one lands on at the 16px default, because that
     is still the number worth recognising when you are looking at a design. */
  --text-10: 0.625rem;   /* 10px — micro-caps: nav group headings, the plan marginal */
  --text-11: 0.6875rem;  /* 11px — eyebrows, tile labels, chart axes */
  --text-12: 0.75rem;  /* 12px — captions, meta, timestamps */
  --text-13: 0.8125rem;  /* 13px — secondary body, buttons, sub-copy */
  --text-15: 0.9375rem;  /* 15px — body */
  --text-17: 1.0625rem;  /* 17px — card titles, section headings */
  --text-20: 1.25rem;  /* 20px — small figures */
  --text-24: 1.5rem;  /* 24px — medium figures */
  --text-30: 1.875rem;  /* 30px — page titles, stat tiles */
  --text-40: 2.5rem;  /* 40px — the four headline counts */
  --text-52: 3.25rem;  /* 52px — the money figure — one per page, at most */

  /* ── Weight ───────────────────────────────────────────────────────────
     Four, matching what a static font actually carries. Nothing here is
     self-hosted (`font-src 'self'` and no @font-face), so on almost every
     visitor's machine this resolves to Segoe UI / Helvetica — families with
     400 / 600 / 700 and no 620 or 650. Those in-between weights were not
     rendering as a subtle ladder; the browser was rounding every one of them
     up to Bold, which is why headings meant to be semibold came out heavy. */
  --weight-normal: 400;
  --weight-medium: 500;
  --weight-semibold: 600;
  --weight-bold: 700;

  /* ── Leading ──────────────────────────────────────────────────────────
     Big type needs proportionally less. */
  --leading-tight: 1.1;    /* display figures */
  --leading-snug: 1.25;    /* headings */
  --leading-normal: 1.55;  /* body */
  --leading-relaxed: 1.65; /* long-form explanation inside a dialog */

  /* ── Spacing scale ────────────────────────────────────────────────────
     Twelve values, named for what they are. Thirty-seven were in use, half of
     them odd numbers arrived at by nudging (5, 7, 9, 11, 13, 15, 17, 19) —
     which is how two cards side by side end up with 17px and 19px of padding
     and neither looks wrong on its own. 2px and 6px stay on the scale because
     an icon sitting next to a word genuinely needs a gap smaller than 8. */
  --space-2: 2px;
  --space-4: 4px;
  --space-6: 6px;
  --space-8: 8px;
  --space-12: 12px;
  --space-16: 16px;
  --space-20: 20px;
  --space-24: 24px;
  --space-32: 32px;
  --space-40: 40px;
  --space-48: 48px;
  --space-64: 64px;

  /* The site's own easings — a long settle rather than a linear slide. */
  --ease: cubic-bezier(.16, 1, .3, 1);
  --ease-in-out: cubic-bezier(.76, 0, .24, 1);

  /* The brand ramp. One declaration, read by every control that carries it —
     the primary button, the mark, the rule under a live figure — so moving
     the brand is one edit here rather than forty across the file. */
  --grad-brand: linear-gradient(135deg, #2f5cf0 0%, #4a72f5 45%, #8b3ce8 100%);
  --grad-brand-flat: linear-gradient(90deg, #2f5cf0 0%, #8b3ce8 100%);
  --iris: #8b3ce8;
  --iris-soft: #f5edfe;
}

/* ─────────────────────────────────────────────────────────────────────────
   The face.

   One variable file covering 400-800, latin only. font-display: swap, so a
   slow connection shows the fallback rather than nothing — this is a working
   tool, and a blank dashboard while a font loads is worse than a plain one.
   ───────────────────────────────────────────────────────────────────────── */
@font-face {
  font-family: Figtree;
  font-style: normal;
  font-weight: 400 800;
  font-display: swap;
  src: url("/fonts/figtree-var.woff2") format("woff2");
  unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA,
    U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193,
    U+2212, U+2215, U+FEFF, U+FFFD;
}


/* ─── Reset & base ──────────────────────────────────────────────────────── */

* { box-sizing: border-box; }
/* No `-webkit-text-size-adjust: 100%`. That is the line every reset carries to
   stop iOS inflating text in landscape, but it is also an explicit refusal to
   let the reader's own text-size setting through — and with `width=device-width`
   in the viewport meta the UA has already stopped auto-inflating, so the only
   thing the declaration was still doing here was the refusing.

   Together with the type scale being in `rem` (see --text-* above), this is
   what Dynamic Type means on the web: the root size is the reader's, every
   size on every page is a multiple of it, and someone who has set Large Text
   gets a larger dashboard rather than the same one. Touch targets and layout
   stay in px on purpose — 44 is a fingertip, not a letter, and it does not
   grow when the type does. */
html { background: var(--bg); }

body {
  margin: 0;
  background: var(--bg);
  color: var(--text);
  font: var(--weight-normal) var(--text-15)/var(--leading-normal) var(--font);
  -webkit-font-smoothing: antialiased;
  text-rendering: optimizeLegibility;
}

/* Tells the browser to render its own widgets (scrollbars, date pickers, the
   checkbox tick) to match — without it a date input on the forest theme opens
   a blinding white calendar. */
:root { color-scheme: light; }

/* Paper is the default, so `data-theme="light"` has nothing left to change.
   The selector is kept because it is what portal.html has been writing into
   localStorage all along, and a returning customer whose stored value is
   "light" must land on the paper theme rather than on no theme at all. */
:root[data-theme="light"] { color-scheme: light; }

/* ─── Forest theme ───────────────────────────────────────────────────────
   Opt-in: nothing changes unless something sets data-theme="dark" on <html>
   (portal.html's toggle does, from localStorage, before first paint). Only
   TOKENS are redefined here — no component rule is duplicated, so a component
   added later is forest-ready without being touched.

   This is the same navy the marketing site uses behind its Rooms panel and
   its footer, not a generic dark mode: the near-blacks carry blue in them
   (#0d142b and friends) rather than being neutral charcoal, and the accent
   lifts to a paler azure — #2f5cf0 on #0d142b is 2.4:1, which is unreadable
   as text and invisible as a border, so the accent cannot simply carry over.

   Note the "-dark" variants inverting their meaning: on paper they are
   genuinely darker; here they have to be LIGHTER than the base to stay
   readable on forest. Same for --track, a black veil on paper and a paper one
   here. */
:root[data-theme="dark"] {
  color-scheme: dark;

  --bg: #070d1e;
  --bg-shell: #0d142b;
  --panel: #0d142b;
  --panel-2: #18224a;
  --panel-3: #222d5c;
  --line: #f3f6fc24;
  --line-strong: #f3f6fc45;

  --text: #f3f6fc;
  --muted: #b9c3dc;
  --muted-2: #8b97b8;

  --accent: #7d9bff;
  --accent-dark: #a8bcff;
  --accent-soft: #7d9bff2e;
  --good: #4fd1a0;
  --good-dark: #8ee5c2;
  --good-soft: #4fd1a026;
  /* Same separation as on paper, lifted for ink: the caution amber stays
     brighter and yellower than the accent so a warning never reads as a
     button. */
  --warn: #f0b840;
  --warn-soft: #f0b84026;
  --warn-text: #f3c86e;
  --bad: #ef8a80;
  --bad-ink: #10060a;   /* 7.4 : 1 — white would be 2.7 : 1 */
  --bad-soft: #d1372c3d;

  /* Re-separated. The straight value swap collided c1 and c2 on the same
     amber, and two chart series in one colour is a chart that lies. */
  --c1: #7d9bff;
  --c2: #c39bf5;
  --c3: #4fd1a0;
  --c4: #9aa6c4;
  --c5: #5fd5ea;
  --c6: #ef8a80;
  /* Same pairing, re-measured against these lighter tints — five of the six
     flip to dark ink here. */
  --c1-ink: #070d1e;   /* 8.2 : 1 */
  --c2-ink: #070d1e;   /* 8.9 : 1 */
  --c3-ink: #070d1e;   /* 10.5 : 1 */
  --c4-ink: #070d1e;   /* 7.6 : 1 */
  --c5-ink: #070d1e;   /* 10.9 : 1 */
  --c6-ink: #070d1e;   /* 7.9 : 1 */
  --track: #f3f6fc14;
  --track-line: #f3f6fc24;

  --invert-bg: #f3f6fc;
  --invert-fg: #0d1526;
  --invert-glow: rgba(243, 246, 252, .42);
  --overlay: rgba(4, 8, 20, .8);
  --fade: rgba(4, 8, 20, .6);

  --shadow-xs: 0 1px 2px rgba(0, 0, 0, .4);
  --shadow-sm: 0 2px 8px rgba(0, 0, 0, .45);
  --shadow-md: 0 12px 32px rgba(0, 0, 0, .5);
  --shadow-lg: 0 28px 64px rgba(0, 0, 0, .62);

  /* 7.4:1 on card stock, 5.1:1 in the deepest well. */
  --focus: #a8bcff;

  /* The ramp survives the theme change unaltered: it is painted on a
     control that supplies its own white text either way, so a lighter
     dark-mode version would only make the one saturated object on the
     page louder than the page it sits on. */
  --iris-soft: #2a1a4a;
}

/* The scrollbar thumb is drawn from --line-strong against --bg, which is the
   one place the values don't simply invert — a translucent rule over paper
   comes out too faint to grab. */
::-webkit-scrollbar-thumb { background: #c9c2ac; }
:root[data-theme="dark"] ::-webkit-scrollbar-thumb { background: #3a3f4a; }
:root[data-theme="dark"] ::-webkit-scrollbar-thumb:hover { background: var(--muted-2); }

a { color: inherit; text-decoration: none; }
a[href]:not([class]) { color: var(--accent-dark); text-decoration: underline; text-underline-offset: 2px; }

h1, h2, h3 {
  margin: 0; color: var(--text);
  font-weight: var(--weight-semibold); letter-spacing: -.02em;
  /* Body leading on a heading is a paragraph's leading on something that is
     not a paragraph — a title that wrapped came apart into two lines with no
     visible relationship. Headings get their own, tighter as they grow. */
  line-height: var(--leading-snug);
  text-wrap: balance;
}
/* The one place the display serif is spent: a page's own title. The site uses
   it exactly this way — editorial at the top of a view, grotesque for every
   label and control underneath, so the serif stays an event rather than a
   texture. */
h1 {
  font-family: var(--font-display); font-size: var(--text-30);
  font-weight: var(--weight-normal); letter-spacing: -.02em;
  line-height: var(--leading-tight);
}
h2 { font-size: var(--text-17); }
h3 { font-size: var(--text-15); }

/* AvenDesk's `.display` / `.display-2` / `.display-3`, available to any page
   that wants the site's voice for a hero line. */
.display { font-family: var(--font-display); font-weight: 400; letter-spacing: -.02em; line-height: 1.04; }
.display-2 { font-size: clamp(28px, 5.2vw, 44px); letter-spacing: -.03em; line-height: 1; }
.display-3 { font-size: clamp(21px, 2.8vw, 30px); letter-spacing: -.02em; line-height: 1.08; }
.display em { font-style: italic; }

::selection { background: var(--accent-soft); color: var(--text); }

::-webkit-scrollbar { width: 10px; height: 10px; }
::-webkit-scrollbar-track { background: transparent; }
::-webkit-scrollbar-thumb { background: var(--line-strong); border-radius: 999px; border: 2.5px solid var(--bg); }
::-webkit-scrollbar-thumb:hover { background: var(--muted-2); }

:focus-visible {
  outline: 2px solid var(--focus);
  outline-offset: 2px;
  border-radius: 4px;
}

/* The site calls this a "marginal": gold, mono, wide-tracked, the smallest
   type on the page. It labels a section the way a margin note labels a
   paragraph. */
.eyebrow {
  display: flex; align-items: center; gap: var(--space-8);
  font-size: var(--text-11); font-weight: var(--weight-semibold);
  letter-spacing: 0;
  /* --warn is a fill colour, and as 11px tracked-out caps it came in at 3.5:1
     on paper: the least legible text on the page was also the smallest.
     --warn-text is the same gold taken down far enough to clear 4.5:1, used
     only where the gold has to be READ. */
  color: var(--warn-text); margin-bottom: var(--space-8);
}
.eyebrow::before { content: ''; width: 14px; height: 1px; background: var(--warn); flex-shrink: 0; opacity: .7; }
.section-head { margin-bottom: var(--space-16); }
.section-head h2 { font-size: var(--text-17); }
.section-head .sub { margin-top: var(--space-4); margin-bottom: 0; }

.sub { color: var(--muted); font-size: 0.8125rem; }
.meta { color: var(--muted); font-size: 0.75rem; }
.mono { font-family: var(--font-mono); font-size: 0.75rem; font-variant-numeric: tabular-nums; }
/* A figure meant to be read as a number — tabular so a counter ticking up
   doesn't shuffle the text beside it. */
.tick { font-family: var(--font-mono); font-variant-numeric: tabular-nums; letter-spacing: -.02em; }
.hidden { display: none !important; }
.grow { flex: 1; }

/* ─── Forest chrome ──────────────────────────────────────────────────────
   Anything that sits on the dark navigation surface — the agency rail, the
   portal's top bar — re-declares the text/line/surface tokens locally instead
   of restating a colour on every child. A `.card`, `.sub` or `.badge` dropped
   inside one of these picks up on-forest values with no rule of its own, and
   the forest theme below needs no second copy of any of it. */

/* The forest surfaces: the agency sidebar and the portal's rail. Both are
   dark panels and need the light-on-dark token set.

   The business portal's TOP BAR is deliberately not in this list any more —
   it sits on the paper canvas and is transparent, so the page reads as one
   surface rather than a green band stuck above a cream one.

   `.m-drawer` IS the agency rail on a phone — mobile-nav.js clones the rail's
   own links into it and it paints itself `--bg-shell`. It stays in this list
   for that reason: whatever the rail is, the drawer has to be, or the phone
   navigation stops matching the one it was cloned from. */
.sidebar, .rail, .m-drawer {
  /* The rail is WHITE now, and this block is what makes that true.
   *
   * It used to do the opposite: it forced the dark theme's tokens onto three
   * elements so a near-black rail could sit on a bone page. The rail is now
   * the same white as the canvas it borders, told apart by one hairline, so
   * the overrides here are down to the two that are genuinely rail-specific.
   *
   * It cannot simply be deleted. `.m-drawer` is the agency rail on a phone
   * and mobile-nav.js clones the rail's own links into it, so whatever is
   * true of the rail has to be true here too or the phone navigation goes
   * back to being invisible — which is exactly the bug this block was
   * originally written to fix, in the other direction.
   *
   * --panel lands one step down from the page rather than one step up: a
   * card inside a rail is a well, not a lifted surface, and on white a
   * "raised" white card on a white rail has nothing to raise itself off. */
  --panel: var(--panel-2);
  --panel-2: var(--panel-3);
  color: var(--text);
}


/* ─── App shell: labelled forest rail + paper canvas (agency side) ───────── */

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

.sidebar {
  width: 236px;
  flex-shrink: 0;
  background: var(--bg-shell);
  /* The rail and the canvas are both white, so the ONLY thing separating
     them is this rule. Without it the navigation appears to float in the
     page rather than to be a region of it. */
  border-right: 1px solid var(--line);
  display: flex;
  flex-direction: column;
  position: sticky;
  top: 0;
  height: 100vh;
  overflow-y: auto;
  overflow-x: hidden;
}

.sidebar-brand {
  display: flex;
  align-items: center;
  gap: 11px;
  padding: 22px 18px 18px;
}
/* A squircle carrying the brand ramp, not an ink disc. An app's mark is a
   rounded square — that is the shape every dock, tab strip and home screen
   has trained people to read as "the application" — and it is the one place
   in the whole product where both brand colours appear at once. */
.sidebar-brand .mark {
  width: 34px; height: 34px; border-radius: 10px;
  background: var(--accent); background-image: var(--grad-brand);
  color: #fff;
  display: flex; align-items: center; justify-content: center;
  flex-shrink: 0;
  box-shadow: var(--shadow-xs);
}
.sidebar-brand .mark svg { width: 18px; height: 18px; }
.sidebar-brand div { min-width: 0; }
.sidebar-brand b {
  font-family: var(--font-display); font-size: 1.0625rem; font-weight: 600;
  letter-spacing: -.022em; display: block; line-height: 1.15;
}
.sidebar-brand span {
  font-size: var(--text-11); color: var(--muted-2); letter-spacing: 0;
}

.sidebar-nav { padding: 4px 12px 18px; display: flex; flex-direction: column; gap: 2px; }

/* The rail is grouped rather than one long list, and the group headings are
   visible — a fourteen-item flat list is a list you scan every time. */
/* Sentence case in plain slate, not tracked-out caps in gold. A group
   heading is scaffolding: it should be findable when you look for it and
   invisible when you are not, and small caps in a warning colour are the
   opposite of that on both counts. */
.sidebar-section-label {
  display: block;
  font-size: var(--text-11); font-weight: 600; letter-spacing: 0;
  color: var(--muted-2);
  padding: 18px 10px 6px;
}
.sidebar-nav > .sidebar-section-label:first-child { padding-top: 4px; }

/* A rounded rectangle, not a full pill. At 236px wide the rail's rows are
   long and shallow, and a 999px radius on a 36px-tall row that spans the
   whole rail reads as a lozenge somebody forgot to size. */
.sidebar-nav a {
  display: flex; align-items: center; gap: 10px;
  padding: 9px 10px; border-radius: var(--radius-sm);
  font-size: 0.8125rem; font-weight: 500; letter-spacing: -.005em; color: var(--muted);
  line-height: 1.2;
  transition: background .2s var(--ease), color .2s var(--ease);
}
/* The generic `a[href]:not([class])` rule below outweighs a plain
   `.sidebar-nav a`, so without matching its specificity here every nav item
   except the active one renders as an underlined body link. */
.sidebar-nav a[href] { color: var(--muted); text-decoration: none; }
.sidebar-nav a[href]:hover { color: var(--text); }
.sidebar-nav a[href].active { color: var(--accent-dark); }
.sidebar-nav a svg { width: 17px; height: 17px; flex-shrink: 0; opacity: .8; }
.sidebar-nav a:hover { background: var(--panel-3); color: var(--text); }
/* The current page is a pale wash with the brand text on it, not an inverted
   black lozenge. On a white rail the inverted version was the loudest object
   on the screen — louder than the page content it was pointing at. */
.sidebar-nav a.active {
  background: var(--accent-soft);
  color: var(--accent-dark);
  font-weight: 600;
}
.sidebar-nav a.active svg { opacity: 1; }
.sidebar-nav a.cta {
  margin-top: 10px; background: transparent; color: var(--text);
  border: 1px solid var(--line); justify-content: center;
}
.sidebar-nav a.cta:hover { background: var(--panel-3); border-color: var(--line-strong); }
.sidebar-nav a.cta svg { opacity: 1; }
/* Counts riding on a nav row — pushed to the far end so the labels stay in a
   column you can read straight down. */
.sidebar-nav .nav-badge {
  margin-left: auto;
  display: inline-flex; align-items: center; justify-content: center;
  min-width: 19px; height: 19px; padding: 0 6px;
  border-radius: var(--radius-full); background: var(--bad); color: var(--bad-ink);
  font-size: 0.625rem; font-weight: 700; font-variant-numeric: tabular-nums;
}
.sidebar-nav .nav-badge[hidden] { display: none; }
.sidebar-nav a.active .nav-badge { background: var(--bad); color: var(--bad-ink); }

/* ─── The console's utility rows ────────────────────────────────────────────
   Search and Activity, injected by console-nav.js above the first section
   heading. Given the same metrics as `.sidebar-nav a` rather than a look of
   their own — they sit in the same column as the destinations, and any
   difference in height or icon size reads as a rendering fault rather than as
   a distinction. What separates them is where they are: above the first group
   label, which is what says "these are not pages". */
.sidebar-nav .sidebar-util {
  display: flex; align-items: center; gap: 10px; width: 100%;
  justify-content: flex-start; text-align: left;
  padding: 9px 12px; border-radius: var(--radius-md);
  background: none; border: 1px solid transparent;
  color: var(--muted); text-decoration: none;
  font: inherit; font-size: 0.8125rem; font-weight: 500; white-space: nowrap;
}
.sidebar-nav a.sidebar-util:link, .sidebar-nav a.sidebar-util:visited { color: var(--muted); }
.sidebar-nav .sidebar-util:hover { background: var(--panel-3); color: var(--text); opacity: 1; }
.sidebar-nav .sidebar-util svg { width: 17px; height: 17px; flex-shrink: 0; opacity: .8; }

/* ─── Go to a page ──────────────────────────────────────────────────────────
   The console's jump list. Deliberately small and near the top rather than a
   full-screen takeover: it is a shortcut to a page, not a search results
   screen, and it should look like something you can dismiss. */
.cnav-search {
  position: fixed; inset: 0; z-index: 200;
  display: flex; align-items: flex-start; justify-content: center;
  padding: 12vh 16px 16px;
  background: #0d152659;
}
.cnav-card {
  width: 100%; max-width: 440px;
  background: var(--panel); border: 1px solid var(--line);
  border-radius: var(--radius-lg); box-shadow: var(--shadow-lg);
  overflow: hidden;
}
.cnav-card input {
  width: 100%; margin: 0; border: 0; border-bottom: 1px solid var(--line);
  border-radius: 0; padding: 14px 16px;
  /* 16px. Under it, iOS zooms the page in the moment this is focused and never
     zooms back out — and this box is opened by a keystroke, so the zoom would
     land on a page the person never chose to leave. */
  font-size: 1rem;
}
.cnav-card input:focus { box-shadow: none; }
.cnav-list { max-height: 46vh; overflow-y: auto; padding: 6px; }
.cnav-row {
  display: block; padding: 10px 12px; border-radius: var(--radius-md);
  color: var(--text); text-decoration: none; font-size: 0.84375rem;
  /* A long section name must not widen the card. */
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
.cnav-row:hover, .cnav-row.is-on { background: var(--panel-2); }
.cnav-none { margin: 0; padding: 14px 12px; color: var(--muted); font-size: 0.8125rem; }


.sidebar-foot {
  margin-top: auto; padding: 14px 18px 18px;
  border-top: 1px solid var(--line);
  font-size: var(--text-11); letter-spacing: 0;
  color: var(--muted-2); line-height: 1.6;
}

/* Sticky and white, with the hairline doing the separating — the reference's
   bar, and the one that survives a long scrolling table, which is most of
   what this product is. */
.topbar {
  position: sticky; top: 0; z-index: 30;
  padding: 14px 26px;
  border-bottom: 1px solid var(--line);
  display: flex; align-items: center; gap: 14px; flex-wrap: wrap;
  background: color-mix(in srgb, var(--panel) 88%, transparent);
  backdrop-filter: blur(12px);
}
.topbar h1 { font-size: var(--text-20); font-weight: 600; letter-spacing: -.02em; }
.topbar-actions { display: flex; align-items: center; gap: 10px; margin-left: auto; flex-wrap: wrap; }

.main { flex: 1; min-width: 0; }
main.content, .content { padding: 26px 30px 72px; max-width: 1480px; }

/* Search field in the topbar — a pill, per the reference. */
.search-pill {
  display: flex; align-items: center; gap: 9px;
  background: var(--panel); border: 1px solid var(--line);
  border-radius: var(--radius-full); padding: 0 16px; min-width: 220px; max-width: 340px; flex: 1;
  box-shadow: var(--shadow-xs);
  transition: border-color .2s var(--ease), box-shadow .2s var(--ease);
}
.search-pill:focus-within {
  border-color: var(--accent);
  box-shadow: 0 0 0 3px var(--accent-soft);
}
.search-pill svg { width: 15px; height: 15px; color: var(--muted-2); flex-shrink: 0; }
.search-pill input {
  border: 0; background: none; padding: 10px 0; font-size: 0.8125rem; box-shadow: none; width: 100%;
}
.search-pill input:focus { outline: none; box-shadow: none; border: 0; }

/* "14 Jan — 20 Jan" / "Jun 6, 2026" chips that label a card's range. */
.date-chip {
  display: inline-flex; align-items: center; gap: var(--space-6);
  background: var(--panel-2); border: 1px solid var(--line);
  border-radius: var(--radius-full); padding: var(--space-6) var(--space-12);
  font-size: 0.75rem; font-weight: 600; color: var(--muted); white-space: nowrap;
}
.date-chip svg { width: 13px; height: 13px; opacity: .8; }

.status-pill {
  display: inline-flex; align-items: center; gap: var(--space-6);
  font-size: var(--text-12); color: var(--muted); font-weight: var(--weight-medium);
}
.dot { display: inline-block; width: 7px; height: 7px; border-radius: 50%; background: var(--muted-2); }
.dot.open { background: var(--good); box-shadow: 0 0 0 4px var(--good-soft); }

/* ─── Portal shell: forest nav bar over paper (business-owner side) ─────── */

.portal-shell { max-width: 1420px; margin: 0 auto; padding: var(--space-16) var(--space-24) var(--space-64); }

/* Transparent, not a green pill. The navigation moved to the rail down the
   side, so this is a header sitting on the paper canvas rather than a bar of
   its own — a filled band here just cut the page in two. */
.portal-topbar {
  display: flex; align-items: center; gap: var(--space-16);
  padding: var(--space-12) 0; margin-bottom: var(--space-24);
  background: transparent;
  border-radius: 0;
  box-shadow: none;
}
.portal-topbar .mark {
  width: 38px; height: 38px; border-radius: 11px; flex-shrink: 0;
  background: var(--accent); background-image: var(--grad-brand); color: #fff;
  display: flex; align-items: center; justify-content: center;
  box-shadow: var(--shadow-xs);
}
.portal-topbar .mark svg { width: 20px; height: 20px; }
/* Business name in the display face, plan underneath in plain slate — the
   site's own pairing, at nav scale. */
/* Capped rather than merely shrinkable: a business with a long name would
   otherwise push a ten-item nav onto a second row for everyone else's sake. */
.portal-topbar .who { min-width: 0; max-width: 190px; }
.portal-topbar .who b {
  display: block; font-family: var(--font-display); font-weight: 600;
  font-size: 1.0625rem; letter-spacing: -.022em; line-height: 1.2;
  white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
}
.portal-topbar .who span {
  display: block;
  font-size: var(--text-12); letter-spacing: 0; color: var(--muted-2);
  white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
}
.portal-topbar .topbar-right { display: flex; align-items: center; gap: 8px; margin-left: auto; }

/* The pill group. The current tab is a pale brand wash carrying brand text,
   not an inverted lozenge: on a white bar an inverted pill is the loudest
   object on the page, competing with the content it is pointing at. */
.pill-nav {
  display: flex; align-items: center; gap: 4px; flex-wrap: wrap;
  margin: 0 auto;
}
.pill-nav button {
  background: transparent; color: var(--muted);
  border: 1px solid transparent; box-shadow: none;
  border-radius: var(--radius-full); padding: 8px 13px;
  font-size: 0.75rem; font-weight: 500; letter-spacing: .01em; white-space: nowrap;
  transition: background .3s var(--ease), color .3s var(--ease);
}
.pill-nav button:hover { background: var(--panel-3); color: var(--text); filter: none; }
.pill-nav button[aria-selected="true"],
.pill-nav button.active {
  background: var(--accent-soft); color: var(--accent-dark); border-color: transparent;
  font-weight: 600;
}
.pill-nav .badge {
  display: inline-flex; align-items: center; justify-content: center;
  min-width: 18px; height: 18px; margin-left: 7px; padding: 0 5px;
  border-radius: var(--radius-full); background: var(--bad); color: var(--bad-ink);
  font-size: 0.625rem; font-weight: 700; font-variant-numeric: tabular-nums;
}
.pill-nav button[aria-selected="true"] .badge { background: var(--bad); color: var(--bad-ink); }
/* The author-level `display` above outranks the browser's own
   `[hidden] { display: none }`, so a badge set to hidden would otherwise still
   show — as a bare "0" next to its tab. portal.html carried this rule inline;
   it belongs with the component. */
.pill-nav .badge[hidden] { display: none; }

/* Round icon buttons at the top-right of the portal bar. */
.icon-btn {
  width: 38px; height: 38px; padding: 0; flex-shrink: 0;
  border-radius: var(--radius-full);
  background: var(--panel); color: var(--muted);
  border: 1px solid var(--line); box-shadow: var(--shadow-xs);
}
.icon-btn:hover { background: var(--panel-2); color: var(--text); }
.icon-btn:hover { background: var(--panel-3); color: var(--text); }
.icon-btn svg { width: 17px; height: 17px; }

/* ─── Page head ─────────────────────────────────────────────────────────── */

.page-head {
  display: flex; align-items: center; gap: var(--space-16); flex-wrap: wrap; margin-bottom: var(--space-24);
}
.page-head h1 {
  font-family: var(--font-display);
  font-size: clamp(28px, 3.4vw, 40px); font-weight: 400; letter-spacing: -.03em; line-height: 1.02;
}
.page-head .page-head-actions { margin-left: auto; display: flex; align-items: center; gap: 10px; flex-wrap: wrap; }

/* ─── Buttons ────────────────────────────────────────────────────────────── */

/* Pills carrying the brand ramp — the site's button language.
   The default action used to be drawn from --invert-bg/--invert-fg, which
   made every button in the product a black lozenge. That was right when the
   page was warm paper and black was the most confident mark available; on
   white it is merely severe, and it left the palette's one colour doing
   nothing but drawing rules and labels.

   The fill is a background-IMAGE, so it cannot be transitioned and the hover
   state is a shadow and a lift instead. --accent sits underneath it as a
   background-COLOR for two reasons: a variant that only sets background-color
   (there are several below) still lands on brand blue rather than on
   transparent, and a printed page drops the image and keeps the colour. */
button, .btn {
  appearance: none; cursor: pointer;
  font: var(--weight-semibold) var(--text-13)/1 var(--font); letter-spacing: -.01em;
  padding: var(--space-12) var(--space-20); min-height: 40px; border-radius: var(--radius-full);
  background-color: var(--accent); background-image: var(--grad-brand); color: #fff;
  border: 1px solid transparent;
  transition: box-shadow .28s var(--ease), border-color .28s var(--ease), color .28s var(--ease), transform .2s var(--ease), opacity .2s var(--ease);
  display: inline-flex; align-items: center; justify-content: center; gap: 8px;
  /* No shadow on the BASE rule, deliberately.
   *
   * Roughly forty variants below and in portal.html opt out of the primary
   * fill by setting background to none or transparent — the rail rows, the
   * tabs, the month strip, .ghost, .danger, .navbtn. None of them reset
   * box-shadow, because until now the base had none to reset, so a shadow
   * here draws a floating rectangle around every one of them. It did: the
   * whole left rail came out as a stack of outlined boxes.
   *
   * The lift belongs to the controls that carry the fill, so it is declared
   * on those by name, further down. */
  box-shadow: none;
}
/* Brightness rather than a lift or a shadow: it is the one hover that reads
   correctly on a gradient fill AND stays invisible on the transparent
   variants, which is what the base rule has to serve. */
button:hover, .btn:hover { filter: brightness(1.06); opacity: 1; }
button:active, .btn:active { transform: translateY(1px); }
button:disabled, .btn:disabled { opacity: .35; cursor: default; transform: none; box-shadow: none; filter: none; }

/* A 13px label with 10px of padding was a 35px target. Six components had
   already been patched to 42, 44 and 46px one at a time; the base never was.
   40 under a pointer, 44 where a finger is doing the pointing. */
@media (pointer: coarse) {
  button, .btn { min-height: 44px; }
}

/* Text controls that happen to be buttons — an underlined "Dismiss" inside a
   sentence, the escape hatches at the foot of a dialog. A 40px box on these
   would lay a large invisible target over the paragraph around them, so they
   opt out by name rather than by accident. On a touch screen they still come
   up to something a thumb can find. */
.linkish, .closed-dismiss, .notif-head button,
.rv-candidate .rv-send-actions button,
button.compact, .btn.compact { min-height: 0; }
@media (pointer: coarse) {
  .linkish, .closed-dismiss, .notif-head button,
  .rv-candidate .rv-send-actions button,
  button.compact, .btn.compact { min-height: 32px; }
}

/* The site's `.btn-outline`: a hairline ring that fills on hover. */
button.ghost, .btn.ghost, a.ghost {
  background: transparent; color: var(--text);
  border: 1px solid var(--line-strong); box-shadow: none;
}
button.ghost:hover, .btn.ghost:hover, a.ghost:hover {
  background: var(--panel-3); border-color: var(--text); filter: none; opacity: 1;
}

button.danger, .btn.danger {
  background: transparent; color: var(--bad);
  border: 1px solid var(--bad-soft); box-shadow: none;
}
button.danger:hover, .btn.danger:hover { background: var(--bad-soft); border-color: var(--bad); filter: none; opacity: 1; }

button.live { background: var(--bad); color: var(--bad-ink); border-color: var(--bad); }
button.live:hover { opacity: .9; }

/* The one place violet is spent as a fill — an action that is about spending
   money or going live, not just navigating. */
button.accent, .btn.accent {
  background: var(--accent); color: #fff; border-color: var(--accent);
}
button.accent:hover, .btn.accent:hover { opacity: .9; }

a.ghost {
  display: inline-flex; align-items: center; justify-content: center;
  padding: var(--space-12) var(--space-20); min-height: 40px;
  border-radius: var(--radius-full); font-weight: var(--weight-medium); font-size: var(--text-13);
}

.navbtn {
  display: inline-flex; align-items: center; justify-content: center; gap: var(--space-6);
  padding: var(--space-8) var(--space-16); min-height: 40px; border-radius: var(--radius-full);
  border: 1px solid var(--line-strong); color: var(--text); font-size: var(--text-13);
  font-weight: var(--weight-medium); background: transparent;
  transition: background .35s var(--ease), border-color .35s var(--ease), color .35s var(--ease);
}
.navbtn:hover { background: var(--panel-3); border-color: var(--text); }
.navbtn.primary {
  background-color: var(--accent); background-image: var(--grad-brand);
  color: #fff; border-color: transparent;
  box-shadow: 0 1px 2px #0d152614, 0 6px 16px -8px color-mix(in srgb, var(--accent) 60%, transparent);
}
.navbtn.primary:hover {
  opacity: 1; border-color: transparent;
  box-shadow: 0 1px 2px #0d152618, 0 12px 26px -8px color-mix(in srgb, var(--accent) 72%, transparent);
}

/* ─── Cards, stats, banners ──────────────────────────────────────────────── */

.card {
  background: var(--panel); border: 1px solid var(--line); border-radius: var(--radius-lg);
  padding: var(--space-24); box-shadow: var(--shadow-sm);
}

/* Card header row: title on the left, a date chip or "…" on the right. */
.card-head {
  display: flex; align-items: center; gap: var(--space-12); flex-wrap: wrap; margin-bottom: var(--space-20);
}
.card-head h2, .card-head h3 {
  font-size: var(--text-17); font-weight: var(--weight-semibold);
  letter-spacing: -.02em; line-height: var(--leading-snug); margin: 0;
}
.card-head .card-head-right { margin-left: auto; display: flex; align-items: center; gap: var(--space-8); }

.stats { display: grid; grid-template-columns: repeat(auto-fit, minmax(168px, 1fr)); gap: var(--space-12); margin-bottom: var(--space-24); }
.stat {
  background: var(--panel); border: 1px solid var(--line); border-radius: var(--radius-md);
  padding: var(--space-20); box-shadow: var(--shadow-xs); transition: border-color .15s var(--ease), transform .15s var(--ease);
}
.stat:hover { border-color: var(--line-strong); transform: translateY(-1px); }
.stat b {
  display: block; font-size: var(--text-30); font-weight: var(--weight-semibold);
  letter-spacing: -.03em; line-height: var(--leading-tight); margin-bottom: var(--space-4);
  font-variant-numeric: tabular-nums;
}
.stat span { font-size: var(--text-11); color: var(--muted); text-transform: uppercase; letter-spacing: .07em; font-weight: var(--weight-semibold); }
/* A dimmed figure is secondary because of its COLOUR. Shrinking it as well
   meant the tiles in one row stopped sharing a baseline, and the row read as
   broken rather than as ranked. */
.stat.dim b { color: var(--muted); opacity: .75; }

/* KPI tile with a sparkline beside the number (agency dashboard reference). */
.kpi-row {
  display: grid; grid-template-columns: repeat(auto-fit, minmax(190px, 1fr));
  gap: var(--space-12); margin-bottom: var(--space-24);
}
.kpi {
  display: flex; align-items: center; gap: var(--space-12);
  padding: var(--space-20); border-radius: var(--radius-md);
  background: var(--panel); border: 1px solid var(--line);
}
.kpi-main { min-width: 0; flex: 1; }
.kpi-label {
  /* --muted-2 is 3.8:1 on card stock. That is a defensible grey for a rule or
     a disabled glyph, and not for an 11px label -- which is the only thing
     saying what the number underneath it counts. */
  font-size: var(--text-11); color: var(--muted); font-weight: var(--weight-semibold);
  letter-spacing: .04em; margin-bottom: var(--space-4); white-space: nowrap;
  overflow: hidden; text-overflow: ellipsis;
}
.kpi-value {
  font-size: var(--text-24); font-weight: var(--weight-semibold); letter-spacing: -.03em;
  line-height: var(--leading-tight); font-variant-numeric: tabular-nums;
}
.kpi-side { display: flex; flex-direction: column; align-items: flex-end; gap: 5px; flex-shrink: 0; }
.kpi-side svg { display: block; }

/* Delta chip — a coloured triangle plus the number, as in both references. */
.delta {
  display: inline-flex; align-items: center; gap: 4px;
  font-size: 0.6875rem; font-weight: 700; font-variant-numeric: tabular-nums;
}
.delta::before {
  content: ''; width: 0; height: 0;
  border-left: 4px solid transparent; border-right: 4px solid transparent;
}
/* --good is the fill green — 4.30:1 on the page, under AA at this size.
   --good-dark is the one the palette comment calls "readable as TEXT on
   the page surface", at 6.96:1. A delta is text. */
.delta.up { color: var(--good-dark); }
.delta.up::before { border-bottom: 5px solid var(--good); }
.delta.down { color: var(--bad); }
.delta.down::before { border-top: 5px solid var(--bad); }
.delta.flat { color: var(--muted-2); }
.delta.flat::before { border-bottom: 0; border-top: 0; width: 7px; height: 2px; background: var(--muted-2); border-radius: 2px; }

.banner {
  background: var(--warn-soft); border: 1px solid #f9731640; border-radius: var(--radius-md);
  padding: var(--space-16) var(--space-20); margin-bottom: var(--space-24);
  font-size: var(--text-13); color: var(--text); line-height: var(--leading-relaxed);
}
.banner b { color: var(--warn-text); }
/* The technical half of a banner: key names, file paths, the list of things
   the server found wrong. Folded away by default because the person reading a
   banner on a phone is almost never the person who can act on it, and an
   environment variable in the second sentence of a dashboard is noise to
   everyone but them. */
.banner-more { margin-top: 10px; }
.banner-more > summary {
  display: inline-flex; align-items: center; gap: 6px;
  min-height: 44px; padding: 0; cursor: pointer; list-style: none;
  font-size: var(--text-13); font-weight: var(--weight-semibold); color: var(--warn-text);
  text-decoration: underline; text-underline-offset: 3px;
}
.banner-more > summary::-webkit-details-marker { display: none; }
.banner-more > summary::after {
  content: ''; width: 7px; height: 7px; flex: none;
  border-right: 2px solid currentColor; border-bottom: 2px solid currentColor;
  transform: rotate(45deg) translate(-2px, -2px);
  transition: transform .15s var(--ease);
}
.banner-more[open] > summary::after { transform: rotate(-135deg) translate(-2px, -2px); }
.banner-more p { margin: 2px 0 0; }
.banner-more ul { margin: 8px 0 0; }
.banner ul { margin: 8px 0 0; padding-left: 20px; color: var(--muted); }
.banner code { font-family: var(--font-mono); font-size: 0.75rem; background: var(--track); padding: 2px 6px; border-radius: 5px; }

/* A banner about something that has already happened and gone wrong, rather
   than something that needs configuring. Losing a client is not a warning. */
.banner.banner-bad { background: var(--bad-soft); border-color: #f8717140; }
.banner.banner-bad b { color: var(--bad); }
.closed-head { display: flex; align-items: baseline; gap: 12px; }
.closed-head b { font-size: 0.9375rem; }
.closed-dismiss {
  margin-left: auto; width: auto; flex: none;
  background: none; border: 0; box-shadow: none; padding: 2px 4px;
  color: var(--muted); font-size: 0.75rem; font-weight: 600; text-decoration: underline;
  text-underline-offset: 2px; cursor: pointer;
}
.closed-dismiss:hover { color: var(--text); background: none; filter: none; }
.closed-list { margin: 8px 0 0; padding-left: 20px; color: var(--text); }
.closed-list li { margin-bottom: 6px; }
.closed-list li:last-child { margin-bottom: 0; }
.closed-reason { display: block; color: var(--muted); font-size: 0.75rem; margin-top: 2px; }
.closed-link {
  display: inline-block; margin-top: 10px;
  font-size: 0.8125rem; font-weight: 600; color: var(--bad);
  text-decoration: underline; text-underline-offset: 2px;
}

/* ─── "What changed" ─────────────────────────────────────────────────────
   The system-update notice. It lives in the top bar next to Sign out rather
   than in the page body: a deploy changes how live calls behave, and that is
   worth a permanent, always-visible marker of WHEN it last happened — not a
   card that scrolls away and is only ever seen on the dashboard.

   Green means an update landed that hasn't been opened yet. Once read it goes
   quiet but keeps reporting how long ago it was, because "when did this
   version go live" is the question being answered even when nothing is new. */
.update-chip {
  display: inline-flex; align-items: center; gap: var(--space-6);
  padding: var(--space-6) var(--space-12); min-height: 0;
  border-radius: var(--radius-full); cursor: pointer;
  font-size: var(--text-12); font-weight: var(--weight-semibold); letter-spacing: -.01em; white-space: nowrap;
  background: var(--panel-2); color: var(--muted);
  border: 1px solid var(--line-strong); box-shadow: none;
}
.update-chip:hover { background: var(--panel-3); color: var(--text); }
.update-chip[hidden] { display: none; }
.update-chip .pip {
  width: 7px; height: 7px; border-radius: 50%; background: var(--muted-2); flex-shrink: 0;
}
.update-chip.is-new {
  background: var(--good-soft); color: var(--good-dark); border-color: #22c55e5c;
}
.update-chip.is-new:hover { background: #22c55e2e; color: var(--good-dark); }
.update-chip.is-new .pip { background: var(--good); animation: update-pulse 2.4s ease-in-out infinite; }
@keyframes update-pulse {
  0%, 100% { box-shadow: 0 0 0 0 var(--good-soft); }
  50% { box-shadow: 0 0 0 5px transparent; }
}
@media (prefers-reduced-motion: reduce) { .update-chip.is-new .pip { animation: none; } }

.update-card {
  border: 1px solid var(--line); border-radius: var(--radius-md);
  background: var(--panel); padding: 15px 19px; margin-bottom: 22px;
  font-size: 0.8125rem; line-height: 1.6;
}
.update-card.is-new { border-color: var(--accent); background: var(--accent-soft); }
/* Inside the popup the card IS the popup — it brings its own frame. */
.update-card.plain { border: 0; background: none; padding: 0; margin: 0; }
.update-head { display: flex; align-items: baseline; gap: 10px; flex-wrap: wrap; }
.update-head h3 { margin: 0; font-size: 0.9375rem; font-weight: 600; letter-spacing: -.01em; }
.update-head .spacer { margin-left: auto; }
.update-tag {
  font-size: 0.625rem; font-weight: 700; letter-spacing: .07em; text-transform: uppercase;
  padding: 3px 8px; border-radius: 999px; white-space: nowrap;
  background: var(--accent-soft); color: var(--accent-dark); border: 1px solid var(--accent);
}
.update-tag.fix { background: var(--good-soft); color: var(--good-dark); border-color: #22c55e40; }
.update-tag.maintenance { background: var(--track); color: var(--muted); border-color: var(--line-strong); }
.update-why { color: var(--muted); margin: 8px 0 0; }
.update-card ul { margin: 10px 0 0; padding-left: 20px; color: var(--muted); }
.update-card li { margin-bottom: 5px; }
.update-card li:last-child { margin-bottom: 0; }
.update-actions { display: flex; gap: 8px; align-items: center; margin-top: 12px; flex-wrap: wrap; }
.update-when { color: var(--muted-2); font-size: 0.75rem; }
.update-older { margin-top: 14px; padding-top: 12px; border-top: 1px solid var(--line); }
.update-older > summary {
  cursor: pointer; color: var(--muted); font-size: 0.75rem; list-style: none;
}
.update-older > summary::-webkit-details-marker { display: none; }
.update-older > summary::before { content: '▸ '; color: var(--muted-2); }
.update-older[open] > summary::before { content: '▾ '; }
.update-older h4 { margin: 12px 0 0; font-size: 0.8125rem; font-weight: 600; }

/* ─── Charts ─────────────────────────────────────────────────────────────
   Everything is inline SVG built by the pages themselves — no chart library,
   nothing loaded off a CDN (the CSP forbids it). These classes are the shared
   frame those SVGs are drawn into. */

.chart { width: 100%; display: block; overflow: visible; }
/* A donut has no reason to grow with the card — past a point it is just a
   bigger ring, and its centre label scales with it. */
.chart.donut-chart { max-width: 340px; margin: 0 auto; }
.chart-grid { stroke: var(--track-line); stroke-width: 1; stroke-dasharray: 4 6; }
/* --muted (6.7:1), not --muted-2 (3.5:1): an axis label is the only thing
   saying what the shape above it is worth. */
.chart-axis { fill: var(--muted); font-size: var(--text-11); font-weight: var(--weight-semibold); font-family: var(--font); }
.chart-label { fill: var(--muted); font-size: 0.6875rem; font-family: var(--font); }

/* Rounded-cap bars. `stroke-linecap: round` on a zero-length line is what
   gives the reference's lozenge shape at both ends of every bar. */
.bar-track { stroke: var(--track); stroke-linecap: round; }
.bar-value { stroke-linecap: round; }

/* Value flags floating off a donut (the white "32%" pills). */
.donut-flag {
  fill: var(--invert-bg); }
.donut-flag-text { fill: var(--invert-fg); font-size: 0.6875rem; font-weight: 700; font-family: var(--font); }
.donut-center { fill: var(--text); font-weight: 600; letter-spacing: -.03em; font-family: var(--font); }

/* Legend under a chart. */
.legend { display: flex; gap: 16px; flex-wrap: wrap; margin-top: 14px; }
.legend-item { display: inline-flex; align-items: center; gap: 7px; font-size: 0.75rem; color: var(--muted); font-weight: 500; }
.legend-swatch { width: 9px; height: 9px; border-radius: 50%; flex-shrink: 0; }

/* Segmented proportion bar — the green / violet / orange strip. */
.split-bar { display: flex; gap: 6px; align-items: stretch; height: 34px; }
/* Ink comes from the segment, via `--seg-ink` set inline alongside the fill —
   a hardcoded #0b0b0f here measured 3.4:1 on the violet and 3.93:1 on the
   green. The fallback keeps an unstyled segment on the dark half of the pair
   rather than leaving it inheriting body colour. */
.split-bar div {
  border-radius: var(--radius-full); display: flex; align-items: center; justify-content: center;
  font-size: 0.75rem; font-weight: 700; color: var(--seg-ink, #0b0b0f); min-width: 42px;
}

/* Thin progress rail used at the foot of the small summary cards. */
/* Thicker and fully rounded. At 4px with square ends a meter reads as an
   underline somebody forgot to remove rather than as a quantity. */
.meter { height: 8px; border-radius: var(--radius-full); background: var(--track); overflow: hidden; }
.meter i { display: block; height: 100%; border-radius: var(--radius-full); }

/* A row inside a card: icon puck, label + value, then a delta on the right. */
.metric-row {
  display: flex; align-items: center; gap: 12px;
  padding: 13px 0; border-top: 1px solid var(--line);
}
.metric-row:first-child { border-top: 0; }
.metric-puck {
  width: 34px; height: 34px; border-radius: var(--radius-full); flex-shrink: 0;
  display: flex; align-items: center; justify-content: center; color: var(--muted);
  background: var(--panel-2); border: 1px solid var(--line);
}
.metric-puck svg { width: 16px; height: 16px; }
.metric-row .metric-body { flex: 1; min-width: 0; }
.metric-row .metric-label { font-size: 0.6875rem; color: var(--muted-2); font-weight: 600; }
.metric-row .metric-value { font-size: 1.0625rem; font-weight: 600; letter-spacing: -.025em; font-variant-numeric: tabular-nums; }

/* Month / range selector strip along the bottom of a chart card. */
.range-strip { display: flex; gap: 6px; flex-wrap: wrap; margin-top: 14px; }
.range-strip button {
  background: var(--panel-2); color: var(--muted); border: 1px solid var(--line);
  border-radius: var(--radius-full); padding: 6px 14px; font-size: 0.75rem; font-weight: 600;
  box-shadow: none;
}
.range-strip button:hover { background: var(--panel-3); color: var(--text); filter: none; }
/* aria-selected is only defined on tab/option/row/gridcell, so on these
   plain buttons it never reached a screen reader. The portal now sends
   aria-pressed, which is what a group of toggles actually is; index.html
   still sends the old one, so both select the same lozenge. */
.range-strip button[aria-selected="true"],
.range-strip button[aria-pressed="true"] {
  background: var(--accent); color: #fff; border-color: var(--accent); box-shadow: none;
}

/* Dashboard grid — the reference's 2/3 + 1/3 then 1/3 × 3 arrangement. */
/* One gutter between cards and the same gutter under the row, so a page of
   cards reads as an even field rather than as rows that happen to abut. */
.grid { display: grid; gap: var(--space-16); margin-bottom: var(--space-16); }
/* A grid item defaults to `min-width: auto`, so a wide child (an SVG chart, a
   long legend) pushes its own column past the track and the whole page scrolls
   sideways. Every card here is allowed to shrink instead. */
.grid > * { min-width: 0; }
.grid.two-one { grid-template-columns: 2fr 1fr; }
.grid.thirds { grid-template-columns: repeat(3, 1fr); }
.grid.halves { grid-template-columns: 1fr 1fr; }
@media (max-width: 1100px) {
  .grid.two-one, .grid.thirds, .grid.halves { grid-template-columns: 1fr; }
}

/* ─── Tables ─────────────────────────────────────────────────────────────── */

.table-card { background: var(--panel); border: 1px solid var(--line); border-radius: var(--radius-lg); overflow-x: auto; box-shadow: var(--shadow-xs); }
table { width: 100%; border-collapse: collapse; font-size: 0.8125rem; }
th {
  text-align: left; font-weight: 600; color: var(--muted-2); font-size: 0.625rem;
  text-transform: uppercase; letter-spacing: .07em; padding: 13px 18px;
  border-bottom: 1px solid var(--line); background: var(--panel-2);
  white-space: nowrap;
}
td { padding: 14px 18px; border-bottom: 1px solid var(--line); vertical-align: top; }
tbody tr { transition: background .1s var(--ease); }
tbody tr:hover { background: var(--panel-2); }
tr:last-child td { border-bottom: 0; }

/* --muted-2 is 3.5:1 on paper, and this is the screen a business meets on
   their first day — no bookings yet, and the most reason they will ever have
   to read what it says. --muted-2 is for rules, tracks and disabled glyphs
   from here on; text takes --muted. */
/* The empty state.
   A list with nothing in it is the screen a new account sees most, so it is
   worth more than one grey sentence: a tinted glyph to say which list this
   is, the sentence, and room for whatever puts something in it. The icon and
   any button are optional — a bare .empty still renders as before, just with
   more air around it. */
.empty {
  display: flex; flex-direction: column; align-items: center; justify-content: center;
  gap: var(--space-8);
  padding: var(--space-48) var(--space-24);
  text-align: center; color: var(--muted); font-size: var(--text-13);
  line-height: 1.6;
}
.empty svg { width: 20px; height: 20px; }
.empty .empty-mark {
  display: grid; place-items: center;
  width: 44px; height: 44px; margin-bottom: var(--space-4);
  border-radius: var(--radius-md);
  background: var(--accent-soft); color: var(--accent);
}
.empty b { display: block; color: var(--text); font-size: var(--text-15); font-weight: var(--weight-semibold); }
.empty p { margin: 0; max-width: 44ch; }
.empty .btn, .empty button { margin-top: var(--space-8); }

/* A count carried by a sidebar link — new signups on the Clients item. Sits
   at the end of the row rather than beside the label so the nav text stays
   aligned down the rail whether or not there is a number. */
.nav-badge {
  margin-left: auto;
  display: inline-flex; align-items: center; justify-content: center;
  min-width: 20px; height: 20px; padding: 0 6px;
  border-radius: var(--radius-full);
  background: var(--bad); color: var(--bad-ink);
  font-size: 0.6875rem; font-weight: 700; line-height: 1;
}
.nav-badge[hidden] { display: none; }

/* ─── Accessibility furniture ────────────────────────────────────────────── */

/* Text for screen readers only. Clipped rather than `display:none` — hiding it
   would take it out of the accessibility tree, which is the one place it is
   supposed to be. Used for table captions, for the word behind a colour-coded
   tag, and for the live region every page announces through. */
.sr-only {
  position: absolute;
  width: 1px; height: 1px;
  padding: 0; margin: -1px;
  overflow: hidden;
  clip: rect(0 0 0 0);
  clip-path: inset(50%);
  white-space: nowrap;
  border: 0;
}

/* WCAG 2.4.1. Off-screen until focused, at which point it is the first thing
   a keyboard user reaches — these dashboards open with a long sidebar, and
   tabbing past it to reach the table is otherwise the whole journey. */
.skip-link {
  position: fixed;
  /* Below the status bar rather than under it — the page runs full-height now
     (`viewport-fit=cover`), and the first thing a Tab key reaches must not
     appear behind the clock. */
  top: calc(12px + env(safe-area-inset-top, 0px)); left: 12px;
  z-index: 999;
  transform: translateY(calc(-100% - 24px));
  display: inline-flex; align-items: center;
  min-height: 44px;
  padding: 11px 18px;
  border-radius: var(--radius-full);
  background: var(--accent); color: #fff;
  font-size: 0.8125rem; font-weight: 600; text-decoration: none;
  transition: transform .2s ease;
}
.skip-link:focus-visible { transform: translateY(0); }

/* ─── Tags / badges ──────────────────────────────────────────────────────── */

.tag {
  display: inline-flex; align-items: center; gap: var(--space-4);
  font-size: var(--text-11); font-weight: var(--weight-semibold);
  padding: var(--space-4) var(--space-8); border-radius: var(--radius-full); white-space: nowrap;
  background: var(--panel-2); color: var(--muted); border: 1px solid var(--line);
}
.tag.ok { color: var(--good-dark); background: var(--good-soft); border-color: #22c55e40; }
/* `--warn-text` rather than `--warn`: this is 12px type sitting on a tint of
   its own colour, which is the hardest case the gold has. `--warn` is the
   FILL gold and measured 4.49:1 here in the dark theme; `--warn-text` is the
   variant that exists precisely because a warn-coloured WORD needs a darker
   (or, on dark, lighter) value than a warn-coloured bar does. */
.tag.warn { color: var(--warn-text); background: var(--warn-soft); border-color: #f9731640; }
.tag.bad, .tag.hot { color: var(--bad); background: var(--bad-soft); border-color: var(--bad); }

.pill {
  display: inline-block; font-size: var(--text-11); font-weight: var(--weight-semibold);
  padding: var(--space-4) var(--space-8); border-radius: var(--radius-full);
  border: 1px solid var(--line-strong); color: var(--muted); margin: 0 var(--space-6) var(--space-6) 0;
  background: var(--panel-2);
}
.pill.tag-active { color: var(--accent-dark); border-color: var(--accent); background: var(--accent-soft); }

/* ─── Forms ──────────────────────────────────────────────────────────────── */

label { display: block; font-size: 0.75rem; color: var(--text); margin-bottom: 6px; font-weight: 600; }
/* White, with the hairline doing the work.
   A field filled with --panel-2 and edged in --line-strong was a grey slab
   with an edge you had to look for: on a page where the CARDS are white, a
   grey box reads as disabled rather than as ready for typing. White ground,
   the ordinary rule, and one inset hairline to seat it. */
input, textarea, select {
  width: 100%; padding: 10px 13px; border-radius: var(--radius-sm); font: inherit; font-size: 0.8125rem;
  background: var(--panel); border: 1px solid var(--line); color: var(--text);
  box-shadow: inset 0 1px 2px #0d152608;
  transition: border-color .12s var(--ease), box-shadow .12s var(--ease);
}
input:hover:not(:disabled), textarea:hover:not(:disabled), select:hover:not(:disabled) {
  border-color: var(--line-strong);
}
input::placeholder, textarea::placeholder { color: var(--muted-2); }
input:focus, textarea:focus, select:focus {
  outline: none; border-color: var(--accent);
  box-shadow: 0 0 0 3px color-mix(in srgb, var(--accent) 18%, transparent);
}
/* Disabled inverts the relationship: now that a live field is white, the
   greyed one has to be the FILLED one or the two look identical. */
input:disabled, textarea:disabled, select:disabled {
  background: var(--panel-2); color: var(--muted-2); box-shadow: none;
}

/**
 * A radio or a checkbox is a glyph, not a text field, and it must be told so
 * explicitly — twice.
 *
 * The rule above dresses every input as a text field: 10px/13px of padding, a
 * border, and on focus a 3px shadow. A native radio ignores all of that right
 * up until the moment it is focused, at which point Blink stops drawing the
 * native widget and lays the element out as an ordinary styled box — so a
 * 13x13 glyph becomes 41x35 the instant you press the mouse on it.
 *
 * That is not a cosmetic problem. The row grows, everything under it shifts
 * down mid-click, mouseup lands on a different element than mousedown did, and
 * the browser never fires a click at all: the option silently refuses to be
 * selected, and clicking it again "fixes" it. It looks exactly like a dead
 * control, which is why it survived so long.
 *
 * Sizing is also pinned inline on every tag (`style="width:auto"`) for the
 * width:100% half of the same trap — see test/radio-hit-area.test.js. This
 * rule is the other half, and it has to neutralise the box on :focus too,
 * where the specificity of `input:focus` would otherwise win.
 */
input[type="checkbox"], input[type="radio"] {
  accent-color: var(--accent);
  padding: 0;
  border: 0;
  background: none;
  box-shadow: none;
}
/* Keyboard focus stays visible through :focus-visible's outline, which draws
   outside the box and so cannot move anything. */
input[type="checkbox"]:focus, input[type="radio"]:focus {
  padding: 0;
  border: 0;
  background: none;
  box-shadow: none;
}

.row { display: grid; grid-template-columns: 1fr 1fr; gap: 14px; margin-bottom: 16px; }
@media (max-width: 640px) { .row { grid-template-columns: 1fr; } }

/* ─── Segmented tab nav (e.g. Bookings / Contact / Leads) ───────────────── */

/* A segmented control, not a row of loose pills.
   The tabs sit in their own tinted trough and the current one is a white pill
   lifted out of it. Loose pills on the page ground gave the selected tab
   nothing to be selected OUT of — at --accent-soft on --bg the difference was
   about two percent of lightness, so the current tab was findable only by
   reading its colour. */
nav.tabs {
  display: inline-flex; gap: 4px; margin-bottom: 20px; flex-wrap: wrap;
  padding: 4px; border-radius: var(--radius-full);
  background: var(--panel-2); border: 1px solid var(--line);
  max-width: 100%;
}
nav.tabs button {
  background: transparent; border: 1px solid transparent; border-radius: var(--radius-full);
  box-shadow: none; color: var(--muted); font-weight: 600;
  padding: 8px 16px; font-size: 0.8125rem; min-height: 36px;
}
nav.tabs button:hover { background: var(--panel-3); color: var(--text); filter: none; }
nav.tabs button[aria-selected="true"] {
  background: var(--panel); color: var(--text); border-color: var(--line);
  box-shadow: var(--shadow-xs);
}

/* ─── Call transcript / chat-bubble log (monitor.html + demo.html) ──────── */

.transcript {
  background: var(--panel-2); border: 1px solid var(--line); border-radius: var(--radius-lg);
  padding: 16px 18px; overflow-y: auto; display: flex; flex-direction: column; gap: 10px;
}
.turn { font-size: 0.8125rem; padding: 9px 13px; border-radius: 13px; max-width: 84%; line-height: 1.45; }
.turn .role { display: block; font-size: 0.625rem; color: var(--muted-2); text-transform: uppercase; letter-spacing: .06em; margin-bottom: 3px; font-weight: 600; }
.turn.sys { align-self: center; font-size: 0.6875rem; color: var(--muted-2); font-style: italic; background: none; text-align: center; }

/* ─── Misc shared bits ───────────────────────────────────────────────────── */

.tooltip {
  position: fixed; pointer-events: none; background: var(--invert-bg); color: var(--invert-fg);
  font-size: 0.75rem; padding: 7px 10px; border-radius: 7px; z-index: 30; opacity: 0;
  transition: opacity .1s var(--ease); white-space: nowrap; box-shadow: var(--shadow-md);
  font-weight: 600;
}
.tooltip b { font-variant-numeric: tabular-nums; }
.tooltip.show { opacity: 1; }

details summary { cursor: pointer; color: var(--muted); font-size: 0.75rem; }
details pre {
  white-space: pre-wrap; font-size: 0.75rem; color: var(--muted); font-family: var(--font-mono);
  margin: 8px 0 0; max-height: 260px; overflow: auto; background: var(--panel-2);
  border-radius: var(--radius-sm); padding: 10px 12px;
}

/* ─── Dialogs ────────────────────────────────────────────────────────────
   The confirm / tell-them / pick-a-time dialogs, driven by /dialog.js. These
   replace window.confirm and window.alert, which arrive wearing the browser's
   chrome and the origin name and cannot be themed at all.

   The card deliberately matches the sign-in gates: paper stock, a gold hairline
   along the top edge, the question set in the display serif. Buttons reuse the
   shared `.ghost` / `.danger` rules above rather than restating them. */

/* The scrim is the forest black rather than `--overlay`'s ink. Ink at 58% over
   warm paper lands on a muddy olive-grey that reads as a rendering accident;
   the darker, greener value plus a slight blur reads as depth instead, and it
   is the same colour the page sits on when the forest theme is active. */
.dlg-overlay {
  position: fixed; inset: 0; z-index: 1000;
  display: flex; align-items: center; justify-content: center;
  padding: 20px;
  background: rgba(11, 16, 12, .62);
  backdrop-filter: blur(3px); -webkit-backdrop-filter: blur(3px);
  animation: dlg-in .16s var(--ease);
}

.dlg-card {
  position: relative; overflow: hidden;
  width: min(100%, 430px); box-sizing: border-box;
  padding: 30px 30px 24px;
  background: var(--panel); border: 1px solid var(--line);
  border-radius: var(--radius-lg); box-shadow: var(--shadow-lg);
  animation: dlg-rise .2s var(--ease);
}
.dlg-card::before {
  content: ''; position: absolute; top: 0; left: 0; right: 0; height: 3px;
  background: linear-gradient(90deg, var(--warn), rgba(160, 119, 52, .22));
}
/* A destructive question is marked at the top edge rather than by making the
   whole card red — the card still has to be readable while it is being read. */
.dlg-card.danger::before { background: linear-gradient(90deg, var(--bad), rgba(168, 52, 47, .22)); }

.dlg-head {
  margin: 0; font-family: var(--font-display);
  font-size: 1.25rem; font-weight: 400; line-height: 1.25; letter-spacing: -.01em;
  color: var(--text);
}
.dlg-body {
  margin: 10px 0 0; font-size: 0.8125rem; line-height: 1.6; color: var(--muted);
}

.dlg-fields { margin-top: 18px; display: grid; gap: 12px; }
.dlg-field { display: block; }
.dlg-field > span {
  display: block; margin-bottom: 5px;
  font-size: var(--text-13); font-weight: 500;
  letter-spacing: 0; color: var(--muted);
}
.dlg-field > input { width: 100%; box-sizing: border-box; }
/* The "that isn't quite it" line under a type-to-confirm box. Overrides the
   label style above, which would otherwise render a sentence as 10px mono
   micro-caps because it matches every span in the field. */
.dlg-field > .dlg-err {
  display: block; margin: 6px 0 0;
  font-family: var(--font); font-size: 0.75rem; font-weight: 500;
  text-transform: none; letter-spacing: 0; line-height: 1.45; color: var(--bad);
}
.dlg-field > .dlg-err:empty { display: none; }

.dlg-actions {
  display: flex; justify-content: flex-end; gap: 10px;
  margin-top: 22px; flex-wrap: wrap;
}
.dlg-actions > button { flex: none; }

@keyframes dlg-in { from { opacity: 0; } to { opacity: 1; } }
@keyframes dlg-rise {
  from { opacity: 0; transform: translateY(8px) scale(.985); }
  to { opacity: 1; transform: none; }
}

/* On a phone the buttons go full width and the confirming one leads, because
   a 44px target in the corner of a 360px screen is the thing people miss.

   560px rather than 480 so these dialogs turn over at the same width as every
   other phone rule in this file — a handset in landscape was landing between
   the two and getting the desktop button row on a phone-sized card. */
@media (max-width: 560px) {
  .dlg-card {
    padding: 24px 20px 20px;
    /* A confirm dialog is short; a prompt with three fields and a paragraph of
       explanation is not. The overlay is `position: fixed` and the card was
       `overflow: hidden`, so anything taller than the screen simply had its
       foot — the buttons — cut off with no way to reach it. It scrolls inside
       itself now, and sits at the top so it grows downwards. `overflow-x`
       stays hidden: that is what keeps the accent bar inside the corners. */
    max-height: calc(100dvh - 40px);
    overflow-y: auto; overflow-x: hidden; -webkit-overflow-scrolling: touch;
  }
  .dlg-overlay { align-items: flex-start; padding: 20px 20px 24px; }
  .dlg-actions { flex-direction: column-reverse; }
  .dlg-actions > button { width: 100%; }
  /* 10px mono, uppercase, at .14em of tracking, in the muted tone — the
     smallest and hardest-shaped text in the app, and it labels the box where
     somebody types the words that delete something. */
  .dlg-field > span { font-size: 0.75rem; letter-spacing: .1em; }
}

@media (prefers-reduced-motion: reduce) {
  .dlg-overlay, .dlg-card { animation: none; }
}

/* ─── Reviews ────────────────────────────────────────────────────────────
   Shared by /reviews.html (agency admin) and the Reviews page of
   /portal.html (a business owner's own account) — both render through
   /reviews-ui.js, so these class names are that file's contract. */

.stars { color: #fbbf24; letter-spacing: 1px; font-size: 0.9375rem; }
.stars.dim { color: var(--line-strong); }

.review-list { display: flex; flex-direction: column; gap: 14px; }
.review-card { padding: 20px 22px; }

/* A bad review is meant to be impossible to scroll past: it is pinned to the
   top of the list by the server, and marked in red here. */
.review-card.flagged { border-left: 3px solid var(--bad); background: linear-gradient(90deg, var(--bad-soft), transparent 40%); }

.review-top { display: flex; align-items: flex-start; gap: 14px; flex-wrap: wrap; margin-bottom: 8px; }
.review-top .who { flex: 1; min-width: 180px; }
.review-top .who b { display: block; font-size: 0.9375rem; }
.review-top .who .meta { margin-top: 2px; }
.review-tags { display: flex; gap: 6px; flex-wrap: wrap; }
.review-actions { display: flex; gap: 8px; flex-wrap: wrap; margin-left: auto; }
.review-comment { font-size: 0.8125rem; line-height: 1.6; margin: 10px 0 14px; white-space: pre-wrap; }
.review-comment.empty-comment { color: var(--muted-2); font-style: italic; }

.panel { background: var(--panel-2); border: 1px solid var(--line); border-radius: var(--radius-md); padding: 14px 16px; margin-top: 10px; }
.panel-head { display: flex; align-items: center; gap: 10px; margin-bottom: 10px; flex-wrap: wrap; }
.panel-head h3 { font-size: 0.75rem; text-transform: uppercase; letter-spacing: .06em; color: var(--muted); font-weight: 700; margin: 0; }
.panel textarea { min-height: 88px; resize: vertical; margin-bottom: 10px; }
.panel-row { display: flex; gap: 10px; flex-wrap: wrap; align-items: center; }

.candidate-list { display: flex; flex-direction: column; gap: 6px; margin-bottom: 10px; }
.candidate, .rv-candidate {
  display: flex; align-items: center; gap: 10px; padding: 9px 12px; border-radius: var(--radius-sm);
  border: 1px solid var(--line-strong); background: var(--panel); font-size: 0.8125rem;
}
.candidate { cursor: pointer; }
.candidate.selected { border-color: var(--accent); background: var(--accent-soft); }
.candidate .meta { margin-left: auto; }
.rv-candidate .rv-send-actions { margin-left: auto; display: flex; gap: 6px; }
.rv-candidate .rv-send-actions button { padding: 5px 11px; font-size: 0.75rem; }

.sent-note { font-size: 0.8125rem; color: var(--good-dark); background: var(--good-soft); border: 1px solid #22c55e40; border-radius: var(--radius-sm); padding: 10px 12px; }
.err-note { font-size: 0.8125rem; color: var(--bad); }
.rv-sent { color: var(--good-dark); font-weight: 600; }

.toolbar { display: flex; align-items: center; gap: 12px; margin-bottom: 16px; flex-wrap: wrap; }

/* Rating filter. Each chip carries its own count, so "are there any 1-stars"
   is answered without clicking anything, and an empty bucket is disabled
   rather than leading to a blank list. */
.rv-filters { display: flex; gap: 6px; flex-wrap: wrap; }
.rv-chip {
  background: var(--panel-2); color: var(--muted); border: 1px solid var(--line-strong);
  box-shadow: none; padding: 7px 12px; font-size: 0.75rem; font-weight: 600;
  display: inline-flex; align-items: center; gap: 6px; border-radius: var(--radius-full);
}
.rv-chip:hover:not(:disabled) { background: var(--panel-3); color: var(--text); filter: none; }
.rv-chip .n {
  font-variant-numeric: tabular-nums; font-size: 0.6875rem; padding: 1px 6px;
  border-radius: var(--radius-full); background: var(--panel); color: var(--muted);
}
.rv-chip.active { background: var(--accent); color: #fff; border-color: var(--accent); }
.rv-chip.active .n { background: color-mix(in srgb, var(--invert-fg) 12%, transparent); color: var(--invert-fg); }
.rv-chip.bad { color: var(--bad); border-color: var(--bad); }
.rv-chip.bad .n { background: var(--bad-soft); color: var(--bad); }
.rv-chip.bad.active { background: var(--bad); color: var(--bad-ink); border-color: var(--bad); }
.rv-chip.bad.active .n { background: rgba(255, 255, 255, .22); color: #fff; }
.rv-chip:disabled { opacity: .4; cursor: default; }

/* Why a control next to this line is greyed out, and what to use instead. */
.rv-unavailable {
  margin-top: 10px; font-size: 0.75rem; color: var(--muted);
  background: var(--panel-2); border: 1px solid var(--line);
  border-radius: var(--radius-sm); padding: 9px 12px; line-height: 1.5;
}

/* ─── The review centre's summary ──────────────────────────────────────────
   The score at the size it deserves, the spread of ratings beside it, and how
   much of it has been replied to.

   This exists because the page used to open with six small tiles and a list —
   which is fine for a business with a hundred reviews and reads as nothing at
   all for the one that has just paid for the feature and has three. Same
   numbers as the tiles below it, given the room to say something. */
.rv-summary {
  display: grid; grid-template-columns: auto minmax(0, 1fr) auto;
  gap: 28px; align-items: center; margin-bottom: 18px;
}
.rv-score { text-align: center; min-width: 120px; }
.rv-score-n {
  font-family: var(--font-display); font-size: 3rem; line-height: 1;
  font-weight: var(--weight-semibold); letter-spacing: -.03em; color: var(--text);
}
.rv-score-stars { font-size: 1rem; letter-spacing: .12em; color: var(--warn); margin-top: 6px; }
.rv-score-sub { font-size: var(--text-11); color: var(--muted); margin-top: 6px; }

.rv-spread { display: grid; gap: 5px; min-width: 0; }
.rv-bar-row { display: flex; align-items: center; gap: 10px; font-size: var(--text-11); }
.rv-bar-row.empty { opacity: .5; }
.rv-bar-n { width: 22px; flex: none; color: var(--muted); font-variant-numeric: tabular-nums; }
.rv-bar {
  flex: 1; min-width: 0; height: 7px; border-radius: 999px;
  background: var(--track); overflow: hidden;
}
.rv-bar i { display: block; height: 100%; border-radius: 999px; background: var(--warn); }
.rv-bar-c { width: 26px; flex: none; text-align: right; color: var(--muted); font-variant-numeric: tabular-nums; }

.rv-answered { max-width: 210px; text-align: right; }
.rv-answered-n {
  font-family: var(--font-display); font-size: 1.75rem; line-height: 1;
  font-weight: var(--weight-semibold); color: var(--text);
}
.rv-answered-sub {
  font-size: var(--text-10); text-transform: uppercase; letter-spacing: .07em;
  font-weight: var(--weight-semibold); color: var(--muted); margin-top: 4px;
}
.rv-answered p { margin: 10px 0 0; font-size: var(--text-12); line-height: 1.55; color: var(--muted); }

/* Nothing to score yet. Not a chart of zeroes — that reads as a bad score
   rather than as no score — but what the page is FOR, which is the thing a
   business that has just paid for it needs to see. */
.rv-summary-empty { grid-template-columns: minmax(0, 1.2fr) minmax(0, 1fr); align-items: start; }
.rv-summary-empty h3 { margin: 0 0 6px; font-size: 1.0625rem; }
.rv-summary-empty p { margin: 0; font-size: var(--text-13); line-height: 1.65; color: var(--muted); }
.rv-summary-will { margin: 0; padding: 0; list-style: none; display: grid; gap: 8px; }
.rv-summary-will li {
  position: relative; padding-left: 22px;
  font-size: var(--text-12); line-height: 1.5; color: var(--text);
}
.rv-summary-will li::before {
  content: '✓'; position: absolute; left: 0; top: 0;
  color: var(--good); font-weight: 700; font-size: var(--text-12);
}

@media (max-width: 860px) {
  .rv-summary, .rv-summary-empty { grid-template-columns: 1fr; gap: 20px; }
  .rv-answered { max-width: none; text-align: left; }
}

.rv-connect, .rv-ask { margin-bottom: 18px; }
.rv-connect h3, .rv-ask h3 { font-size: 0.9375rem; margin: 0 0 4px; }
.rv-coverage { margin-top: 10px; font-size: 0.75rem; color: var(--warn); background: var(--warn-soft); border-radius: var(--radius-sm); padding: 9px 12px; line-height: 1.55; }
.rv-coverage.ok { color: var(--good-dark); background: var(--good-soft); }
.stat.rv-alert b { color: var(--bad); }

/* ─── Sub-navigation: sections within one page ───────────────────────────
   System Monitor and Analytics are each several screens' worth of material,
   split into addressable sections. Every section is a real URL (#/backups),
   so a bookmark, the back button, a link in an alert email and the sidebar
   all land on the section they name. */

.subnav {
  display: flex; align-items: center; gap: 6px;
  padding: 10px 30px;
  background: var(--bg-shell); border-bottom: 1px solid var(--line);
  overflow-x: auto; scrollbar-width: none;
  position: sticky; top: 0; z-index: 20;
}
.subnav::-webkit-scrollbar { display: none; }
.subnav a[href] {
  display: inline-flex; align-items: center; gap: 7px; white-space: nowrap;
  padding: 9px 15px; border-radius: var(--radius-full);
  font-size: 0.8125rem; font-weight: 600; color: var(--muted); text-decoration: none;
  transition: background .12s var(--ease), color .12s var(--ease);
}
.subnav a[href]:hover { background: var(--panel-2); color: var(--text); }
.subnav a[href].active { background: var(--accent-soft); color: var(--accent-dark); }

/* A number on a tab. Hidden at zero rather than shown as "0" — the whole
   point of these is that a visible one means go and look. */
.subnav-count {
  font-size: 0.6875rem; font-weight: 700; line-height: 1; padding: 3px 7px;
  border-radius: var(--radius-full); background: var(--panel-3); color: var(--muted);
  font-variant-numeric: tabular-nums;
}
.subnav-count.bad { background: var(--bad-soft); color: var(--bad); }
.subnav-count.warn { background: var(--warn-soft); color: var(--warn); }
.subnav a[href].active .subnav-count { background: color-mix(in srgb, var(--invert-fg) 12%, transparent); color: var(--invert-fg); }
/* `hidden` is a no-op against any author `display` on the same selector. */
.subnav-count[hidden] { display: none; }

/* One section of such a page. Deliberately given no `display` of its own, so
   the plain `hidden` attribute the router sets is what decides. */
.page[hidden] { display: none; }

/* Anything too wide for a phone (a table, a chart) scrolls inside its own box
   instead of pushing the whole page sideways. */
.scroll-x { overflow-x: auto; -webkit-overflow-scrolling: touch; }

/* A stat tile that is also a link to the section explaining the number. */
a.stat { display: block; color: inherit; text-decoration: none; }

/* ─── Phone navigation ──────────────────────────────────────────────────────
   The agency pages navigate from the icon rail; the business portal navigates
   from its rail too. Both work by being wide, and a phone is not.

   Below 880px both become the same thing: a fixed bottom bar carrying the four
   everyday sections plus More, and a bottom sheet behind More listing every
   section there is. The agency side used to be a "Menu" button and a left
   drawer instead — which solved the discoverability but left one half of the
   product navigating from a hamburger while the other half had a tab bar, and
   put the only way between screens at the top of a 6.1in phone where a thumb
   is not.

   The rule behind both: on a phone, nothing you need may be reachable only by
   discovering that a strip scrolls sideways, and where you ARE should be
   visible without opening anything.

   The agency's bar is built by mobile-nav.js; the portal's is its own rail
   restyled in place (see .rail in portal.html). Shared below: the sheet, the
   scrim and the row styling, so a section looks the same on both sides. */

.m-tabs { display: none; }

/* Only once mobile-nav.js has actually built the bar — otherwise a blocked
   script would leave the page with no navigation whatsoever. */
html.has-mnav .m-tabs {
  display: none;
  position: fixed; inset: auto 0 0 0; z-index: 50;
  align-items: stretch; gap: 2px;
  padding: 5px 6px;
  padding-bottom: calc(5px + env(safe-area-inset-bottom, 0px));
  background: var(--bg-shell);
  /* A tab bar has to sit visibly ON TOP of what scrolls beneath it, and
     `var(--line)` is the paper hairline — against a forest bar over a
     near-black page in the dark theme it disappears entirely. */
  border-top: 1px solid #f7f5f029;
  box-shadow: 0 -1px 0 #16181d3d, 0 -10px 28px #16181d42;
}

.m-tabs .m-tab,
.m-tabs a.m-tab:link,
.m-tabs a.m-tab:visited {
  position: relative;
  display: flex; flex: 1 1 0; min-width: 0;
  flex-direction: column; align-items: center; justify-content: center; gap: 3px;
  min-height: 50px; padding: 6px 2px;
  background: none; border: 0; border-radius: 9px;
  color: #f7f5f0; text-decoration: none;
  font: inherit; font-size: var(--text-11); font-weight: var(--weight-semibold);
  text-align: center; cursor: pointer;
}
.m-tabs .m-tab svg { width: 21px; height: 21px; flex: none; opacity: .82; }
/* The label is the only thing telling them what the icon means, so it is never
   dropped — it truncates instead. */
.m-tab-label {
  max-width: 100%; overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
.m-tabs .m-tab.is-current,
.m-tabs a.m-tab.is-current:link,
.m-tabs a.m-tab.is-current:visited,
.m-tabs .m-more.holds-current {
  background: #ffffff24; color: #fff;
}
.m-tabs .m-tab.is-current svg, .m-tabs .m-more.holds-current svg { opacity: 1; }

/* A count on a tab belongs to the glyph it is counting, not to the end of a
   row — so it rides the icon's top-right corner, ringed in the bar's own
   colour so it reads as sitting above it. */
.m-tabs .nav-badge {
  position: absolute; top: 1px; right: 10px; margin: 0;
  min-width: 18px; height: 18px; padding: 0 5px;
  font-size: 0.65625rem;
  border: 1.5px solid var(--bg-shell);
}

.m-scrim {
  position: fixed; inset: 0; z-index: 998;
  background: var(--overlay); backdrop-filter: blur(2px);
  opacity: 0; pointer-events: none; transition: opacity .18s var(--ease);
}
body.m-nav-open .m-scrim { opacity: 1; pointer-events: auto; }

/* The sheet. A bottom sheet rather than the left drawer this used to be:
   it comes from the same edge the bar it opens from is on, which is what makes
   More read as "the rest of this bar" instead of as a second, unrelated menu. */
.m-drawer {
  position: fixed; inset: auto 0 0 0; z-index: 999;
  width: auto; max-height: 88vh; box-sizing: border-box;
  display: flex; flex-direction: column;
  background: var(--bg-shell); border-top: 1px solid var(--line);
  border-radius: 22px 22px 0 0;
  box-shadow: var(--shadow-lg);
  overflow: hidden;
  /* Closed state is `visibility: hidden` rather than `display: none` so the
     panel can slide; the zero-length visibility transition is delayed to the
     end of the slide out, and runs immediately on the way in. */
  visibility: hidden; transform: translateY(100%);
  transition: transform .22s var(--ease), visibility 0s linear .22s;
}
body.m-nav-open .m-drawer {
  visibility: visible; transform: none;
  transition: transform .22s var(--ease), visibility 0s;
}
/* The page behind must not scroll under an open sheet. */
body.m-nav-open { overflow: hidden; }

/* ─── The phone sheet reads off the palette ──────────────────────────────
   Every colour in `.m-drawer` below used to be a hardcoded white or a white
   alpha — `#fff`, `#ffffff1a`, `#f7f5f024`, `#f7f5f054`. That was correct when
   the sheet was a clone of a NEAR-BLACK rail. The rail turned white in the
   Clear Sky redesign and nothing here followed, so on the light theme the
   sheet was a white panel carrying white text: the "All sections" title, the
   drag handle, the close button and every group heading were invisible, and
   the only thing a phone user could read was the row labels.

   `color-mix` against `--text` rather than a second set of hardcoded values,
   so the sheet follows whichever theme is on without a third copy of the
   palette to keep in step. */

/* The header is a drag handle, so it must not also be a text selection. */
.m-drawer-head {
  flex: none; touch-action: none; user-select: none; -webkit-user-select: none;
  padding: 8px 14px 0;
  border-bottom: 1px solid var(--line);
}
.m-grip {
  display: block; width: 38px; height: 4px; margin: 0 auto;
  border-radius: var(--radius-full);
  background: color-mix(in srgb, var(--text) 28%, transparent);
}
.m-drawer-title { display: flex; align-items: center; gap: 10px; padding: 7px 0 8px; }
.m-drawer-title b {
  font-size: 0.9375rem; font-weight: var(--weight-semibold); letter-spacing: -.01em;
  color: var(--text);
}
.m-drawer-close {
  margin-left: auto; flex-shrink: 0;
  width: 44px; height: 44px; padding: 0; border-radius: var(--radius-full);
  display: inline-flex; align-items: center; justify-content: center;
  background: color-mix(in srgb, var(--text) 8%, transparent);
  color: var(--text); border: 0; box-shadow: none;
}
.m-drawer-close:hover {
  background: color-mix(in srgb, var(--text) 15%, transparent);
  color: var(--text); filter: none;
}
.m-drawer-close svg { width: 18px; height: 18px; }
/* Opening the sheet moves focus into it so a keyboard is not left behind on a
   button that just went out of reach. A finger does not need to be told about
   it, though — without this every tap on More painted a focus ring. */
.m-drawer-close:focus:not(:focus-visible) { outline: none; box-shadow: none; }

/* The list scrolls inside the sheet; the header above it does not move. */
.m-drawer-nav {
  flex: 1; min-height: 0; overflow-y: auto; -webkit-overflow-scrolling: touch;
  padding: 6px 10px 14px; display: flex; flex-direction: column; gap: 2px;
}
/* The page-wide scrollbar thumb rings itself in `var(--bg)` so it reads as
   floating on the paper canvas. `--bg` is not part of the on-forest token set
   (nothing else inside a dark panel scrolls), so in here that ring painted a
   solid cream 10px bar down the edge — louder than any of the navigation
   beside it. The sheet supplies its own. */
.m-drawer-nav {
  scrollbar-width: thin;
  scrollbar-color: color-mix(in srgb, var(--text) 24%, transparent) transparent;
}
.m-drawer-nav::-webkit-scrollbar { width: 6px; }
.m-drawer-nav::-webkit-scrollbar-track { background: transparent; }
.m-drawer-nav::-webkit-scrollbar-thumb {
  background: color-mix(in srgb, var(--text) 24%, transparent);
  border: 0; border-radius: 999px;
}
.m-drawer-nav::-webkit-scrollbar-thumb:hover {
  background: color-mix(in srgb, var(--text) 40%, transparent);
}

/* A heading with a rule above it, so the groups read as groups instead of as
   sixteen rows with the odd word between them. */
.m-group {
  padding: 14px 12px 6px; margin-top: 4px;
  border-top: 1px solid var(--line);
  font-size: var(--text-11); font-weight: var(--weight-bold);
  letter-spacing: .09em; text-transform: uppercase; color: var(--muted-2);
}
.m-group:first-child { border-top: 0; margin-top: 0; padding-top: 4px; }

.m-drawer-nav a.m-item,
.m-drawer-nav a.m-item:link,
.m-drawer-nav a.m-item:visited,
.m-drawer .m-item {
  display: flex; align-items: center; justify-content: flex-start; gap: 14px;
  width: 100%; min-height: 46px; padding: 0 12px; border-radius: 11px;
  background: none; border: 0; box-shadow: none;
  font: inherit; font-size: 0.90625rem; font-weight: var(--weight-medium);
  letter-spacing: -.01em;
  color: var(--text); text-decoration: none; text-align: left; cursor: pointer;
}
/* The icons line up in a column of their own rather than each sitting wherever
   its glyph happens to start, which is most of what makes a list read loose. */
.m-item svg { width: 20px; height: 20px; flex-shrink: 0; opacity: .82; }
.m-item:hover { background: color-mix(in srgb, var(--text) 7%, transparent); }
.m-drawer-nav a.m-item.is-current,
.m-drawer-nav a.m-item.is-current:link,
.m-drawer-nav a.m-item.is-current:visited {
  background: var(--invert-bg); color: var(--invert-fg); font-weight: var(--weight-semibold);
}
.m-item.is-current svg { opacity: 1; }
.m-drawer-nav a.m-item.is-cta,
.m-drawer-nav a.m-item.is-cta:link,
.m-drawer-nav a.m-item.is-cta:visited {
  margin-top: 8px; background: var(--panel-2); border: 1px solid var(--line-strong);
}
.m-drawer-nav .nav-badge { margin-left: auto; }

/* Signing out is not a section, so it is not in the list of them: its own
   strip at the foot of the sheet, behind a rule, carrying the home-indicator
   inset so the last row is not under the bottom of the handset. */
.m-foot {
  flex: none; padding: 8px 10px calc(10px + env(safe-area-inset-bottom, 0px));
  border-top: 1px solid var(--line);
}
.m-signout, .m-signout:hover { color: var(--bad); }
.m-signout svg { opacity: 1; }
.m-signout:hover { background: var(--bad-soft); }

/* The desktop half of the same move: a row at the foot of the rail, on every
   page. Collapsed to the 64px strip it is the icon alone, like every other
   row up there. */
.sidebar-foot .rail-signout {
  display: flex; align-items: center; gap: 12px;
  width: 100%; min-height: 40px; margin-bottom: 10px; padding: 0 10px;
  background: none; border: 0; border-radius: 9px; box-shadow: none;
  font: inherit; font-size: var(--text-13); font-weight: var(--weight-medium);
  color: var(--muted); text-align: left; cursor: pointer;
}
.sidebar-foot .rail-signout svg { width: 18px; height: 18px; flex: none; }
.sidebar-foot .rail-signout:hover {
  background: color-mix(in srgb, var(--text) 7%, transparent);
  color: var(--bad); filter: none;
}
/* ─── Responsive ────────────────────────────────────────────────────────── */

@media (max-width: 880px) {
  .app-shell { flex-direction: column; }
  .sidebar {
    width: 100%; height: auto; position: static; border-right: 0; border-bottom: 1px solid var(--line);
    flex-direction: row; align-items: center; padding-right: 8px;
  }
  .sidebar-brand { padding: 12px 14px; }
  .sidebar-nav { flex-direction: row; overflow-x: auto; padding: 8px 6px; flex: 1; scrollbar-width: none; }
  .sidebar-nav::-webkit-scrollbar { display: none; }
  .sidebar-nav a { flex-direction: row; gap: 8px; font-size: 0.75rem; padding: 10px 13px; white-space: nowrap; }
  .sidebar-nav a.cta { margin-top: 0; }
  .sidebar-foot { display: none; }
  .topbar, main.content, .content, .subnav { padding-left: 18px; padding-right: 18px; }

  /* The tab bar takes over from the rail. */
  html.has-mnav .sidebar { display: none; }
  html.has-mnav .m-tabs { display: flex; }
  /* The section tabs of a multi-screen page (Analytics, System) stick to the
     top of the viewport now that nothing is above them — the page's own title
     is the orientation cue, the way a large title is on iOS, and the "Menu"
     bar that used to occupy those 53px is gone. */
  html.has-mnav .subnav { top: 0; }
  /* The bar is fixed and grows by the home-indicator inset, so the gap the
     page leaves under itself has to grow with it — otherwise the last row of
     every table sits behind the tabs on exactly the phones that have one. */
  html.has-mnav main.content,
  html.has-mnav .content {
    padding-bottom: calc(78px + env(safe-area-inset-bottom, 0px));
  }
  /* …and the sticky top bar clears the status bar, since the page now runs
     under it (`viewport-fit=cover`). */
  html.has-mnav .topbar {
    padding-top: calc(18px + env(safe-area-inset-top, 0px));
  }

  .portal-shell { padding: 12px 14px 60px; }
  .portal-topbar { flex-wrap: wrap; border-radius: var(--radius-lg); }
  /* Every portal section visible at once, as a grid of full-width buttons —
     the horizontally scrolling strip this replaces cut off Reviews, Insights
     and Settings with nothing on screen to say they were there. */
  .pill-nav {
    order: 3; width: 100%; margin: 2px 0 0;
    display: grid; grid-template-columns: repeat(3, 1fr); gap: 8px;
    overflow: visible; flex-wrap: wrap;
  }
  .pill-nav button {
    width: 100%; min-height: 46px; padding: 10px 8px;
    justify-content: center; white-space: nowrap; font-size: 0.8125rem;
  }
  .page-head h1 { font-size: 1.5rem; }

  /* ── Touch sizing ──────────────────────────────────────────────────────────
     Everything from here down is about fingers rather than layout, so it
     starts at the same width the rail gives way at — a tablet is touched
     exactly like a phone is, and a 26px month button is as easy to miss on
     one as on the other. The layout changes stay in the phone block below.

     44px is the size below which a fingertip starts landing on the wrong
     control, and every one of these can cancel an appointment or delete a
     company. */
  button, .btn, a.ghost, a.navbtn, input[type="submit"] {
    padding: 11px 16px; min-height: 44px;
  }
  .icon-btn { width: 44px; height: 44px; }
  .icon-btn svg { width: 19px; height: 19px; }
  nav.tabs button { min-height: 44px; padding: 10px 18px; font-size: 0.8125rem; }
  .range-strip { gap: 7px; }
  .range-strip button { min-height: 44px; padding: 10px 16px; font-size: 0.8125rem; }
  .subnav { gap: 7px; }
  .subnav a[href] { min-height: 44px; padding: 11px 15px; }
  /* Filter chips that are really buttons (Reports, Reviews). A chip is
     smaller than a button by design, but it is still a thing a finger has to
     land on, so the TARGET is 44 even where the ink is not. */
  button.pill { min-height: 44px; padding: 11px 16px; font-size: 0.75rem; }
  /* Row actions inside a table that is still a table at this width — below
     620px these become pill buttons instead (see `table.stack`), but on a
     tablet they stay inline and just need height under the finger. */
  td a { display: inline-block; padding: 8px 0; }
  /* Links that are standalone controls rather than words inside a sentence —
     "← Dashboard", "Call the receptionist →", "Add a company", and the
     "Connect" that is the only action on an integration row. Named one by one
     on purpose: a blanket rule would also pad out links sitting mid paragraph,
     where the extra height collides with the lines above and below. */
  a.navbtn, a.sub, .topstrip > a, .console-topbar > a, .sheets-line a, .tenant .meta a,
  #banner a, .banner a, #dashLink, #testLink, #onboardLink {
    display: inline-block; padding: 8px 0; min-height: 40px; line-height: 24px;
  }

  /* Under 16px, iOS Safari zooms the whole page in when a field is focused and
     never zooms back out — which is what makes a form feel broken on a phone
     even when the layout is fine. The search box sets its own smaller size, so
     it needs saying twice. */
  input, textarea, select, .search-pill input { font-size: 1rem; }
}

/* A table with more columns than a phone is wide stops being a table and
   becomes one labelled card per row. Opt-in via `table.stack`, so the wide
   diagnostic tables elsewhere keep their existing sideways scroll — this
   treatment is for the tables someone actually works through: the bookings,
   messages and leads lists on the dashboard and in the portal.

   The alternative was leaving them scrolling sideways, where a phone shows
   two of ten columns and the Cancel link is somewhere off to the right. */
@media (max-width: 620px) {
  table.stack, table.stack tbody, table.stack tr, table.stack td { display: block; width: 100%; }
  table.stack thead { display: none; }
  table.stack { font-size: 0.8125rem; }
  /* Alternating fills, because ten stacked fields is tall enough that the line
     between one customer and the next has to be unmissable. */
  table.stack tr { padding: 6px 0 10px; border-bottom: 1px solid var(--line-strong); }
  table.stack tr:nth-child(even) { background: var(--panel-2); }
  table.stack tr:last-child { border-bottom: 0; }
  table.stack tr.busy { opacity: .6; }
  table.stack td { padding: 7px 15px 8px; border: 0; }
  /* The column heading travels with the value, since the header row is gone.
     Deliberately a block above the value rather than a column beside it: these
     cells hold several elements each (a name and a phone split by <br>, three
     action links separated by dots), and any two-column layout puts each of
     those pieces on a line of its own. */
  table.stack td[data-label]::before {
    content: attr(data-label); display: block; margin-bottom: 2px;
    font-size: 0.6875rem; font-weight: 700; letter-spacing: .06em; text-transform: uppercase;
    color: var(--muted-2); line-height: 1.5;
  }
  /* Row links (Reschedule / Cancel / Delete) become real buttons — on a phone
     they are the only way to act on a booking, and an 18px-tall underlined
     word in a list of them is a coin flip to hit. */
  table.stack td[data-label="Actions"] a {
    display: inline-flex; align-items: center; min-height: 44px;
    padding: 0 15px; margin: 4px 6px 2px 0;
    border: 1px solid var(--line-strong); border-radius: var(--radius-full);
    background: var(--panel-2); text-decoration: none; font-weight: 600; font-size: 0.8125rem;
  }
  table.stack .mono { font-size: 0.8125rem; }
  /* The row-select box, which is the one cell with no heading to inherit. */
  table.stack .rowsel { display: inline-flex; align-items: center; gap: 10px; min-height: 44px; }
  table.stack .rowsel span {
    display: inline; font-size: 0.6875rem; font-weight: 700; letter-spacing: .06em;
    text-transform: uppercase; color: var(--muted-2);
  }
  table.stack .called { display: inline-flex; align-items: center; min-height: 44px; }
}

/* ─── Secondary text has to survive a phone ───────────────────────────────
   `--muted-2` is the token every caption, column label, hint and placeholder
   on the site is painted in. Measured against the surfaces it actually sits
   on it comes out at 3.5:1 on the page and 2.9:1 on `--panel-3` — under the
   4.5:1 that text below 18pt needs, and it is exactly the text that is
   already the smallest thing on screen.

   That is a fair trade on a laptop at reading distance and a bad one on a
   handset held at arm's length in a van, so the token is darkened for phones
   only. Nothing is renamed and no rule moves: every `.sub`, `.hint`,
   `.empty`, `[data-label]::before` and placeholder picks the new value up
   where it stands. The desktop palette is untouched.

   Values were chosen as the lightest that clears 4.5:1 against the WORST
   surface each theme puts them on (`--panel-3`), so no combination is left
   failing:

     light  #5c6145  5.6 : 1 on page   4.7 : 1 on panel-3
     dark   #adb2bd  8.9 : 1 on page   5.1 : 1 on panel-3
     forest #adb2bd  6.2 : 1 on rail   5.1 : 1 on its panel-3

   `--warn` gets the same treatment: it is not a warning colour here, it is
   what every eyebrow and group heading is set in, at 12px and under — and it
   is also the ink of a `.tag warn` ("pending"), which sits on `--panel-3`
   inside a stacked table row. #7f5b20 measured 4.44:1 there, just under, so
   it is one step darker than the eyebrows alone would have needed:

     warn   #5f3a0c  5.65 : 1 on page  5.38 : 1 on panel-3  4.81 : 1 on the
                       warn tint over panel-3, which is the worst surface of
                       the lot and the one a "pending" tag actually sits on in
                       an alternating stacked row.
*/
@media (max-width: 640px) {
  :root {
    --muted-2: #5c6145;
    --warn: #5f3a0c;
    /* The eyebrow/tag gold comes down with it — on the phone it is read at
       arm's length on a tint of itself. */
    --warn-text: #5f3a0c;
  }
  :root[data-theme="dark"] {
    --muted-2: #adb2bd;
  }
  .sidebar, .rail, .m-drawer {
    --muted-2: #adb2bd;
  }
}

/* A tick box is 13px wide until something says otherwise, and a fingertip is
   not. The portal pins `style="width:auto"` on every one of them to escape
   the site-wide `input { width: 100% }` (see test/radio-hit-area.test.js) —
   an inline style, which then beats any stylesheet width we set here. So the
   touch size has to be declared !important or it silently does nothing: the
   height applied, the width did not, and the boxes stayed 13x22.

   `pointer: coarse` rather than a width, because what decides whether 22px is
   enough is what is touching the screen, not how wide the screen is. */
@media (pointer: coarse) {
  input[type="checkbox"], input[type="radio"] {
    width: 22px !important; height: 22px;
  }
  /* The whole label is the target, not the box inside it. */
  label:has(> input[type="checkbox"]), label:has(> input[type="radio"]) { min-height: 44px; }
}

/* Phones. Everything here is about the three things that actually break on a
   360px screen: horizontal overflow, controls too small to hit, and type too
   small to read. */
@media (max-width: 560px) {
  .topbar { padding: 14px 16px; gap: 10px; }
  .topbar h1 { font-size: 1.0625rem; }
  /* The actions wrap onto their own full-width row and share it, rather than
     being squeezed against the title. */
  .topbar-actions { margin-left: 0; width: 100%; }
  .topbar-actions > button, .topbar-actions > .btn { flex: 1; }
  .topbar, main.content, .content, .subnav { padding-left: 14px; padding-right: 14px; }
  main.content, .content { padding-top: 20px; padding-bottom: 56px; }

  .card { padding: 18px 16px; }
  .card-head { margin-bottom: 14px; }
  .card-head h2, .card-head h3 { font-size: 0.9375rem; }
  .stats { gap: 10px; margin-bottom: 20px; }
  .stat { padding: 14px 15px; }
  .stat b { font-size: 1.5rem; }
  .stat.dim b { font-size: 1.0625rem; }
  th, td { padding: 11px 12px; }
  h1 { font-size: 1.0625rem; }
  .page-head h1 { font-size: 1.5rem; }
  .empty { padding: 36px 12px; }

  /* Nothing on a phone should be smaller than this to read. The desktop sizes
     are fine at desktop viewing distance; 10.5px on a handset is not.
     Counters inside a badge or a dot are left alone — those are sized by the
     circle around them, and the number is not the thing being read. */
  .eyebrow, .sub, .stat span, .tag, .pill, .kpi-label, .kpi-sub, .metric-label,
  .mini-label, .subnav-count, .notif-item .when, .list-panel-head, .sidebar-section-label,
  .metric-row .metric-label, .mini-card .mini-label, .portal-topbar .who span {
    font-size: 0.75rem;
  }

  /* The search box and the open/closed pill were sharing one row, which left
     the box about 200px wide — "Search the table below…" truncated to "Search
     the table b" — and pushed "Closed — after-hours line" hard against the
     right edge of the screen. A search field is the widest thing in a toolbar
     on iOS for a reason: it gets the row. */
  .search-pill { min-width: 0; max-width: none; flex: 1 0 100%; }
  .topbar > .status-pill { margin-left: 0; }

  /* KPI tiles two-up. `minmax(190px, 1fr)` needs 392px for two columns and a
     393px handset has 365 to give, so every tile went full width and the three
     on the agency dashboard ate the whole first screen. Below this width they
     stack internally instead — label and number over the sparkline — which
     fits two across with room to spare. */
  .kpi-row { grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 10px; }
  .kpi {
    flex-direction: column; align-items: stretch; gap: 8px;
    padding: 14px 15px;
  }
  .kpi-side {
    flex-direction: row; align-items: flex-end; justify-content: space-between;
    gap: 8px; min-height: 24px;
  }
  .kpi-side svg { max-width: 100%; height: auto; }
  .kpi-label { white-space: normal; }

  /* A dialog on a phone is the screen, not a card floating on one. Anything
     taller than the viewport scrolls inside itself so the buttons at the
     bottom stay reachable. */
  .modal-overlay, .gate-overlay { padding: 10px; align-items: flex-start; }
  .modal-card, .modal, .gate {
    width: 100%; max-width: none; max-height: calc(100dvh - 20px);
    overflow-y: auto; -webkit-overflow-scrolling: touch;
  }
  .modal-actions { flex-direction: column-reverse; align-items: stretch; gap: 8px; }
  .modal-actions > button, .modal-actions > .btn { width: 100%; }
  /* … and they stick to the foot of it. A dialog that scrolls inside itself
     puts its buttons at the end of the content, which on Add a customer is
     ten fields and 550px below the fold: the thing you opened the dialog to
     press is the one thing you cannot see. Sticky keeps it under your thumb
     the whole way down, and the rule above it says there is more behind. */
  .modal .modal-actions, .modal-card .modal-actions {
    position: sticky; bottom: 0; z-index: 2;
    background: var(--panel);
    border-top: 1px solid var(--line);
    /* Bled out sideways over the dialog's padding, and given the dialog's
       bottom padding as its own — see the rule below. A sticky box is clamped
       to its containing block, which is the card's CONTENT box, so with the
       card keeping its own padding-bottom the bar stops 24px short of the
       edge and the next field of the form shows through the gap underneath
       it. That reads as the bar having come loose rather than as it being
       pinned. Handing the padding to the bar removes the strip it was
       stopping short of. */
    margin: 16px calc(-1 * var(--modal-pad-x)) 0;
    padding: 12px var(--modal-pad-x) var(--modal-pad-b);
  }
  .modal:has(.modal-actions), .modal-card:has(.modal-actions) { padding-bottom: 0; }
  .modal { --modal-pad-x: 26px; --modal-pad-b: 24px; }
  .modal-card { --modal-pad-x: 26px; --modal-pad-b: 22px; }

  /* Toolbars above a table: full-width buttons stacked, rather than four
     half-legible ones crushed onto one line. */
  .table-toolbar { flex-wrap: wrap; gap: 8px; }
  .table-toolbar > button { flex: 1 1 auto; }
  .table-toolbar .spacer { display: none; }

  .page-head { gap: 10px; }
  .page-head .page-head-actions { margin-left: 0; width: 100%; }
  .page-head .page-head-actions > button { flex: 1; }
}

/* ─── Video meetings (portal → Meetings) ───────────────────────────────────
   The room itself is /meet.html and carries its own dark palette; everything
   here is the SCHEDULING page inside the portal, so it uses the same tokens as
   every other card on it. Nothing below is used by the room. */

/* The permanent room, given the weight of the thing an owner reaches for most.
   It is the only card on the page with the number set in the display face —
   a meeting number is read aloud far more often than it is clicked, and 11
   digits at body size is a number people misread. */
.mt-now { border-color: var(--line-strong); }
.mt-room {
  display: flex; align-items: center; gap: var(--space-24); flex-wrap: wrap;
  margin-top: var(--space-16);
}
.mt-room-main { min-width: 0; }
.mt-number {
  font-family: var(--font-mono); font-size: 1.5rem; letter-spacing: .02em;
  font-variant-numeric: tabular-nums; color: var(--text);
}
.mt-room-facts { display: flex; gap: var(--space-16); flex-wrap: wrap; margin-top: 6px; }
.mt-fact { font-size: var(--text-12); color: var(--muted); }
.mt-fact b { font-family: var(--font-mono); color: var(--text); letter-spacing: .04em; }
.mt-room-actions { display: flex; gap: var(--space-8); flex-wrap: wrap; margin-left: auto; }

/* "Somebody is in there right now." Green rather than the brand blue: it is a
   state of the world, not something to click. */
.mt-live {
  display: inline-flex; align-items: center; gap: 6px;
  color: var(--good-dark); font-size: var(--text-12); font-weight: var(--weight-semibold);
}
.mt-live .pip {
  width: 7px; height: 7px; border-radius: 999px; background: var(--good);
  animation: mt-pulse 1.6s ease-in-out infinite;
}
@keyframes mt-pulse { 50% { opacity: .25; } }
@media (prefers-reduced-motion: reduce) { .mt-live .pip { animation: none; } }

/* The two collapsed groups on the schedule form. Collapsed because eight
   toggles and an invitee list above the Schedule button is how a two-field
   form comes to look like an application to open a bank account. */
.mt-more { margin: var(--space-16) 0; }
.mt-more > summary {
  cursor: pointer; font-size: var(--text-13); font-weight: var(--weight-semibold);
  color: var(--text); padding: var(--space-8) 0; list-style: none;
}
.mt-more > summary::-webkit-details-marker { display: none; }
.mt-more > summary::after {
  content: '›'; display: inline-block; margin-left: 8px; color: var(--muted);
  transition: transform .18s var(--ease);
}
.mt-more[open] > summary::after { transform: rotate(90deg); }

.mt-checks { display: grid; gap: var(--space-12); margin: var(--space-8) 0 var(--space-16); }
.mt-check { display: flex; gap: 10px; align-items: flex-start; margin: 0; font-weight: var(--weight-regular); }
.mt-check input { margin-top: 2px; flex-shrink: 0; }
.mt-check span { display: block; }
.mt-check b { display: block; font-size: var(--text-13); font-weight: var(--weight-semibold); color: var(--text); }
.mt-check em { display: block; font-style: normal; font-size: var(--text-12); color: var(--muted); margin-top: 2px; }

.mt-passline {
  display: flex; align-items: center; gap: var(--space-12); flex-wrap: wrap;
  padding: var(--space-12); border: 1px solid var(--line); border-radius: var(--radius-md);
  background: var(--panel-2); font-size: var(--text-13);
}

.mt-invitee { display: flex; gap: var(--space-8); margin-bottom: var(--space-8); }
.mt-invitee input { flex: 1; min-width: 0; }
.mt-form-actions { display: flex; gap: var(--space-8); flex-wrap: wrap; margin-top: var(--space-16); }
.mt-msg { font-size: var(--text-13); color: var(--muted); min-height: 20px; margin: var(--space-8) 0 0; }
.mt-msg.bad { color: var(--bad); font-weight: var(--weight-semibold); }

/* One booked meeting. Three columns — when, what, what you can do about it —
   because "when" is what the eye scans down and it should be a column rather
   than the first few words of a sentence. */
.mt-row {
  display: flex; align-items: center; gap: var(--space-16); flex-wrap: wrap;
  padding: var(--space-16) 0; border-bottom: 1px solid var(--line);
}
.mt-row:last-child { border-bottom: none; padding-bottom: 0; }
.mt-row.off { opacity: .55; }
.mt-row-when { width: 178px; flex-shrink: 0; display: flex; flex-direction: column; gap: 2px; }
.mt-row-when b { font-size: var(--text-13); }
.mt-row-main { flex: 1; min-width: 180px; display: flex; flex-direction: column; gap: 3px; }
.mt-row-main b { font-size: var(--text-15); font-weight: var(--weight-semibold); }
.mt-row-actions { display: flex; gap: var(--space-8); flex-wrap: wrap; margin-left: auto; }
.mt-repeat {
  align-self: flex-start; font-size: var(--text-11); color: var(--muted);
  background: var(--panel-3); border-radius: var(--radius-full); padding: 2px 8px;
}
.mt-cancelled {
  align-self: flex-start; font-size: var(--text-11); font-weight: var(--weight-semibold);
  color: var(--bad); background: var(--bad-soft); border-radius: var(--radius-full); padding: 2px 8px;
}

.mt-join { display: flex; gap: var(--space-8); flex-wrap: wrap; margin-top: var(--space-12); }
.mt-join input { flex: 1; min-width: 160px; }

/* ─── AI notes ─────────────────────────────────────────────────────────────
   What the assistant made of a recorded meeting. Laid out as prose rather
   than as a table on purpose: this is the one thing on the page somebody
   READS instead of scanning, and a summary chopped into cells stops being a
   summary. The transcript is folded away behind a <details> because it is
   long, dull, and the reason the summary above it exists. */
.mt-note {
  border-top: 1px solid var(--line);
  padding: var(--space-16) 0;
}
.mt-note:first-child { border-top: 0; padding-top: 0; }
.mt-note-head {
  display: flex; align-items: flex-start; gap: var(--space-16); flex-wrap: wrap;
}
.mt-note-actions {
  display: flex; align-items: center; gap: var(--space-8);
  margin-left: auto; flex-wrap: wrap;
}
.mt-note-summary { margin-top: var(--space-12); max-width: 68ch; }
/* The one line somebody reads if they read nothing else. */
.mt-note-headline { font-weight: var(--weight-semibold); color: var(--text); }
.mt-note-block { margin-top: var(--space-12); }
.mt-note-block h4 {
  font-size: var(--text-12); text-transform: uppercase; letter-spacing: .06em;
  color: var(--muted); margin: 0 0 4px;
}
.mt-note-block ul { margin: 0; padding-left: 1.1em; }
.mt-note-block li { margin: 2px 0; }
.mt-note-detail {
  margin-top: var(--space-16); padding-top: var(--space-16);
  border-top: 1px dashed var(--line);
}
.mt-ask { display: flex; gap: var(--space-8); margin: var(--space-8) 0 var(--space-16); }
.mt-ask > input { flex: 1 1 auto; min-width: 0; }
/* A question and its answer read as one exchange, so they are one block with
   the question carrying the weight — scanning for "did I already ask this"
   is the common case, reading the answer is the rarer one. */
.mt-qa { margin-bottom: var(--space-12); max-width: 68ch; }
.mt-q { font-weight: var(--weight-semibold); margin: 0 0 2px; }
.mt-a { margin: 0; color: var(--muted); white-space: pre-wrap; }
.mt-player {
  width: 100%; max-width: 560px; margin-top: var(--space-8);
  border-radius: var(--radius-8, 8px); background: #000;
}
.mt-transcript { margin-top: var(--space-8); }
.mt-transcript > summary { cursor: pointer; font-size: var(--text-12); color: var(--muted); }
.mt-transcript pre {
  white-space: pre-wrap; word-break: break-word;
  max-height: 420px; overflow-y: auto;
  margin-top: var(--space-8); padding: var(--space-12);
  background: var(--surface-sunken, rgba(0,0,0,.03));
  border-radius: var(--radius-8, 8px);
  font-size: var(--text-12); line-height: 1.6;
}

@media (max-width: 720px) {
  /* The actions go full width and the "when" column stops being a column: at
     390px a 178px fixed column leaves the topic four characters wide. */
  .mt-row-when { width: 100%; flex-direction: row; align-items: baseline; gap: 8px; }
  .mt-row-actions { margin-left: 0; width: 100%; }
  .mt-row-actions > button { flex: 1 1 auto; }
  .mt-room-actions { margin-left: 0; width: 100%; }
  .mt-room-actions > button { flex: 1 1 auto; }
  .mt-note-actions { margin-left: 0; width: 100%; }
  .mt-ask { flex-direction: column; }
}

/* ─── Appointments, and every list like it ─────────────────────────────────
   The Appointments table carried twelve columns. Twelve does not fit a
   laptop: the table ran past the right edge of its own card, so Reschedule
   and Cancel could only be reached by scrolling a table sideways, and every
   cell that did fit wrapped to three lines. The columns were merged in
   portal.html; these are the rules that keep the result one line per row. */

/* One strip: what you are looking at on the left, what you can do on the
   right. The tabs and the buttons used to be two rows and 90px of nothing. */
.appt-bar { align-items: center; gap: var(--space-10); }
.appt-bar .tabbar { margin: 0; flex-shrink: 0; }

.appt-search {
  display: inline-flex; align-items: center; gap: var(--space-8);
  background: var(--panel); border: 1px solid var(--line-strong);
  border-radius: var(--radius-full); padding: 0 var(--space-14);
  min-height: 40px; min-width: 0; flex: 0 1 300px;
  transition: border-color .15s var(--ease), box-shadow .15s var(--ease);
}
/* The third component in this file to need this line, so it is worth naming
   the trap rather than the symptom: a class that sets `display` outranks the
   user-agent's `[hidden] { display: none }`, so `el.hidden = true` on anything
   styled `display: inline-flex` does nothing at all. Here that left a search
   box floating over the calendar tab, filtering something with no rows. */
.appt-search[hidden] { display: none; }
.appt-search:focus-within { border-color: var(--accent); box-shadow: 0 0 0 3px var(--accent-soft); }
.appt-search svg { width: 15px; height: 15px; color: var(--muted-2); flex-shrink: 0; }
.appt-search input {
  border: 0; background: none; padding: 0; min-height: 0; width: 100%;
  font-size: var(--text-13);
}
.appt-search input:focus { outline: none; box-shadow: none; border: 0; }
/* Chrome draws its own clear cross on type=search and it lands on the ring. */
.appt-search input::-webkit-search-cancel-button { appearance: none; }

/* Everything about reaching one person, stacked in one cell rather than
   spread across three columns. */
.reach { display: flex; flex-direction: column; gap: 2px; min-width: 0; line-height: 1.45; }
.reach .reach-mail { font-size: var(--text-12); }
.reach .mail-link, .reach .tel-link { display: inline-block; max-width: 100%; overflow-wrap: anywhere; }
/* "Text from my phone" was a bordered button on every row — eleven of them
   down the page, all louder than the appointment they belonged to. It is the
   third thing you might do to a number, so it reads as a link under it. */
.reach .tap-text {
  font-size: var(--text-12); color: var(--accent-dark); text-decoration: none;
  border: 0; background: none; padding: 0; min-height: 0;
}
.reach .tap-text:hover { text-decoration: underline; }

/* An address is looked up when somebody is already on their way there, so it
   gets one line and a tooltip rather than three lines on every row. */
.addr-cell {
  display: block; max-width: 22ch;
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
  color: var(--muted);
}

/* The callback tick now sits with Reschedule and Cancel. On its own line so
   it does not read as a fourth link in the sentence above it. */
td .called { display: flex; align-items: center; gap: 6px; margin-top: 10px; font-size: var(--text-12); color: var(--muted); font-weight: var(--weight-regular); }
td .called input { width: 15px; height: 15px; }

/* Rows read down the page, so the eye needs the columns it scans to hold
   their shape. Without these the browser hands its width to whichever cell
   has the longest unbroken string — usually an email — and the day, the
   customer and the money all wrap to three lines to pay for it. */
#view table th:nth-child(2), #view table td:nth-child(2) { white-space: nowrap; }  /* When */
#view table td:nth-child(3) { min-width: 9rem; }                                    /* Customer */
#view table td:nth-child(4) { min-width: 7rem; }                                    /* Job */
#view table td:nth-child(5) { white-space: nowrap; }                                /* Value */
#view table td:nth-child(8) { white-space: nowrap; }                                /* Actions */

@media (max-width: 900px) {
  /* Below this the strip stops being one line and the search takes the width
     it needs, rather than being crushed to an icon and four characters. */
  .appt-bar { flex-wrap: wrap; }
  .appt-search { flex: 1 1 100%; order: 3; }
}

/* A checklist row whose status has not come back yet. Neither ticked nor
   flagged: it is drawn rather than dropped so the total underneath it does
   not move as the five reads land — see startChecks in portal.html. */
.start-step.is-checking { opacity: .55; }
.start-step.is-checking .start-step-mark { border-style: dashed; }
