/* Authenticated application design system: calm, dense, and operational. */
:root {
  --va-canvas: #f6f8fc;
  --va-surface: #ffffff;
  --va-surface-subtle: #f8fafc;
  --va-ink: #172033;
  --va-muted: #64748b;
  --va-border: #dbe3ef;
  --va-brand: #2563eb;
  --va-brand-dark: #1d4ed8;
  --va-navy: #162033;
  --va-danger: #c2413a;
  --va-success: #16745a;
  /* STATUS COLOUR, AND ONLY STATUS COLOUR. The reception dashboard is the first
     screen in this app where a row means one of two things at a glance, so it
     gets two token families and no third. They are a green-teal and a slate off
     the same neutral ramp the rest of the app already uses — not a second
     palette, and never the only thing that says which state a row is in: every
     use of them below sits beside a word. `off` is the third, rarer state
     (cancelled), kept quiet on purpose. */
  --va-in: #0f766e;
  --va-in-soft: #e7f5f1;
  --va-in-line: #a3d5c9;
  --va-out: #475569;
  --va-out-soft: #eef2f7;
  --va-out-line: #cbd5e1;
  --va-off: #9a3412;
  --va-off-soft: #fff4ec;
  --va-off-line: #f6cfae;
  /* ---------------------------------------------------------------------
     THE SCALES (2026-09-20). Until this block there were two radii and one
     shadow, and every screen built since has invented the rest in its own
     <style> tag: thirty-nine templates, twelve class prefixes and six corner
     radii, three of them on the same page. That is not a minimal design, it
     is twelve designs — and it is most of what reads as "unfinished" beside a
     product built on a component library.

     A SCALE IS A SHORT LIST YOU CHOOSE FROM, which is the whole point. If a
     value here is wrong, change it here; if a page needs a seventh radius,
     that is a conversation and not a keystroke.
     --------------------------------------------------------------------- */
  --r-xs: 6px;      /* a chip, a small control */
  --r-sm: 8px;      /* an input, a button */
  --r-md: 10px;     /* a row, a nested box */
  --r-lg: 12px;     /* a card */
  --r-xl: 14px;     /* a section, a column */
  --r-pill: 999px;  /* a badge, a filter */

  /* Three depths and no more: resting, raised on hover, and floating over the
     page. Every shadow in the product is one of these. */
  --sh-1: 0 1px 2px rgba(16, 24, 40, .05);
  --sh-2: 0 5px 16px rgba(16, 24, 40, .08);
  --sh-3: 0 18px 44px rgba(16, 24, 40, .16);

  /* A 4px rhythm. Named by size rather than by use, because the same gap is a
     stack gap here and an inline gap there. */
  --s-1: 4px;  --s-2: 8px;  --s-3: 12px; --s-4: 16px;
  --s-5: 22px; --s-6: 28px; --s-7: 40px;

  /* The type ramp. Five sizes; a sixth is a sign that something on the page is
     doing a job a heading should do. */
  --t-xs: 11.5px;   /* a badge, a hint under a label */
  --t-sm: 12.5px;   /* secondary text, a table's small print */
  --t-md: 13.5px;   /* controls, dense tables */
  --t-base: 14.5px; /* body, a card's own text */
  --t-lg: 17px;     /* a card heading */
  --t-xl: 26px;     /* a figure worth reading across a room */

  /* SECTION LABELS are uppercase, tracked and small everywhere — the pipeline
     columns, the report's blocks and the day's groups each invented this. */
  --label-size: 13px;
  --label-track: .02em;

  /* ---------------------------------------------------------------------
     TONE. Four meanings, each a trio: the ink, a wash to sit it on, and a
     line. Every badge, pill and figure in the product is one of these four —
     and never colour alone, which is why each is used beside a word.
     --------------------------------------------------------------------- */
  --tone-ok: #047857;    --tone-ok-soft: #ecfdf5;    --tone-ok-line: #a7f3d0;
  --tone-warn: #92400e;  --tone-warn-soft: #fffbeb;  --tone-warn-line: #fde68a;
  --tone-bad: #b42318;   --tone-bad-soft: #fef2f2;   --tone-bad-line: #fecaca;
  --tone-info: #1d4ed8;  --tone-info-soft: #eff6ff;  --tone-info-line: #bfdbfe;
  --tone-mute: #475569;  --tone-mute-soft: #f1f5f9;  --tone-mute-line: #e2e8f0;

  /* One focus ring, so tabbing through the product looks like one product. */
  --focus: 0 0 0 3px rgba(37, 99, 235, .22);

  /* The old names, kept pointing at the scale rather than deleted: they are
     used in a hundred places and renaming them buys nothing a reader can see. */
  --va-radius: var(--r-lg);
  --va-radius-sm: var(--r-sm);
  --va-shadow: var(--sh-1);
  --line: var(--va-border);
}

* { box-sizing: border-box; }
/* EVERY form control, not a list of types. The rule below used to name
   text/email/password/textarea/select, so input[type=time], tel, number,
   search and date fell back to the UA font — the opening-hours grid rendered
   its 28 time fields in Arial next to Inter labels. A list of types is a list
   somebody has to remember to extend; `button, input, select, textarea` is not. */
button, input, select, textarea { font: inherit; }
body { min-height: 100vh; margin: 0; background: var(--va-canvas); color: var(--va-ink); font-family: Inter, ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif; line-height: 1.5; }
/* ``body > header`` AND NOT ``header`` (2026-09-20). This is the product's own
   top bar, and a bare element selector claimed EVERY <header> in the document.
   Two of them existed: help.html's group headings, and — the one that made it
   visible — the vendored calendar widget, which renders a real <header> for its
   day names and came out navy, sticky, 64px tall and white-on-navy, so the day
   of the week was invisible. A component cannot defend itself against this; the
   selector has to be the one that is specific. The bar is a direct child of
   <body> in base.html and in nothing else. */
:root { --va-sticky-top: 80px; }  /* 64px bar + 16px gap — see .side-nav */
body > header { position: sticky; top: 0; z-index: 20; min-height: 64px; padding: 10px max(20px, calc((100vw - 1440px) / 2)); display: flex; align-items: center; gap: 18px; background: var(--va-navy); box-shadow: 0 1px 0 rgba(255,255,255,.06); }
header .brand { display: inline-flex; align-items: center; gap: 9px; color: #fff; font-weight: 700; letter-spacing: -.015em; text-decoration: none; white-space: nowrap; }
/* THE WHITE TILE IS DRAWN HERE NOW. logo.png used to be an opaque white square
   and this header leaned on that: the mark is navy line art and this bar is
   --va-navy, so with a transparent file it would be navy on navy and all but
   invisible. The file lost its background on 2026-09-14 because the PUBLIC
   header, which is light, was showing the white square as a tile nobody had
   asked for. Both headers now get what they need, from one asset. */
header .brand img { display: block; border-radius: 7px; background: #fff; padding: 2px; }
/* Under ~360px the brand, the menu button and the three action icons stopped
   sharing one row and the icons wrapped, giving the smallest phone we support a
   115px header. At 320px the row is 292px wide and those four things want 332,
   so the wordmark goes and the logo stays; the link keeps its name for a screen
   reader through aria-label on the anchor. (The per-product title used to fall
   back to its first word here; "Digital" on its own says nothing, and one name
   everywhere since 2026-09-09 means the logo alone is not ambiguous.) */
@media (max-width: 360px) {
  header .brand img { width: 26px; height: 26px; }
}
/* THE PHONE'S LOGO is the drawn mark (base.html says why), 34px tall with no
   tile; the white-tiled logo.png stays the desktop's. Hidden, not removed, at
   desktop width so the markup is one header for both. */
header .brand .brand-mark { display: none; }
@media (max-width: 760px) {
  header .brand img { display: none; }
  header .brand .brand-mark { display: block; width: 28px; height: 34px; flex: 0 0 auto; }
  /* NO WORDMARK ON A PHONE (owner, v6 review, 2026-09-26): the mark alone,
     at every phone width. The link's aria-label still names the product. */
  header .brand { gap: 0; }
  header .brand span { display: none; }
  /* THE BAR'S EDGES ARE THE PAGE'S: 8px (or the safe area), with the menu
     button at the left edge. .nav-toggle has margin-left:auto for the
     desktop's order, and on a phone that split the free space with the
     actions and floated the button ~100px in from the left. */
  body > header { padding-left: max(8px, env(safe-area-inset-left));
    padding-right: max(8px, env(safe-area-inset-right)); column-gap: 8px; }
  body > header .nav-toggle { margin-left: 0; }  /* outranks the later .nav-toggle { margin-left: auto } */
  /* THE BELL IN THE BAR IS NOT A MENU ROW. The phone rule further down gives
     .notice-bell a 1px top border and auto width, which is right for the copy
     listed inside the open menu and was drawing a faint line over the bell in
     the bar itself, at 22px wide (v6 review). */
  header .header-actions .notice-bell { border-top: 0; width: 44px; height: 44px;
    justify-content: center; border-radius: 9px; }
  /* 8px above and below, not 10: with the 48px menu button that keeps the
     64px bar it has always been rather than growing it (2026-09-26). */
  body > header { padding-top: 8px; padding-bottom: 8px; }
}
header .tenant-context { min-width: 0; max-width: 260px; padding: 4px 10px; overflow: hidden; border: 1px solid rgba(203,213,225,.18); border-radius: 999px; color: #cbd5e1; font-size: 12px; text-overflow: ellipsis; white-space: nowrap; }
header nav { display: flex; align-items: center; gap: 4px; margin-left: auto; font-size: 14px; }
header nav a { display: inline-flex; min-height: 36px; align-items: center; padding: 7px 10px; border-radius: 7px; color: #cbd5e1; text-decoration: none; }
header nav a:hover, header nav a[aria-current="page"] { color: #fff; background: rgba(255,255,255,.1); }
header nav a[aria-current="page"] { box-shadow: inset 0 -2px 0 #60a5fa; }
.account-menu { position: relative; }
.account-toggle { max-width: 240px; padding: 7px 10px; overflow: hidden; border: 0; border-radius: 7px; background: none; color: #cbd5e1; cursor: pointer; font: inherit; font-size: 13px; text-overflow: ellipsis; white-space: nowrap; }
.account-toggle:hover, .account-toggle:focus-visible { background: rgba(255,255,255,.1); color: #fff; }
.account-email { display: none; }
.account-menu-items { position: absolute; top: calc(100% + 8px); right: 0; z-index: 30; display: none; min-width: 190px; padding: 5px; border: 1px solid #334155; border-radius: var(--va-radius-sm); background: #1e293b; box-shadow: 0 14px 34px rgba(0,0,0,.28); }
.account-menu.open .account-menu-items { display: block; }
.account-menu-items a { display: block; width: 100%; padding: 9px 10px; }
.notice-bell { position: relative; display: inline-flex; align-items: center; justify-content: center; width: 38px; height: 38px; padding: 0; border-radius: 50%; color: #cbd5e1; font-size: 19px; line-height: 1; }
.notice-bell:hover { background: rgba(255,255,255,.1); color: #fff; }
.notice-bell .notice-count { position: absolute; top: -3px; right: -5px; min-width: 18px; height: 18px; padding: 0 3px; border: 2px solid var(--va-navy); border-radius: 999px; background: #e11d48; color: #fff; font-size: 10px; font-weight: 800; line-height: 14px; text-align: center; }
.nav-toggle { display: none; margin-left: auto; border: 0; border-radius: 7px; background: none; color: #fff; cursor: pointer; font-size: 22px; line-height: 1; padding: 9px; }
main { width: min(1120px, calc(100% - 40px)); margin: 30px auto 48px; }
/* THE WIDE VARIANT, for pages built around a table rather than around prose.
   1120 is a reading measure and stays the default; a table with eight columns
   inside a card inside a two-column shell has about 870px to work with there,
   and scrolls. This trades the measure for the columns, and the paragraphs on
   such a page get their own cap below so the words do not run the full width. */
/* max-width AS WELL AS width, and that is not belt and braces. base.html
   carries an older inline stylesheet with its own `main { max-width: 980px }`,
   which is the rule actually deciding this page's width — app.css sets 1120 and
   the inline 980 clamps it. A class selector outranks a bare element one, so
   this wins wherever the two are loaded; setting only `width` did not, and the
   page measured 980 with the class correctly applied. */
main.main-wide { width: min(1760px, calc(100% - 24px)); max-width: none; }
/* A WIDER SHELL IS NOT A LICENCE FOR WIDER SENTENCES. The cards on these pages
   still explain things in paragraphs, and prose running 1700px is unreadable
   however much room there is. Tables, forms and the layout keep the full
   width; only the words are measured. */
main.main-wide .card > p,
main.main-wide .card > .muted { max-width: 88ch; }
/* A REGION IS NOT A SENTENCE (2026-09-27, found capturing the visitor page's
   screenshots). The visitor log's #visitors-container is `.muted` only so its
   "Loading…" text is grey, and the rule above squeezed the whole table it is
   filled with to 88ch: at 1280px the Erase button sat past the scroll edge of
   every row. */
main.main-wide .card > .muted[role="region"] { max-width: none; }

/* THE CALL SUMMARY IS CLAMPED, NOT TRUNCATED (owner, 2026-09-21: "show 3 or 4
   rows and then … and full is visible after expansion"). The whole summary is
   in the DOM — line-clamp only hides the overflow — so opening the row shows it
   complete and the browser's own find-in-page still matches the hidden lines.
   Cutting the string server-side would have made both of those false. */
.calls-summary { display: -webkit-box; -webkit-box-orient: vertical;
  -webkit-line-clamp: 4; line-clamp: 4; overflow: hidden; }
/* Give the column the room the two removed ones were using, so four lines is
   a real four lines rather than four words. */
.calls-table td[data-label="Summary"] { min-width: 42ch; }
@media (max-width: 760px) {
  /* ON A PHONE THE SUMMARY IS TWO LINES (owner, v7 review, 2026-09-26). It was
     unclamped here — the card's body — which made each call a screen tall; the
     card now shows two lines and the call's own dialog, which the whole card
     opens, shows the rest with the transcript. Still clamped, not cut, so
     find-in-page matches the hidden words. */
  .calls-summary { -webkit-line-clamp: 2; line-clamp: 2; }
  .calls-table td[data-label="Summary"] { min-width: 0; }
}
a { color: var(--va-brand); text-decoration: none; }
a:hover { color: var(--va-brand-dark); text-decoration: underline; }
.card { margin-bottom: 18px; padding: 22px; border: 1px solid var(--va-border); border-radius: var(--va-radius); background: var(--va-surface); box-shadow: var(--va-shadow); }
/* A SCALE, NOT FIVE UNRELATED SIZES. Before this the page title was 21.6px and
   a section heading 16.8px — close enough that neither won, while a stat card's
   "0" rendered at 32px. The largest thing on the screen was an empty number and
   the smallest was the name of the page. These four steps are a 1.25 ratio off a
   16px body, so the jumps are audible rather than arbitrary. */
h1, h2, h3, h4 { color: var(--va-ink); letter-spacing: -.021em; text-wrap: balance; }
h1 { margin: 0 0 4px; font-size: 1.95rem; font-weight: 750; line-height: 1.18; }
h2 { margin: 0 0 4px; font-size: 1.5rem;  font-weight: 700; line-height: 1.22; }
h3 { margin: 0 0 4px; font-size: 1.15rem; font-weight: 680; line-height: 1.3; }
/* h4 is the small uppercase label above a number or a column. It is a LABEL, so
   it gets label treatment rather than being a fourth heading nobody reads as one. */
h4 { margin: 0 0 6px; color: var(--va-muted); font-size: .75rem; font-weight: 650;
     letter-spacing: .055em; text-transform: uppercase; }

/* HEADINGS BELONG TO WHAT FOLLOWS THEM. A section heading and its one-line
   explanation were separated by the same gap as the explanation and the content
   below, so the sentence read as floating between two sections instead of
   introducing one. Proximity is the only thing that says which. */
h1 + p, h2 + p, h3 + p, h1 + .muted, h2 + .muted, h3 + .muted { margin-top: 0; }
.card > h2:first-child, .card > h3:first-child, .card > legend + p { margin-top: 0; }
.card > h2 + p.muted, .card > h3 + p.muted, .card > legend + p.muted { margin-bottom: 16px; }
table { width: 100%; border-collapse: separate; border-spacing: 0; border: 1px solid var(--va-border); border-radius: var(--va-radius-sm); background: var(--va-surface); overflow: hidden; }
th, td { padding: 11px 13px; border-bottom: 1px solid #e8edf5; font-size: 14px; text-align: left; vertical-align: top; } tr:last-child td { border-bottom: 0; } th { background: var(--va-surface-subtle); color: #475569; font-size: 12px; letter-spacing: .025em; text-transform: uppercase; }
/* EMPTY TABLES LOOK EMPTY ON PURPOSE, NOT BY ACCIDENT. "No calls yet." sat in
   a table row at body size and full ink, indistinguishable from a row of real
   data until you read it — the one moment a new customer is guaranteed to see,
   rendered as if something had gone wrong. Every empty table in this app is the
   same shape: a tbody whose only row is a single colspan cell. That is why this
   is one rule and not a class threaded through fourteen templates. */
tbody tr:only-child td[colspan] { padding: 34px 20px; color: var(--va-muted);
  font-size: .95rem; text-align: center; }

/* TABS. The tenant page carries its own copy of these four rules inline; this
   is the canonical one so a second screen does not have to invent a third
   pattern. Same class names, so the two look identical and a person who has
   used one has used both. */
.tabs { display: flex; gap: 4px; flex-wrap: wrap; margin: 8px 0 18px; border-bottom: 2px solid var(--va-border); }
.tab-btn { margin-bottom: -2px; padding: 10px 16px; border: 0; border-bottom: 2px solid transparent; background: none; color: var(--va-muted); cursor: pointer; font: inherit; font-size: 14px; font-weight: 600; }
.tab-btn:hover { color: var(--va-ink); }
.tab-btn.active { border-bottom-color: var(--va-brand); color: var(--va-brand); }
/* PANEL VISIBILITY IS DELIBERATELY NOT HERE. Two conventions exist in this
   codebase and neither is wrong: settings.html closes a panel with the `hidden`
   ATTRIBUTE, which is server-rendered and what a screen reader already
   understands, while kiosk_admin.html and tenant.html toggle an `.active` class
   from JS. A global `.tab-panel { display: none }` silently wins over the
   attribute convention, so it hid every panel on the settings page — its open
   tab has no class to mark it open, because it never needed one. Each page that
   switches panels with a class says so in its own stylesheet. */

.pill { display: inline-block; padding: 3px 9px; border-radius: 999px; background: #e8edf5; color: #475569; font-size: 12px; font-weight: 650; }.pill.spam { background: #fee2e2; color: #991b1b; }.pill.followup { background: #fef3c7; color: #92400e; }.pill.blocked { background: #e5e7eb; color: #6b7280; }
label { display: block; margin: 14px 0 5px; color: #334155; font-size: 13px; font-weight: 700; }


/* A COLUMN OF TICK BOXES IS NOT A COLUMN OF FIELDS. `label` above is spaced for
   a label sitting above its own control — 14px of air over each — which down a
   list of six reads as six separate questions rather than one. Flex rather than
   the default inline flow so a label that wraps lines up under its own text
   instead of running back under the box: on a phone "Contacts and deals — a
   record of who you spoke to" wraps, and the second line was starting at the
   left margin. */
.checks label { display: flex; gap: 9px; align-items: center; margin: 0;
                padding: 6px 0; font-weight: 400; color: var(--va-ink);
                font-size: 14px; cursor: pointer; }
.checks input[type=checkbox] { flex: none; margin: 0; }

/* THE SIGN-UP AND ACTIVATION PAGES HAD NO WIDTH AT ALL. `.auth-card` was on
   both and defined nowhere, so the form ran the full width of a desktop
   window — which is how a four-field form came to look like a questionnaire.
   Narrow enough that a line of help text under a field is readable, wide
   enough that two columns fit. */
.auth-card { max-width: 660px; margin: 32px auto 56px; }
/* ONE HEIGHT FOR EVERY CONTROL, and 38px because that is what .btn is. They
   were 40 and 42: a <select> gets line-height:normal from the UA while an
   <input> inherits the body's 1.5, so a row of three dropdowns and a text box
   sat two pixels apart all the way across the product. Two pixels is invisible
   on one control and is exactly what makes a row of eight look hand-assembled
   (owner, 2026-09-20: "the height of boxes and drop downs are not the same and
   it looks ugly"). min-height rather than height, so a textarea can still be
   taller and a control with a wrapped value is not clipped. */
input[type=text], input[type=email], input[type=password], input[type=tel], input[type=number], input[type=date], input[type=time], textarea, select { width: 100%; min-height: 40px; font: inherit; line-height: 1.35; padding: 8px 11px; border: 1px solid #cbd5e1; border-radius: var(--va-radius-sm); background: #fff; color: var(--va-ink); } textarea { min-height: 88px; resize: vertical; }
.btn { display: inline-flex; min-height: 40px; align-items: center; justify-content: center; padding: 8px 14px; border: 1px solid var(--va-brand); border-radius: var(--va-radius-sm); background: var(--va-brand); color: #fff; cursor: pointer; font: inherit; line-height: 1.35; font-weight: 700; text-decoration: none; }.btn:hover { background: var(--va-brand-dark); color: #fff; text-decoration: none; }.btn.secondary { border-color: #cbd5e1; background: #fff; color: #334155; }.btn.secondary:hover { border-color: #94a3b8; background: #f8fafc; color: #172033; }/* DESTRUCTIVE ACTIONS ARE QUIET UNTIL YOU MEAN THEM. A solid red fill made
/* A HIDDEN BUTTON IS HIDDEN (2026-09-26). .btn's display:inline-flex outranks
   the browser's own [hidden] rule, so any .btn carrying ``hidden`` was drawn —
   found as a "Try again" beside every receipt picker before anything failed. */
.btn[hidden] { display: none !important; }
   "Remove" the highest-contrast element on the kiosk admin screen — three of
   them, louder than the page title and louder than every Save beside them.
   Weight should follow intent, and nobody opens that page intending to delete
   a clinician. Outline by default, solid only where the whole screen exists to
   confirm a deletion (.btn.danger.solid). */
.btn.danger { border-color: #e7c9c7; background: #fff; color: var(--va-danger); }
.btn.danger:hover { border-color: var(--va-danger); background: #fdf3f2; color: #9f2f2a; }
.btn.danger.solid { border-color: var(--va-danger); background: var(--va-danger); color: #fff; }
.btn.danger.solid:hover { background: #9f2f2a; color: #fff; }
/* A DISABLED CONTROL LOOKS DISABLED (2026-09-28). There was no :disabled rule
   anywhere in this file, so a disabled .btn was drawn exactly like a live one:
   the owner pressed "Start a draft" on Invoices, with no deal to bill, and
   nothing happened and nothing said why. Grey, not faded: an opacity fade
   takes the label down with the fill, and although WCAG exempts a disabled
   control from contrast, a person still has to read what it would have done.
   The first :is() names .btn.danger.solid so the whole rule carries that
   selector's weight: .btn.secondary:hover and .btn.danger.solid:hover outrank
   a plain .btn:disabled and would repaint the grey. A disabled button that is
   the CURRENT answer — the sending address in Settings, a submission's status
   — carries aria-current and keeps its own look: it is a choice already made,
   not a control that is off. The reason a control is off belongs next to it
   in words; this rule only stops the button pretending it is live. */
:is(.btn, .btn.secondary, .btn.danger, .btn.danger.solid):is(:disabled, [aria-disabled="true"]):not([aria-current]),
:is(.btn, .btn.secondary, .btn.danger, .btn.danger.solid):is(:disabled, [aria-disabled="true"]):not([aria-current]):hover {
  border-color: #d5dbe3; background: #eef1f5; color: #64748b;
  cursor: not-allowed; box-shadow: none; text-decoration: none; }
.btn[aria-current]:disabled { cursor: default; }
button:disabled:not([aria-current]) { cursor: not-allowed; }
/* A disabled field is read-only to the person, so it stops looking like a box
   to type in; a page with its own treatment (accounting's .acc-in) is more
   specific and keeps it. */
:is(input, select, textarea):disabled { background: #f1f5f9; color: #64748b; cursor: not-allowed; }
/* WHY A CONTROL IS OFF, said beside it (2026-09-28). The pages' own small
   grey captions (#94a3b8, about 2.6:1 on white) were where these reasons used
   to live, and the owner read past one straight to the dead button. Body
   size and slate so it reads as a sentence addressed to them; its link is
   where the thing is fixed. Paired with the control by aria-describedby. */
.off-reason { color: #475569; font-size: 14px; }
.off-reason a { font-weight: 600; }
.ok, .err { padding: 10px 12px; border: 1px solid; border-radius: var(--va-radius-sm); font-size: 14px; }.ok { border-color: #a7e3cf; background: #ecfdf5; color: #166534; }.err { border-color: #fecaca; background: #fff1f2; color: #9f1239; }.muted { color: var(--va-muted); font-size: 13px; }.row2 { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 14px; }
/* A CELL THAT WANTS THE WHOLE ROW: a textarea, a list of tick boxes, a submit
   button. Added 2026-09-20 with the sign-up form (owner: "one field per row is
   too long and unnecessary") — .row2 already paired the short fields, and
   without this the long ones had to sit outside the grid, which put a
   different gap above them than between them. */
.row2 > .span2 { grid-column: 1 / -1; } fieldset { margin: 18px 0; padding: 14px 16px; border: 1px solid var(--va-border); border-radius: var(--va-radius-sm); } legend { padding: 0 6px; color: #475569; font-size: 13px; font-weight: 700; }
:where(a, button, input, select, textarea, [tabindex]):focus-visible { outline: 3px solid rgba(96,165,250,.75); outline-offset: 2px; }
@media (max-width: 760px) { header { min-height: 58px; flex-wrap: wrap; gap: 9px; padding: 9px 14px; } header .brand { font-size: 15px; } header .tenant-context { flex: 1 1 0; max-width: none; } .nav-toggle { display: block; } header nav { display: none; width: 100%; flex-direction: column; align-items: stretch; gap: 2px; margin-left: 0; padding-top: 6px; } header nav.open { display: flex; } header nav a { min-height: 44px; padding: 11px 8px; border-top: 1px solid rgba(255,255,255,.08); } .notice-bell { width: auto; justify-content: flex-start; border-top: 1px solid rgba(255,255,255,.08); border-radius: 0; } .notice-bell .notice-count { position: static; margin-left: 7px; border-color: transparent; } .account-menu { width: 100%; border-top: 1px solid rgba(255,255,255,.08); } .account-toggle { display: none; }.account-email { display: block; padding: 11px 8px; overflow: hidden; color: #94a3b8; font-size: 13px; text-overflow: ellipsis; white-space: nowrap; }.account-menu-items { position: static; display: block; min-width: 0; padding: 0; border: 0; border-radius: 0; background: transparent; box-shadow: none; }.account-menu-items a { min-height: 44px; padding: 11px 8px; border-top: 1px solid rgba(255,255,255,.08); } main { width: min(100% - 24px, 1120px); margin: 16px auto 32px; }.card { padding: 16px; border-radius: 10px; }.row2 { grid-template-columns: 1fr; } table { display: block; max-width: 100%; overflow-x: auto; -webkit-overflow-scrolling: touch; } th, td { white-space: nowrap; } }
@media (prefers-reduced-motion: reduce) { *, *::before, *::after { scroll-behavior: auto !important; transition-duration: .01ms !important; animation-duration: .01ms !important; } }

/* Files: the label, the shelves, and the search box.
   .file-tag is a link that reads as a chip; `on` marks the shelf in view. */
.file-description { margin: 2px 0 4px; color: #334155; font-size: 14px; }
.file-tags { margin: 4px 0 0; display: flex; flex-wrap: wrap; gap: 6px; }
.file-tag { display: inline-flex; align-items: center; gap: 5px; padding: 2px 9px;
  border: 1px solid #cbd5e1; border-radius: 999px; background: #fff; color: #334155;
  font-size: 13px; text-decoration: none; }
.file-tag:hover { border-color: #94a3b8; text-decoration: none; }
.file-tag.on { background: var(--va-brand); border-color: var(--va-brand); color: #fff; }
.file-tag span { color: #64748b; font-size: 12px; }
.file-tag.on span { color: #e2e8f0; }
.file-search { display: flex; gap: 8px; margin: 10px 0 14px; flex-wrap: wrap; }
.file-search input[type=search] { flex: 1 1 260px; min-height: 38px; padding: 8px 12px;
  border: 1px solid #cbd5e1; border-radius: var(--va-radius-sm); font: inherit; }
.file-edit input[type=text] { width: 100%; min-height: 36px; padding: 7px 10px;
  border: 1px solid #cbd5e1; border-radius: var(--va-radius-sm); font: inherit; margin: 4px 0 8px; }

/* Files — shared by the Files page and anything else that lists documents.
   These lived in a <style> block inside settings.html; when the list moved to
   its own page it arrived unstyled, with every action stacked one per line. */
.file-list { display: grid; gap: 10px; }
.file-row { display: grid; grid-template-columns: minmax(0, 1fr) auto; gap: 10px;
  align-items: start; padding: 12px; border: 1px solid #e5e7eb; border-radius: 10px; }
.file-row h3 { margin: 0; font-size: 16px; overflow-wrap: anywhere; }
.file-meta { margin: 6px 0 0; color: #5f6773; font-size: 13px; }
.file-actions { display: flex; flex-wrap: wrap; gap: 6px; justify-content: flex-end; }
.file-actions form { margin: 0; }
/* A <details> is block-level, so without this each action claims its own row
   and one file becomes six lines tall. */
.file-actions details { margin: 0; }
.file-actions > details > summary { list-style: none; cursor: pointer; }
.file-actions > details > summary::-webkit-details-marker { display: none; }
/* The editor a summary opens spans the row rather than squeezing into the
   button column, so the text field is wide enough to read what you typed. */
.file-actions > details[open] { grid-column: 1 / -1; flex: 1 0 100%; }
.file-actions > details > form { margin-top: 8px; }
.file-edit textarea { width: 100%; min-height: 180px; box-sizing: border-box; }

@media (max-width: 640px) {
  .file-row { grid-template-columns: 1fr; }
  .file-actions { justify-content: flex-start; }
  .file-actions .btn { min-height: 36px; padding: 7px 10px; font-size: 13px;
    white-space: nowrap; }
}

/* --------------------------------------------------------------------------
   Files: folders, the table, and the row menu.

   MOBILE FIRST IN THE PARTS THAT MATTER. A phone is the primary device here, so
   the table stops being a table below 720px and each row becomes a card with
   its own labels — a four-column grid on a 390px screen is either unreadable or
   scrolls sideways, and both are worse than a card.
   -------------------------------------------------------------------------- */
.files-page .ico { width: 18px; height: 18px; flex: 0 0 auto; vertical-align: -3px; }
.files-views { display: flex; flex-wrap: wrap; gap: 8px; margin: 14px 0 12px; }
.files-view { display: inline-flex; align-items: center; gap: 7px; padding: 8px 14px;
  border: 1px solid #cbd5e1; border-radius: 999px; color: #334155; background: #fff;
  text-decoration: none; font-weight: 600; font-size: 14px; min-height: 40px; }
.files-view:hover { border-color: #94a3b8; text-decoration: none; }
.files-view.on { background: var(--va-brand); border-color: var(--va-brand); color: #fff; }
.files-icon-action { display: inline-flex; align-items: center; justify-content: center;
  min-width: 40px; min-height: 40px; padding: 8px 10px; border: 1px solid #cbd5e1;
  border-radius: 999px; color: #334155; background: #fff; text-decoration: none; }
.files-icon-action:hover { border-color: #94a3b8; text-decoration: none; }
.files-action-label { margin-left: 7px; font-size: 14px; font-weight: 600; }

.file-search { display: flex; gap: 8px; margin: 0 0 8px; }
.file-search-box { position: relative; flex: 1 1 auto; display: flex; align-items: center; }
.file-search-box .ico { position: absolute; left: 11px; color: #64748b; }
.file-search input[type=search] { width: 100%; min-height: 44px; padding: 9px 12px 9px 36px;
  border: 1px solid #cbd5e1; border-radius: var(--va-radius-sm); font: inherit; }

.folder-grid { display: grid; gap: 10px; margin-top: 6px;
  grid-template-columns: repeat(auto-fill, minmax(190px, 1fr)); }
.folder { display: flex; align-items: center; gap: 10px; padding: 14px;
  border: 1px solid #e5e7eb; border-radius: 12px; background: #fff; color: #0f172a;
  text-decoration: none; min-height: 56px; }
.folder:hover { border-color: var(--va-brand); text-decoration: none; }
.folder .ico { width: 22px; height: 22px; color: var(--va-brand); }
.folder-name { font-weight: 600; overflow-wrap: anywhere; }
.folder-count { margin-left: auto; color: #64748b; font-size: 13px; }
.folder-unfiled .ico { color: #94a3b8; }
.new-folder { position: fixed; right: max(16px, env(safe-area-inset-right));
  bottom: max(20px, env(safe-area-inset-bottom)); z-index: 10; }
.new-folder > summary { display: inline-flex; align-items: center; gap: 8px; min-height: 48px;
  padding: 12px 16px; border-radius: 999px; background: var(--va-brand); color: #fff;
  box-shadow: 0 8px 22px rgba(37,99,235,.28); cursor: pointer; font-weight: 700;
  list-style: none; }
.new-folder > summary::-webkit-details-marker { display: none; }
.new-folder-form { position: absolute; right: 0; bottom: calc(100% + 10px);
  width: min(340px, calc(100vw - 32px)); padding: 14px; border: 1px solid var(--va-border);
  border-radius: 12px; background: #fff; box-shadow: 0 12px 32px rgba(15,23,42,.16); }
.new-folder-form label { margin-top: 0; }
.new-folder-fields { display: flex; gap: 8px; }
.new-folder-fields input { min-width: 0; flex: 1; min-height: 44px; }
.new-folder-form .muted { margin: 8px 0 0; }

/* OVERFLOW VISIBLE, or the row menu is invisible. The global `table` rule sets
   overflow:hidden to clip its own rounded corners, which also clips anything
   absolutely positioned inside — so the actions panel opened and could not be
   seen. The border and radius move to the wrapper instead, which keeps the
   corners and stops the clipping. Also: the global mobile rule turns tables
   into horizontally scrolling blocks, which is exactly what a phone must not
   do here, so this one opts out and becomes cards instead. */
.files-table-wrap { border: 1px solid var(--va-border); border-radius: var(--va-radius-sm);
  background: var(--va-surface); }
.files-table { width: 100%; border-collapse: collapse; border: 0;
  border-radius: 0; overflow: visible; background: transparent; }
.files-table th { text-align: left; font-size: 12px; letter-spacing: .04em;
  text-transform: uppercase; color: #64748b; padding: 6px 10px; border-bottom: 1px solid #e5e7eb; }
.files-table td { padding: 10px; border-bottom: 1px solid #f1f5f9; vertical-align: top; }
.f-title { font-weight: 600; overflow-wrap: anywhere; }
.f-note { color: #475569; font-size: 13px; margin-top: 2px; }
.f-when { color: #94a3b8; font-size: 12px; margin-top: 2px; }
.f-folder { white-space: normal; }
.f-folder .file-tag { margin: 0 4px 4px 0; }
.f-size { white-space: nowrap; color: #475569; }
.f-actions { text-align: right; white-space: nowrap; }
.f-actions .btn { padding: 7px 10px; min-height: 38px; }

/* The row menu. A <details> so it opens with no script and by keyboard. */
.f-menu { display: inline-block; position: relative; }
.f-menu > summary { list-style: none; cursor: pointer; display: inline-flex; }
.f-menu > summary::-webkit-details-marker { display: none; }
.f-menu-panel { position: absolute; right: 0; top: calc(100% + 6px); z-index: 20;
  width: 280px; max-width: 88vw; padding: 8px; text-align: left;
  background: #fff; border: 1px solid #e5e7eb; border-radius: 12px;
  box-shadow: 0 12px 32px rgba(15, 23, 42, .16); }
.f-item { display: flex; align-items: center; gap: 9px; width: 100%; padding: 9px 10px;
  min-height: 42px; border: 0; border-radius: 8px; background: none; color: #0f172a;
  font: inherit; text-align: left; cursor: pointer; }
.f-item:hover { background: #f1f5f9; }
.f-danger { color: var(--va-danger); }
.f-danger:hover { background: #fef2f2; }
.f-form { padding: 8px 10px; border-top: 1px solid #f1f5f9; margin: 0; }
.f-label { display: flex; align-items: center; gap: 8px; font-size: 13px;
  font-weight: 600; color: #334155; margin-bottom: 5px; }
.f-form input[type=text] { width: 100%; min-height: 40px; padding: 8px 10px;
  border: 1px solid #cbd5e1; border-radius: var(--va-radius-sm); font: inherit; margin-bottom: 6px; }
.f-form .btn { width: 100%; }
.files-pager { display: flex; align-items: center; gap: 10px; margin-top: 14px; }

@media (max-width: 720px) {
  /* The outer canvas and desktop card must not spend phone width. The card still
     owns its 16px content padding; only the decorative frame/gutter disappears. */
  main:has(.files-page) { width: 100%; margin: 0; padding: 0; }
  .files-page.wrap { width: 100%; }
  .files-page .card { width: 100%; margin: 0; border: 0; border-radius: 0;
    box-shadow: none; }
  .files-views { align-items: center; }
  .files-view { justify-content: center; min-width: 44px; padding: 8px 12px; }
  .files-icon-action { margin-left: auto; }
  .files-action-label { display: none; }
  /* The global rule makes every table a scrolling block on a phone; here that
     would be the four-column horizontal scroll this layout exists to avoid. */
  .files-table { display: table; max-width: none; overflow-x: visible; }
  .files-table-wrap { border: 0; background: transparent; }
  .files-table td, .files-table th { white-space: normal; }
  /* EACH ROW BECOMES A CARD. Four columns on a 390px screen either wrap into
     nonsense or scroll sideways; both lose. */
  .files-table thead { position: absolute; width: 1px; height: 1px; overflow: hidden;
    clip: rect(0 0 0 0); white-space: nowrap; }
  .files-table, .files-table tbody, .files-table tr, .files-table td { display: block; width: 100%; }
  .files-table tr { border: 1px solid #e5e7eb; border-radius: 12px; padding: 10px;
    margin-bottom: 10px; position: relative; }
  .files-table td { border: 0; padding: 3px 0; }
  .f-folder::before, .f-size::before { content: attr(data-label) ": ";
    color: #94a3b8; font-size: 12px; }
  /* The actions sit at the top-right of the card, where a thumb reaches. */
  .f-actions { position: absolute; top: 10px; right: 10px; width: auto;
    display: flex; gap: 6px; }
  /* `.files-table td` above is (0,1,1) and this was (0,1,0), so the cell's own
     padding won and the name ran underneath the buttons — hidden behind them
     rather than wrapped. Qualified so it actually applies. */
  .files-table .f-name { padding-right: 104px; }
  .f-menu-panel { position: fixed; left: 12px; right: 12px; top: auto; bottom: 12px;
    width: auto; max-width: none; }
  .new-folder { bottom: max(16px, env(safe-area-inset-bottom)); }
}


/* --------------------------------------------------------------------------
   The reception dashboard (/admin/{slug}/kiosk).

   A screen somebody looks at all day, so colour carries meaning and nothing
   else: the count of people in the building, and which state a row is in. No
   colour appears without a word beside it — the repo's accessibility rule —
   and the row tint is a wash rather than a fill so a table of thirty arrivals
   still reads as a table.
   -------------------------------------------------------------------------- */
.lobby-head { display: flex; align-items: flex-start; justify-content: space-between;
  gap: 14px; flex-wrap: wrap; }
.lobby-actions { display: flex; flex-wrap: wrap; gap: 8px; }

/* THE NUMBER IS THE PAGE. It is the one fact worth crossing a room to read, so
   it gets the weight — a 6px status edge, a tinted ground and a 3rem figure —
   and everything else on the screen is quieter than it. */
.lobby-count { display: flex; align-items: center; gap: 20px; padding: 20px 24px;
  border: 1px solid var(--va-in-line); border-left: 6px solid var(--va-in);
  border-radius: var(--va-radius); background: var(--va-in-soft); }
.lobby-count .n { color: var(--va-in); font-size: 3rem; font-weight: 750;
  line-height: 1; letter-spacing: -.03em; font-variant-numeric: tabular-nums; }
.lobby-count .what { font-size: 1.05rem; font-weight: 650; }
.lobby-count .when { margin: 2px 0 0; color: var(--va-muted); font-size: 13px; }
/* NOBODY IN THE BUILDING IS A CALM STATE, NOT A BROKEN ONE. Same shape, the
   neutral ramp instead of the live one: an empty lobby reads as quiet rather
   than as a screen that failed to load. */
.lobby-count.quiet { border-color: var(--va-border); border-left-color: #94a3b8;
  background: var(--va-surface-subtle); }
.lobby-count.quiet .n { color: var(--va-muted); }

.status-tag { display: inline-flex; align-items: center; gap: 6px; padding: 3px 10px;
  border: 1px solid; border-radius: 999px; font-size: 12px; font-weight: 650;
  white-space: nowrap; }
.status-tag::before { content: ""; width: 8px; height: 8px; border-radius: 50%;
  background: currentColor; flex: 0 0 auto; }
.status-tag.in { color: var(--va-in); border-color: var(--va-in-line); background: var(--va-in-soft); }
.status-tag.out { color: var(--va-out); border-color: var(--va-out-line); background: var(--va-out-soft); }
.status-tag.off { color: var(--va-off); border-color: var(--va-off-line); background: var(--va-off-soft); }
/* THE CLAIM TAG (owner, 2026-09-02: "Served by" must be unmistakable). Blue,
   which no visit STATUS uses, so a served arrival cannot be misread as a
   presence state — and the word is still the fact, per the colour rule. */
.status-tag.served { color: #1d4ed8; border-color: #bfdbfe; background: #eff6ff; }
/* The visitor log's one status cell: status, then when they left, then the
   claim tag, each on its own line. The wrap scrolls sideways on a narrow
   screen instead of letting the table push past the card (2026-09-10). */
.visitors-table-wrap { max-width: 100%; overflow-x: auto; }
.visitors-table .visit-left { margin-top: 2px; font-size: 13px; }
.visitors-table .visit-claim { margin-top: 6px; }
/* IN A TABLE CELL THE SERVED TAG MAY WRAP. "Served by Hannah Whitfield ·
   9:49am" is the longest thing in the Reception and log tables, and as a
   nowrap flex pill it set the status column's width and pushed both tables
   past their cards (2026-09-10). A flex pill that is allowed to wrap breaks
   one flex item per line — "Served by / Hannah / Whitfield" — so in a cell it
   is an inline block with the dot inline, and the text breaks like a
   sentence. The status words themselves ("In the building") stay on one line. */
td .status-tag.served { display: inline-block; white-space: normal; line-height: 1.45; }
td .status-tag.served::before { display: inline-block; vertical-align: middle; margin-right: 6px; }

/* SORTABLE COLUMN HEADINGS (owner, 2026-09-18: "simple click to sort").
   The heading is a real button so it is tabbable and announced as one; it is
   styled to look like the heading it replaced rather than like the page's
   other buttons, because a row of pills where the column names used to be
   would read as a toolbar. The arrow is drawn only on the column currently
   ordering the table — an arrow on every heading is a row of noise, and the
   hover state is what says the others can be pressed. */
th > .th-sort {
  display: inline-flex; align-items: center; gap: 6px;
  background: none; border: 0; padding: 0; margin: 0; cursor: pointer;
  font: inherit; color: inherit; letter-spacing: inherit; text-align: left;
  border-radius: 4px;
}
th > .th-sort:hover { color: var(--va-accent, #0f766e); }
th > .th-sort:focus-visible { outline: 2px solid var(--va-accent, #0f766e); outline-offset: 2px; }
th > .th-sort .th-arrow { font-size: 10px; line-height: 1; opacity: 0; transition: opacity .12s ease; }
th > .th-sort:hover .th-arrow { opacity: .45; }
th > .th-sort .th-arrow::before { content: "▲"; }
th > .th-sort[data-dir="desc"] .th-arrow::before { content: "▼"; }
th > .th-sort.is-sorted { color: var(--va-accent, #0f766e); }
th > .th-sort.is-sorted .th-arrow { opacity: 1; }

/* The phone's version, hidden wherever the column headings themselves are
   readable. The breakpoints match the ones that stack each table into cards. */
.table-sortbar { display: none; align-items: center; gap: 8px; margin: 0 0 10px; font-size: 13px; }
.table-sortbar select { padding: 5px 8px; max-width: 170px; }
.table-sortbar .table-sortdir { padding: 5px 10px; white-space: nowrap; }
@media (max-width: 760px) { .table-sortbar { display: flex; } }

.lobby-table td { vertical-align: middle; }
/* The wash that groups the open visits together down the page. Deliberately
   fainter than the tag: it is a secondary cue for the same fact, which is the
   only thing a colour on its own is allowed to be. */
.lobby-table tr.is-in td { background: rgba(15, 118, 110, .045); }
.lobby-table tr.is-in td:first-child { box-shadow: inset 3px 0 0 var(--va-in); }
.lobby-when { white-space: nowrap; font-variant-numeric: tabular-nums; font-weight: 650; }
.lobby-who .sub { color: var(--va-muted); font-size: 12.5px; }
/* HOW LONG THEY HAVE BEEN WAITING, under the arrival clock and quieter than
   it: the clock is the fact, the wait is what somebody acts on. Tabular
   numerals so a column of them can be scanned for the big one. */
.lobby-waited { margin-top: 2px; color: var(--va-muted); font-size: 12.5px;
  font-weight: 600; font-variant-numeric: tabular-nums; }
.lobby-act { text-align: right; white-space: nowrap; }
.lobby-act form { margin: 0; }
/* Two buttons in one cell: the gap lived in an inline style on the second
   form, which the phone layout below then had to shout down. It is a spacing
   rule and belongs here. */
.lobby-act form + form { margin-left: 8px; }
.lobby-act .btn { min-height: 34px; padding: 6px 12px; }
/* THE CALM EMPTY STATE. Not a table with one apologetic row in it: a short
   sentence in the space the table would occupy, saying what will happen rather
   than what is missing. */
.lobby-empty { padding: 40px 24px; border: 1px dashed var(--va-border);
  border-radius: var(--va-radius-sm); background: var(--va-surface-subtle);
  text-align: center; }
.lobby-empty p { margin: 0; color: var(--va-muted); }
.lobby-empty p.lead { margin-bottom: 4px; color: var(--va-ink);
  font-size: 1.05rem; font-weight: 650; }

/* THE STAFF CARD. The count sits in the heading as a pill rather than as a
   second big number: the big number is the visitors, and two figures of the
   same weight on one screen would make a reader add them by eye. Same green
   ramp as the open-visit tag — signed in IS "in the building". */
.lobby-staff { border-left: 4px solid var(--va-in); }
.lobby-staff-count { display: inline-block; margin-left: 8px; padding: 2px 10px;
  border: 1px solid var(--va-in-line); border-radius: 999px; background: var(--va-in-soft);
  color: var(--va-in); font-size: .85rem; font-weight: 700; vertical-align: middle;
  font-variant-numeric: tabular-nums; }
.lobby-staff-line { margin-top: 4px; }
/* The record under the roll call: quieter than the live rows above it (no
   green wash, no edge), because these people have left. */
.lobby-staff-record { margin-top: 18px; padding-top: 14px; border-top: 1px solid var(--va-border); }
.lobby-staff-record h4 { margin: 0 0 8px; font-size: .95rem; color: var(--va-muted); font-weight: 650; }
.lobby-staff-record .lobby-when.muted { font-weight: 500; white-space: normal; }

/* THE CARRIED-OVER CARD. Amber on the edge, not red: nothing is wrong, it is
   a list of people who almost certainly left. The colour is a second cue for
   what the heading already says, never the only one. */
.lobby-carried { border-left: 4px solid #b45309; }
.lobby-carried .lobby-head { align-items: flex-start; }

@media (max-width: 640px) {
  .lobby-count { gap: 14px; padding: 16px; }
  .lobby-count .n { font-size: 2.4rem; }
}

/* THE LOBBY TABLES BECOME REAL CARDS ON A PHONE.
   The site-wide phone rule turns every <table> into `display: block` with a
   horizontal scrollbar and nowrap cells. On a two-column table that is a
   reasonable trade; on these it is not, twice over. `display: block` on a
   <table> drops the row and column relationships from the accessibility tree,
   so the seven-column lobby table reads as one undifferentiated run of cells —
   a name, a time, a word, with nothing saying which is which. And staff
   walking a building with a phone in their hand is a stated use of this
   screen, not an edge case.
   THIS IS .users-table's TREATMENT, not a new one: hide the header row, stack
   the cells, and let each cell's own data-label say what it is. The markup
   side is in kiosk_dashboard.html and _file_row.html, which explains it.
   Nothing here scrolls sideways afterwards, so no scroll container on this
   page needs tabindex to be reachable from a keyboard. */
/* THE CALL LIST GETS THE LOBBY TABLE'S TREATMENT (voice review, 2026-09-17).
   Seven columns — time, direction, caller, length, flags, sentiment, summary —
   in 390px was a sideways scroll on the page a practice checks between
   patients. Same rules as .lobby-table below: hide the header, stack the
   cells, let each cell's data-label say what it is. */
@media (max-width: 760px) {
  .calls-table, .calls-table tbody, .calls-table tr, .calls-table td {
    display: block; width: 100%; }
  .calls-table { overflow-x: visible; }
  .calls-table thead { position: absolute; width: 1px; height: 1px;
    overflow: hidden; clip: rect(0 0 0 0); white-space: nowrap; }
  .calls-table tr { padding: 10px 0; border-bottom: 1px solid var(--va-border); }
  .calls-table tr:last-child { border-bottom: 0; }
  /* THE LABEL IN A GUTTER, NOT ON A LINE OF ITS OWN (2026-09-26): a label
     above every value made each call eight lines and the day's calls a very
     long scroll. The same layout as table.m-rows below. */
  .calls-table td { display: block; position: relative; min-height: 1.4em;
    padding: 2px 0 2px 6.4rem; border: 0; white-space: normal !important; }
  .calls-table td::before { content: attr(data-label); position: absolute;
    left: 0; top: 3px; width: 6rem; color: var(--va-muted);
    font-size: 12px; font-weight: 600; line-height: 1.3; }
  /* An empty cell would otherwise be a label with nothing under it: a call
     nobody has read yet has no sentiment, and most calls carry no flag. */
  .calls-table td:empty { display: none; }
  .calls-table td.calls-flags:not(:has(*)) { display: none; }
  .calls-table td.calls-flags { display: flex; flex-wrap: wrap; gap: 8px;
    align-items: center; padding-left: 0; }
  .calls-table td.calls-flags::before { content: none; }
}

@media (max-width: 760px) {
  .lobby-table, .lobby-table tbody, .lobby-table tr, .lobby-table td {
    display: block; width: 100%; }
  .lobby-table { overflow-x: visible; }
  /* Visually gone, still read aloud in source order by anything that wants it,
     and still the thing data-label is copied from. */
  .lobby-table thead { position: absolute; width: 1px; height: 1px;
    overflow: hidden; clip: rect(0 0 0 0); white-space: nowrap; }
  .lobby-table tr { padding: 10px 0; border-bottom: 1px solid var(--va-border); }
  .lobby-table tr:last-child { border-bottom: 0; }
  /* LABEL ABOVE VALUE, NOT BESIDE IT. .users-table can use a two-column grid
     because each of its cells holds exactly one thing; these cells do not —
     the arrival cell carries a clock AND a waited-for line, and Visitor
     carries a name AND a tag. In a grid every extra child auto-places into
     the label column and reads as a field name. A block label does not care
     how many children a cell has. */
  .lobby-table td { display: block; padding: 5px 0; border: 0;
    white-space: normal; }
  .lobby-table td::before { content: attr(data-label); display: block;
    margin-bottom: 2px; color: var(--va-muted);
    font-size: 12.5px; font-weight: 700; }
  /* The buttons get the full width and no label at all. */
  .lobby-table td.lobby-act { display: flex; flex-wrap: wrap; gap: 8px;
    padding: 8px 0 2px; text-align: left; white-space: normal; }
  .lobby-table td.lobby-act::before { content: none; }
  .lobby-table td.lobby-act form { display: inline-block; }
  /* The flex gap above already separates them. */
  .lobby-table td.lobby-act form + form { margin-left: 0; }
  /* The open-visit cue moves from the first cell to the whole card, because
     "the first cell" is now the top line of a stack rather than a left edge. */
  .lobby-table tr.is-in { padding-left: 10px;
    box-shadow: inset 3px 0 0 var(--va-in); }
  .lobby-table tr.is-in td:first-child { box-shadow: none; }
  .lobby-when { white-space: normal; }
}

/* SOMEBODY THE LOBBY OFFERS AND NOBODY CAN REACH. Amber, not red: nothing is
   broken and no data is at risk — a visitor checks in perfectly well against a
   name with no channels, and the arrival is on the dashboard either way. What
   is wrong is that the person expecting them is never told, and that was
   invisible until a real check-in on 2026-08-30 went unnoticed. */
.warn { color: #b45309; }
.warn-banner {
  margin: 0 0 14px; padding: 11px 14px; border-radius: 8px;
  border: 1px solid #fcd34d; background: #fffbeb; color: #78350f;
  font-size: 14px; line-height: 1.5;
}
.warn-banner b { color: #78350f; }

/* Text for screen readers and nothing else — icon buttons keep their words
   this way, and so do the labels on search boxes whose surroundings already
   say what they search (files.html, the kiosk staff filter). */
.sr-only { position: absolute; width: 1px; height: 1px; padding: 0; margin: -1px;
  overflow: hidden; clip: rect(0, 0, 0, 0); white-space: nowrap; border: 0; }

/* THE SKIP LINK (base.html, WCAG 2.4.1). Hidden the same way .sr-only hides
   things, but it comes BACK when it takes focus — which is the whole point, and
   why it is a separate class: .sr-only is used by icon buttons and field labels
   that must stay silent-and-invisible always, so its behaviour is left alone.

   ``fixed``, NOT ``absolute``: absolute pins it to the top of the DOCUMENT, so
   Shift+Tab back to the top of the order from halfway down a long staff list
   paints the chip off-screen and the focus indicator is invisible (2.4.7).
   Fixed looks identical at scroll 0 and survives the scrolled case. */
.skip-link { position: fixed; left: 8px; top: 8px; z-index: 200;
  width: 1px; height: 1px; padding: 0; overflow: hidden; clip: rect(0, 0, 0, 0);
  white-space: nowrap; }
.skip-link:focus { width: auto; height: auto; overflow: visible; clip: auto;
  padding: 10px 14px; border-radius: 8px; background: #fff; color: var(--va-ink);
  border: 2px solid var(--va-brand); font-size: 14px; font-weight: 600;
  text-decoration: none; box-shadow: 0 6px 18px rgba(0, 0, 0, .18); }

.btn-icon { display: inline-flex; align-items: center; justify-content: center;
  padding: 8px 10px; line-height: 0; }
/* The Download CSV button sits at the far right of the count tile. */
.lobby-count { position: relative; }
.lobby-count-csv { position: absolute; right: 16px; top: 16px; }
@media (max-width: 640px) { .lobby-count-csv { position: static; margin-top: 10px; } }

/* THE "WATCH TUTORIAL" LINK (owner, 2026-09-10: "more red, similar to
   YouTube"). A quiet text link with a play ring, in YouTube's red so the eye
   reads it as video before it reads the words; never a filled button, which
   would compete with the page's own primary action beside it. */
.watch-tutorial { display: inline-flex; align-items: center; gap: 7px; font-weight: 600; font-size: 15px;
  color: #e5251b; text-decoration: none; white-space: nowrap; line-height: 1; }
.watch-tutorial:hover { color: #b91c1c; text-decoration: underline; }
.watch-tutorial svg { flex: none; }
.tutorial-line { margin: 0 0 12px; }
/* THE TAB'S NAME AT THE TOP OF ITS PANEL (owner, 2026-09-26: a page's name
   must match the item clicked). Several tabs open on a card about something
   narrower — Kiosk settings' Privacy on "One thing to decide first" — so the
   panel says which tab it is, in the small capitals the Voice and Expenses
   screens use, beside its Watch tutorial link. */
.tab-panelhead { font-size: 13px; font-weight: 700; letter-spacing: .08em;
                 text-transform: uppercase; color: #64748b; margin-right: 14px; }
/* THE PLAYER DIALOG. 16:9 that scales with the viewport rather than a fixed
   height, so a phone gets a full-width video and a desk gets a big one. */
.tutorial-player { width: min(960px, calc(100vw - 32px)); max-width: none; padding: 0; border: 0;
  border-radius: 12px; background: #0f172a; color: #fff; box-shadow: 0 30px 80px rgba(0,0,0,.5); }
.tutorial-player::backdrop { background: rgba(15, 23, 42, .72); }
.tutorial-player-head { display: flex; align-items: center; justify-content: space-between; gap: 12px;
  padding: 12px 16px; font-size: 15px; }
.tutorial-player-close { background: none; border: 0; color: #cbd5e1; font-size: 26px; line-height: 1;
  cursor: pointer; padding: 0 4px; }
.tutorial-player-close:hover { color: #fff; }
.tutorial-player-frame { position: relative; padding-top: 56.25%; background: #000; }
.tutorial-player-frame video, .tutorial-player-frame iframe { position: absolute; inset: 0; width: 100%;
  height: 100%; border: 0; background: #000; }
.tutorial-player-foot { margin: 0; padding: 10px 16px; font-size: 13px; }
.tutorial-player-foot a { color: #93c5fd; }


/* ---------------------------------------------------------------------------
   THE TWO-STEP VISIT INDICATOR, on the sessionless claim and check-out pages.
   Markup and the argument for it: app/templates/_visit_steps.html.

   Two dots and a rule between them, drawn with borders rather than images so
   it costs no request on a phone on mobile data. The connector is the ::before
   of the second step, which means the row works at any width without a
   measured line. */
.vsteps {
  list-style: none; display: flex; align-items: flex-start; justify-content: center;
  gap: 0; margin: 22px 0 0; padding: 0;
}
.vstep {
  position: relative; flex: 1 1 0; max-width: 150px;
  display: flex; flex-direction: column; align-items: center; gap: 8px;
  text-align: center;
}
.vstep + .vstep::before {
  content: ""; position: absolute; top: 19px; right: 50%; left: -50%;
  height: 2px; background: var(--va-border);
}
.vstep.done + .vstep.done::before { background: var(--va-success); }
.vdot {
  position: relative; z-index: 1;
  width: 38px; height: 38px; border-radius: 50%;
  display: inline-flex; align-items: center; justify-content: center;
  font-size: 17px; font-weight: 700; line-height: 1;
  background: var(--va-surface); color: var(--va-muted);
  /* Dashed while it has not happened: the shape says "still to come" before
     the colour does, which is the half of it somebody colour-blind reads. */
  border: 2px dashed var(--va-border);
}
.vstep.done .vdot {
  color: #fff; background: var(--va-success);
  border: 2px solid var(--va-success);
}
.vlabel { font-size: 14px; font-weight: 650; color: var(--va-muted); }
.vstep.done .vlabel { color: var(--va-ink); }

.vsteps-status {
  margin: 14px 0 0; text-align: center;
  font-size: 14px; color: var(--va-muted);
}
.vsteps-status.is-done { color: var(--va-success); font-weight: 650; }

/* --------------------------------------------------------------------------
   THE CRM SECTION: a menu on the left, the page on the right.
   Added 2026-09-20, when six top-level addresses became one section under /crm
   with a sidebar (owner's ask). Every class is crm- prefixed because .card,
   .panel and .plan are global here and reusing one restyles a page elsewhere.
   -------------------------------------------------------------------------- */
.section-shell { display: grid; grid-template-columns: 212px minmax(0, 1fr); gap: 28px; align-items: start; }
.section-main { min-width: 0; }  /* or a wide table stretches the grid column */

/* UNDER THE BAR, NOT BEHIND IT (owner, 2026-09-21: "sidebar in many pages
   scroll up and down a bit which is annoying"). ``body > header`` above is
   sticky at top:0 and min-height:64px, so a sidebar sticking at 16px travelled
   48px further than it looked like it should and tucked its first item behind
   the navy bar before it stopped. The movement is what you notice; the hidden
   row is why it happens.

   --va-sticky-top IS THE HEADER PLUS A GAP, declared once here because three
   places had each guessed their own: this sidebar and help.html both said 16px
   and were wrong, tutorials.html said 80px after the same bug was found there
   on 2026-09-21 and was right by hand. A fourth sticky thing should read this
   rather than pick a fourth number. */
.side-nav { position: sticky; top: var(--va-sticky-top); padding: 6px; border: 1px solid #e9edf3;
  border-radius: 14px; background: #fff; box-shadow: 0 1px 2px rgba(16,24,40,.04); }
.side-nav ul { margin: 0; padding: 0; list-style: none; }
/* THE CHIP ROW ABOVE A LIST (_list_filters.html, 2026-09-26): the statuses
   of quotes and the views of invoices, filters rather than sidebar entries. */
.lf-row { display: flex; flex-wrap: wrap; gap: 7px; margin: 4px 0 16px; }
.lf-chip { display: inline-flex; align-items: center; gap: 6px; padding: 5px 12px;
  border: 1px solid #e2e8f0; border-radius: 999px; background: #fff; color: #475569;
  font-size: 13px; font-weight: 600; text-decoration: none;
  transition: border-color .15s ease, background .15s ease, color .15s ease; }
.lf-chip:hover { border-color: #cbd5e1; background: #f8fafc; color: #0f172a; }
.lf-chip.is-here { background: #1d4ed8; border-color: #1d4ed8; color: #fff; }
.lf-n { min-width: 18px; padding: 0 6px; border-radius: 999px; background: #f1f5f9;
  color: #334155; font-size: 11.5px; text-align: center; }
.lf-chip.is-here .lf-n { background: rgba(255,255,255,.22); color: #fff; }
.lf-chip.is-alert:not(.is-here) { border-color: #fecaca; color: #b91c1c; }
.lf-chip.is-alert .lf-n { background: #fee2e2; color: #b91c1c; }
/* THE SEARCH, DATE CHIPS, SUMMARY AND PAGER ON THE QUOTES AND INVOICES LISTS
   (_list_search.html, _list_pager.html; owner, 2026-09-29). Server-side, so
   they are a GET form and links: one row on a desk — the search box grows,
   the date chips sit beside it — and a stack on a phone, box and button on
   one line and the chips under them, with nothing wider than the screen. */
.lf-tools { display: flex; flex-wrap: wrap; align-items: center; gap: 10px 16px; margin: 0 0 12px; }
.lf-search { display: flex; flex: 1 1 320px; min-width: 0; align-items: center; gap: 8px; margin: 0; }
.lf-search input[type=search] { flex: 1 1 auto; min-width: 0; min-height: 40px; padding: 8px 11px;
  font: inherit; border: 1px solid #cbd5e1; border-radius: var(--va-radius-sm); background: #fff; color: var(--va-ink); }
.lf-search input[type=search]:focus { outline: 2px solid #bfdbfe; outline-offset: 1px; border-color: #93c5fd; }
.lf-search .btn { flex: 0 0 auto; }
.lf-clear { flex: 0 0 auto; font-size: 13px; white-space: nowrap; }
.lf-tools .lf-row { margin: 0; }
.lf-rule { margin: -4px 0 8px; color: #64748b; font-size: 12.5px; }
.lf-sum { margin: 0 0 10px; color: #475569; font-size: 13px; line-height: 1.5; }
.lf-sum b { color: #0f172a; font-variant-numeric: tabular-nums; }
.lf-sum .lf-cur { white-space: nowrap; }
.lf-pager { display: flex; flex-wrap: wrap; align-items: center; justify-content: space-between;
  gap: 8px 12px; margin: 10px 0; color: #475569; font-size: 13px; }
.lf-pager-nav { display: flex; align-items: center; gap: 8px; }
.lf-pager a { display: inline-flex; align-items: center; min-height: 36px; padding: 3px 12px;
  border: 1px solid var(--line); border-radius: 999px; background: #fff; color: #1e3a8a; text-decoration: none; }
.lf-pager a:hover { border-color: #cbd5e1; background: #f8fafc; }
.lf-none { padding: 22px 10px; color: #64748b; }
@media (max-width: 640px) {
  .lf-tools { flex-direction: column; align-items: stretch; }
  .lf-search { flex: 0 0 auto; flex-wrap: wrap; }
  .lf-search input[type=search] { flex: 1 1 140px; min-height: 44px; }
  .lf-search .btn { min-height: 44px; }
  .lf-pager a { min-height: 44px; }
  .lf-clear { padding: 12px 4px; margin: -12px 0; }
}
.side-nav li + li { margin-top: 2px; }
.side-item { display: block; padding: 9px 11px; border-radius: 10px;
  color: #334155; text-decoration: none; transition: background .12s ease, color .12s ease; }
.side-item:hover { background: #f4f7fb; color: #0f172a; }
.side-label { display: flex; align-items: center; gap: var(--s-2);
  font-size: var(--t-md); font-weight: 600; }
.side-hint { display: block; margin-top: 1px; color: #94a3b8; font-size: 11.5px; }
/* The hint lines up under the label's TEXT, not under its icon — and only
   where there is an icon to clear. A section whose entries count instead of
   drawing (quotes) would otherwise indent every hint past nothing. */
.side-item.has-ic .side-hint { padding-left: calc(1.05em + var(--s-2)); }
/* The count sits at the far end of the label's row, where a reader scanning
   the menu finds all six in one column. Tabular figures so 4 and 11 line up
   and the column does not shimmer as the numbers change. */
.side-count { margin-left: auto; padding: 1px 7px; border-radius: var(--r-pill);
  background: #eef2f7; color: #64748b; font-size: 11.5px; font-weight: 700;
  font-variant-numeric: tabular-nums; }
.side-item.is-here .side-count { background: #dbe6ff; color: #1d4ed8; }
/* WHERE YOU ARE, said twice — a filled tint and a bar — because a tint alone
   is invisible to anyone who cannot tell #eef2ff from white, and this menu has
   no other way of answering "which page is this". */
.side-item.is-here { position: relative; background: #eff4ff; }
.side-item.is-here::before { content: ""; position: absolute; left: 0; top: 9px; bottom: 9px;
  width: 3px; border-radius: 0 3px 3px 0; background: #2563eb; }
.side-item.is-here .side-label { color: #1d4ed8; }
.side-item.is-here .side-hint { color: #7d9bd6; }

/* A GRID, NOT A DRAWER AND NOT A SCROLLING STRIP, on a phone. A hamburger
   hides the entries behind a tap and a stacked column pushes the page below
   the fold; the one-line strip this replaced (2026-09-26, owner's phone) cut
   its third entry mid-word — "Due for a vis" — and hid the rest off the edge
   with nothing saying they were there. Every entry is now whole and visible:
   as many columns as fit at 104px, labels wrapping inside them, and 44px as
   the floor a thumb needs. Six CRM entries are two rows at 390px. */
@media (max-width: 820px) {
  .section-shell { grid-template-columns: minmax(0, 1fr); gap: 14px; }
  .side-nav { position: static; padding: 4px; }
  .side-nav ul { display: grid; grid-template-columns: repeat(auto-fill, minmax(104px, 1fr)); gap: 4px; }
  .side-nav li + li { margin-top: 0; }
  .side-item { display: flex; align-items: center; min-height: 44px; height: 100%;
    padding: 6px 9px; border-radius: 9px; box-sizing: border-box; }
  .side-label { gap: 6px; font-size: 13.5px; line-height: 1.2; }
  /* THE ICON ABOVE THE WORD, as a phone's own tab bar does it, so a column is
     as wide as its longest word and no wider: beside it, "Appointments" broke
     after "Appointm" at three columns (2026-09-26). Entries with a count and
     no icon (Financials) keep the count beside the word. */
  .side-item.has-ic { justify-content: center; }
  .side-item.has-ic .side-label { flex-direction: column; gap: 3px; text-align: center; }
  /* THE SECTION MENU READS AS NAVIGATION (owner, 2026-09-26: on a phone the
     white grid looked like one more set of buttons on the page). A navy bar
     like the header above it; the entries in light semibold type; the page
     you are on in solid blue and bold type, and aria-current for a screen
     reader — so "which page is this" never rests on colour alone. A white
     bar under it was a third cue until the owner's v4 review took it off.
     The same treatment is on Voice's rail (tenant.html). */
  .side-nav { background: #16254a; border-color: #16254a; box-shadow: none; }
  .side-item { color: #e2e8f0; }
  .side-item .side-label { color: inherit; font-weight: 600; }
  .side-item .ic { color: #a5b4fc; }
  .side-item:hover { background: rgba(255,255,255,.08); color: #fff; }
  .side-item.is-here { background: #2563eb; box-shadow: none; }
  .side-item.is-here .side-label { color: #fff; font-weight: 750; }
  .side-item.is-here .ic { color: #fff; }
  .side-count { background: rgba(255,255,255,.14); color: #e2e8f0; }
  .side-item.is-here .side-count { background: rgba(255,255,255,.25); color: #fff; }
  .side-item:focus-visible { outline: 3px solid #93c5fd; outline-offset: 1px; }
  .side-item.is-here::before { display: none; }
  .side-hint { display: none; }  /* the label alone: the grid has no room for a second line */
}

/* A TABLE THAT BECOMES LABELLED ROWS ON A PHONE — opt in with class="m-rows"
   (owner's phone pass, 2026-09-26). The site-wide phone rule makes every
   table a sideways scroller, which hid the Contacts phone numbers off the
   edge; the lobby treatment puts each label on its own line above its value,
   which made a five-column row as tall as a screen. This is the middle: a
   fixed label gutter on the left and the value reading after it, one line per
   field. ``td.m-lead`` is the row's title and takes the full width with no
   label; ``td.m-acts`` holds its buttons in a row, thumb-sized. The markup
   keeps its <th>s (visually hidden here, still read aloud) and each <td>
   carries the data-label drawn in the gutter. Its own name because .lobby-,
   .calls- and .users-table are other pages' own treatments. */
@media (max-width: 760px) {
  table.m-rows, table.m-rows tbody, table.m-rows tr { display: block; width: 100%; }
  table.m-rows { overflow-x: visible; }
  table.m-rows thead { position: absolute; width: 1px; height: 1px; overflow: hidden;
    clip: rect(0 0 0 0); white-space: nowrap; }
  table.m-rows tr { padding: 10px 0; border-bottom: 1px solid var(--va-border); }
  table.m-rows tr:last-child { border-bottom: 0; }
  table.m-rows td { display: block; position: relative; min-height: 1.4em; border: 0;
    padding: 2px 0 2px 6.4rem; white-space: normal !important; text-align: left; }
  table.m-rows td[data-label]::before { content: attr(data-label); position: absolute;
    left: 0; top: 3px; width: 6rem; margin: 0; color: var(--va-muted);
    font-size: 12px; font-weight: 600; line-height: 1.3; }
  table.m-rows td:empty { display: none; }
  table.m-rows td.m-lead { padding-left: 0; font-weight: 700; font-size: 15px; }
  table.m-rows td.m-lead::before { content: none; }
  table.m-rows td.m-acts { display: flex; flex-wrap: wrap; gap: 8px; padding: 8px 0 0; }
  table.m-rows td.m-acts::before { content: none; }
  table.m-rows td.m-acts .btn, table.m-rows td.m-acts > a { min-height: 40px;
    display: inline-flex; align-items: center; }
  table.m-rows td[colspan] { padding-left: 0; }
}

/* FULL WIDTH ON A PHONE (owner, 2026-09-26, from the v2 screenshots: "even
   small wasted margins must be fixed"). Measured at 390px, a Contacts record
   sat 46px in from each edge: main's 12px margin, its 16px padding, the
   card's 1px border and 16px padding, and the table's own 1px frame — a card
   inside a card inside a padded page, with the text flush against the
   innermost border anyway. On a phone, in the sections that pass covered
   — at first CRM, Financials and Voice; since the owner's v7 direction
   (2026-09-26) every signed-in page, Visitors included, except the two that
   are full-bleed on a phone by their own earlier decision — the chat
   (assistant.html) and Files (main:has(.files-page), above):
     * the page is the viewport less an 8px gutter, or the safe-area inset
       where a notch is wider;
     * a card whose whole job is to hold a list of records loses its frame and
       padding, so the list is not framed twice;
     * a form card keeps a light frame, with 12px inside rather than 16px;
     * each record (a table.m-rows row, a call) becomes its own card, 10-12px
       inside, 8px apart — the padding that was outside the text now sits
       beside it. */
@media (max-width: 760px) {
  body:has(#nav-toggle) main:not(:has(.chat-wrap, .files-page)) {
    width: 100%; max-width: none; box-sizing: border-box; margin: 8px 0 24px;
    padding: 0 max(8px, env(safe-area-inset-right)) 0 max(8px, env(safe-area-inset-left)); }
  body:has(#nav-toggle) main:not(:has(.chat-wrap, .files-page)) :is(.card, .invo-box):has(table.m-rows, table.calls-table, #crm-container) {
    padding: 0; border: 0; border-radius: 0; background: transparent; box-shadow: none; }
  body:has(#nav-toggle) main:not(:has(.chat-wrap, .files-page)) :is(.card, .invo-box):not(:has(table.m-rows, table.calls-table, #crm-container)) {
    padding: 12px; }
  body:has(#nav-toggle) main:not(:has(.chat-wrap, .files-page)) :is(table.m-rows, table.calls-table) {
    border: 0; border-radius: 0; background: transparent; box-shadow: none; }
  body:has(#nav-toggle) main:not(:has(.chat-wrap, .files-page)) :is(table.m-rows, table.calls-table) tbody tr {
    margin: 0 0 8px; padding: 10px 12px; border: 1px solid var(--va-border);
    border-radius: 10px; background: #fff; }
  body:has(#nav-toggle) main:not(:has(.chat-wrap, .files-page)) :is(table.m-rows, table.calls-table) tbody tr:last-child {
    border-bottom: 1px solid var(--va-border); margin-bottom: 0; }
  /* The gutter label is narrower now the records are: a 5.3rem label in a
     5.6rem gutter holds "Last contact" and "Oldest due" on one line at 12px. */
  body:has(#nav-toggle) main:not(:has(.chat-wrap, .files-page)) table.m-rows td:not(.m-lead):not(.m-acts):not([colspan]),
  body:has(#nav-toggle) main:not(:has(.chat-wrap, .files-page)) table.calls-table td:not(.calls-flags) { padding-left: 5.6rem; }
  body:has(#nav-toggle) main:not(:has(.chat-wrap, .files-page)) :is(table.m-rows, table.calls-table) td[data-label]::before { width: 5.3rem; }
  /* The one label longer than the gutter wraps to two lines; the cell is
     held open for them so it cannot run into the row under it. */
  .voiceshell table.calls-table td[data-label="Calls in last 72h"] { min-height: 2.7em; }

  /* CONTACTS' CONTROLS: the search fills the row beside a compact "⬇ CSV"
     button (the word "Download" is dropped here; _crm_card.html);
     owner and stage share a row, the tag box and the 30-day tick the next. */
  .crm-searchrow { flex-wrap: nowrap; gap: 8px !important; margin-bottom: 8px !important; }
  .crm-searchrow #crm-search { flex: 1 1 auto !important; min-width: 0; }
  .crm-searchrow .btn { min-height: 44px; flex: 0 0 auto; padding-left: 12px; padding-right: 12px; }
  .crm-searchrow .crm-csv-word { display: none; }
  #crm-filters { display: grid !important; grid-template-columns: 1fr 1fr; gap: 8px !important;
    margin-bottom: 10px !important; }
  #crm-filters select, #crm-filters input[type=text] { width: 100%; max-width: none !important;
    min-width: 0; flex: none; }
  #crm-filters > * { align-self: center; }
  #crm-filters label { min-height: 44px; font-size: 13.5px; line-height: 1.2; }

  /* WHO OWES YOU, TWO LINES A CUSTOMER: name and balance on the first, the
     count and the oldest due date under them. Every value is still there and
     still labelled — the labels just ride inline rather than in a gutter. */
  table.invo-owed tbody tr { display: grid; grid-template-columns: minmax(0, 1fr) auto;
    column-gap: 10px; row-gap: 2px; align-items: baseline; }
  table.invo-owed td { padding: 0 !important; }
  table.invo-owed td[data-label]::before { position: static; display: inline; width: auto;
    margin-right: 5px; }
  table.invo-owed td.m-lead { grid-column: 1; grid-row: 1; }
  table.invo-owed td[data-label="Balance"] { grid-column: 2; grid-row: 1; text-align: right; font-size: 15px; }
  table.invo-owed td[data-label="Balance"]::before { content: none; }
  table.invo-owed td[data-label="Invoices"] { grid-column: 1; grid-row: 2; font-size: 13px; }
  table.invo-owed td[data-label="Oldest due"] { grid-column: 2; grid-row: 2; text-align: right; font-size: 13px; }
  table.invo-owed td[data-label="Oldest due"]::before { content: "oldest due"; }
}

/* CONTACTS AS COLLAPSED CARDS ON A PHONE (owner, v4 review, 2026-09-26).
   Every card open made eighteen contacts a very long scroll. Closed, a card is
   its .m-toggle: the name, the phone (or the company when there is no name to
   put first), a chevron, 44px or more to press. Open, it is the labelled rows
   and the actions it had before. The button is the only thing that toggles;
   the script is in _crm_card.html. Desktop never draws the button. */
.m-toggle { display: none; }
@media (max-width: 760px) {
  .js table.crm-contacts .crm-name { display: none; }
  .js table.crm-contacts .m-toggle { display: flex; align-items: center; justify-content: space-between;
    gap: 10px; width: 100%; min-height: 44px; margin: -4px 0; padding: 4px 0; border: 0;
    background: none; color: inherit; font: inherit; text-align: left; cursor: pointer;
    -webkit-tap-highlight-color: transparent; }  /* the chevron turning is the feedback */
  table.crm-contacts .m-toggle:focus-visible { outline: 3px solid #93c5fd; outline-offset: 2px; border-radius: 6px; }
  table.crm-contacts .m-toggle-text { display: flex; flex-direction: column; min-width: 0; }
  table.crm-contacts .m-toggle-name { font-weight: 700; font-size: 15px; overflow-wrap: anywhere; }
  table.crm-contacts .m-toggle-sub { color: var(--va-muted); font-size: 13px; font-weight: 500;
    overflow-wrap: anywhere; }
  table.crm-contacts .m-chev { flex: 0 0 auto; color: #64748b; transition: transform .15s ease; }
  table.crm-contacts tr.is-open .m-chev { transform: rotate(180deg); }
  .js table.crm-contacts tr[data-contact]:not(.is-open) > td:not(.m-lead) { display: none; }
  table.crm-contacts tr[data-contact].is-open > td.m-lead { margin-bottom: 6px; }
}

/* A ROW THE SEARCH HID STAYS HIDDEN (review, 2026-09-26). tablefilter.js
   hides a row with the hidden attribute, and every "display: block" given to
   a stacked row on a phone outranks the browser's own [hidden] rule — so on a
   phone the search box narrowed nothing. */
@media (max-width: 760px) {
  :is(table.m-rows, table.appt-table, table.lobby-table, table.calls-table) tr[hidden] { display: none !important; }
}

/* THE SHARED DISCLOSURE: a list row that is a collapsed card on a phone
   (2026-09-26). Markup: the row carries data-disclosure-root and an id; its
   first cell (.m-disclosure-cell) holds a .m-disclosure <button> — title,
   one line under it, a chevron — with aria-expanded and aria-controls, and
   nothing pressable inside it. Pressed, the row gets .is-open and its other
   cells show. The script is one delegated listener in base.html. Contacts,
   calls and due rows predate this and keep their own, identical, pattern. */
/* table td.… so a page's own stacked-row rule (".appt-table td { display:
   block }") cannot draw the summary with no script to press it. */
table td.m-disclosure-cell { display: none; }
@media (max-width: 760px) {
  .js body:has(#nav-toggle) main [data-disclosure-root] > td.m-disclosure-cell { display: block; padding: 0; }
  body:has(#nav-toggle) main [data-disclosure-root] > td.m-disclosure-cell::before { content: none; }
  .js body:has(#nav-toggle) main [data-disclosure-root]:not(.is-open) > td:not(.m-disclosure-cell) { display: none; }
  body:has(#nav-toggle) main [data-disclosure-root].is-open > td.m-disclosure-cell { margin-bottom: 6px; }
  .m-disclosure { display: flex; align-items: center; justify-content: space-between; gap: 10px;
    width: 100%; min-height: 44px; margin: -4px 0; padding: 4px 0; border: 0; background: none;
    color: inherit; font: inherit; text-align: left; cursor: pointer; -webkit-tap-highlight-color: transparent; }
  .m-disclosure:focus-visible { outline: 3px solid #93c5fd; outline-offset: 2px; border-radius: 6px; }
  .m-disclosure-text { display: flex; flex-direction: column; min-width: 0; }
  .m-disclosure-title { font-weight: 700; font-size: 15px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
  .m-disclosure-sub { color: var(--va-muted); font-size: 13px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
  .m-disclosure-chev { flex: 0 0 auto; color: #64748b; transition: transform .15s ease; }
  [data-disclosure-root].is-open .m-disclosure-chev { transform: rotate(180deg); }
}

/* ON A PHONE THE MENU IS THE TITLE (owner, 2026-09-26, reviewing v3). The
   highlighted entry in the section menu already says which page this is, so
   the page's own heading, the sentence under it and its "Watch tutorial"
   link were three more lines saying it again above the work. On a phone, and
   only there:
     * the heading in a .m-pagehead row (and Voice's .m-pagetitle) is visually
       hidden but still the page's heading for a screen reader and the
       document outline;
     * the row's Watch tutorial link is not drawn — /tutorials still lists
       every guide, and the desktop page still has its link;
     * a .m-intro sentence is not drawn.
   Everything else in the heading row stays: count pills, Download CSV, the
   reminders switch. The hooks are template classes on those exact elements,
   never a rule about h2 or p in general, so a status line, an error, a
   total or a filter cannot be caught by this. Desktop is unchanged. */
@media (max-width: 760px) {
  body:has(#nav-toggle) main:not(:has(.chat-wrap, .files-page)) .m-pagehead > :is(h1, h2, h3),
  body:has(#nav-toggle) main:not(:has(.chat-wrap, .files-page)) .m-pagetitle {
    position: absolute; width: 1px; height: 1px; padding: 0; margin: -1px;
    overflow: hidden; clip: rect(0 0 0 0); white-space: nowrap; border: 0; }
  body:has(#nav-toggle) main:not(:has(.chat-wrap, .files-page)) .m-pagehead .watch-tutorial,
  body:has(#nav-toggle) main:not(:has(.chat-wrap, .files-page)) .m-notutorial .watch-tutorial,
  /* A tab's "name + Watch tutorial" line (Reception, Kiosk settings): the lit
     tab above already names it. */
  body:has(#nav-toggle) main:not(:has(.chat-wrap, .files-page)) p.tutorial-line,
  body:has(#nav-toggle) main:not(:has(.chat-wrap, .files-page)) .m-intro { display: none; }
  body:has(#nav-toggle) main:not(:has(.chat-wrap, .files-page)) .m-pagehead { margin: 0 0 8px; }
  body:has(#nav-toggle) main:not(:has(.chat-wrap, .files-page)) .m-pagehead:not(:has(> :not(h1, h2, h3, .watch-tutorial))) { margin: 0; }
}

/* A CALL IS A COLLAPSED CARD ON A PHONE (owner, v7 review, 2026-09-26), the
   contact list's pattern: closed, the card is .call-toggle — caller (or the
   number), date, time and length, a chevron, 44px or more; open, it is every
   labelled cell it always had, the buttons that open the call's dialog
   included, with the summary in full. Desktop never draws the .call-sum cell.
   The script is in tenant.html beside the row's own listener. */
.calls-table td.call-sum { display: none; }
@media (max-width: 760px) {
  .js body:has(#nav-toggle) main .voicebody table.calls-table td.call-sum { display: block; padding: 0; }
  body:has(#nav-toggle) main .voicebody table.calls-table td.call-sum::before { content: none; }
  .js body:has(#nav-toggle) main .voicebody table.calls-table tr.call-row:not(.is-open) > td:not(.call-sum) { display: none; }
  body:has(#nav-toggle) main .voicebody table.calls-table tr.call-row.is-open > td.call-sum { margin-bottom: 6px; }
  body:has(#nav-toggle) main .voicebody tr.call-row.is-open .calls-summary { -webkit-line-clamp: unset; line-clamp: none; display: block; }
  .call-toggle { display: flex; align-items: center; justify-content: space-between; gap: 10px;
    width: 100%; min-height: 44px; margin: -4px 0; padding: 4px 0; border: 0; background: none;
    color: inherit; font: inherit; text-align: left; cursor: pointer; -webkit-tap-highlight-color: transparent; }
  .call-toggle:focus-visible { outline: 3px solid #93c5fd; outline-offset: 2px; border-radius: 6px; }
  .call-toggle-text { display: flex; flex-direction: column; min-width: 0; }
  .call-toggle-who { font-weight: 700; font-size: 15px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
  .call-toggle-meta { color: var(--va-muted); font-size: 13px; }
  .call-chev { flex: 0 0 auto; color: #64748b; transition: transform .15s ease; }
  tr.call-row.is-open .call-chev { transform: rotate(180deg); }
  /* The desktop row's hover tint (tenant.html) is a phone's tap tint: it
     greyed the open card. The chevron is the feedback here. */
  body:has(#nav-toggle) main .voicebody tr.call-row:hover td { background: none; }
  /* The account's line number above the menu (tenant.html) — Phone lines
     lists and manages them; this was a line of chrome before the page. */
  body:has(#nav-toggle) main .m-accountline { display: none; }
}

/* A PAGE'S HEADING ROW ON A PHONE. Nearly every section page opens with a
   wrapping row — heading, "Watch tutorial", a count pill, sometimes a button —
   and then a muted paragraph pulled UP by an inline margin-top:-6px, which on
   a desktop tucks the sentence under a one-line heading. On a phone the row
   wraps to two or three lines and that same pull landed the sentence on the
   pill or the Download CSV button (owner's phone pass, 2026-09-26). Here the
   wrapped rows get a gap between their lines and the sentence its own space.
   Every signed-in page since the owner's v7 direction (2026-09-26). */
@media (max-width: 640px) {
  body:has(#nav-toggle) main:not(:has(.chat-wrap, .files-page)) .section-main > :is(div[style*="flex-wrap:wrap"], .pipe-head, .quo-head, .appt-head, .invo-head) { row-gap: 8px; }
  body:has(#nav-toggle) main:not(:has(.chat-wrap, .files-page)) .section-main > :is(div[style*="flex-wrap:wrap"], .pipe-head, .quo-head, .appt-head, .invo-head) + p.muted { margin-top: 8px !important; }
  /* (The appointment book's reminder switch had a rule here until
     2026-10-01, when it left the header for the Settings tab.) */
}

/* THUMB-SIZED CONTROLS ON A PHONE, in the sections the owner's phone pass
   covered (2026-09-26) — every signed-in page since the v7 direction. The audit at
   390px found the shared small controls under 40px on every page — the
   "Watch tutorial" link and the "Add contacts" / "Correct this" expanders at
   20px, the chip rows at 29-32px. Each keeps its look; the chips grow to the
   44px floor, and the two text-sized links get the height as padding, taken
   back with a negative margin so the line they sit on does not move. */
@media (max-width: 640px) {
  /* The appointment tabs and Day | Week | List size themselves to 44px in
     their own partials since 2026-10-01 (_appt_views.html,
     _appt_segments.html), so they are not in this list. */
  body:has(#nav-toggle) main:not(:has(.chat-wrap, .files-page)) :is(.lf-chip, .filter-btn) {
    min-height: 44px; display: inline-flex; align-items: center; box-sizing: border-box; }
  body:has(#nav-toggle) main:not(:has(.chat-wrap, .files-page)) :is(.watch-tutorial, details > summary) {
    padding-top: 12px; padding-bottom: 12px; margin-top: -12px; margin-bottom: -12px; }
}

/* THE REST OF THE SMALL TARGETS (the all-page audit, 2026-09-26). Each is a
   named control the 390px audit measured under 40px, not a rule about every
   link: the buttons and chips reach 44px; the text links that are a record's
   title or a way back get the height as padding, taken back by a negative
   margin, so the line they sit on does not move. */
@media (max-width: 760px) {
  body:has(#nav-toggle) main:not(:has(.chat-wrap, .files-page)) :is(td.lobby-act .btn, a.appt-step, .appt-head a.pill,
      .acc-quick a, button.itk-copy, .itk-q-acts button, button.ask-chip, .help-contents a) {
    min-height: 44px; display: inline-flex; align-items: center; box-sizing: border-box; }
  body:has(#nav-toggle) main:not(:has(.chat-wrap, .files-page)) .itk-q-acts button { min-width: 44px; justify-content: center; }
  /* The logo drop zone (settings.html) would not shrink below its file box
     and button, and ran 47px past the edge at 320px. */
  body:has(#nav-toggle) main .drop-zone { min-width: 0; max-width: 100%; flex: 1 1 100%; }
  body:has(#nav-toggle) main .drop-zone input[type=file] { min-width: 0; max-width: 100%; }
  body:has(#nav-toggle) main:not(:has(.chat-wrap, .files-page)) :is(.pipe-title a, .appt-top h3 a, .day-who a,
      .itk-form-card h3 a, .quo-top a.muted, .invo-top a.muted, .itk-bar a.muted, .acc-sub a) {
    display: inline-block; padding-top: 12px; padding-bottom: 12px; margin-top: -12px; margin-bottom: -12px; }
}

/* FILTER ROWS ARE ROWS. `select { width: 100% }` is global above, so a flex
   row of filters becomes one control per line — which is what the CRM's three
   filter bars did from the day they shipped until somebody looked at them on
   2026-09-20. Width auto with a floor, so a long owner address still reads. */
#crm-filters select, #crm-filters input[type=text],
.pipe-filter select, .rep-when select { width: auto; min-width: 130px; max-width: 230px; }
@media (max-width: 560px) {
  #crm-filters select, #crm-filters input[type=text],
  .pipe-filter select, .rep-when select { max-width: none; flex: 1 1 140px; }
}

/* --------------------------------------------------------------------------
   THE PER-TABLE SEARCH BOX (app/static/tablefilter.js, owner 2026-09-20).
   Deliberately quiet: it sits above the table it narrows and must not compete
   with the page's own heading or with the global search in the top bar. Its
   own prefix, because .card and .panel are global here.
   -------------------------------------------------------------------------- */
.tfilter { display: flex; align-items: center; gap: 10px; margin: 0 0 10px; }
.tfilter-box { width: auto; min-width: 220px; max-width: 320px; flex: 0 1 320px;
  padding: 7px 11px; border: 1px solid #dbe3ef; border-radius: 9px;
  background: #fff; font: inherit; font-size: 13.5px; }
.tfilter-box:focus { outline: 2px solid #bfdbfe; outline-offset: 1px; border-color: #93c5fd; }
.tfilter-said { color: var(--va-muted); font-size: 12.5px;
  font-variant-numeric: tabular-nums; }
.tfilter-none { padding: 14px 12px; color: #94a3b8; font-size: 13px; }
@media (max-width: 560px) {
  .tfilter-box { flex: 1 1 auto; max-width: none; }
}

/* The one-off that lives on the contact list rather than in the menu
   (app/templates/contacts.html). Shut, it is one line; open, it is a form. */
.crm-import { margin: 0 0 16px; padding: 10px 14px; border: 1px solid #e9edf3;
  border-radius: 12px; background: #fbfcfe; }
.crm-import > summary { cursor: pointer; font-size: 13.5px; font-weight: 600;
  color: #1d4ed8; list-style: none; }
.crm-import > summary::-webkit-details-marker { display: none; }
/* A PLUS AND NOT A DOWNLOAD ARROW. The glyph was "⤓" while the summary said
   "Import a list"; it now says "Add contacts", of which importing is one of
   two ways, and an arrow into a box is a picture of only the other one. */
.crm-import > summary::before { content: "+ "; }
/* The one-person form: a row that wraps, and the button sits on the baseline
   of the boxes rather than under them. */
.crm-one-row { display: flex; flex-wrap: wrap; gap: var(--s-3);
  align-items: flex-end; }
.crm-one-row > div { flex: 1 1 170px; min-width: 0; }
.crm-one-row label { display: block; }
.crm-one h4, .crm-import > form + h4 { color: #475569; }
.crm-import[open] > summary { margin-bottom: 10px; }
.crm-import input[type=file] { margin-bottom: 10px; }

/* ==========================================================================
   THE SHARED VOCABULARY (2026-09-20).
   Five things every screen in this product draws, defined once. Before this,
   each new section wrote its own: the CRM board, the quotes list, the intake
   builder, the report and the day all had their own card, their own badge and
   their own empty state, agreeing on nothing. A page may still style what is
   genuinely its own — a pipeline column's stage colour is not a token — but
   it should not be re-deciding what a card is.
   ========================================================================== */

/* 1. A SECTION LABEL. Small, tracked, uppercase; the thing above a group. */
.hd {
  display: flex; align-items: baseline; gap: var(--s-2);
  margin: 0 0 var(--s-1);
  color: #334155; font-size: var(--label-size); font-weight: 700;
  letter-spacing: var(--label-track); text-transform: uppercase;
}

/* 2. A BADGE. One shape, five tones, always beside a word — never the only
      thing carrying the meaning, which is the rule the status colours in the
      block above were introduced with. */
.badge {
  display: inline-block; padding: 2px 9px;
  border: 1px solid var(--tone-mute-line); border-radius: var(--r-pill);
  background: var(--tone-mute-soft); color: var(--tone-mute);
  font-size: var(--t-xs); font-weight: 700; white-space: nowrap;
  font-variant-numeric: tabular-nums;
}
.badge.ok   { border-color: var(--tone-ok-line);   background: var(--tone-ok-soft);   color: var(--tone-ok); }
.badge.warn { border-color: var(--tone-warn-line); background: var(--tone-warn-soft); color: var(--tone-warn); }
.badge.bad  { border-color: var(--tone-bad-line);  background: var(--tone-bad-soft);  color: var(--tone-bad); }
.badge.info { border-color: var(--tone-info-line); background: var(--tone-info-soft); color: var(--tone-info); }

/* 3. A FIGURE. A number worth reading across a room, with its label under it
      and a tone stripe down the side. The report invented this; the pipeline
      column totals and the day's counts want the same thing. */
.stat {
  --accent: var(--tone-mute-line);
  position: relative; overflow: hidden;
  padding: var(--s-4) var(--s-4) var(--s-3);
  border: 1px solid var(--line); border-radius: var(--r-xl);
  background: var(--va-surface); box-shadow: var(--sh-1);
}
.stat::before { content: ""; position: absolute; inset: 0 auto 0 0; width: 3px; background: var(--accent); }
.stat .n { font-size: var(--t-xl); font-weight: 750; line-height: 1.05;
           letter-spacing: -.02em; color: var(--va-ink); font-variant-numeric: tabular-nums; }
.stat .l { margin-top: var(--s-1); color: var(--va-muted); font-size: var(--t-sm); line-height: 1.35; }
.stat.ok   { --accent: #10b981; } .stat.ok .n   { color: var(--tone-ok); }
.stat.bad  { --accent: #ef4444; } .stat.bad .n  { color: var(--tone-bad); }
.stat.info { --accent: var(--va-brand); } .stat.info .n { color: var(--tone-info); }

/* 4. AN EMPTY STATE. Dashed, quiet, and it always says what to do next — a
      blank area where a list should be reads as a page that failed to load. */
.emptystate {
  padding: var(--s-4); border: 1px dashed var(--line); border-radius: var(--r-lg);
  background: var(--va-surface-subtle); color: var(--va-muted); font-size: var(--t-md);
}

/* 5. A TOOLBAR. The row of controls above a list. `select { width: 100% }` is
      global here, so without this every filter row becomes one control per
      line — which is exactly what the CRM's three filter bars did for a week. */
.toolbar { display: flex; align-items: center; gap: var(--s-2); flex-wrap: wrap;
           margin: 0 0 var(--s-3); }
.toolbar select, .toolbar input[type=text], .toolbar input[type=search] {
  width: auto; min-width: 130px; max-width: 230px;
}
@media (max-width: 560px) {
  .toolbar select, .toolbar input[type=text], .toolbar input[type=search] {
    max-width: none; flex: 1 1 140px;
  }
}

/* A ROW that behaves like a card — the day's follow-ups and the pipeline's
   cards both want this and both wrote it. */
.rowcard {
  padding: var(--s-3) var(--s-4); margin-bottom: var(--s-2);
  border: 1px solid var(--line); border-radius: var(--r-lg);
  background: var(--va-surface); box-shadow: var(--sh-1);
  transition: box-shadow .15s ease, border-color .15s ease, transform .15s ease;
}
.rowcard:hover { box-shadow: var(--sh-2); border-color: #d7dfea; }

/* One focus ring for the whole product. :focus-visible, so a mouse click does
   not draw it and a Tab press does. */
:where(a, button, input, select, textarea, summary, [tabindex]):focus-visible {
  outline: none; box-shadow: var(--focus); border-radius: var(--r-xs);
}

/* THE ICON SIZE, and only the size — the shapes are in app/templates/_icons.html
   and take their colour from the text they sit beside. 1em so an icon scales
   with whatever it is labelling rather than needing a size per context. */
.ic { width: 1.05em; height: 1.05em; flex: 0 0 auto; vertical-align: -.15em; }

/* THE FILE PICKER, WEARING WHAT EVERY OTHER CONTROL WEARS. Seven screens ask
   for a file — a logo, a staff list, a contact import, a support attachment —
   and each one showed the browser's own grey "Choose File" next to a product
   that had been carefully dressed everywhere else. ::file-selector-button
   restyles the button half in place, without the label-wrapping-a-hidden-input
   trick: that swaps a real control for a fake one, and the fake one loses the
   filename, the keyboard behaviour and the drop target the real one has.
   The filename half stays the browser's, because a reader deciding whether the
   right file is attached should be reading the browser and not us. */
input[type="file"] {
  max-width: 100%; padding: 6px 10px; border: 1px solid #cbd5e1;
  border-radius: var(--r-sm); background: #fff; color: #475569;
  font: inherit; font-size: var(--t-md); cursor: pointer;
}
input[type="file"]:hover { border-color: #94a3b8; }
input[type="file"]:focus-visible { outline: none; border-color: #93c5fd;
  box-shadow: var(--focus); }
input[type="file"]::file-selector-button {
  margin-right: var(--s-3); padding: 6px 12px; border: 1px solid #cbd5e1;
  border-radius: var(--r-xs); background: #f8fafc; color: #0f172a;
  font: inherit; font-size: var(--t-md); font-weight: 650; cursor: pointer;
}
input[type="file"]:hover::file-selector-button { background: #eef2f7; }

/* THE SETUP CHECKLIST on the reception page (2026-09-27,
   services/setup_checklist.py). setup- because .card and .panel are taken. */
.setup-card { border: 1px solid var(--va-border); border-radius: 12px; background: var(--va-surface);
  padding: 18px 20px; margin: 14px 0 18px }
.setup-card[hidden] { display: none }
.setup-head { display: flex; gap: 12px; justify-content: space-between; align-items: flex-start;
  flex-wrap: wrap }
.setup-head h3 { margin: 0 0 2px }
.setup-head p { margin: 0 }
.setup-hide[hidden] { display: none !important }
.setup-list { list-style: none; margin: 14px 0 0; padding: 0; display: grid; gap: 10px }
.setup-list li { display: flex; gap: 12px; align-items: flex-start }
.setup-list p { margin: 2px 0 0; font-size: 14px }
.setup-mark { flex: none; width: 26px; height: 26px; border-radius: 13px; display: inline-flex;
  align-items: center; justify-content: center; font-weight: 700; font-size: 13px;
  background: var(--va-surface-subtle); border: 1px solid var(--va-border); color: var(--va-ink) }
.setup-list li.is-done .setup-mark { background: var(--va-in-soft); border-color: var(--va-in-line);
  color: var(--va-in) }
/* NO STRIKE-THROUGH ON A DONE STEP (2026-09-30): done titles now say what is
   there ("Your enquiry form is live"), and a line through that reads as
   "cancelled", on the very step whose link we want copied. */
.setup-list li.is-done .setup-title { color: var(--va-ink) }
.setup-share { overflow-wrap: anywhere }
.setup-links { display: flex; flex-wrap: wrap; gap: 4px 16px }
.setup-title { font-weight: 600 }
.setup-optional { margin-left: 8px; font-size: 12px; color: var(--va-muted); border: 1px solid var(--va-border);
  border-radius: 999px; padding: 1px 8px }
.setup-note { color: var(--va-off); }

/* THE GUIDE (2026-09-27, _guide.html). guide- because nothing else uses it. */
.guide { position: fixed; right: 18px; bottom: 18px; z-index: 60 }
/* ROOM FOR THE BUTTON WHEN THE BROWSER SCROLLS (2026-09-29). Tabbing to a
   field, find-in-page and a jump link bring the target to the nearest edge,
   and at the bottom edge the fixed button sat on it: the quote editor's
   Discount box, at 1440 wide and on a phone, found re-taking the quotes film.
   The button is 43px tall and 18px up, so 80px keeps anything scrolled to
   clear of it, and only on pages that draw it. */
html:has(.guide) { scroll-padding-bottom: 80px }
.guide-open { display: inline-flex; align-items: center; gap: 8px; border: 0; cursor: pointer;
  background: var(--va-brand); color: #fff; font-weight: 700; font-size: 14px;
  padding: 11px 16px; border-radius: 999px; box-shadow: 0 6px 20px rgba(23, 32, 51, .22) }
.guide-open:hover, .guide-open:focus-visible { background: var(--va-brand-dark) }
.guide-panel { position: absolute; right: 0; bottom: 56px; width: min(380px, calc(100vw - 36px));
  max-height: min(560px, calc(100vh - 110px)); display: flex; flex-direction: column;
  background: var(--va-surface); border: 1px solid var(--va-border); border-radius: 14px;
  box-shadow: 0 16px 44px rgba(23, 32, 51, .22); overflow: hidden }
.guide-panel[hidden] { display: none }
/* EXPANDED (2026-09-29, owner): a tall side panel, not full screen — the page
   the person is asking about stays in view (_guide.html says why). Below the
   header, above the Assistant button, about twice the width. Desktop only:
   on a phone the panel already fills the screen and the button is hidden. */
.guide-head-acts { display: flex; align-items: center; gap: 2px }
.guide-size { border: 0; background: none; cursor: pointer; color: var(--va-muted);
  width: 36px; height: 36px; border-radius: 8px; display: inline-flex; align-items: center;
  justify-content: center }
.guide-size:hover, .guide-size:focus-visible { background: var(--va-surface-subtle); color: var(--va-ink) }
.guide-size .guide-size-shrink, .guide-size[aria-pressed="true"] .guide-size-grow { display: none }
.guide-size[aria-pressed="true"] .guide-size-shrink { display: inline }
@media (min-width: 761px) {
  .guide-panel.is-wide { position: fixed; top: 76px; right: 18px; bottom: 80px;
    width: min(720px, calc(100vw - 36px)); max-height: none }
}
@media (max-width: 760px) {
  .guide-size { display: none }
}
.guide-head { display: flex; justify-content: space-between; align-items: center;
  padding: 12px 14px; border-bottom: 1px solid var(--va-border) }
.guide-head h2 { margin: 0; font-size: 16px }
.guide-close { border: 0; background: none; font-size: 22px; line-height: 1; cursor: pointer;
  color: var(--va-muted); width: 36px; height: 36px; border-radius: 8px }
.guide-close:hover, .guide-close:focus-visible { background: var(--va-surface-subtle); color: var(--va-ink) }
/* ONE COLUMN THE WIDTH OF THE PANEL, NEVER WIDER (2026-09-28, owner review of
   the staff table): an auto-sized grid column grew to the widest thing in the
   log — a table that does not wrap — and every bubble, the person's own
   included, ran past the panel and was cut off at its edge. minmax(0, 1fr)
   pins the column to the panel; a wide table scrolls inside its own bubble
   (.guide-card-wrap) and long words wrap. */
.guide-log { padding: 12px 14px; overflow-y: auto; overflow-x: hidden; flex: 1; display: grid;
  grid-template-columns: minmax(0, 1fr); gap: 10px; align-content: start }
.guide-msg { max-width: 92%; min-width: 0; overflow-wrap: break-word; border-radius: 12px;
  padding: 9px 12px; font-size: 14px; line-height: 1.5 }
.guide-msg p { margin: 0 }
.guide-from { background: var(--va-surface-subtle); border: 1px solid var(--va-border); justify-self: start }
.guide-me { background: #eaf1ff; justify-self: end }
.guide-wait { color: var(--va-muted) }
.guide-link { display: block; margin-top: 6px; font-weight: 600; color: var(--va-brand); text-decoration: none }
.guide-link:hover { text-decoration: underline }
.guide-starters { display: flex; flex-wrap: wrap; gap: 6px }
.guide-starters[hidden] { display: none }
.guide-starters button { border: 1px solid var(--va-border); background: var(--va-surface); color: var(--va-ink);
  border-radius: 999px; padding: 6px 10px; font-size: 13px; cursor: pointer }
.guide-starters button:hover { border-color: var(--va-brand); color: var(--va-brand) }
/* The other half of a two-part question (2026-10-01): a starter's look, under the answer. */
.guide-also { display: block; margin-top: 8px; border: 1px solid var(--va-border); background: var(--va-surface);
  color: var(--va-ink); border-radius: 999px; padding: 6px 10px; font-size: 13px; cursor: pointer; text-align: left }
.guide-also:hover { border-color: var(--va-brand); color: var(--va-brand) }
/* .guide-form and the composer inside it are styled in _guide.html (2026-09-28). */
.guide-foot { margin: 0; padding: 0 14px 12px; font-size: 12px; color: var(--va-muted) }
@media (max-width: 520px) {
  .guide { right: 12px; bottom: 12px }
  .guide-panel { position: fixed; left: 8px; right: 8px; bottom: 70px; width: auto }
}
/* A PAGE'S OWN FLOATING ACTION KEEPS THE CORNER. Files puts "+ New folder"
   bottom-right (.new-folder); the Guide sits above it rather than on it, which
   on a phone had covered the button (browser/test_files_page.py found it). */
body:has(.new-folder) .guide { bottom: 88px }
body:has(.new-folder) .guide-panel { bottom: 56px }
@media (max-width: 520px) {
  body:has(.new-folder) .guide { bottom: 80px }
  body:has(.new-folder) .guide-panel { bottom: 138px }
}
@media print { .guide { display: none } }
