/* Kwickly Answers — shared component primitives.
   Loaded after tokens.css by index.html and portal.html.
   See docs/design-spec.md §2. Page-specific layout stays in each page. */

/* ---------- 2.1 The tick ----------
   The one decorative element in the system: a 3px bar. It appears on the
   active rail item, the left edge of an open inline form, and the head of a
   card. Its colour says which system you are in. Nothing else uses a 3px bar. */
.tick { width:3px; height:15px; border-radius:3px; background:var(--accent); flex:0 0 auto; }
.tick-private { background:var(--private); }
.tick-attention { background:var(--onboarding); }

/* ---------- 2.2 Card ----------
   The spec draws this as a head strip over a body. Rather than wrap 33
   sections that are generated inside JS template literals, the heading bleeds
   to the card edges with a negative margin — same result, no markup to break. */
.section {
  background:var(--panel);
  border:1px solid var(--line);
  border-radius:var(--r-lg);
  box-shadow:var(--shadow-2);
  margin-bottom:var(--s4);
  padding:0 var(--s5) var(--s4);
  /* `clip`, not `hidden`. Both clip the header strip's negative-margin bleed,
     but `hidden` makes this a SCROLL CONTAINER, and a sticky descendant is
     offset against its nearest scroll container rather than the viewport.
     That is why the page builder's sticky preview has never taken effect —
     measured at 390px, the frame went from y=931 to y=-469 over a 1,400px
     scroll. With `clip` the same scroll leaves it at y=152, on screen. */
  overflow:clip;
}
.section > h3 {
  margin:0 calc(-1 * var(--s5)) var(--s4);
  padding:var(--s3) var(--s5);
  font-family:var(--font-display);
  font-weight:600;
  font-size:var(--t-md);
  border-bottom:1px solid var(--line);
}
/* When a hint follows the heading, the strip covers both and the rule moves down. */
.section > h3:has(+ .section-hint) { margin-bottom:0; padding-bottom:4px; border-bottom:none; }
.section > .section-hint {
  margin:0 calc(-1 * var(--s5)) var(--s4);
  padding:0 var(--s5) var(--s3);
  border-bottom:1px solid var(--line);
  font-size:var(--t-xs);
  line-height:1.5;
  color:var(--text-2);
}
.section-head {
  display:flex; align-items:center; justify-content:space-between; gap:var(--s4);
  margin:0 calc(-1 * var(--s5)) var(--s5);
  padding:var(--s4) var(--s5);
  border-bottom:1px solid var(--line);
}
.section-head h3 { margin:0; padding:0; border:none; font-family:var(--font-display); font-weight:600; font-size:var(--t-md); }
.section-count {
  font-size:11px; font-weight:700; color:var(--text-2);
  background:var(--sunken); border:1px solid var(--line);
  border-radius:var(--r-pill); padding:2px 8px;
}

/* ---------- 2.3 Field ----------
   Labels go 400 -> 600: a stack of fifteen 400-weight labels does not scan.
   Inputs move from --bg to --panel; they were sitting on the page colour
   inside a panel-coloured card, which inverts figure and ground. */
.field { display:flex; flex-direction:column; gap:6px; margin-bottom:var(--s4); }
.field[hidden] { display:none; }
.field label { display:block; font-size:var(--t-xs); font-weight:600; color:var(--text-2); margin:0; }
input:not([type=checkbox]):not([type=radio]), select, textarea {
  width:100%; padding:9px 11px; font-size:13.5px; font-family:inherit;
  color:var(--text); background:var(--panel);
  border:1px solid var(--line-2); border-radius:var(--r-md);
  transition:border-color var(--t-fast), box-shadow var(--t-fast);
}
input:not([type=checkbox]):not([type=radio]):focus, select:focus, textarea:focus {
  outline:none; border-color:var(--accent); box-shadow:0 0 0 3px var(--accent-line);
}
textarea { resize:vertical; min-height:70px; }
.field-hint, .field-hint2 { font-size:var(--t-xs); color:var(--text-2); margin:0; line-height:1.5; }
.field-row { display:flex; gap:var(--s3); }
.field-row .field { flex:1; }
/* Two fields side by side stop being readable well before a phone: a select
   squashes to "Not spec⌄" and its hint wraps to six lines beside it. Stacking
   is the only sensible answer at this width, and it applies to every existing
   field-row (price min/max, promo, stock) as well as the booking form. */
@media (max-width: 720px) {
  .field-row { flex-direction: column; gap: 0; }
}
.radio-row, .check-row {
  display:flex; align-items:center; gap:7px;
  margin-bottom:6px; font-size:var(--t-base); font-weight:400; color:var(--text);
}
.radio-row input, .check-row input { width:auto; flex:0 0 auto; accent-color:var(--accent); }
.radio-group, .check-group { margin-top:var(--s1); }

/* Keyboard focus is near-absent today; this is what makes a tab pass feel finished. */
button:focus-visible, a:focus-visible, [tabindex]:focus-visible,
input[type=checkbox]:focus-visible, input[type=radio]:focus-visible {
  outline:2px solid var(--accent); outline-offset:2px; border-radius:var(--r-sm);
}

/* ---------- 2.35 Theme picker ----------
   Mounted by theme.js into any [data-theme-switch]. It sits in a page header
   next to "Sign out", so it borrows that control's quiet vernacular rather
   than the form-field styling every other <select> on the page gets: no
   full-width, no panel fill, muted until you touch it. */
select.theme-select {
  width:auto; min-width:0; min-height:0;
  padding:3px 22px 3px 8px;
  font-size:var(--t-xs); font-family:inherit; line-height:1.4;
  color:var(--text-2); background:transparent;
  border:1px solid transparent; border-radius:var(--r-sm);
  cursor:pointer;
  /* The native arrow is drawn in the OS's own colour, which reads as a
     bright blue-grey speck in low blue light. Ours is drawn in currentColor
     so it follows the palette like everything else. */
  appearance:none; -webkit-appearance:none;
  background-image:
    linear-gradient(45deg, transparent 50%, currentColor 50%),
    linear-gradient(135deg, currentColor 50%, transparent 50%);
  background-position:calc(100% - 12px) calc(50% + 1px), calc(100% - 8px) calc(50% + 1px);
  background-size:4px 4px, 4px 4px;
  background-repeat:no-repeat;
}
select.theme-select:hover { color:var(--text); border-color:var(--line-2); }
select.theme-select:focus {
  outline:none; color:var(--text);
  border-color:var(--accent); box-shadow:0 0 0 3px var(--accent-line);
}
/* The popup itself is native, so its list has to be legible on the OS's own
   menu ground rather than on ours. */
select.theme-select option { color:var(--text); background:var(--panel); }

/* ---------- 2.4 Badges ----------
   Two shapes, and every one is colour + colour-weak ground + 1px colour border.
   Never a saturated fill with white text — that is what makes the current
   product rows read as a warning strip. */
.badge {
  display:inline-flex; align-items:center; gap:6px;
  font-size:10px; font-weight:700; letter-spacing:.07em; text-transform:uppercase;
  padding:3px 9px; border-radius:var(--r-pill);
  border:1px solid transparent; white-space:nowrap;
}
.badge-live { color:var(--live); background:var(--live-weak); border-color:var(--live); }
.badge-onboarding { color:var(--onboarding); background:var(--onboarding-weak); border-color:var(--onboarding); }
.badge-prospect,
.badge-paused { color:var(--paused); background:var(--paused-weak); border-color:var(--paused); }
.badge-danger { color:var(--danger); background:var(--danger-weak); border-color:var(--danger); }
/* The one status indicator in the whole chrome rendered with no status colour:
   the header badge says SETUP, and there was a rule for live, onboarding,
   prospect, paused and danger — and none for setup. It came out as body text
   on a transparent ground, next to an --onboarding amber that exists for
   exactly this state. */
.badge-setup { color:var(--onboarding); background:var(--onboarding-weak); border-color:var(--onboarding); }

/* A fact about a row: PROMO, ONLY 2 LEFT, INTERNAL, MAIN, HIDDEN FROM SUPPORT */
.promo-badge {
  display:inline-block; vertical-align:middle; margin-left:6px;
  font-size:9.5px; font-weight:700; letter-spacing:.08em; text-transform:uppercase;
  padding:1px 6px; border-radius:var(--r-sm);
  color:var(--onboarding); background:var(--onboarding-weak); border:1px solid var(--onboarding);
}
.promo-badge.cl-private { color:var(--private); background:var(--private-weak); border-color:var(--private); }
.promo-badge.cl-stock   { color:var(--danger);  background:var(--danger-weak);  border-color:var(--danger); }
.promo-badge.cl-blocked,
.promo-badge.cl-neutral { color:var(--text-2);  background:var(--sunken);       border-color:var(--line-2); }
.promo-meta { color:var(--onboarding); }

/* ---------- 2.5 Buttons ----------
   Four, no more. One primary per view. Transitions on background and
   border-color only — never `all`. */
.primary, .btn-primary, button.primary {
  font:600 13px/1 var(--font-body); color:var(--on-accent);
  background:var(--accent); border:1px solid var(--accent);
  border-radius:var(--r-md); padding:9px 15px; cursor:pointer;
  transition:background var(--t-fast), border-color var(--t-fast);
}
.primary:hover, .btn-primary:hover, button.primary:hover {
  background:var(--accent-hover); border-color:var(--accent-hover);
}

.secondary {
  font:600 13px/1 var(--font-body); cursor:pointer;
  border-radius:var(--r-md); padding:9px 15px;
  color:var(--text); background:var(--panel); border:1px solid var(--line-2);
  transition:background var(--t-fast), border-color var(--t-fast);
}
.secondary:hover { background:var(--sunken); border-color:var(--text-3); }

/* The "+ Add" button. */
.quiet {
  font:600 13px/1 var(--font-body); cursor:pointer;
  border-radius:var(--r-md); padding:9px 15px;
  color:var(--accent); background:transparent; border:1px solid var(--accent-line);
  transition:background var(--t-fast), border-color var(--t-fast);
}
.quiet:hover { background:var(--accent-weak); border-color:var(--accent); }

.link-btn, .btn-link {
  background:none; border:none; padding:0; cursor:pointer;
  font:600 12.5px/1 var(--font-body); color:var(--accent); text-decoration:none;
}
.link-btn:hover, .btn-link:hover { text-decoration:underline; }
.link-btn[data-delete], .btn-link[data-delete] { color:var(--text-2); }
.product-row:hover .link-btn[data-delete],
.product-row:hover .btn-link[data-delete] { color:var(--danger); }

/* "More" on a hint (more-toggle.js, Fix List C14). A hint is one line; the
   rest sits in a .more-text span behind this button. Inline with the line it
   ends, in the hint's own size, underlined so it reads as something to press
   rather than as bold text. The negative margins give it a bigger target
   without making its line any taller. */
.more-toggle {
  display:inline; margin:-4px 0 -4px 4px; padding:4px 3px;
  background:none; border:0; cursor:pointer;
  font-family:var(--font-body); font-weight:600; font-size:inherit; line-height:inherit;
  color:var(--accent); text-decoration:underline; text-underline-offset:2px;
}
.more-toggle:hover { color:var(--accent-hover); }
.more-text:not([hidden]) { display:block; margin-top:var(--s1); }
@media (max-width: 720px) {
  /* 16px like every control on a phone, and a thumb-sized target. */
  .more-toggle { font-size:16px; padding:10px 6px; margin:-10px 0 -10px 2px; }
}

button.danger {
  font:600 13px/1 var(--font-body); cursor:pointer;
  border-radius:var(--r-md); padding:9px 15px;
  background:none; color:var(--danger); border:1px solid var(--danger);
  transition:background var(--t-fast);
}
button.danger:hover { background:var(--danger-weak); }
button.small { padding:7px 12px; font-size:12.5px; }

/* ---------- 2.6 Empty state ----------
   One shape for all eight lists. Only the sentence changes.

   THE MARK USED TO BE A 34px DASHED SQUARE WITH NOTHING IN IT, and three of
   them are visible at once on the Calendar tab. An empty dashed box is the
   universal appearance of an image that failed to load, so a working screen
   read as a broken one. It now carries a drawn glyph — a light rule and two
   dots, the shape of a list with nothing on it — on the panel ground rather
   than a dashed hole. Same 34px box, same one shape everywhere; it just no
   longer looks like a failure. */
.empty {
  padding:var(--s7) var(--s5);
  display:flex; flex-direction:column; align-items:center; gap:var(--s3);
  text-align:center;
}
.empty-mark {
  width:34px; height:34px; border-radius:var(--r-md); flex:none;
  border:1px solid var(--line); background:var(--sunken);
  position:relative;
}
.empty-mark::before {
  content:''; position:absolute; left:8px; right:8px; top:12px; height:1px;
  background:var(--line-2); box-shadow:0 6px 0 var(--line-2);
}
.empty-mark::after {
  content:''; position:absolute; left:8px; top:11px;
  width:3px; height:3px; border-radius:50%;
  background:var(--text-3); box-shadow:0 6px 0 var(--text-3);
}
.empty-title { margin:0; font-size:var(--t-base); font-weight:600; }
.empty-body { margin:0; max-width:330px; font-size:13px; line-height:1.55; color:var(--text-2); }
/* A search that found nothing is not an untouched list: the box is dashed
   there, because something IS missing. */
.empty-search .empty-mark { border-style:dashed; }
/* A bare `<p class="empty">` — the inline "No bookings yet" kind, used inside
   an already-titled card. It gets the sentence, not the ceremony: the full
   treatment inside a card that says "Their bookings" is a second heading. */
p.empty {
  padding:var(--s3) 0 var(--s3) var(--s3); text-align:left; display:block;
  border-left:2px solid var(--line); color:var(--text-2);
  font-size:13px; line-height:1.55;
}
.crm-help-empty { color:var(--text-2); font-size:13px; margin:0; }

/* ---------- 2.6c The read-only row list ----------
   `.crud-list` / `.crud-row` HAD NO CSS AT ALL. Not overridden, not partly
   styled — no rule anywhere matched them. The editable lists are crud-list.js
   and its `cl-*` classes; these are the hand-written read-only lists that
   borrowed the name, and there are four of them: the day panel under the
   diary, the waiting list beside it, a customer's bookings, a customer's
   calls, and the FAQ suggestions.

   So the thing directly under a client's calendar — the list of who is coming
   in today — rendered as a browser-default `<ul>`: disc bullets, 40px of
   indent, no rules, no meta hierarchy. It was the least designed surface in
   the product and it sits under the most designed one. */
.crud-list {
  list-style:none; margin:0; padding:0;
  border:1px solid var(--line); border-radius:var(--r-md);
  background:var(--panel); overflow:hidden;
}
.crud-row {
  display:flex; align-items:flex-start; gap:var(--s4);
  padding:11px var(--s4); border-top:1px solid var(--line);
}
.crud-row:first-child { border-top:0; }
.crud-row-main { display:flex; flex-direction:column; gap:2px; min-width:0; flex:1 1 auto; }
.crud-row-main > strong {
  font-size:var(--t-base); font-weight:600; color:var(--text); line-height:1.35;
}
.crud-row-meta { font-size:var(--t-xs); color:var(--text-2); line-height:1.45; }
.crud-row-meta:empty { display:none; }
.crud-row-main .field-hint { margin:2px 0 0; }
.crud-row-actions {
  display:flex; align-items:center; gap:var(--s3); flex:none; padding-top:2px;
}

@media (max-width: 720px) {
  .crud-row { flex-direction:column; gap:var(--s2); }
  .crud-row-actions { padding-top:0; }
}

/* ---------- 2.6c A chat, word for word ----------
   The transcript kept when a chat booking was made (db/chat-transcript.sql).

   COLLAPSED BY DEFAULT, and that is the design rather than a space saving. A
   customer record answers "who is this and what have they had done" at a
   glance; a wall of dialogue at the top of it buries the bookings underneath.
   A <details> keeps the answer one click away and costs no script.

   `white-space:pre-wrap` is load-bearing: the transcript is line-per-speaker
   plain text and collapsing its newlines would run the whole conversation into
   one paragraph with no way to tell who said what. `overflow-wrap` is what
   stops a pasted URL widening the card past the screen. */
.chat-note { border-top:1px solid var(--line); padding:var(--s2) 0 0; }
.chat-note + .chat-note { margin-top:var(--s2); }
.chat-note > summary {
  cursor:pointer; font-size:var(--t-xs); color:var(--text-2); line-height:1.45;
}
.chat-note > summary::marker { color:var(--text-3); }
.chat-note-body {
  margin:var(--s2) 0 0; padding:var(--s3);
  background:var(--bg); border-radius:var(--r-sm);
  font-family:inherit; font-size:13px; line-height:1.6; color:var(--text);
  white-space:pre-wrap; overflow-wrap:anywhere;
}

/* ---------- 2.6b Saved, and refused ----------
   Both were the least-designed things in the product. "Saved" was the word
   Saved in 13px --text-2 grey beside a bright blue button — the same size,
   weight and colour as the placeholder hint that had been sitting there a
   moment before, so there was no way to tell a save had landed. --live green
   is defined in all three themes and was used for none of it. A refusal was
   the same grey sentence in red, at hint weight, sometimes a long way from the
   control that caused it.

   Both now have a ground, a rule down the left edge and a glyph, so they read
   as a state rather than as more copy. */
.form-msg {
  margin:var(--s2) 0 0; min-height:0;
  display:flex; align-items:flex-start; gap:7px;
  font-size:13px; line-height:1.45; font-weight:600;
}
.form-msg:empty { display:none; }
.msg-ok, .msg-bad {
  padding:7px 11px; border-radius:var(--r-md);
  border:1px solid transparent; border-left-width:3px;
}
.msg-ok {
  color:var(--live); background:var(--live-weak);
  border-color:var(--live-weak); border-left-color:var(--live);
  animation:msg-in var(--t-med) both;
}
.msg-bad {
  color:var(--danger); background:var(--danger-weak);
  border-color:var(--danger-weak); border-left-color:var(--danger);
  animation:msg-in var(--t-med) both;
}
.msg-ok::before, .msg-bad::before {
  font-weight:700; line-height:1.45; flex:none;
}
.msg-ok::before { content:'\2713'; }   /* a tick — the save landed */
.msg-bad::before { content:'\0021'; }  /* not a cross: nothing was destroyed */
@keyframes msg-in { from { opacity:0; transform:translateY(-3px); } to { opacity:1; transform:none; } }

/* A panel that could not load. Distinct from an empty state on purpose: those
   two used to render identically, so "your list is down" and "you have nothing
   yet" were the same screen. */
.state-bad {
  padding:var(--s5); border:1px solid var(--danger);
  border-left-width:3px; border-radius:var(--r-md);
  background:var(--danger-weak);
}
.state-bad-title {
  margin:0 0 var(--s2); font-size:var(--t-base); font-weight:700; color:var(--danger);
}
.state-bad-body { margin:0 0 var(--s2); font-size:13px; line-height:1.55; color:var(--text); }
.state-bad-body:last-child { margin-bottom:0; }

/* A box that is not what it looks like, saying so where it is typed.
   Deliberately not a status colour and deliberately not the violet: --private
   means support redaction and nothing else, and amber or red would read as a
   fault on states that are all intentional. admin/featureReadiness.js owns the
   words; scripts/check-auth.js holds both apps to them. */
.seam-note {
  margin: var(--s2) 0 0;
  padding: var(--s2) var(--s3);
  background: var(--sunken);
  border-left: 2px solid var(--line-2);
  border-radius: 0 var(--r-sm) var(--r-sm) 0;
  font-size: var(--t-xs);
  line-height: 1.5;
  color: var(--text-2);
}
.seam-note b { color: var(--text); font-weight: 600; }

/* ---------- 3.0 When a save takes effect ----------
   Always visible rather than a toast: this is a rule about how the product
   behaves, not feedback about one action. */
.save-timing {
  margin: 0 0 var(--s5);
  padding: var(--s3) var(--s4);
  border: 1px solid var(--line);
  /* NEUTRAL, not the accent. This note is on every tab and never goes away,
     so painting it with the accent made a permanent disclaimer the loudest
     accented thing in the portal — louder than any button, and competing with
     the one rail item that is actually current. The accent is reserved for
     what a client can act on; a standing rule about how saving works is not
     that. It keeps the marker, in the line colour, so it still reads as an
     aside rather than as body copy. */
  border-left: 3px solid var(--line-2);
  border-radius: var(--r-md);
  background: var(--sunken);
  color: var(--text-2);
  font-size: var(--t-sm);
  line-height: 1.5;
  max-width: 78ch;
}

/* ---------- 3.1 The "+ Add" list row ----------
   Rows bleed to the card edges so the hover wash reaches them. The card
   carries the padding (see 2.2), so a row cancels it and re-applies its own. */
.product-row {
  display:flex; justify-content:space-between; align-items:flex-start; gap:var(--s4);
  margin:0 calc(-1 * var(--s5));
  padding:13px var(--s5);
  border-top:1px solid var(--line);
  transition:background var(--t-fast);
}
.product-row:first-child { border-top:none; }
.product-row:hover { background:var(--sunken); }
.product-name { font-size:var(--t-base); font-weight:600; }
.product-meta { font-size:12.5px; color:var(--text-2); margin-top:2px; line-height:1.5; }
.product-row.inactive { opacity:.6; }
.product-row.inactive .product-name { text-decoration:line-through; text-decoration-color:var(--text-3); }
/* Actions stay in the DOM at 45% so they remain tappable on touch, and come
   to full strength on hover. */
.product-row .product-actions {
  display:flex; gap:var(--s3); flex-shrink:0;
  opacity:.45; transition:opacity var(--t-fast);
}
.product-row:hover .product-actions,
.product-row:focus-within .product-actions { opacity:1; }
/* A locked row shows a note instead of buttons; it is not a hover affordance. */
.product-row .product-actions:has(.cl-locked-note) { opacity:1; }
.cl-locked-note { color:var(--text-2); font-style:italic; margin-top:0; }

/* ---------- 3.2 The inline form ---------- */
.product-form, .cl-form {
  margin:var(--s3) 0 var(--s5);
  padding:var(--s5);
  background:var(--sunken);
  border:1px solid var(--accent-line);
  border-left:3px solid var(--accent);
  border-radius:var(--r-md);
  display:flex; flex-direction:column; gap:var(--s4);
  transition:opacity var(--t-fast);
}
.cl-form-eyebrow {
  font-size:var(--t-micro); font-weight:700; letter-spacing:.14em;
  text-transform:uppercase; color:var(--accent-hover);
}
.product-form .field, .cl-form .field { margin-bottom:0; }
.product-form .product-actions, .cl-form .product-actions {
  display:flex; align-items:center; gap:var(--s3); opacity:1;
}
/* Per-entry privacy is a different kind of decision from name and price;
   burying it in the same stack is why nobody notices it. */
.cl-privacy-block {
  display:flex; flex-direction:column; gap:var(--s3);
  margin-top:var(--s1); padding-top:var(--s4);
  border-top:1px solid var(--line);
}
.cl-privacy-eyebrow {
  font-size:var(--t-micro); font-weight:700; letter-spacing:.14em;
  text-transform:uppercase; color:var(--private);
}

/* "Show all of these" — the escape from one-panel-at-a-time, above the rail
   because it is about the tab rather than about any panel in it. Quiet: it is
   an escape hatch, not the primary move on the screen. */
.rail-all {
  display:block; width:100%; text-align:left; cursor:pointer;
  background:none; border:none; padding:0 0 var(--s3);
  font-family:var(--font-body); font-size:var(--t-xs); font-weight:600;
  color:var(--text-3);
}
.rail-all:hover { color:var(--accent); }
/* A picker is a control, not a link to somewhere further down the page — the
   underline a rail anchor gets on hover would promise a jump it does not do. */
.rail-item.is-picker { text-decoration:none; }

/* WHICH CHANNEL A SCREEN SERVES — stamped on the heading by tabs.js from
   SECTION_CHANNEL. Outlined like every other badge in this product (colour +
   -weak ground + 1px border), and NEUTRAL rather than violet: the violet means
   support redaction and nothing else, and a phone-only panel is not a privacy
   state. `.tab-chan` sits inside the <h3>, which is a CSS bleed to the card
   edges, so it needs its own vertical alignment rather than the heading's. */
.tab-chan {
  display:inline-block; vertical-align:middle; margin-left:var(--s3);
  font-family:var(--font-body); font-size:var(--t-micro); font-weight:700;
  letter-spacing:.08em; text-transform:uppercase; white-space:nowrap;
  color:var(--text-2); background:var(--sunken);
  border:1px solid var(--line-2); border-radius:var(--r-pill);
  padding:3px 9px;
}
/* One FIELD inside a shared screen can still be one channel's own — the tone
   picker on "What it knows" is the phone's, because a chat has its own. */
.field-chan { margin-left:6px; padding:2px 7px; }

/* PER-ENTRY SCOPE — db/entry-scope.sql. The same shape as the privacy block
   above and DELIBERATELY NOT ITS COLOUR: the violet means support redaction
   and nothing else, and "this one is phone only" is not a privacy state. A
   neutral eyebrow, because the ordinary answer here is the ordinary answer. */
.cl-scope-block {
  display:flex; flex-direction:column; gap:var(--s3);
  margin-top:var(--s1); padding-top:var(--s4);
  border-top:1px solid var(--line);
}
.cl-scope-eyebrow {
  font-size:var(--t-micro); font-weight:700; letter-spacing:.14em;
  text-transform:uppercase; color:var(--text-3);
}

/* ---------- 3.3 Validation and saving ----------
   Replaces two alert() calls. The message lands on the field that caused it. */
.field.invalid input:not([type=checkbox]):not([type=radio]),
.field.invalid select, .field.invalid textarea {
  border-color:var(--danger); box-shadow:0 0 0 3px var(--danger-weak);
}
.field.invalid .field-hint { color:var(--danger); font-weight:600; }
.cl-form-error {
  font-size:12.5px; font-weight:600; color:var(--danger);
  background:var(--danger-weak); border:1px solid var(--danger);
  border-radius:var(--r-sm); padding:8px 11px;
}
/* A WEEK SHOULD LOOK LIKE A WEEK. `days` is the one crud-list field type whose
   base class serves it badly: seven labels of three letters, stacked one per
   line by .check-row, read as a long column of separate decisions. Wrapped,
   the whole week fits one line at the width a .cl-form gets.

   Its two siblings are deliberately NOT here. `checkset`'s labels are whole
   sentences ("New customers — a price, a quote, or a first booking"), so the
   stack it inherits is already right and a grid would cramp it. `image` is
   styled by image-upload.js, which injects its own CSS on purpose so the
   widget's appearance travels with the widget — see the note above `CSS`
   there before adding anything for it here. */
.cl-days-group { display:flex; flex-wrap:wrap; gap:var(--s1) var(--s4); }
.cl-days-group .check-row { margin-bottom:0; }

.cl-form.saving { opacity:.72; pointer-events:none; }
/* A consent's links (the Privacy Policy and Terms after "Text me for urgent
   leads") are tap targets, 44px each way, still straight after the consent
   words (walk6 spot check: "Terms" was 36x15 at 390px). The padding makes the
   target; the equal negative margin keeps the line where it was. */
.cl-field-links a { display:inline-block; padding:14px 0; margin:-14px 0; min-width:44px; text-align:center; }
.cl-spinner {
  display:inline-block; width:11px; height:11px; margin-right:6px;
  vertical-align:-1px; border-radius:50%;
  border:2px solid currentColor; border-right-color:transparent;
  animation:cl-spin 600ms linear infinite;
}
@keyframes cl-spin { to { transform:rotate(360deg); } }
@media (prefers-reduced-motion: reduce) {
  .cl-spinner { animation-duration:2s; }
  * { scroll-behavior:auto !important; }
  /* The save confirmation still appears; it just stops moving to do it. */
  .msg-ok, .msg-bad { animation:none; }
}

/* A list showing its empty state already offers the add action inside it;
   the "+ Add" directly under THAT LIST would repeat it. Scoped to the list
   the button follows, never to the section: the conflict panel holds its name
   list AND an empty "Calls we stopped" box, and a section-wide rule hid
   "+ Add a name" from the moment the first name was on the list. Every "+ Add"
   sits straight after its own list mount, in both pages. */
[id]:has(> .empty) + .quiet { display:none; }

/* A number with its unit beside it, so "10" cannot be read as dollars when it
   is a percentage. The unit is outside the input: the value stays a plain
   number for the browser and the server. */
.input-unit { display:flex; align-items:center; gap:var(--s2); }
.input-unit > input { flex:1 1 auto; min-width:0; }
.input-unit-mark { flex:none; font-weight:600; color:var(--text-2); }
/* Read out, not drawn — for a unit a screen reader should hear in a label. */
.visually-hidden {
  position:absolute; width:1px; height:1px; overflow:hidden;
  clip-path:inset(50%); white-space:nowrap;
}

/* ---------- 4. The weekly hours grid ----------
   Three columns rather than a flex stack, so seven rows read as a table. */
.hours-grid { display:flex; flex-direction:column; }
.hours-head, .hours-row {
  display:grid; grid-template-columns:104px 104px 1fr;
  gap:0 var(--s4); align-items:center;
}
.hours-head {
  padding:var(--s3) 0 9px; border-bottom:1px solid var(--line);
  font-size:var(--t-micro); font-weight:700; letter-spacing:.13em;
  text-transform:uppercase; color:var(--text-3);
}
.hours-row { padding:9px 0; border-bottom:1px solid var(--line); }
.hours-row:last-child { border-bottom:none; }
.hours-day { font-size:13.5px; font-weight:600; }
.hours-row .check-row { margin-bottom:0; font-size:var(--t-sm); }
.hours-times { display:flex; align-items:center; gap:var(--s2); }
.hours-row input[type=time] {
  width:auto; min-width:92px; padding:7px 11px; font-size:13px;
  font-variant-numeric:tabular-nums; background:var(--sunken);
}
.hours-dash { font-size:var(--t-xs); color:var(--text-3); }
.hours-len { font-size:var(--t-xs); color:var(--text-3); margin-left:6px; }

/* A closed day says what the caller actually gets. Two greyed boxes say
   nothing. The inputs stay in the DOM so save() can still read them and so
   reopening the day brings its old times back. */
.hours-closed-note { font-size:13px; color:var(--text-3); font-style:italic; }
.hours-row:not(.is-closed) .hours-closed-note { display:none; }
.hours-row.is-closed .hours-times { display:none; }

/* THE LUNCH BREAK, ON ITS OWN LINE UNDER THE TIMES (db/hours-break.sql).
   Explicitly column 3 because it is the fourth child of a three-column grid:
   auto-placement would drop it under the day name. Quieter than the times
   above it — a day HAS hours and MAY have an hour out of the middle of them,
   and blank means no break, which is exactly what the two columns store. */
.hours-break {
  grid-column:3; display:flex; align-items:center; gap:var(--s2);
  margin-top:6px;
}
.hours-row.is-closed .hours-break { display:none; }
.hours-break-label {
  font-size:var(--t-micro); font-weight:700; letter-spacing:.08em;
  text-transform:uppercase; color:var(--text-3);
}
.hours-break input[type=time] { opacity:.9; }
.hours-break:focus-within input[type=time] { opacity:1; }
.hours-break-note { font-size:var(--t-xs); color:var(--danger); font-weight:600; }

/* The two error states are told apart, so four reddened boxes never point at
   one wrong time: `invalid` is the open/close pair, `break-invalid` the pair
   underneath it. */
.hours-row.invalid .hg-open, .hours-row.invalid .hg-close,
.hours-row.invalid .sw-open, .hours-row.invalid .sw-close,
.hours-row.break-invalid .hg-break-start, .hours-row.break-invalid .hg-break-end {
  border-color:var(--danger); box-shadow:0 0 0 3px var(--danger-weak);
}
.hours-row.invalid .hours-len { color:var(--danger); font-weight:600; }
.hours-status { font-size:var(--t-sm); color:var(--text-2); align-self:center; }
.hours-status.is-error { color:var(--danger); font-weight:600; }

/* NARROW BY ITS CARD, NOT BY THE WINDOW. The three columns need about 560px
   of grid, and the grid is not always given it on a wide screen: at 1280 the
   portal's Calendar tab pairs its panels, so Hours is a ~500px card, and the
   closing time and the lunch break's second box ran past its edge, where
   .section clips them — unreachable on a laptop. So the stacked layout the
   phone uses (see the 720px block) is decided by the grid's own width, the
   way the month view decides its chips. */
.hours-grid { container:hours / inline-size; }
@container hours (max-width: 600px) {
  .hours-head { display:none; }
  .hours-row { grid-template-columns:1fr auto; gap:var(--s2) var(--s3); padding:var(--s4) 0; }
  .hours-times, .hours-break { grid-column:1 / -1; flex-wrap:wrap; gap:var(--s1); }
  .hours-row input[type=time] { flex:1 1 0; min-width:0; }
  .hours-len { margin-left:0; flex:1 0 100%; }
  .hours-break { margin-top:2px; }
  .hours-break-label, .hours-break-note { flex:1 0 100%; }
  .hours-closed-note { grid-column:1 / -1; }
}

/* ---------- 5. Privacy and redaction ----------
   Every one of these is the client's deliberate choice being respected, so:
   neutral plus one violet that appears nowhere else, never red, never amber,
   never a padlock. Every string ends by saying what still works. */

/* A plain disabled input reads as "failed to load"; the hatch says withheld. */
.pv-hidden,
input.pv-hidden:not([type=checkbox]):not([type=radio]),
select.pv-hidden, textarea.pv-hidden {
  background:repeating-linear-gradient(135deg, var(--sunken) 0 6px, transparent 6px 12px);
  border:1px dashed var(--line-2); color:var(--text-3);
}
/* The opposite treatment: fully legible, because support can read it to help
   — they just cannot overwrite it. */
.pv-locked,
input.pv-locked:not([type=checkbox]):not([type=radio]),
select.pv-locked, textarea.pv-locked {
  background:var(--sunken); border-color:var(--line); color:var(--text-2);
}
.pv-chip {
  display:inline-block; margin-left:7px;
  font-size:9.5px; font-weight:700; letter-spacing:.08em; text-transform:uppercase;
  padding:1px 6px; border-radius:var(--r-sm);
  color:var(--private); background:var(--private-weak); border:1px solid var(--private);
}
.privacy-note { font-size:var(--t-xs); color:var(--private); font-style:normal; line-height:1.5; }
.field.locked-by-client input, .field.locked-by-client select,
.field.locked-by-client textarea { opacity:1; }

/* A band at the foot of the card, not grey italic text lost in the body. */
.cl-withheld {
  display:flex; align-items:center; gap:9px;
  margin:var(--s4) calc(-1 * var(--s5)) 0;
  padding:11px var(--s5);
  font-size:12.5px; font-style:normal; color:var(--text);
  background:var(--private-weak); border-top:1px solid var(--private);
}
/* Only the last band reaches the foot of the card; stacked bands sit flush. */
.cl-withheld:last-child { margin-bottom:calc(-1 * var(--s5)); }
.cl-withheld + .cl-withheld { margin-top:0; border-top-color:var(--line); }
.cl-withheld::before {
  content:''; width:7px; height:7px; border-radius:2px;
  background:var(--private); flex-shrink:0;
}
/* A whole list kept private turns the card's edge violet, so it reads as a
   different system rather than an error. */
.empty-private .empty-mark { border-color:var(--private); background:var(--private-weak); }
.section:has(.empty-private) { border-left:3px solid var(--private); }

.admin-preview-banner {
  background:var(--private-weak); color:var(--text);
  border-bottom:1px solid var(--private);
  padding:10px 16px; font-size:13px;
  display:flex; justify-content:space-between; align-items:center; gap:12px;
}
.admin-preview-banner a { color:var(--private); font-weight:600; white-space:nowrap; flex-shrink:0; }

/* WHAT IS SWITCHED OFF BECAUSE IT IS NOT PAID FOR (portal, drawPaidBanner).
   The status colours' own pairs — the same two the admin's list flags use — so
   their contrast guarantees hold here too; the body copy stays in --text. */
.paid-banner {
  color:var(--text); border-bottom:1px solid;
  padding:10px 16px; font-size:13px;
  display:flex; justify-content:space-between; align-items:center; gap:12px;
}
.paid-banner strong { display:block; }
.paid-banner.is-bad { background:var(--danger-weak); border-color:var(--danger); }
.paid-banner.is-bad strong { color:var(--danger); }
.paid-banner.is-warn { background:var(--onboarding-weak); border-color:var(--onboarding); }
.paid-banner.is-warn strong { color:var(--onboarding); }
.paid-banner a { color:var(--text); font-weight:600; text-decoration:underline; white-space:nowrap; flex-shrink:0; }
@media (max-width:720px) { .paid-banner { flex-direction:column; align-items:flex-start; } }

/* The privacy panel reads as a matrix: the two columns are scanned downward
   instead of each row being parsed on its own. */
.privacy-head, .privacy-row {
  display:grid; grid-template-columns:1fr 84px 96px; gap:var(--s3);
  align-items:center;
}
.privacy-head {
  padding:var(--s3) 0 9px; border-bottom:1px solid var(--line);
  font-size:var(--t-micro); font-weight:700; letter-spacing:.13em;
  text-transform:uppercase; color:var(--text-3); text-align:center;
}
.privacy-head > :first-child { text-align:left; }
.privacy-row { padding:10px 0; border-top:1px solid var(--line); }
.privacy-row:first-of-type { border-top:none; }
.privacy-label { font-size:var(--t-base); }
.privacy-toggles { display:contents; }
.privacy-toggles label { display:flex; align-items:center; justify-content:center; gap:5px; }
.privacy-group-title {
  font-size:var(--t-micro); font-weight:700; letter-spacing:.13em;
  text-transform:uppercase; color:var(--text-3); margin:var(--s5) 0 var(--s1);
}
/* The header carries the column names, so the per-row copies stay for screen
   readers only — until the columns collapse. */
.pv-cell-label {
  position:absolute; width:1px; height:1px;
  overflow:hidden; clip-path:inset(50%); white-space:nowrap;
}
@media (max-width: 720px) {
  .privacy-head { display:none; }
  .privacy-row { grid-template-columns:1fr; gap:var(--s2); }
  .privacy-toggles { display:flex; gap:var(--s5); }
  .privacy-toggles label { justify-content:flex-start; }
  .pv-cell-label { position:static; width:auto; height:auto; clip-path:none; }
}

/* ---------- 6. The rail ----------
   Navigation over an unchanged document: every section stays in the DOM and
   in the single form, so collectForm() is untouched. */
.app { display:grid; grid-template-columns:216px 1fr; align-items:start; }
/* Without this the rail's content sets the column's min width and the whole
   page scrolls sideways instead of the rail scrolling inside itself. */
.app > * { min-width:0; }
.rail {
  position:sticky; top:var(--rail-top, 0px); align-self:start;
  max-height:calc(100vh - var(--rail-top, 0px)); overflow-y:auto;
  border-right:1px solid var(--line); background:var(--panel);
  /* The bottom used to be --s8. Forty pixels of nothing INSIDE the scroll box
     is forty pixels of rail pushed past the bottom of the window on a laptop,
     and it was the cheapest of the five that were pushing it. */
  padding:var(--s5) 0 var(--s5);
  display:flex; flex-direction:column; gap:var(--s4);
}
/* THE SIDEBAR WAS "AWKWARDLY CUT OFF", AND THIS IS WHAT THAT WAS.
   Not the sticky offset (`--rail-top` is measured and correct), not `--rail-h`
   (that is the phone bar, and it is 0px on a desktop), and not a missing
   overflow. It was `max-height` doing exactly what it says: the admin's rail
   is 21 items in 6 groups and, at a 1280x700 window, SEVEN of them — the whole
   Setup group — sat below the fold of a box that scrolls inside itself with
   macOS overlay scrollbars, so there was no scrollbar, no fade, and nothing at
   all to say there was more. It only stopped happening above ~967px of
   viewport, which is taller than the laptop this is used on.

   A rail that does not fit stops being pinned and scrolls with the page
   instead. Losing the pin on a short window costs the rail staying put; the
   alternative costs seven links nobody can find. rail.js measures it and sets
   this class, because "does the content fit" is not a thing CSS can ask. */
.rail.rail-loose {
  position:static; max-height:none; overflow-y:visible;
}
.rail-group { display:flex; flex-direction:column; gap:1px; }
.rail-group-label {
  font-size:var(--t-micro); font-weight:700; letter-spacing:.14em;
  text-transform:uppercase; color:var(--text-3); padding:0 var(--s5) 5px;
}
.rail-item {
  display:flex; align-items:center; justify-content:space-between; gap:var(--s2);
  padding:6px var(--s5); font-size:13.5px; color:var(--text-2);
  text-decoration:none; position:relative;
  transition:background var(--t-fast), color var(--t-fast);
}
/* display:flex above outranks the UA's [hidden] rule, so say it explicitly. */
.rail-item[hidden] { display:none; }
.rail-item:hover { background:var(--sunken); color:var(--text); }
.rail-item.active { background:var(--accent-weak); color:var(--accent-hover); font-weight:600; }
.rail-item.active::before {
  content:''; position:absolute; left:0; top:6px; bottom:6px;
  width:3px; border-radius:0 3px 3px 0; background:var(--accent);
}
.rail-count { font-size:var(--t-xs); color:var(--text-3); font-variant-numeric:tabular-nums; }
.rail-item.active .rail-count { color:var(--accent-hover); }
/* A rail click is a plain anchor jump, so the heading needs room to clear. */
.section { scroll-margin-top:var(--s5); }

/* ---------- 6.3 The save bar ----------
   With a rail, someone can jump to Hours, edit, jump to FAQs and never pass
   the button. It follows them instead. */
.save-row {
  position:sticky; bottom:0; z-index:5;
  display:flex; align-items:center; justify-content:space-between; gap:var(--s4);
  padding:var(--s4) var(--s6);
  background:var(--panel); border-top:1px solid var(--line-2);
  border-radius:var(--r-lg) var(--r-lg) 0 0;
  box-shadow:0 -6px 20px rgba(0,0,0,.05);
}
.save-row.clean { display:none; }
.save-row-note { font-size:var(--t-sm); font-weight:600; color:var(--text-2); }
.save-row-actions { display:flex; align-items:center; gap:var(--s3); }
/* SAVED, FOR THE FEW SECONDS IT SAYS SO (rail.js `saved()`). The bar stays up
   to show "Saved — …", but as a confirmation, not a question: "Unsaved changes
   on this page" is not drawn beside it, and a tap goes through to whatever the
   bar is covering. It sat over "+ Add a name" at the foot of a phone's screen
   and took the first tap after every save. */
.save-row.is-saved { pointer-events:none; }
.save-row.is-saved .save-row-note,
.save-row.is-saved .save-row-actions > button { visibility:hidden; }
.save-row-text { display:flex; flex-direction:column; gap:2px; min-width:0; }
/* WHEN A SAVE TAKES EFFECT, WHERE SAVING HAPPENS (SAVE_TIMING in rail.js,
   word for word). It was a framed block above the first panel of every tab —
   four lines of a phone's first screen, on every tab. In the bar it is one
   quiet paragraph under "Unsaved changes on this page", beside the button it
   is about. The frame is the page version's; none of it belongs in a bar. */
.save-row .save-timing {
  margin:0; padding:0; border:0; border-radius:0; background:none;
  font-size:var(--t-xs); line-height:1.45; color:var(--text-2); max-width:64ch;
}
/* THE RULE IN ONE LINE ON A PHONE (walk4, U5; D10). saveTimingHint() puts both
   versions of SAVE_TIMING in the page; the full sentence shows from 720px up
   and its one-line version below (see the 720px block), so neither is behind
   "More". */
.save-timing-phone { display:none; }
/* A FIELD BEING TYPED IN IS NEVER UNDER THE BAR (U5). rail.js publishes the
   lit bar's height as --save-h, 0 while it is out, and the page's scrolling —
   the document on the portal, #main on the admin — keeps that much clear at
   its foot when it brings a focused field into view. */
html, #main { scroll-padding-bottom:var(--save-h, 0px); }

/* ---------- 6.4 A panel that saves itself, and how a save went (DZ2) ----------
   rail.js SAVE_WORDS. The chip is said in the heading of every panel that
   saves on its own, and above a part that does inside a panel the Save bar
   saves; everything without it is the Save bar's. Neutral like `.tab-chan`,
   not the accent and not a status: it is how the panel works, not news. */
.saves-chip {
  display:inline-block; vertical-align:middle; margin-left:var(--s3);
  font-family:var(--font-body); font-size:var(--t-xs); font-weight:600;
  letter-spacing:0; text-transform:none; white-space:nowrap;
  color:var(--text-2); background:var(--sunken);
  border:1px solid var(--line-2); border-radius:var(--r-pill);
  padding:2px 9px;
}
/* Nearer what it is about, below it, than to whatever sits above. */
.saves-chip-line { margin:var(--s5) 0 var(--s2); }
.saves-chip-line > .saves-chip { margin-left:0; }
/* A part that is not shown for this business takes its line with it. */
.saves-chip-line:has(+ [hidden]) { display:none; }
/* "Saved." and "Not saved." (sayHowSaveWent), wherever a save is answered. */
.save-note.is-good { color:var(--live); font-weight:600; }
.save-note.is-bad { color:var(--danger); font-weight:600; }
/* "Not saved yet — press …": a panel waiting for its own button. */
.save-note.is-pending { color:var(--onboarding); font-weight:600; }

@media (max-width: 720px) {
  /* The IA does not change on a phone; the rail lies down. */
  .app { grid-template-columns:1fr; }
  .rail {
    position:sticky; top:0; z-index:4;
    max-height:none; flex-direction:row; align-items:center; gap:var(--s4);
    overflow-x:auto; overflow-y:hidden;
    padding:var(--s3) var(--s4);
    border-right:none; border-bottom:1px solid var(--line);
  }
  .rail-group { flex-direction:row; gap:var(--s1); align-items:center; }
  .rail-group-label { display:none; }
  .rail-item { padding:9px var(--s3); white-space:nowrap; min-height:44px; }
  .rail-item.active::before { top:auto; bottom:0; left:var(--s3); right:var(--s3);
                              width:auto; height:3px; border-radius:3px 3px 0 0; }
  input:not([type=checkbox]):not([type=radio]), select { min-height:44px; }
  #logoutBtn, .link-btn, .btn-link { min-height:44px; display:inline-flex; align-items:center; }
  /* Its own rule because select.theme-select outranks the bare `select`
     above — deliberately, since everything else that rule does is wrong for
     a header control. The tap target is not. */
  select.theme-select { min-height:44px; }
  .save-row { padding:var(--s3) var(--s4); }
  .save-row-note { font-size:var(--t-xs); }
  /* The one-line rule (D10): rail.js SAVE_TIMING.phone. */
  .save-timing-full { display:none; }
  .save-timing-phone { display:inline; }
  /* ONE LINE ON A PHONE (U5); NAMING THE SECTION (U7) IS THE COMPUTER'S. The
     bar measured 57px with "Unsaved changes on this page"; naming the section
     beside two buttons wrapped it to a second line. rail.js writes the lead
     words and each section's button as nodes, so a phone hides them and says
     the plain words, while the page's text — which check:portal-render reads —
     keeps the whole sentence. */
  .save-row-note:has(> .save-row-lead) { font-size:0; }
  .save-row-note:has(> .save-row-lead) > * { display:none; }
  .save-row-note:has(> .save-row-lead)::before { content:'Unsaved changes on this page'; font-size:var(--t-xs); }
  /* Every target big enough to hit with a thumb. */
  .primary, .btn-primary, .secondary, .quiet, button.danger { min-height:44px; padding:11px 16px; }
  /* The checklist's note is a block hint; in a row-direction rail it would
     stretch the bar to three lines. */
  .rail-note { display:none; }
  .product-form .product-actions button,
  .cl-form .product-actions button { flex:1; min-height:44px; }
  .product-row .product-actions { opacity:1; }
  .product-row .product-actions button { min-height:44px; padding:0 var(--s2); }
  /* AN ORDER'S ROW CAN CARRY THREE ACTIONS ("They paid me", "Ready", "Cancel";
     orders.js). The actions column never shrinks, so at 390px the third sat
     past the card's edge, where the section clips it — an order waiting for
     payment could not be canceled from a phone. A row with three or more puts
     them on their own line under the words; a row with one or two is as it was. */
  .product-row:has(.product-actions > :nth-child(3)) { flex-wrap:wrap; }
  .product-row:has(.product-actions > :nth-child(3)) .product-actions { width:100%; justify-content:flex-start; }

  /* THE ONE THAT MATTERS MOST ON A PHONE.
     iOS Safari zooms the whole page when a focused field is under 16px, and
     every field on both pages was 13.5px — so tapping any input threw the
     layout sideways before a character was typed, and the only way back was
     pinch-to-zoom. 16px is a browser threshold, not a taste preference. */
  input:not([type=checkbox]):not([type=radio]), select, textarea { font-size:16px; }

  /* 13px is not a thumb. Every .check-row and .radio-row is a <label>, so the
     whole line is already the tap target — growing the box and giving the row
     height makes that target big enough to actually hit. */
  input[type=checkbox], input[type=radio] { width:20px; height:20px; }
  .check-row, .radio-row { min-height:44px; gap:var(--s3); margin-bottom:0; }

  /* The rail is a sticky bar across the top here, not a column beside the
     page, so an anchor jump left the section heading UNDERNEATH it — every
     rail link scrolled to a heading you could not read. --rail-h is published
     by rail.js from the bar's real height. */
  .section { scroll-margin-top:calc(var(--rail-h, 72px) + var(--s3)); }

  /* The weekly hours grid is three fixed columns (104px + 104px + the times).
     Inside a card on a 375px screen that leaves about 55px for two time
     inputs and the word "to", so the closing time was clipped off the edge of
     the card and could not be reached at all. Stack it instead. */
  .hours-head { display:none; }
  .hours-row {
    grid-template-columns:1fr auto;
    gap:var(--s2) var(--s3);
    padding:var(--s4) 0;
  }
  .hours-day { font-size:var(--t-base); }
  .hours-times { grid-column:1 / -1; flex-wrap:wrap; gap:var(--s1); }
  /* A native time input clips its own AM/PM segment before it overflows its
     box, so the fix is horizontal room rather than a wider element: trim the
     padding and the gap, and the "9h" length label drops to its own line when
     it no longer fits. */
  .hours-row input[type=time] { flex:1 1 0; min-width:0; padding:9px 6px; }
  /* The length label takes its OWN line here rather than dropping to one only
     when it stops fitting. Sharing the line, it charges the two time inputs
     13px for a word — and a time input spends an overflow on its AM/PM
     segment, so that is paid in the one place this grid has been unreadable
     before. "7h open" is also longer than the "8h" it replaced. */
  .hours-len { margin-left:0; flex:1 0 100%; }
  /* Same reason the times stack: three fixed columns leave no room for two
     time inputs, let alone four. The label takes its own line so the two
     inputs keep the full width and their AM/PM segment. */
  /* TWO UP, NOT FIVE DOWN. .kpi-row's auto-fit puts one 170px tile per row at
     375px, which is five full-height cards to scroll past before the chart —
     the thing the panel is for. Scoped to .an-tiles so the usage panel above
     it, whose three tiles carry a cap and a plan name, is left alone. */
  /* `.kpi-row.an-tiles`, not `.an-tiles` — .kpi-row's auto-fit is defined
     LATER in this file than this media block, so at equal specificity it wins
     on source order and the tiles stayed one per row. */
  /* The badge drops under the heading rather than squeezing it — at 375px a
     heading and a pill on one line clips the heading, and the heading is the
     half that has to be readable. */
  .tab-chan { display:block; margin-left:0; margin-top:6px; width:fit-content; }

  .kpi-row.an-tiles { grid-template-columns:1fr 1fr; gap:var(--s3); }
  .an-tiles .kpi { padding:var(--s4); }
  .an-tiles .kpi-figure { font-size:var(--t-xl); }

  /* THE LABELS STAY. Four weekly bars or three monthly ones fit their labels
     at 375px, which is the whole reason the ranges bucket coarsely — an
     earlier build drew thirty daily columns here and had to hide the labels
     to cope with nine pixels each. */
  .an-chart { gap:3px; }
  .an-tick { font-size:10px; }

  /* THE RAIL LIES DOWN ON A PHONE, so "Show all of these" is a flex item in a
     ROW and `width:100%` shrank it to 32px and broke the words over three
     lines. It sits at the head of the row here, sized by its own text. */
  .rail-all {
    width:auto; flex:0 0 auto; white-space:nowrap;
    padding:0 var(--s3) 0 0; align-self:center;
    /* R7-27 (walk6): 15px tall under a thumb. The whole 44px is the button. */
    min-height:44px; display:inline-flex; align-items:center;
  }

  .hours-break { grid-column:1 / -1; flex-wrap:wrap; gap:var(--s1); margin-top:2px; }
  /* Label above, reason below, inputs with the width to themselves — the
     reason is the longest string in the row and it appears exactly when the
     boxes most need reading. */
  .hours-break-label, .hours-break-note { flex:1 0 100%; }
  .hours-break input[type=time] { flex:1 1 0; min-width:0; padding:9px 6px; }
  .hours-closed-note { grid-column:1 / -1; font-size:var(--t-sm); }
}

/* A group whose sections are all hidden (the portal hides its privacy panel
   from an admin preview) should not leave an empty heading behind. */
.rail-group:not(:has(.rail-item:not([hidden]))) { display:none; }

/* ---------- 7. Overview screens ----------
   The anchor stays a .section so the rail can target it, but the overview is
   a stack of cards rather than one card. */
.section.overview {
  background:none; border:none; box-shadow:none;
  padding:0; overflow:visible; border-radius:0;
}
.overview-greet { margin-bottom:var(--s5); }
.overview-greet h2 {
  font-family:var(--font-display); font-weight:600; font-size:28px;
  letter-spacing:-.015em; margin:0 0 4px;
}
.overview-greet p { margin:0; color:var(--text-2); font-size:var(--t-base); line-height:1.5; }

.kpi-row {
  display:grid; grid-template-columns:repeat(auto-fit, minmax(170px, 1fr));
  gap:var(--s4); margin-bottom:var(--s5);
}
.kpi {
  background:var(--panel); border:1px solid var(--line);
  border-radius:var(--r-lg); box-shadow:var(--shadow-2);
  padding:var(--s5); display:flex; flex-direction:column; gap:var(--s2);
}
.kpi-label {
  font-size:var(--t-micro); font-weight:700; letter-spacing:.14em;
  text-transform:uppercase; color:var(--text-3);
}
.kpi-figure {
  font-family:var(--font-display); font-weight:600; font-size:var(--t-2xl);
  letter-spacing:-.02em; line-height:1; font-variant-numeric:tabular-nums;
}
.kpi-cap {
  font-family:var(--font-body); font-size:var(--t-base); font-weight:400;
  color:var(--text-3); letter-spacing:0;
}
.kpi-note { font-size:var(--t-xs); color:var(--text-2); line-height:1.45; }
.kpi .badge { align-self:flex-start; }

/* HOW IT IS DOING — `sec-analytics`. The counts reuse .kpi; this is the
   picker and the chart. */

/* 26px, not the usage panel's --t-2xl. These five sit four or five across
   where that panel has three, and one of them is a DURATION — "3h 34m" broke
   over two lines at the larger size, which reads as two figures. */
.an-tiles .kpi-figure { font-size:26px; white-space:nowrap; }
.an-tiles .kpi { padding:var(--s4) var(--s5); gap:3px; }

/* A SEGMENTED CONTROL, not a row of chips: these are two questions with one
   answer each, and a pill on a sunken track says "pick one of these" where
   separate outlined chips say "tick any of these". */
.an-controls { display:flex; flex-wrap:wrap; gap:var(--s4); margin:var(--s4) 0 var(--s5); }
.an-seg {
  display:inline-flex; gap:2px; padding:3px;
  background:var(--sunken); border:1px solid var(--line); border-radius:var(--r-pill);
}
.an-seg-btn {
  font:600 var(--t-sm)/1 var(--font-body); padding:7px 13px; border:0; cursor:pointer;
  border-radius:var(--r-pill); white-space:nowrap;
  background:transparent; color:var(--text-2);
}
.an-seg-btn:hover { color:var(--text); }
.an-seg-btn.active { background:var(--panel); color:var(--text); box-shadow:var(--shadow-1); }

/* One column per bucket: the count above, the bar, the label under. Every bar
   is labelled, which the coarse bucketing in services/analytics.js is what
   makes affordable — seven days, four weeks, three months, twelve months.
   NO EXPLICIT HEIGHT: the column is taller than the bar by its count and its
   label, and a fixed height on the row let both hang out of the bottom of it
   and land on the line underneath. */
.an-chart {
  display:flex; align-items:flex-end; gap:var(--s2);
  padding:var(--s4) 0 0; border-top:1px solid var(--line);
}
.an-col { display:flex; flex-direction:column; justify-content:flex-end; align-items:center; gap:5px; min-width:0; flex:1 1 0; }
.an-bar {
  width:100%; max-width:46px; border-radius:4px 4px 0 0; background:var(--accent);
  transition:height var(--motion-1, .18s) ease;
}
.an-n { font-size:11.5px; font-weight:700; color:var(--text-2); font-variant-numeric:tabular-nums; }
.an-tick { font-size:11px; color:var(--text-3); white-space:nowrap; }
/* `.kb-clear.an-busiest`, not `.an-busiest` — every one of these carries
   .kb-clear too, and `.kb-clear { margin:0 }` is defined LOWER in this file,
   so at equal specificity it wins on source order and the line sat flush
   against the chart's tick row. THE THIRD TIME THIS FILE'S ORDER HAS BITTEN
   (see `.kpi-row.an-tiles` and `.an-chart + .an-span` before it): a component
   block here is almost always defined below the rules that want to override
   it, so an override needs two classes rather than one. */
.kb-clear.an-busiest, .kb-clear.an-note, .kb-clear.an-empty { margin-top:var(--s3); }

/* Needs a look — a to-do list generated from calls, not a dashboard widget. */
.nal-row {
  display:flex; gap:var(--s4); align-items:flex-start;
  margin:0 calc(-1 * var(--s5)); padding:13px var(--s5);
  border-top:1px solid var(--line);
}
.nal-row:first-of-type { border-top:none; }
.nal-body { flex:1; min-width:0; }
.nal-quote { font-size:var(--t-base); line-height:1.45; }
.nal-meta { font-size:var(--t-xs); color:var(--text-2); margin-top:3px; }
.nal-action { flex-shrink:0; }

/* Compact call rows, shared by the portal overview and the admin history. */
.call-line {
  display:grid; grid-template-columns:1fr auto auto;
  gap:var(--s3); align-items:baseline;
  margin:0 calc(-1 * var(--s5)); padding:11px var(--s5);
  border-top:1px solid var(--line);
  font-variant-numeric:tabular-nums;
}
.call-line:first-of-type { border-top:none; }
.call-line .call-who { font-size:var(--t-base); font-weight:600; }
.call-line .call-what { font-size:var(--t-xs); color:var(--text-2); margin-top:2px; grid-column:1 / -1; line-height:1.45; }
.call-line .call-len, .call-line .call-when { font-size:var(--t-xs); color:var(--text-3); }
.card-foot {
  margin:var(--s4) calc(-1 * var(--s5)) calc(-1 * var(--s5));
  padding:11px var(--s5); border-top:1px solid var(--line);
  background:var(--sunken); border-radius:0 0 var(--r-lg) var(--r-lg);
}

/* ---------- 7.4 My KwickBoard ----------
   Two panels: what needs you, and what you see. The rows ARE `.nal-row` —
   "needs a look" already had exactly the shape a reminder wants (a mark, a
   line, an action) and a second set of rules for the same row would be a
   second thing to keep in step. Only what is genuinely new is here.

   NOTHING NEW IS PAINTED. Every colour on this board is an existing status
   token reached through an existing `.badge-*` class, so `check-contrast.js`
   already measures all of it and there is no new pair to argue about. */

/* The all-clear, and the "checking" that precedes it. Deliberately a plain
   sentence and not an `.empty` block with its mark and its two lines: an
   empty state is a place where something is MISSING, and nothing is missing
   when there is nothing to do. It is one line high, which is the point —
   a board that shouts as loudly about nothing as it does about something is
   a board a client stops reading. */
.kb-clear { margin:0; color:var(--text-2); font-size:var(--t-sm); line-height:1.55; max-width:62ch; }
.kb-clear + .kb-clear { margin-top:var(--s2); }
.kb-quiet { color:var(--text-3); font-size:var(--t-xs); }

.kb-card .nal-quote { font-weight:600; }
/* The count is the first thing read, so it is set as a figure rather than as
   a label: tabular so a column of them lines up, and wide enough that 1 and
   12 are the same width of badge. */
.kb-count {
  flex-shrink:0; min-width:2.4em; text-align:center;
  font-variant-numeric:tabular-nums; font-weight:700;
  font-size:var(--t-sm); padding:3px 8px; margin-top:1px;
}
/* A reminder with no number — the website notice, the plan — still needs the
   same mark in the same column, or the rows do not line up. */
.kb-count-none { min-width:2.4em; }
.kb-actions { display:flex; flex-direction:column; align-items:flex-end; gap:4px; }
.kb-hide { font-size:var(--t-xs); color:var(--text-3); }

/* What you see. A list of tick boxes grouped by the tab they belong to, so
   the client is turning off "the FAQs panel on What it says" rather than an
   id out of a map. */
.kb-vis > summary {
  cursor:pointer; font-weight:600; font-size:var(--t-sm);
  padding:var(--s2) 0; color:var(--accent);
}
.kb-vis > summary:hover { text-decoration:underline; }
.kb-vis-group { margin-top:var(--s4); }
.kb-vis-group h4 {
  margin:0 0 var(--s2); font-size:var(--t-micro); font-weight:700;
  letter-spacing:.14em; text-transform:uppercase; color:var(--text-3);
}
.kb-vis-row {
  display:flex; align-items:flex-start; gap:var(--s3);
  padding:7px 0; font-size:var(--t-sm); line-height:1.45; cursor:pointer;
}
.kb-vis-row input { margin:2px 0 0; flex-shrink:0; }
.kb-vis-row em { color:var(--text-3); font-style:normal; font-size:var(--t-xs); }
/* Locked rows are the two that cannot be turned off — the panel that gives
   everything back, and the last panel standing on a tab. Greyed AND told why,
   never greyed alone: a disabled box with no sentence beside it reads as a
   bug. */
.kb-vis-row.is-locked { cursor:default; color:var(--text-3); }

@media (max-width: 720px) {
  /* The action stack goes back beside the text: on a phone the row is already
     narrow and a right-hand column of two buttons squeezes the sentence into
     three words a line. */
  .kb-card { flex-wrap:wrap; }
  .kb-actions { flex-direction:row; width:100%; justify-content:flex-start; gap:var(--s4); }
}

/* ---------- 7.5 Admin client detail ---------- */
/* Plain CSS bars — a 30-day count does not need a chart library. */
.spark { display:flex; align-items:flex-end; gap:2px; height:64px; margin-top:var(--s3); }
.spark-bar {
  flex:1; min-height:2px; border-radius:2px 2px 0 0;
  background:var(--line-2); transition:background var(--t-fast);
}
.spark-bar.has { background:var(--accent); }
.spark-axis {
  display:flex; justify-content:space-between;
  font-size:var(--t-xs); color:var(--text-3); margin-top:6px;
}

.conn-row {
  display:flex; align-items:baseline; gap:10px; flex-wrap:wrap;
  margin:0 calc(-1 * var(--s5)); padding:11px var(--s5);
  border-top:1px solid var(--line);
}
.conn-row:first-of-type { border-top:none; }
.conn-row.attention { background:var(--onboarding-weak); }
.conn-dot { width:8px; height:8px; border-radius:50%; flex-shrink:0; align-self:center; }
.conn-dot.ok { background:var(--live); }
.conn-dot.warn { background:var(--onboarding); }
.conn-dot.danger { background:var(--danger); }
.conn-name { font-size:var(--t-base); font-weight:600; flex:1; }
.conn-state { font-size:var(--t-xs); color:var(--text-2); }
.conn-note { font-size:var(--t-xs); color:var(--text-2); flex-basis:100%; padding-left:18px; line-height:1.45; }

/* Status filter chips over the client list. */
.filter-row { display:flex; flex-wrap:wrap; gap:6px; padding:0 8px var(--s2); }
.filter-chip {
  font-size:var(--t-xs); font-weight:600; padding:4px 9px;
  border-radius:var(--r-pill); border:1px solid var(--line);
  background:var(--panel); color:var(--text-2); cursor:pointer;
  transition:background var(--t-fast), border-color var(--t-fast), color var(--t-fast);
}
.filter-chip:hover { background:var(--sunken); color:var(--text); }
.filter-chip.active { background:var(--accent-weak); border-color:var(--accent); color:var(--accent-hover); }
.client-dot {
  display:inline-block; width:7px; height:7px; border-radius:50%;
  margin-right:7px; flex-shrink:0;
}
.client-dot.live { background:var(--live); }
.client-dot.onboarding { background:var(--onboarding); }
.client-dot.prospect, .client-dot.paused { background:var(--paused); }

/* ---------- 7.3 Call history ---------- */
.call-toolbar {
  display:flex; flex-wrap:wrap; gap:var(--s3); align-items:center;
  margin:0 calc(-1 * var(--s5)) 0; padding:0 var(--s5) var(--s4);
  border-bottom:1px solid var(--line);
}
.call-toolbar .filter-row { padding:0; flex:1; }
.call-toolbar input[type=search], .call-toolbar select { width:auto; min-width:150px; flex:0 1 auto; }
.call-head {
  display:grid; grid-template-columns:1fr 118px 1fr 62px 96px;
  gap:var(--s3); align-items:center;
  margin:0 calc(-1 * var(--s5)); padding:var(--s3) var(--s5) 9px;
  border-bottom:1px solid var(--line);
  font-size:var(--t-micro); font-weight:700; letter-spacing:.13em;
  text-transform:uppercase; color:var(--text-3);
}
.call-row {
  margin:0 calc(-1 * var(--s5)); padding:11px var(--s5);
  border-top:1px solid var(--line);
  transition:background var(--t-fast);
}
.call-row:first-of-type { border-top:none; }
.call-row:hover { background:var(--sunken); }
.call-grid {
  display:grid; grid-template-columns:1fr 118px 1fr 62px 96px;
  gap:var(--s3); align-items:baseline;
  font-variant-numeric:tabular-nums;
}
.call-who { font-size:var(--t-base); font-weight:600; }
.call-what { font-size:var(--t-xs); color:var(--text-2); line-height:1.45; }
.call-len, .call-when { font-size:var(--t-xs); color:var(--text-3); text-align:right; }
.call-expand { margin-top:var(--s3); display:flex; gap:var(--s4); }
.call-panel {
  margin-top:var(--s3); padding:var(--s4);
  background:var(--sunken); border:1px solid var(--line); border-radius:var(--r-md);
  font-size:var(--t-xs); line-height:1.55; max-height:260px; overflow-y:auto;
  white-space:pre-wrap;
}
.call-panel ul { margin:4px 0 0; padding-left:18px; }
.call-panel .review-section { margin-bottom:var(--s3); white-space:normal; }
.call-panel .review-section:last-child { margin-bottom:0; }
.review-issues strong { color:var(--danger); }
.review-ok { color:var(--live); }
.review-suggestions strong { color:var(--text-2); }
@media (max-width: 720px) {
  .call-head { display:none; }
  .call-grid { grid-template-columns:1fr auto; }
  .call-grid .call-what { grid-column:1 / -1; }
}

/* ---------- 7.4 Greetings ----------
   Show the spoken line, not the field that sets it. */
.greeting-card {
  border:1px solid var(--line); border-radius:var(--r-md);
  padding:var(--s5); margin-bottom:var(--s4); background:var(--panel);
}
.greeting-card:last-of-type { margin-bottom:0; }
.greeting-card > h4 {
  margin:0 0 var(--s3); font-family:var(--font-body);
  font-size:var(--t-micro); font-weight:700; letter-spacing:.14em;
  text-transform:uppercase; color:var(--text-3);
}
.greeting-say {
  font-family:var(--font-display); font-size:19px; line-height:1.45;
  background:var(--accent-weak); border:1px solid var(--accent-line);
  border-radius:var(--r-md); padding:var(--s4); margin-bottom:var(--s4);
}
.greeting-say.off {
  font-family:var(--font-body); font-size:var(--t-base); font-style:italic;
  color:var(--text-3);
  background:repeating-linear-gradient(135deg, var(--sunken) 0 6px, transparent 6px 12px);
  border:1px dashed var(--line-2);
}
.greeting-note { font-size:var(--t-xs); color:var(--text-2); line-height:1.5; margin:0 0 var(--s3); }
.greeting-promise {
  font-size:var(--t-xs); color:var(--text-2); line-height:1.5;
  border-left:3px solid var(--live); padding-left:var(--s3); margin:0;
}

/* ---------- 6.4 Global search ----------
   The other half of the F-pattern: the sidebar is the vertical scan, this is
   the "I know what I want" path. Admin only. */
.palette-backdrop {
  position:fixed; inset:0; z-index:50;
  background:rgba(28,28,26,.34);
  display:flex; align-items:flex-start; justify-content:center;
  padding:12vh var(--s5) var(--s5);
}
.palette {
  width:100%; max-width:520px;
  background:var(--panel); border:1px solid var(--line-2);
  border-radius:var(--r-lg); box-shadow:var(--shadow-3);
  overflow:hidden; display:flex; flex-direction:column; max-height:70vh;
}
.palette input {
  width:100%; border:none; border-bottom:1px solid var(--line);
  border-radius:0; background:var(--panel);
  padding:var(--s4) var(--s5); font-size:var(--t-md);
}
.palette input:focus { outline:none; box-shadow:none; border-color:var(--line); }
.palette-results { overflow-y:auto; padding:var(--s2) 0; }
.palette-group {
  font-size:var(--t-micro); font-weight:700; letter-spacing:.14em;
  text-transform:uppercase; color:var(--text-3);
  padding:var(--s3) var(--s5) 5px;
}
.palette-item {
  display:flex; align-items:center; justify-content:space-between; gap:var(--s3);
  padding:9px var(--s5); font-size:var(--t-base); cursor:pointer;
  border:none; background:none; width:100%; text-align:left; color:var(--text);
}
.palette-item:hover, .palette-item.on { background:var(--accent-weak); color:var(--accent-hover); }
.palette-item .palette-where { font-size:var(--t-xs); color:var(--text-3); }
.palette-item.on .palette-where { color:var(--accent-hover); }
.palette-empty { padding:var(--s5); font-size:var(--t-sm); color:var(--text-2); text-align:center; }
.palette-foot {
  border-top:1px solid var(--line); background:var(--sunken);
  padding:8px var(--s5); font-size:var(--t-xs); color:var(--text-3);
  display:flex; gap:var(--s4);
}
.palette-hint {
  margin:8px 8px 0; padding:7px 12px; border-radius:var(--r-md);
  border:1px solid var(--line); background:var(--sunken);
  font-size:var(--t-xs); color:var(--text-3); cursor:pointer;
  display:flex; align-items:center; justify-content:space-between;
}
.palette-hint:hover { color:var(--text); border-color:var(--line-2); }
.palette-hint kbd {
  font-family:var(--font-body); font-size:10px; font-weight:700;
  border:1px solid var(--line-2); border-radius:4px; padding:1px 5px; color:var(--text-2);
}

/* ---------- 7.2 Onboarding checklist ----------
   "6 of 9" means nothing if you don't know which three matter, so the list
   splits into what blocks going live and what is already done. */
.check-line {
  display:flex; gap:var(--s3); align-items:flex-start;
  margin:0 calc(-1 * var(--s5)); padding:13px var(--s5);
  border-top:1px solid var(--line);
}
.check-line:first-of-type { border-top:none; }
.check-mark {
  width:18px; height:18px; border-radius:50%; flex-shrink:0; margin-top:2px;
  border:2px solid var(--line-2);
}
.check-body { flex:1; min-width:0; }
.check-title { font-size:var(--t-base); font-weight:600; }
.check-why { font-size:var(--t-xs); color:var(--text-2); margin-top:3px; line-height:1.5; }
.check-who {
  font-size:9.5px; font-weight:700; letter-spacing:.08em; text-transform:uppercase;
  padding:1px 6px; border-radius:var(--r-sm); margin-left:6px; vertical-align:1px;
  color:var(--text-2); background:var(--sunken); border:1px solid var(--line-2);
}
.check-done-grid {
  display:grid; grid-template-columns:1fr 1fr; gap:var(--s2) var(--s4);
  margin-top:var(--s3);
}
.check-done-item {
  display:flex; gap:8px; align-items:baseline;
  font-size:var(--t-sm); color:var(--text-2);
}
.check-tick { color:var(--live); font-weight:700; flex-shrink:0; }
.check-ask {
  margin:var(--s4) calc(-1 * var(--s5)) calc(-1 * var(--s5));
  padding:12px var(--s5); border-top:1px solid var(--line);
  background:var(--sunken); border-radius:0 0 var(--r-lg) var(--r-lg);
  font-size:var(--t-xs); color:var(--text-2); line-height:1.5;
}
/* Everything stays reachable during onboarding — the checklist is the short
   path, not a gate. THE ENTRIES DIM, NOT THE BAR: on a phone the rail is a
   sticky bar the page scrolls under, and a bar at 60% opacity showed every
   heading and field it passed over straight through itself. */
.rail.dimmed .rail-group, .rail.dimmed .rail-all { opacity:.6; }
.rail-note {
  font-size:var(--t-xs); color:var(--text-3);
  padding:0 var(--s5); line-height:1.45;
}
@media (max-width: 720px) { .check-done-grid { grid-template-columns:1fr; } }

/* A branch's own hours, inside its row's edit form. */
.loc-hours-off {
  display:flex; align-items:center; justify-content:space-between;
  gap:var(--s3); flex-wrap:wrap;
  font-size:var(--t-sm); color:var(--text-2);
}
.cl-field-ownHours .hours-grid { margin-top:var(--s2); }

/* --- Integration connection panel ---------------------------------------
   Three numbered steps because Make enforces that order: a scenario cannot
   be built before the client has authorised. Locked steps stay visible and
   dimmed rather than hidden — someone setting this up for the first time
   needs to see what is coming, not discover it one reveal at a time. */
.ig-panel {
  border: 1px solid var(--line);
  border-radius: var(--r-lg);
  background: var(--sunken);
  padding: var(--s5);
  margin-bottom: var(--s6);
}
.ig-head { display: flex; align-items: center; justify-content: space-between; gap: var(--s3); margin-bottom: var(--s2); }
.ig-head h4 { margin: 0; font-family: var(--font-display); font-size: var(--t-md); font-weight: 600; }

.ig-step { display: flex; gap: var(--s4); padding: var(--s4) 0; border-top: 1px solid var(--line); }
.ig-step:first-of-type { border-top: 0; }
.ig-step-n {
  flex: 0 0 26px; height: 26px; border-radius: var(--r-pill);
  background: var(--accent-weak); color: var(--accent); border: 1px solid var(--accent-line);
  font-size: var(--t-xs); font-weight: 700;
  display: flex; align-items: center; justify-content: center;
}
.ig-step-body { flex: 1 1 auto; min-width: 0; }
.ig-step-title { font-weight: 600; font-size: var(--t-base); margin-bottom: var(--s1); }

/* Dimmed, not disabled-looking: the inputs inside are genuinely inert until
   the previous step is done, and the buttons carry their own disabled state. */
.ig-step-locked { opacity: .5; }
.ig-step-locked .ig-step-n { background: var(--paused-weak); color: var(--text-3); border-color: var(--line-2); }

.ig-link { display: flex; gap: var(--s2); align-items: center; }
.ig-link input { flex: 1 1 auto; min-width: 0; }

.ig-live { background: var(--live-weak); border: 1px solid var(--live); border-radius: var(--r-md); padding: var(--s3) var(--s4); }
.ig-meta { font-size: var(--t-xs); color: var(--text-2); margin-top: var(--s1); }

.ig-error { background: var(--danger-weak); border: 1px solid var(--danger); color: var(--danger);
  border-radius: var(--r-md); padding: var(--s3) var(--s4); font-size: var(--t-sm); margin-bottom: var(--s4); }
.ig-warn { background: var(--onboarding-weak); border: 1px solid var(--onboarding); color: var(--onboarding);
  border-radius: var(--r-md); padding: var(--s3) var(--s4); font-size: var(--t-sm); margin: var(--s3) 0; }

.ig-caps { margin: var(--s3) 0; font-size: var(--t-sm); color: var(--text-2); }
.ig-caps-title { font-size: var(--t-micro); text-transform: uppercase; letter-spacing: .14em; font-weight: 700; color: var(--text-3); margin-bottom: var(--s2); }
.ig-caps ul { margin: 0; padding-left: var(--s5); }
.ig-caps li { margin-bottom: var(--s1); }
.ig-cap-limited { color: var(--onboarding); }
.ig-note { font-style: italic; }
.ig-opt { font-weight: 400; color: var(--text-3); font-size: var(--t-xs); }

.ig-status { font-size: var(--t-sm); color: var(--live); min-height: 18px; margin-top: var(--s2); }
.ig-status-bad { color: var(--danger); }

@media (max-width: 640px) {
  .ig-step { gap: var(--s3); }
  .ig-link { flex-wrap: wrap; }
  .ig-link input { flex: 1 1 100%; }
}
/* Separates the live connection above from the legacy note fields below it,
   which route nothing. */
.ig-legacy-head {
  margin: var(--s5) 0 var(--s1); font-family: var(--font-display);
  font-size: var(--t-base); font-weight: 600; color: var(--text-2);
}
/* A disabled button used to render identically to a live one, which reads as
   "this is ready, click it" and then silently does nothing. This was scoped to
   .ig-panel when the integrations flow needed it; it is global now that the
   booking form needs it too. The only other disabled buttons in these pages
   are the transient "Saving…" states in crud-list.js and hours-grid.js, where
   dimming is also the right answer. */
button:disabled {
  opacity: .45;
  cursor: not-allowed;
  box-shadow: none;
}
/* THE PAGE BUILDER'S "Saved" AND "Published" ARE ANSWERS, NOT UNAVAILABLE
   CONTROLS (walk7: "look disabled", dark text on dim blue at .45 opacity).
   They are disabled because there is nothing to press, and they read as done:
   full strength, the status tokens' own text on their own tinted ground, each
   measured at 4.5:1 or better in all three themes by check:portal-render. */
/* A HELD SAVE IS WORKING, NOT UNAVAILABLE (holdSave sets data-holding): full
   strength with its spinner, and the busy cursor instead of the not-allowed one. */
button[data-holding]:disabled { opacity: 1; cursor: progress; }
.sb-bar #sbSave:disabled { opacity: 1; cursor: default; color: var(--text-2); background: transparent; border-color: var(--line-2); }
.sb-bar #sbPublish:disabled { opacity: 1; cursor: default; color: var(--live); background: var(--live-weak); border: 1px solid var(--live); }
.sb-bar #sbPublish[data-holding]:disabled { color: var(--on-accent); background: var(--accent); border-color: var(--accent); cursor: progress; }

/* ---------------------------------------------------------------------------
   The booking diary (appointments.js). Shared by both pages.
   --------------------------------------------------------------------------- */

.appt-days { display: flex; flex-direction: column; gap: var(--s5); }

.appt-day-label {
  font-size: var(--t-micro); font-weight: 700; letter-spacing: .14em;
  text-transform: uppercase; color: var(--text-3);
  padding-bottom: var(--s2); border-bottom: 1px solid var(--line);
}

.appt-row {
  display: flex; align-items: flex-start; justify-content: space-between;
  gap: var(--s4); padding: var(--s3) 0; border-bottom: 1px solid var(--line);
}
.appt-row:last-child { border-bottom: none; }
.appt-row:hover { background: var(--sunken); }

.appt-when { font-weight: 600; font-variant-numeric: tabular-nums; }
.appt-who { font-size: var(--t-sm); color: var(--text-2); margin-top: 2px; }
.appt-note { font-size: var(--t-xs); color: var(--text-3); margin-top: 2px; }

/* Same reveal-on-hover as .product-row, so a long diary is not a wall of
   Cancel buttons. Kept visible on focus for keyboard use. */
.appt-actions { opacity: 0; transition: opacity var(--t-fast); flex: 0 0 auto; }
.appt-row:hover .appt-actions,
.appt-row:focus-within .appt-actions { opacity: 1; }
.appt-actions .link-btn { color: var(--text-2); }
.appt-row:hover .appt-actions .link-btn { color: var(--danger); }

.appt-form {
  margin-top: var(--s5); padding: var(--s5);
  background: var(--sunken); border: 1px solid var(--line); border-radius: var(--r-md);
}

.appt-slots { display: flex; flex-wrap: wrap; gap: var(--s2); }

/* A time is picked, never typed — these buttons are the only bookable times,
   because they are the ones the server said were free. */
.appt-slot {
  display: inline-flex; flex-direction: column; align-items: center; gap: 1px;
  padding: var(--s2) var(--s4); min-width: 84px;
  background: var(--panel); border: 1px solid var(--line-2); border-radius: var(--r-sm);
  font-family: inherit; font-size: var(--t-sm); font-weight: 600; color: var(--text);
  font-variant-numeric: tabular-nums; cursor: pointer;
  transition: border-color var(--t-fast), background var(--t-fast);
}
.appt-slot:hover { border-color: var(--accent); }
.appt-slot.is-chosen {
  background: var(--accent-weak); border-color: var(--accent); color: var(--accent);
}
.appt-slot-who { font-size: var(--t-xs); font-weight: 400; color: var(--text-2); }
.appt-slot.is-chosen .appt-slot-who { color: var(--accent); }

@media (max-width: 640px) {
  /* No hover on a phone, so the Cancel button has to be there already. */
  .appt-actions { opacity: 1; }
  .appt-actions button { min-height: 44px; }
  .appt-slot { min-height: 44px; justify-content: center; }
  /* U2 — a phone row is the job over its buttons, and Cancel sits at the far
     end of them, away from Move (walk4/cleaning: "Move Make this a regular
     Cancel" squeezed together in the top corner). */
  .appt-row { flex-direction: column; align-items: stretch; gap: var(--s2); }
  .appt-actions { display: flex; flex-wrap: wrap; align-items: center; gap: var(--s1) var(--s4); }
  .appt-actions .is-destructive { margin-left: auto; }
}

/* CANCEL IS NOT WHERE A THUMB AIMS FOR MOVE (walk4, U2). The day panel's
   actions under a job are one row: Move and Skip at its start, the button that
   destroys something pushed to its far end in the danger colour — and it asks
   before it does anything. The list's Cancel wears the same colour. */
.cal-day-actions { display: flex; flex-wrap: wrap; align-items: center; gap: var(--s2) var(--s5); margin-top: var(--s2); }
.cal-day-actions .is-destructive { margin-left: auto; }
.cal-day-actions .is-destructive,
.appt-actions .is-destructive { color: var(--danger); }

/* ---------------------------------------------------------------------------
   The month grid (calendar-view.js).

   Seven equal columns, and the cells are BUTTONS rather than divs so the whole
   grid is keyboard-reachable and a screen reader announces the date and how
   many bookings are on it. A grid of clickable divs is the commonest way a
   calendar becomes unusable without a mouse.

   Sized so a phone gets a real month rather than a squashed one: the cells
   shrink and the chips drop to a time only. Below 720px this is still seven
   columns, because a month that reflows to a list is not a month. */
.cal-head {
  display: flex; align-items: center; gap: var(--s3);
  margin-bottom: var(--s3); flex-wrap: wrap;
}
/* The month is the title of the screen, not a label on it. A diary you are
   scanning has to say WHICH month at a glance, or a client checking next
   Thursday reads this Thursday's row. */
.cal-head strong {
  font-family: var(--font-display); font-size: var(--t-xl); font-weight: 600;
  letter-spacing: -.015em;
}
.cal-head .quiet { padding: 4px 12px; line-height: 1.2; font-size: var(--t-base); }
.cal-count { color: var(--text-2); font-size: var(--t-xs); margin-left: auto; }

/* SEVEN EQUAL COLUMNS, AT EVERY WIDTH. That is the rule for this grid and it
   was one keyword short of holding: `1fr` is `minmax(auto, 1fr)`, and `auto`
   floors a track at its own min-CONTENT. `.cal-chip` is `white-space:nowrap`,
   so the first day with a booking on it — "9:00 AM Ruth" — pushed its column
   wide and squeezed the six empty ones, and at 390px Thursday and Saturday
   came out three times the width of Monday. The grid only looked even because
   the account it was designed against had nothing in it. minmax(0,1fr) is the
   floor the columns were always meant to have. */
.cal-grid {
  display: grid; grid-template-columns: repeat(7, minmax(0, 1fr)); gap: 2px;
  background: var(--line); border: 1px solid var(--line); border-radius: var(--r-md);
  overflow: hidden;
  /* WHAT FITS ON A MONTH CHIP IS DECIDED BY THIS GRID'S WIDTH, NOT THE
     WINDOW'S. The same component is drawn beside the admin's 280px sidebar and
     full-bleed in the portal, so at one viewport width the cells are 131px in
     one app and 171px in the other — a viewport media query would show the
     service name on a column too narrow to hold it in exactly one of them.
     `inline-size` is safe here: seven minmax(0,1fr) tracks already take their
     width from the parent and never from their contents.
     A browser without container queries simply keeps the narrow chip, which is
     more than the month said before any of this. */
  container-type: inline-size;
  container-name: calmonth;
}
/* And the cell itself needs the same floor, or a nowrap chip inside a flex
   column pushes the cell out instead of the track. */
.cal-cell { min-width: 0; }
.cal-cell > * { min-width: 0; max-width: 100%; }
.cal-dow {
  background: var(--sunken); color: var(--text-2);
  font-size: var(--t-xs); font-weight: 700; text-align: center; padding: 6px 0;
  text-transform: uppercase; letter-spacing: .06em;
}
.cal-cell {
  background: var(--panel); border: 0; cursor: pointer; text-align: left;
  min-height: 92px; padding: 6px; display: flex; flex-direction: column; gap: 3px;
  font: inherit; color: var(--text);
  transition: background var(--t-fast);
}
.cal-cell:hover { background: var(--accent-weak); }
.cal-cell:focus-visible { outline: 2px solid var(--accent); outline-offset: -2px; }
/* Days from the neighbouring months. Dimmed, not hidden: an empty first row
   reads as a broken grid. */
.cal-out { background: var(--sunken); }
.cal-out .cal-num { color: var(--text-3); }
.cal-today .cal-num {
  background: var(--accent); color: var(--on-accent);
  border-radius: 999px; width: 26px; height: 26px;
  display: inline-flex; align-items: center; justify-content: center;
}
.cal-selected { box-shadow: inset 0 0 0 2px var(--accent); }
/* THE DATE IS THE BIGGEST THING IN THE CELL. It was --t-xs, which made the
   number the smallest text on a screen whose whole job is answering "what is
   on the 14th" — the chips were louder than the date they belonged to. */
.cal-num { font-size: var(--t-base); font-weight: 700; color: var(--text); line-height: 1.1; }

/* WHAT IS ACTUALLY ON A DAY — the month chip.
   The month is the view a client OPENS on, and it used to draw every booking
   in one shade of the accent carrying "9:00 AM Ruth": one colour for thirteen
   jobs, and at 375px a 42px column that rendered that string as "9:0…", so
   every chip on a phone said the same three characters and none of them said
   a time. The day and week views were fixed and this was not.

   It carries the same facts they do now, dropped in this order as the column
   narrows: the service's own colour (free — it is the chip's ground and its
   left bar, and it is the ONLY one of the four that survives a phone), the
   time, the customer, the service in words.

   The eight tones are the same eight the day and week blocks wear. Bound once
   here to three local custom properties so the chip's own rules stay single —
   and so a chip with no service falls back through the same `var(..., x)`
   rather than needing a ninth ruleset. */
.cal-grid .tone-1 { --chip-ink: var(--tone-1); --chip-bg: var(--tone-1-weak); --chip-line: var(--tone-1-line); }
.cal-grid .tone-2 { --chip-ink: var(--tone-2); --chip-bg: var(--tone-2-weak); --chip-line: var(--tone-2-line); }
.cal-grid .tone-3 { --chip-ink: var(--tone-3); --chip-bg: var(--tone-3-weak); --chip-line: var(--tone-3-line); }
.cal-grid .tone-4 { --chip-ink: var(--tone-4); --chip-bg: var(--tone-4-weak); --chip-line: var(--tone-4-line); }
.cal-grid .tone-5 { --chip-ink: var(--tone-5); --chip-bg: var(--tone-5-weak); --chip-line: var(--tone-5-line); }
.cal-grid .tone-6 { --chip-ink: var(--tone-6); --chip-bg: var(--tone-6-weak); --chip-line: var(--tone-6-line); }
.cal-grid .tone-7 { --chip-ink: var(--tone-7); --chip-bg: var(--tone-7-weak); --chip-line: var(--tone-7-line); }
.cal-grid .tone-8 { --chip-ink: var(--tone-8); --chip-bg: var(--tone-8-weak); --chip-line: var(--tone-8-line); }

.cal-chips { display: flex; flex-direction: column; gap: 3px; min-width: 0; }
.cal-chip {
  display: flex; align-items: baseline; gap: 4px; min-width: 0;
  font-size: 11px; line-height: 1.35; padding: 1px 4px 1px 5px;
  border-radius: var(--r-sm);
  /* The neutral is the "no service recorded" case, exactly as .cal-ev's is —
     a paused grey rather than the accent, because the accent means something a
     client can act on and thirteen accented rectangles meant nothing. */
  background: var(--chip-bg, var(--paused-weak));
  border: 1px solid var(--chip-line, var(--line-2));
  border-left: 3px solid var(--chip-ink, var(--paused));
  color: var(--text);
  white-space: nowrap; overflow: hidden;
}
/* The time is the tone's ink; the customer is body text. Two colours, not
   three — the name is the one word on the chip that has to read as a NAME. */
.cal-chip-time { flex: none; font-weight: 700; color: var(--chip-ink, var(--text-2)); }
/* WHICH ONE GIVES WAY. Left to flexbox's defaults both shrink together, and
   "8am Delphine Wedding updo" came out "8am Delphi… Wedding …" — two facts,
   each sliced, neither read. The order the day view drops things in is time,
   then customer, then service, and `flex: 1 1 0` on the service below is what
   makes that true here without a magic shrink weight: with a basis of zero the
   service never contributes to the deficit, so it takes the room LEFT OVER
   after the time and the name are laid out whole, and gives it all back before
   the name loses a character. */
.cal-chip-who { flex: 0 1 auto; font-weight: 600; overflow: hidden; text-overflow: ellipsis; }
/* Hidden until the grid proves it has room. See the container query below. */
.cal-chip-what { display: none; flex: 1 1 0; min-width: 0; color: var(--chip-ink, var(--text-3)); overflow: hidden; text-overflow: ellipsis; }
/* ONE JOB, NOT THREE — the same doubled bar the day and week blocks carry, so
   a crew booking is recognisable as a crew booking in all three views. The
   month draws one chip per booking, so it is one job here by construction. */
.cal-chip.is-crew { border-left-style: double; border-left-width: 5px; }
/* A skipped visit, or one still waiting for a time, is not work in the diary.
   Drawn like a booking it reads as one.

   IT SAYS SO BY FORM, NOT BY FADING, and that is a correction rather than a
   preference. Opacity was the obvious way and it cannot work here: the time
   and the service on a chip are drawn in the tone's own ink, which starts at
   about 5.2:1 on its own ground, so ANY meaningful fade takes it under the
   bar — .55 measured 2.2:1 and even .62 only reached 2.55:1. There is no
   opacity that both reads as ghosted and stays legible. So the ink stays at
   full strength and the CHIP changes shape instead: the tinted fill drops
   away to the panel and the outline goes dashed, which is a provisional thing
   next to seven solid ones, and the customer's name is struck through. */
.cal-chip.is-off { background: var(--panel); border-style: dashed; }
.cal-chip.is-off .cal-chip-who { text-decoration: line-through; }

/* THE SERVICE NAME COSTS ROOM, so it is asked for rather than assumed.
   Measured in a real browser, not guessed: at 11px, "9am Ruth Cut & finish" is
   ~132px of content and the chip spends 14px on its bar, padding and border,
   so a column below ~150px shows the name sliced instead of the service. Seven
   columns of 150px plus their gaps is 1064px of grid. Below that the chip
   keeps the time and the customer — the same trade the day view makes at 55
   minutes — and the colour still says which service it is. */
@container calmonth (min-width: 1064px) {
  .cal-chip-what { display: block; }
}
.cal-more { font-size: 11px; color: var(--text-3); white-space: nowrap; }
/* C12 — the day's count, for the phone's month (see the 720px block). Hidden
   here, where every chip is drawn and "+N more" says the rest. */
.cal-daycount { display: none; }
/* A day somebody wanted and could not have. Amber rather than red: it is an
   opportunity, not a problem. */
.cal-wanting {
  font-size: 10px; font-weight: 700; color: var(--onboarding);
  /* --onboarding-line does not exist in tokens.css — the only `-line` token is
     the accent's. An undefined custom property in a shorthand makes the whole
     `border` declaration invalid, so this pill had NO border at all in any
     theme, and looked it against the tinted ground. --onboarding is the
     colour a badge on this ground is supposed to be outlined in. */
  background: var(--onboarding-weak); border: 1px solid var(--onboarding);
  border-radius: var(--r-sm); padding: 0 4px; align-self: flex-start;
  /* Its own max-width, not the cell's: `.cal-cell > *` caps the CHIPS at the
     cell width, and this pill is a grandchild of nothing — it sat straight
     through the side of a 42px cell and over the day beside it. */
  max-width: 100%; white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
}

/* ---------------------------------------------------------------------------
   Day and week (calendar-view.js). One layout, two densities: week hides the
   customer's name because seven columns have no room for it, and the hour
   labels are the same either way.

   The grid is drawn from the CLIENT'S OWN opening hours, not a fixed 9-to-5,
   and it stretches to cover anything booked outside them — see windowFor(). */
.cal-views { display: inline-flex; gap: 2px; padding: 2px; background: var(--sunken);
  border: 1px solid var(--line); border-radius: var(--r-pill); }
.cal-view {
  border: 0; background: none; font: inherit; font-size: var(--t-xs); font-weight: 600;
  color: var(--text-2); padding: 5px 12px; border-radius: var(--r-pill); cursor: pointer;
}
.cal-view.on { background: var(--panel); color: var(--text); box-shadow: var(--shadow-1); }
.cal-view:focus-visible { outline: 2px solid var(--accent); outline-offset: 1px; }

.cal-colhead { display: grid; grid-template-columns: 54px 1fr; margin-bottom: 2px; }
.cal-dayheads { display: grid; }
.cal-dayhead {
  border: 0; background: none; font: inherit; cursor: pointer; padding: 6px 2px;
  display: flex; flex-direction: column; align-items: center; gap: 1px;
  border-radius: var(--r-sm); color: var(--text-2);
}
.cal-dayhead:hover { background: var(--accent-weak); }
.cal-dayhead.on { background: var(--accent-weak); color: var(--accent); }
.cal-dayhead:focus-visible { outline: 2px solid var(--accent); outline-offset: -2px; }
.cal-dayhead-dow { font-size: var(--t-micro); font-weight: 700; letter-spacing: .08em; text-transform: uppercase; }
.cal-dayhead-num { font-size: var(--t-md); font-weight: 700; color: var(--text); line-height: 1.1; }
/* A column headed by a person rather than a date. */
.cal-dayhead.is-name { cursor: default; padding: 9px 6px; }
.cal-dayhead-name {
  font-size: var(--t-sm); font-weight: 600; color: var(--text);
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap; max-width: 100%;
}
.cal-bystaff {
  display: inline-flex; align-items: center; gap: 6px; font-size: var(--t-xs);
  color: var(--text-2); cursor: pointer; white-space: nowrap;
}
.cal-bystaff input { accent-color: var(--accent); }
.cal-dayhead.is-today .cal-dayhead-num {
  background: var(--accent); color: var(--on-accent); border-radius: 999px;
  width: 26px; height: 26px; display: inline-flex; align-items: center; justify-content: center;
}

.cal-time {
  --cal-hour: 54px;
  display: grid; grid-template-columns: 54px 1fr;
  height: calc(var(--cal-rows) * var(--cal-hour));
  border: 1px solid var(--line); border-radius: var(--r-md); overflow: hidden;
  background: var(--panel);
}
.cal-gutter { display: grid; grid-template-rows: repeat(var(--cal-rows), 1fr); background: var(--sunken);
  border-right: 1px solid var(--line); }
.cal-hour { position: relative; }
/* The label sits ON the line it names rather than inside the hour below it,
   which is where the eye looks for it. */
.cal-hour span {
  position: absolute; top: -1px; right: 6px; transform: translateY(-50%);
  font-size: var(--t-micro); font-weight: 700; color: var(--text-3); letter-spacing: .02em;
}
.cal-hour:first-child span { transform: none; top: 3px; }
.cal-cols { display: grid; }
.cal-col {
  position: relative; border-left: 1px solid var(--line);
  display: grid; grid-template-rows: repeat(var(--cal-rows), 1fr);
}
.cal-col:first-child { border-left: 0; }
.cal-col.is-shut { background: var(--sunken); }
.cal-line { border-top: 1px solid var(--line); }
.cal-line:first-child { border-top: 0; }
/* An appointment block. It is a JOB, and it says whose and what.
   Thirteen bookings used to render in one shade of the accent with the start
   time as their only word — and the start time is already printed in the
   gutter six pixels to the left, so the block said nothing the screen had not
   already said. The neutral below is the "no service recorded" case; a block
   with a service wears one of the eight tones (.tone-1 … .tone-8). */
.cal-ev {
  position: absolute; z-index: 1; overflow: hidden; text-align: left;
  background: var(--paused-weak); color: var(--text);
  border: 1px solid var(--line-2); border-left: 3px solid var(--paused);
  border-radius: var(--r-sm);
  font: inherit; font-size: var(--t-xs); line-height: 1.25; padding: 2px 5px;
  cursor: pointer; display: flex; flex-direction: column; gap: 0;
}
/* A crew job is ONE job drawn in several columns. The doubled left bar and
   the team name on every block are what stop an owner reading three blocks as
   three separate appointments; .is-linked outlines every copy at once when one
   is hovered or focused. Every value is an existing token. */
.cal-ev.is-crew { border-left-style: double; border-left-width: 5px; }
.cal-ev-crew {
  font-size: 10px; font-weight: 700; letter-spacing: .04em;
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
  opacity: .9;
}
.cal-ev.is-tight .cal-ev-crew { display: none; }
.cal-ev.is-slim .cal-ev-crew { display: none; }
.cal-ev.is-linked { outline: 2px dashed var(--accent); outline-offset: 1px; }
.crud-row-crew { font-weight: 600; }

.cal-ev.tone-1 { background: var(--tone-1-weak); border-color: var(--tone-1-line); border-left-color: var(--tone-1); }
.cal-ev.tone-2 { background: var(--tone-2-weak); border-color: var(--tone-2-line); border-left-color: var(--tone-2); }
.cal-ev.tone-3 { background: var(--tone-3-weak); border-color: var(--tone-3-line); border-left-color: var(--tone-3); }
.cal-ev.tone-4 { background: var(--tone-4-weak); border-color: var(--tone-4-line); border-left-color: var(--tone-4); }
.cal-ev.tone-5 { background: var(--tone-5-weak); border-color: var(--tone-5-line); border-left-color: var(--tone-5); }
.cal-ev.tone-6 { background: var(--tone-6-weak); border-color: var(--tone-6-line); border-left-color: var(--tone-6); }
.cal-ev.tone-7 { background: var(--tone-7-weak); border-color: var(--tone-7-line); border-left-color: var(--tone-7); }
.cal-ev.tone-8 { background: var(--tone-8-weak); border-color: var(--tone-8-line); border-left-color: var(--tone-8); }
.cal-ev.tone-1 .cal-ev-time, .cal-ev.tone-1 .cal-ev-what { color: var(--tone-1); }
.cal-ev.tone-2 .cal-ev-time, .cal-ev.tone-2 .cal-ev-what { color: var(--tone-2); }
.cal-ev.tone-3 .cal-ev-time, .cal-ev.tone-3 .cal-ev-what { color: var(--tone-3); }
.cal-ev.tone-4 .cal-ev-time, .cal-ev.tone-4 .cal-ev-what { color: var(--tone-4); }
.cal-ev.tone-5 .cal-ev-time, .cal-ev.tone-5 .cal-ev-what { color: var(--tone-5); }
.cal-ev.tone-6 .cal-ev-time, .cal-ev.tone-6 .cal-ev-what { color: var(--tone-6); }
.cal-ev.tone-7 .cal-ev-time, .cal-ev.tone-7 .cal-ev-what { color: var(--tone-7); }
.cal-ev.tone-8 .cal-ev-time, .cal-ev.tone-8 .cal-ev-what { color: var(--tone-8); }
/* Hover lifts rather than repaints: swapping the fill to the accent threw away
   the one thing the block is now carrying, which is its colour. */
.cal-ev:hover { box-shadow: var(--shadow-2); filter: brightness(.97); }
:root[data-theme="dark"] .cal-ev:hover,
:root[data-theme="lowblue"] .cal-ev:hover { filter: brightness(1.28); }
.cal-ev:focus-visible { outline: 2px solid var(--accent); outline-offset: 1px; }
.cal-ev { touch-action: none; }
.cal-ev.is-moving { opacity: .35; }
/* A cancelled booking is a record, not an obstacle — it reads as struck out
   rather than as something still in the chair.
   `opacity: .55` → dashed. Same correction as the month chip above and for the
   same measured reason: the block's time and service are the tone's ink at
   about 5.2:1, and .55 composited them to 2.2:1 — a booking a business could
   not read on the screen it runs its day from. The fade is replaced by the
   panel ground and a dashed outline, which says "not happening" without
   spending any of the contrast that says WHAT is not happening. */
.cal-ev.is-off { background: var(--panel); border-style: dashed; }
.cal-ev.is-off .cal-ev-who { text-decoration: line-through; }
/* Where it would land, at the time it would land — the grid says what it is
   about to do before the server is asked whether it may. */
.cal-ev-ghost {
  position: absolute; left: 1px; right: 1px; z-index: 2; pointer-events: none;
  border: 2px dashed var(--accent); border-radius: var(--r-sm);
  background: var(--accent-weak); color: var(--accent);
  font-size: var(--t-xs); font-weight: 700; padding: 1px 5px; overflow: hidden;
}
.cal-col { cursor: crosshair; }
.cal-col.is-shut { cursor: default; }
.cal-note { margin: var(--s2) 0 0; min-height: 18px; font-size: var(--t-xs); color: var(--text-2); }
.cal-note.is-bad { color: var(--danger); font-weight: 600; }
.cal-ev-time { font-weight: 700; flex: none; }
.cal-ev-who,
.cal-ev-what { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.cal-ev-who { font-weight: 600; }
.cal-ev-what { font-size: 11px; opacity: .95; }
/* GETTING IN — the customer's access note, on the job. One line, sliced
   rather than wrapped, and left off any block too short or too narrow to hold
   it: the job card under the grid carries it in full at every width, and that
   is what gets read on a phone at somebody's gate. Violet only when the
   client has hidden it from support — the one meaning that colour has. */
.cal-ev-access { font-size: 11px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.cal-ev-access.is-hidden { color: var(--private); }
.cal-ev.is-tight .cal-ev-access,
.cal-ev.is-short .cal-ev-access,
.cal-ev.is-slim .cal-ev-access { display: none; }

/* A 30-MINUTE BLOCK IS 27px AND HOLDS ONE LINE.
   Two 12px lines at 1.25 need about 34px of content, so the old block put "9am"
   on the first and the top third of "Amara Ellis" on the second, sliced through
   the x-height — and 30 minutes is the blow-dry, the one duration guaranteed to
   appear on every busy Saturday. Below 40 minutes the block stops trying to
   stack and puts the time and a first name on one line instead. A short name is
   legible; half a tall one is not. */
.cal-ev.is-tight { flex-direction: row; align-items: baseline; gap: 4px; }
.cal-ev.is-tight .cal-ev-what,
.cal-ev.is-short .cal-ev-what { display: none; }
/* Narrow, not short: a 118px column, or a lane inside one, has no room for a
   third line. Width is measured per block in calendar-view.js — the old rule
   was "more than three staff", which hid the client's name at exactly the
   staff count (four) that makes the by-person view worth having. */
/* THE WEEK ON A PHONE (walk3/trades: every block read "7–9am" and nothing
   else — `.is-slim` hid the customer and `.is-narrow` the job). A slim or
   narrow block keeps who and what, held to the phone month's rule — the text
   fits or there is no text: calendar-view.js fitBlocks() swaps a name too wide
   for its column for its initial and a service for its first word, or drops
   it (its colour and the key still say which). Clipped, never ellipsised, in
   the moment before that has run. */
.cal-ev.is-slim { padding: 1px 2px; }
.cal-ev.is-slim .cal-ev-time,
.cal-ev.is-slim .cal-ev-who,
.cal-ev.is-slim .cal-ev-what,
.cal-ev.is-narrow .cal-ev-what { text-overflow: clip; }
.cal-ev .is-dropped { display: none; }

/* The key to the eight tones. Only the services actually on screen, because a
   legend listing forty services is a second thing to read rather than a way to
   read the first. */
.cal-key {
  display: flex; flex-wrap: wrap; gap: var(--s2) var(--s4);
  margin: var(--s3) 0 0; font-size: var(--t-xs); color: var(--text-2);
}
.cal-key-item { display: inline-flex; align-items: center; gap: 6px; }
/* Solid, not the weak ground: a 10px square of the pale fill with a 1px rule
   around it reads as an empty checkbox, which is the wrong thing entirely on a
   line of text. The chip carries the tone's INK — the same colour as the rule
   down the left edge of every block wearing it. */
.cal-key-item i, .cal-key-dot {
  width: 10px; height: 10px; border-radius: 3px; display: inline-block; flex: none;
  background: var(--paused);
}
.cal-key-dot { margin-right: 6px; vertical-align: 0; }
.cal-key-item.tone-1 i, .cal-key-dot.tone-1 { background: var(--tone-1); }
.cal-key-item.tone-2 i, .cal-key-dot.tone-2 { background: var(--tone-2); }
.cal-key-item.tone-3 i, .cal-key-dot.tone-3 { background: var(--tone-3); }
.cal-key-item.tone-4 i, .cal-key-dot.tone-4 { background: var(--tone-4); }
.cal-key-item.tone-5 i, .cal-key-dot.tone-5 { background: var(--tone-5); }
.cal-key-item.tone-6 i, .cal-key-dot.tone-6 { background: var(--tone-6); }
.cal-key-item.tone-7 i, .cal-key-dot.tone-7 { background: var(--tone-7); }
.cal-key-item.tone-8 i, .cal-key-dot.tone-8 { background: var(--tone-8); }

/* Whose week. Each chip carries that person's count for the seven days on
   screen, which is the whole question — "who is empty next Thursday" is
   answered by the numbers before anything is clicked. */
.cal-people {
  display: flex; flex-wrap: wrap; gap: var(--s2);
  margin: 0 0 var(--s3);
}
@media (max-width: 720px) {
  /* One line that scrolls, not three that stack. Five chips wrapping is 150px
     of a 844px screen spent on a control, above the diary it controls. */
  .cal-people {
    flex-wrap: nowrap; overflow-x: auto; scrollbar-width: none;
    scroll-snap-type: x proximity; margin-left: calc(var(--s4) * -1);
    margin-right: calc(var(--s4) * -1); padding: 2px var(--s4);
  }
  .cal-people::-webkit-scrollbar { display: none; }
  .cal-chipbtn { flex: none; scroll-snap-align: start; }
}
.cal-chipbtn {
  display: inline-flex; align-items: center; gap: 6px; cursor: pointer;
  font: 600 12.5px/1 var(--font-body); color: var(--text-2);
  background: var(--panel); border: 1px solid var(--line-2);
  border-radius: var(--r-pill); padding: 7px 12px; min-height: 34px;
  transition: background var(--t-fast), border-color var(--t-fast), color var(--t-fast);
}
.cal-chipbtn:hover { color: var(--text); border-color: var(--text-3); }
.cal-chipbtn.on { color: var(--accent); background: var(--accent-weak); border-color: var(--accent); }
.cal-chipbtn:focus-visible { outline: 2px solid var(--accent); outline-offset: 1px; }
.cal-chipbtn-n {
  font-size: 11px; font-weight: 700; color: var(--text-3);
  background: var(--sunken); border-radius: var(--r-pill); padding: 1px 6px;
}
.cal-chipbtn.on .cal-chipbtn-n { color: var(--accent); background: var(--panel); }

/* U1 — WHO DOES IT, on a block where several people work (walk4/salon: no
   block named the stylist). Whole or dropped (fitBlocks), and dropped before
   the service when a block runs out of room. */
.cal-ev-staff { font-size: 11px; overflow: hidden; text-overflow: clip; white-space: nowrap; }
.cal-ev.is-tight .cal-ev-staff,
.cal-ev.is-short .cal-ev-staff,
.cal-ev.is-slim .cal-ev-staff { display: none; }

/* U1 — A PHONE'S WEEK IS A LIST BY DAY (calendar-view.js weekList). Each day a
   heading, each job one line — the time, "Mia C.", the service, who — on the
   service's own colour bar, wrapping rather than cut, and a tap opens the
   day's panel below. Nothing to scroll sideways across 7 days × 3 stylists. */
.cal-weeklist { display: flex; flex-direction: column; gap: var(--s3); }
.cal-weeklist .tone-1 { --chip-ink: var(--tone-1); --chip-bg: var(--tone-1-weak); --chip-line: var(--tone-1-line); }
.cal-weeklist .tone-2 { --chip-ink: var(--tone-2); --chip-bg: var(--tone-2-weak); --chip-line: var(--tone-2-line); }
.cal-weeklist .tone-3 { --chip-ink: var(--tone-3); --chip-bg: var(--tone-3-weak); --chip-line: var(--tone-3-line); }
.cal-weeklist .tone-4 { --chip-ink: var(--tone-4); --chip-bg: var(--tone-4-weak); --chip-line: var(--tone-4-line); }
.cal-weeklist .tone-5 { --chip-ink: var(--tone-5); --chip-bg: var(--tone-5-weak); --chip-line: var(--tone-5-line); }
.cal-weeklist .tone-6 { --chip-ink: var(--tone-6); --chip-bg: var(--tone-6-weak); --chip-line: var(--tone-6-line); }
.cal-weeklist .tone-7 { --chip-ink: var(--tone-7); --chip-bg: var(--tone-7-weak); --chip-line: var(--tone-7-line); }
.cal-weeklist .tone-8 { --chip-ink: var(--tone-8); --chip-bg: var(--tone-8-weak); --chip-line: var(--tone-8-line); }
.cal-wl-day { display: flex; flex-direction: column; gap: 4px; }
.cal-wl-head {
  display: flex; align-items: center; gap: var(--s2); min-height: 36px; padding: 0;
  background: none; border: 0; border-bottom: 1px solid var(--line); cursor: pointer; text-align: left;
  font: 700 var(--t-sm)/1.2 var(--font-body); color: var(--text);
}
.cal-wl-day.is-today .cal-wl-head { color: var(--accent); }
.cal-wl-row {
  display: block; width: 100%; min-height: 44px; padding: 8px 10px; cursor: pointer; text-align: left;
  background: var(--chip-bg, var(--sunken)); border: 1px solid var(--chip-line, var(--line));
  border-left: 3px solid var(--chip-ink, var(--line-2)); border-radius: var(--r-sm);
  font: 400 var(--t-sm)/1.35 var(--font-body); color: var(--text);
}
.cal-wl-row.is-crew { border-left-style: double; border-left-width: 5px; }
.cal-wl-row.is-off { background: var(--panel); border-style: dashed; }
.cal-wl-row.is-off .cal-wl-who { text-decoration: line-through; }
.cal-wl-time { font-weight: 700; color: var(--chip-ink, var(--text-2)); margin-right: 4px; }
.cal-wl-who { font-weight: 600; }
.cal-wl-none { margin: 0; padding: 2px 0 4px; font-size: var(--t-xs); color: var(--text-3); }

/* U1 — WHAT A PHONE'S MONTH CELL CARRIES, by kind of business (the portal's
   SEGMENT_UI `calendar.month`). Hidden at desktop widths, where a chip holds
   the time, the customer and the service. */
.cal-dots { display: none; flex-wrap: wrap; gap: 2px; }
.cal-dot {
  display: inline-flex; align-items: center; justify-content: center;
  width: 15px; height: 15px; border-radius: 999px; font: normal 700 9px/1 var(--font-body);
  color: var(--text); background: var(--panel); border: 1px solid var(--text-3);
}
.cal-more.is-past-two { display: none; }

@media (max-width: 720px) {
  .cal-time { --cal-hour: 46px; grid-template-columns: 42px 1fr; }
  .cal-colhead { grid-template-columns: 42px 1fr; }
  .cal-ev { font-size: 10px; padding: 1px 3px; }
  .cal-views { order: 3; width: 100%; justify-content: center; }
  /* The switcher was 25px tall inside a 44px pill, so the pill looked like the
     target and was not. Switching Month→Day is the gesture made most often on
     a phone, one-handed, and it is the only one on this screen that missed. */
  .cal-view { min-height: 44px; padding: 0 16px; }
  .cal-chipbtn { min-height: 40px; }
}

@media (max-width: 720px) {
  .cal-cell { min-height: 64px; padding: 4px 3px; }
  /* A PHONE COLUMN IS 42-44px. The desktop chip — "9:00 AM Ruth" — rendered
     there as "9:0…", the same three characters on every chip, so the phone
     month first dropped the text altogether for one solid 10px mark of the
     service's colour. That kept the rule it was held to — EITHER THE TEXT
     FITS OR THERE IS NO TEXT — and said which SERVICE a day held, never WHO.
     C12 — NAMES, NOT DOTS. A chip is now the customer's first name on the
     chip's own ground, with the service's colour as its left bar — the
     desktop chip keeping one of its four facts — and there is room for two a
     cell without growing the row. Higher specificity than the container query
     above on purpose: at this width the answer is the same in either app.
     The day's count sits in the corner, so two names and a 3 says there is
     one more, where "+1 more" would wrap onto two lines of a 44px cell.
     THE RULE IS UNCHANGED: EITHER THE TEXT FITS OR THERE IS NO TEXT. A name
     too wide for its column is swapped for its initial by calendar-view.js
     (fitMonthNames), measured in the browser, so nothing here is ever drawn
     cut short; `clip` below is a floor under that, not the mechanism. The
     bar is the tone's own ink, which clears 4.5:1 on every day it can sit on,
     and the name is body text on the tone's weak ground — the same pair the
     desktop chip is measured at. */
  .cal-cell { position: relative; }
  /* Room in the corner for the count: 22px + 14px fits a 36px column. */
  .cal-today .cal-num { width: 22px; height: 22px; }
  .cal-daycount {
    display: block; position: absolute; top: 4px; right: 3px;
    min-width: 12px; padding: 0 2px; border-radius: 999px;
    font-size: 10px; font-weight: 700; line-height: 13px; text-align: center;
    color: var(--text-2); background: var(--panel); border: 1px solid var(--line-2);
  }
  .cal-grid .cal-chips { gap: 2px; }
  .cal-grid .cal-chip {
    display: block; font-size: 10px; line-height: 1.3;
    border-width: 0 0 0 3px; border-radius: 2px; padding: 0 0 0 2px;
  }
  .cal-grid .cal-chip > * { display: none; }
  .cal-grid .cal-chip > .cal-chip-who {
    display: block; overflow: hidden; white-space: nowrap; text-overflow: clip;
  }
  /* R7-19 — the time beside the name, only where both fit whole: set by
     fitMonthNames, which measures. Before the nth-child rule below, which has
     the same weight and so still hides a third chip. */
  .cal-grid .cal-chip.is-timed { display: flex; gap: 2px; }
  .cal-grid .cal-chip.is-timed > .cal-chip-time { display: block; flex: none; }
  .cal-grid .cal-chip.is-timed > .cal-chip-who { flex: 0 1 auto; min-width: 0; }
  /* Two names a cell. The count above says the rest. */
  .cal-grid .cal-chip:nth-child(n+3) { display: none; }
  /* Still one job, and still said with the doubled bar the desktop draws. */
  .cal-grid .cal-chip.is-crew { border-left-width: 5px; }
  /* A skipped visit says so by shape, as it does on the desktop: the panel
     ground, a dashed bar at full-strength ink, and the name struck through. */
  .cal-grid .cal-chip.is-off { border-left-style: dashed; }
  /* The count stands in for "+N more", which counts past THREE chips — wrong
     here, where two are drawn, as well as too wide. "N cancelled" on an empty
     day is a different sentence and stays. */
  .cal-grid .cal-more.is-over { display: none; }
  /* U1 — A salon's month: the day's count and one dot per stylist, and no
     chip words at all. A cleaner's: two first names, then "+N" where the
     count would sit. "N canceled" does not fit a 42px column; the day panel
     under the month says it. */
  .cal-grid.is-people .cal-chips { display: none; }
  .cal-grid.is-people .cal-dots { display: flex; }
  .cal-grid.is-names .cal-daycount { display: none; }
  .cal-grid.is-names .cal-more.is-past-two { display: block; }
  .cal-grid .cal-more.is-cancelled { display: none; }
  /* NOTHING HERE IS GHOSTED WITH OPACITY, for the reason the dots learned:
     a skipped visit's mark at .4 measured 1.83:1 against its own day, under
     the 3:1 floor for a mark that carries meaning. The dashed bar above keeps
     the ink at full strength. */
  /* "+3 more" wrapped onto two lines in a 42px cell and spent more height than
     the three chips above it. On a phone the word is the part that does not
     earn its room. */
  .cal-more-word { display: none; }
  .cal-more { font-size: 10px; font-weight: 700; }
  /* Same reason, same trade — and the pill keeps its amber, which is what says
     this is an opportunity rather than a booking. The title spells it out. */
  .cal-wanting-word { display: none; }
  /* The key is the only way to read the colours here, so it gets to be
     legible rather than compact. */
  .cal-key { gap: var(--s2) var(--s3); }
  /* The header used to spend four stacked rows on ‹, the month, ›, the count
     and Today — on the screen with the least room in the product. The two
     arrows also sat either side of a 316px title, so paging a month was a
     two-handed gesture. Now: arrows and month on one row with the arrows
     together, everything else on the next. */
  .cal-head { gap: var(--s2) var(--s3); }
  .cal-head strong { flex: 1 0 100%; font-size: var(--t-lg); order: 1; }
  .cal-head #calPrev { order: 2; }
  .cal-head #calNext { order: 3; }
  .cal-bystaff { order: 4; }
  .cal-count { order: 5; width: auto; margin-left: auto; }
  .cal-head #calToday { order: 6; }
  .cal-views { order: 7; width: 100%; justify-content: center; }
}

/* R8 (walk7 cleaning and pm: a phone's month read "8 M", "9 T", "10 D"). At
   520px and under, a cell is: the date, the day's count, and the FIRST booking
   of the day on two lines, its time whole ("9am") over the customer's name,
   whole or ending in an ellipsis, never an initial. The day's count says how
   many more; tapping the cell opens the day, which lists them all. The
   ladder in calendar-view.js (fitMonthNames) stands aside at this width. */
@media (max-width: 520px) {
  .cal-cell { min-height: 78px; padding: 3px 2px; }
  .cal-grid .cal-chip {
    display: flex; flex-direction: column; align-items: stretch; gap: 0;
    font-size: 10px; line-height: 1.3; padding: 1px 0 1px 1px;
  }
  .cal-grid .cal-chip > .cal-chip-time,
  .cal-grid .cal-chip > .cal-chip-who {
    display: block; flex: none; min-width: 0; overflow: hidden; white-space: nowrap;
  }
  .cal-grid .cal-chip > .cal-chip-who { text-overflow: ellipsis; }
  /* An arrival window ("9:30–11:30am") breaks after its dash rather than being
     cut; a plain time is one word and stays on one line. */
  .cal-grid .cal-chip > .cal-chip-time { white-space: normal; }
  /* The first booking only: the count and the day's own list carry the rest. */
  .cal-grid:not(.is-people) .cal-chip:nth-child(n+2) { display: none; }
  .cal-grid.is-names .cal-daycount { display: block; }
  .cal-grid.is-names .cal-more.is-past-two { display: none; }
}

/* R8 (walk7, every phone reviewer): TAP TARGETS OF 44px AT 520px AND UNDER.
   Padding and min-height only, so a desktop is untouched. The setup list's
   buttons (23px), "Choose what you see" (32), the Analytics range buttons
   (27), the page builder's arrows and remove (26 wide, remove touching move),
   ticks (a 20px box in a 26px row), the calendar's phone link (15) and its
   header buttons (30 wide). check:portal-render measures each at 390px. */
@media (max-width: 520px) {
  /* "Delete saves at once. Add and Edit wait for Save." is ~350px on one line,
     wider than a phone's column: it wraps inside the column, same look. */
  .saves-chip { white-space: normal; max-width: 100%; }
  .an-seg-btn { min-height: 44px; }
  .link-btn, .btn-link {
    display: inline-flex; align-items: center; justify-content: center;
    min-width: 44px; min-height: 44px;
  }
  .kb-vis > summary, .sb-fold > summary, .chat-note > summary {
    min-height: 44px; display: flex; align-items: center;
  }
  .kb-vis > summary { padding-block: 0; }
  .kb-vis-row, .sb-booking, label.check { min-height: 44px; align-items: center; }
  /* A tick row is the whole label: 44px tall, the box inside it as it was. */
  label.check-row, label.lb-tick, .cal-bystaff { min-height: 44px; display: flex; align-items: center; }
  .spend-cap-choice label { min-height: 44px; display: flex; align-items: center; }
  .kb-vis-row { padding-block: 0; }
  label.check { display: flex; gap: var(--s2); }
  .sb-page { min-height: 44px; padding: 6px 16px; }
  .sb-block-name { min-height: 44px; }
  /* Move up, move down and remove: 44 wide each, and remove set apart from
     the arrows by a gap so a thumb aiming at one cannot land on the other. */
  .sb-block-tools { gap: 6px; }
  .sb-block-tools .link-btn { padding: 6px 8px; }
  .sb-block-tools .link-btn[data-delete] { margin-left: var(--s3); }
  .cal-head .quiet, .cal-head .link-btn { min-width: 44px; min-height: 44px; }
  .cal-chipbtn { min-height: 44px; }
  /* A phone number is a tap-to-call target wherever it is written. */
  a[href^="tel:"] { display: inline-flex; align-items: center; min-height: 44px; }
  .sb button.sb-who, .sb button.sb-line { min-height: 44px; }
  .kwc-x { min-width: 44px; min-height: 44px; }
  /* The header's theme picker was 12px text: 16px is the size a phone will not zoom on. */
  select.theme-select { font-size: 16px; min-height: 44px; }
}

/* ---------------------------------------------------------------------------
   Portal tabs (tabs.js). A row of tabs above the rail, not beside it: the rail
   navigates within a tab, the tabs choose which set of sections exists at all.

   Scrolls horizontally on a phone rather than wrapping to two rows — five tabs
   wrapping is a header that changes height as you move through it, which
   shifts the page under your thumb. */
/* min-width:0 IS THE WHOLE FIX, AND IT IS NOT OPTIONAL.
   .tab-row already said `overflow-x:auto` and still pushed the body 81px wide
   at 390px: the bar is a flex ITEM of <header>, a flex item's min-width
   defaults to `auto` — its min-CONTENT width — and five tabs are 447px of
   min-content. So the item refused to shrink, the scroller never had less room
   than its contents, and it never scrolled. The Calendar tab, the only one a
   stylist needs mid-appointment, sat with three pixels on screen and the whole
   page slid sideways. max-width pins it for the non-flex case. */
.tab-bar { margin-bottom: var(--s4); min-width: 0; max-width: 100%; }
/* In the page header, the bar is chrome rather than content: no bottom rule of
   its own (the header already has one) and no margin under it. */
header .tab-bar { margin-bottom: 0; }
#tabHint { margin: 0 0 var(--s4); }
.tab-row {
  display: flex; gap: var(--s1); border-bottom: 1px solid var(--line);
  overflow-x: auto; scrollbar-width: none;
  /* A scroller with no visible scrollbar needs to snap, or a half-tab parked
     under the right edge reads as a clipped tab rather than as more to come. */
  scroll-snap-type: x proximity; overscroll-behavior-x: contain;
}
.tab-row > .tab { scroll-snap-align: start; }
.tab-row::-webkit-scrollbar { display: none; }
.tab {
  appearance: none; background: transparent; border: 0; cursor: pointer;
  font: inherit; font-weight: 600; font-size: var(--t-base); color: var(--text-2);
  padding: 10px 14px; white-space: nowrap;
  border-bottom: 2px solid transparent; margin-bottom: -1px;
  transition: color var(--t-fast), border-color var(--t-fast);
}
.tab:hover { color: var(--text); }
.tab.active { color: var(--accent); border-bottom-color: var(--accent); }
.tab:focus-visible { outline: 2px solid var(--accent); outline-offset: -4px; border-radius: var(--r-sm); }
.tab-hint { font-size: var(--t-xs); color: var(--text-2); margin: var(--s2) 0 0; }

@media (max-width: 720px) {
  .tab { padding: 12px 12px; min-height: 44px; }
  .tab-hint { display: none; }
}

/* ON A PHONE, ONE MENU FOR THE TABS AND ONE FOR THE PANELS (tabs.js).
   Two sideways-scrolling rows of names clipped at the screen's edge ("Wha",
   "Custo", "Availability E") were most of a phone's first screen. A menu names
   every entry whole, in one line. Hidden above 720px, where the tabs are a row
   and the rail a column. */
.tab-select, .rail-select { display: none; }
@media (max-width: 720px) {
  .tab-row { display: none; }
  .tab-select { display: block; width: 100%; }
  .rail > .rail-group { display: none; }
  .rail-select { display: block; flex: 1 1 auto; min-width: 0; width: auto; }
  .rail-select[hidden] { display: none; }
}

/* ---------------------------------------------------------------------------
   The page builder (site-builder.js). Editor on the left, live preview on the
   right; stacked below 1000px, because a preview squeezed into half of a phone
   shows nothing useful.

   On a computer a section is dragged into place, and resized on the preview;
   on a phone the move buttons do it and a finger scrolls, because these pages
   get edited from a van. Only a mouse or a trackpad drags (site-builder.js). */
.sb-bar {
  display: flex; align-items: center; gap: var(--s3); flex-wrap: wrap;
  padding-bottom: var(--s3); border-bottom: 1px solid var(--line); margin-bottom: var(--s3);
}
.sb-bar .primary, .sb-bar .quiet { padding: 7px 16px; }
.sb-bar .muted { margin-right: auto; font-size: var(--t-xs); }

/* The preview is the point of the screen, so it gets the room. A 50/50 split
   left the page being built rendering into half a column, which is why it
   looked cramped: the editor needs a readable column, not an equal one. */
.sb-cols { display: grid; grid-template-columns: minmax(0,1fr) minmax(0,1fr); gap: var(--s5); align-items: start; }
@media (min-width: 1180px) { .sb-cols { grid-template-columns: minmax(0,400px) minmax(0,1fr); } }
@media (min-width: 1500px) { .sb-cols { grid-template-columns: minmax(0,440px) minmax(0,1fr); } }
.sb-edit h4 { margin: 0 0 var(--s2); font-size: var(--t-base); }

.sb-pages { display: flex; gap: var(--s2); flex-wrap: wrap; margin-bottom: var(--s3); align-items: center; }
.sb-page {
  appearance: none; border: 1px solid var(--line-2); background: var(--panel); color: var(--text);
  font: inherit; font-weight: 600; font-size: var(--t-xs); padding: 6px 12px;
  border-radius: 999px; cursor: pointer;
}
.sb-page.on { border-color: var(--accent); color: var(--accent); background: var(--accent-weak); }
/* A page tab is a drop target while a section is in the air. */
.sb-page.drop-onto {
  border-color: var(--accent); background: var(--accent); color: var(--on-accent);
  box-shadow: 0 0 0 3px var(--accent-line);
}

.sb-blocks { list-style: none; margin: 0 0 var(--s3); padding: 0; display: flex; flex-direction: column; gap: var(--s2); }
.sb-block { border: 1px solid var(--line); border-radius: var(--r-md); background: var(--panel); }
.sb-block.is-open { border-color: var(--accent); }
.sb-block-head { display: flex; align-items: center; gap: var(--s2); padding: 4px 8px 4px 0; }
/* The grip is the sign that the header drags — the whole header does, its
   name included (wireDrag()), and it shows only for a mouse or a trackpad
   (below). The arrows beside it are not a fallback for taste: they are the
   whole interaction on a phone and for a keyboard. */
.sb-grip {
  display: none; flex: none; user-select: none; padding: 8px 4px 8px 8px;
  color: var(--text-3); font-size: 15px; line-height: 1; letter-spacing: -1px;
}
.sb-block.is-dragging { opacity: .45; }
.sb-block.drop-above { box-shadow: 0 -3px 0 -1px var(--accent); }
.sb-block.drop-below { box-shadow: 0 3px 0 -1px var(--accent); }
/* Sideways drops get a vertical bar, so the shape of the indicator says which
   kind of move it is before you let go. */
.sb-block.drop-left { box-shadow: -3px 0 0 -1px var(--accent); }
.sb-block.drop-right { box-shadow: 3px 0 0 -1px var(--accent); }
/* A section that shares a band says so on its own row, or the page and the
   list disagree about what the page looks like. */
/* What is dragging, under the finger. pointer-events:none so
   elementFromPoint sees the list underneath it and not the ghost. */
.sb-ghost {
  position: fixed; top: 0; left: 0; z-index: 60; pointer-events: none;
  opacity: .92; box-shadow: var(--shadow-3); border-radius: var(--r-md);
  background: var(--panel); border: 1px solid var(--accent);
}
.sb-ghost .sb-block-body { display: none; }
/* Sections that share a band on the page are bracketed in the list, because a
   flat list beside a two-column page is a client publishing to find out. */
.sb-block.in-band { border-left: 3px solid var(--accent-line); border-radius: 0 var(--r-md) var(--r-md) 0; }
.sb-block.band-first { border-top-left-radius: 0; margin-top: 2px; }
.sb-block.band-last { border-bottom-left-radius: 0; margin-bottom: 2px; }
.sb-blocks .sb-block.in-band:not(.band-last) { margin-bottom: -6px; }
/* The header is the handle. The dots are the sign that says so. */
.sb-block-head[data-drag] { user-select: none; }
.sb-block-head[data-drag] button { cursor: pointer; }
/* A mouse or a trackpad drags, and only there are the grip, the grab cursor
   and the drag hint drawn; everywhere else the arrow hint shows and a finger
   on a header scrolls the page (Harley, 14 Sep 2026). */
.sb-if-mouse { display: none; }
@media (hover: hover) and (pointer: fine) {
  .sb-grip { display: inline; }
  .sb-block-head[data-drag] { cursor: grab; }
  .sb-block-head[data-drag]:active { cursor: grabbing; }
  p.sb-if-mouse { display: block; }
  span.sb-if-mouse { display: inline; }
  .sb-if-touch { display: none; }
}
/* The looks. Each swatch is painted in its OWN colours, so the choice is made
   by looking rather than by reading a name. */
.sb-label { display:block; font-weight:600; font-size:var(--t-base); margin: var(--s4) 0 2px; }
.sb-palettes { display:grid; grid-template-columns:repeat(auto-fill,minmax(112px,1fr)); gap:var(--s2); margin:var(--s2) 0 var(--s4); }
.sb-pal {
  position:relative; border:2px solid var(--line); border-radius:var(--r-md); cursor:pointer;
  padding:10px 10px 8px; text-align:left; font:inherit; display:flex; flex-direction:column; gap:6px;
  min-height:66px;
}
.sb-pal.on { outline:2px solid var(--accent); outline-offset:1px; }
.sb-pal:focus-visible { outline:2px solid var(--accent); outline-offset:2px; }
.sb-pal-ink, .sb-pal-accent { position:absolute; top:9px; width:13px; height:13px; border-radius:999px; }
.sb-pal-ink { right:26px; }
.sb-pal-accent { right:9px; }
.sb-pal-name { font-size:var(--t-xs); font-weight:700; margin-top:auto; line-height:1.2; }
/* Typed colours. A swatch next to the code, because some people know the hex
   they want and some want to poke at a wheel until it looks right. */
.sb-hexes { display:grid; grid-template-columns:repeat(auto-fill,minmax(168px,1fr)); gap:var(--s3); margin:var(--s2) 0 var(--s3); }
.sb-hex label { display:block; font-size:var(--t-xs); color:var(--text-2); margin-bottom:3px; }
.sb-hex-row { display:flex; gap:6px; align-items:center; }
.sb-hex-row input[type="color"] {
  flex:none; width:36px; height:34px; padding:2px; border:1px solid var(--line);
  border-radius:var(--r-sm); background:var(--panel); cursor:pointer;
}
.sb-hex-row input[type="text"] {
  flex:1; min-width:0; font-family:ui-monospace,SFMono-Regular,Menlo,monospace;
  font-size:var(--t-sm); text-transform:lowercase;
}
/* Said while they are still typing, not after they save — an unfinished "#a2"
   is not an error, it is a person mid-word. */
.sb-hex-row input[type="text"].is-bad { border-color:var(--danger); }
/* A width is a slider now, not a menu of four. */
.sb-steps { display:flex; align-items:center; gap:var(--s3); }
.sb-steps input[type="range"] { flex:1; min-width:0; accent-color:var(--accent); }
.sb-steps output {
  flex:none; min-width:8ch; text-align:right; font-size:var(--t-xs);
  font-weight:700; color:var(--text-2); font-variant-numeric:tabular-nums;
}
.sb-span {
  flex: none; font-size: var(--t-micro); font-weight: 700; letter-spacing: .06em;
  text-transform: uppercase; padding: 3px 8px; border-radius: var(--r-pill);
  background: var(--accent-weak); color: var(--accent); border: 1px solid var(--accent-line);
}

/* The editor column is long. Folding the settings a client sets once keeps
   the part they actually work in — the sections on the page — at the top. */
.sb-fold { margin-top: var(--s5); border-top: 1px solid var(--line); padding-top: var(--s3); }
.sb-fold > summary {
  cursor: pointer; list-style: none; display: flex; align-items: center; gap: var(--s2);
}
.sb-fold > summary::-webkit-details-marker { display: none; }
.sb-fold > summary::before {
  content: "\25B8"; color: var(--text-3); font-size: 11px; transition: transform var(--t-fast);
}
.sb-fold[open] > summary::before { transform: rotate(90deg); }
.sb-fold > summary h4 { margin: 0; font-size: var(--t-base); }
.sb-fold > summary:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; border-radius: var(--r-sm); }
.field-hint.is-good { color: var(--live); font-weight: 600; }
.field-hint.is-bad { color: var(--danger); font-weight: 600; }
.sb-block-name {
  appearance: none; border: 0; background: transparent; font: inherit; color: var(--text);
  text-align: left; cursor: pointer; padding: 10px 12px; flex: 1; min-width: 0;
  display: flex; flex-direction: column; gap: 2px;
}
.sb-block-name .muted { font-size: var(--t-xs); color: var(--text-2); }
.sb-block-tools { display: flex; gap: 2px; flex: none; }
.sb-block-tools .link-btn { padding: 6px 8px; }
.sb-block-tools .link-btn[disabled] { opacity: .35; cursor: default; }
.sb-block-body { padding: 0 12px 12px; border-top: 1px solid var(--line); margin-top: 4px; padding-top: 12px; }

.sb-thumb { max-width: 160px; border-radius: var(--r-sm); border: 1px solid var(--line); margin-bottom: var(--s2); }
.sb-thumbs { display: flex; gap: var(--s2); flex-wrap: wrap; }
.sb-thumb-wrap { position: relative; display: inline-flex; flex-direction: column; align-items: flex-start; }

.sb-preview { position: sticky; top: var(--s4); }
.sb-preview-bar { display: flex; align-items: center; gap: var(--s3); margin-bottom: var(--s2); flex-wrap: wrap; }
.sb-preview-bar .muted { font-size: var(--t-xs); color: var(--text-2); margin-right: auto; }
.sb-preview-bar select { width: auto; }
#sbFrame {
  height: min(80vh, 940px); min-height: 460px; border: 1px solid var(--line);
  border-radius: var(--r-md); background: #fff; display: block; margin: 0 auto;
}

@media (max-width: 1000px) {
  .sb-cols { grid-template-columns: 1fr; }
  .sb-preview { position: static; }
  #sbFrame { height: 60vh; }
}

/* ---------------------------------------------------------------------------
   The customer sheet (customers.js).

   A customer list is read by scanning ONE COLUMN — every phone number under
   every other one — and stacked cards make that impossible: the eye has to
   re-find each field on every row. So on a screen WIDE ENOUGH TO HOLD THE
   COLUMNS it stays a table, and the reflow-to-cards it replaced stays gone.

   At 390px it was holding none of them. The table is 1,102px inside a 314px
   scroller: 750px of itself hidden, with no fade, no arrow, and on iOS no
   scrollbar until a finger touches it. Name and two-thirds of a phone number
   is what a phone showed — with the address, the thing wanted while standing
   outside a house, 180px past the right edge, and the column carrying the sort
   arrow 600px past it. Below 600px the same rows lay out as two lines instead;
   see the media query at the end of this block. */
.sheet-wrap {
  overflow-x: auto; border: 1px solid var(--line); border-radius: var(--r-md);
  background: var(--panel);
  /* A scroller with a hidden scrollbar has to say it is one. The right edge
     fades only while there is more to the right of it. */
  background-image: linear-gradient(to right, var(--panel), transparent 12px),
                    linear-gradient(to left, var(--panel), transparent 12px),
                    linear-gradient(to right, rgba(120,113,108,.20), transparent 12px),
                    linear-gradient(to left, rgba(120,113,108,.20), transparent 12px);
  background-position: left center, right center, left center, right center;
  background-repeat: no-repeat;
  background-size: 26px 100%, 26px 100%, 12px 100%, 12px 100%;
  background-attachment: local, local, scroll, scroll;
}
.sheet { border-collapse: collapse; width: 100%; font-size: var(--t-sm); }
.sheet th, .sheet td {
  text-align: left; padding: 8px 12px; border-bottom: 1px solid var(--line);
  white-space: nowrap; max-width: 22ch; overflow: hidden; text-overflow: ellipsis;
}
.sheet th.grow, .sheet td.grow { max-width: 30ch; }
.sheet thead th {
  position: sticky; top: 0; z-index: 1; background: var(--sunken);
  padding: 0; border-bottom: 1px solid var(--line-2);
}
.sheet thead th button {
  width: 100%; text-align: left; background: none; border: 0; cursor: pointer;
  font: inherit; font-size: var(--t-micro); font-weight: 700; letter-spacing: .06em;
  text-transform: uppercase; color: var(--text-2); padding: 9px 12px;
  display: flex; gap: 5px; align-items: center;
}
.sheet thead th button:hover { color: var(--text); background: var(--accent-weak); }
.sheet thead th button:focus-visible { outline: 2px solid var(--accent); outline-offset: -2px; }
.sheet tbody tr { cursor: pointer; }
.sheet tbody tr:hover { background: var(--accent-weak); }
.sheet tbody tr:focus-visible { outline: 2px solid var(--accent); outline-offset: -2px; }
.sheet tbody tr:last-child td { border-bottom: 0; }
/* Digits under digits: a phone number is compared column by column. */
.sheet td.mono { font-variant-numeric: tabular-nums; }
.sheet td.strong { font-weight: 600; color: var(--text); }
.sheet .thin { color: var(--text-3); }

/* The phone's sort control. Hidden wherever the header row fits, because the
   header row is the better one — it shows the current column and its direction
   in place, and it is one click rather than two. */
.sheet-sort { display: none; align-items: center; gap: var(--s2); margin: 0 0 var(--s2); }
.sheet-sort label { font-size: var(--t-xs); color: var(--text-2); font-weight: 600; }
.sheet-sort select { width: auto; flex: 1 1 auto; min-width: 0; }

@media (max-width: 600px) {
  /* THE SAME ROWS, TWO LINES EACH. No second markup and no second sort: the
     table is still a table to the DOM, to the keyboard and to a screen reader
     — only its boxes change. Name and phone on the first line, the address on
     the second, the rest beneath it. The whole row stays one tap target and
     still opens the detail sheet, which is where the gate code lives. */
  .sheet-sort { display: flex; }
  .sheet-wrap { overflow-x: visible; background-image: none; }
  .sheet, .sheet tbody, .sheet tr, .sheet td { display: block; width: auto; }
  .sheet thead { display: none; }
  .sheet tbody tr {
    padding: 11px var(--s4); border-bottom: 1px solid var(--line);
    display: grid; grid-template-columns: 1fr auto; gap: 1px 10px;
    min-height: 56px; align-content: center;
  }
  .sheet tbody tr:last-child { border-bottom: 0; }
  .sheet td {
    border: 0; padding: 0; white-space: normal; max-width: none;
    font-size: var(--t-sm); color: var(--text-2); overflow: visible;
  }
  .sheet td[data-col="name"] {
    grid-column: 1; grid-row: 1; font-size: var(--t-base); font-weight: 600; color: var(--text);
  }
  .sheet td[data-col="phone"] { grid-column: 2; grid-row: 1; text-align: right; }
  /* THE LINES UNDER THE NAME ARE ORDERED, NOT PINNED TO ROWS. Pinned to rows
     2 to 5, a customer with no address or email left those rows empty, and
     whatever came next in line (customers.js puts Open and the merge tick last)
     filled them — so Open sat above "Last heard from" on one card and below it
     on the next. Hidden lines take no row, and the order holds on every card. */
  .sheet td[data-col="address"] { grid-column: 1 / -1; order: 2; }
  .sheet td[data-col="email"] { grid-column: 1 / -1; order: 3; }
  .sheet td[data-col="lastSeenAt"] { grid-column: 1 / -1; order: 4; color: var(--text-3); }
  /* Every stacked line has to say what it is, since the headings are gone —
     except the name and the phone, which say it themselves. */
  .sheet td[data-col="address"]::before,
  .sheet td[data-col="email"]::before,
  .sheet td[data-col="tags"]::before,
  .sheet td[data-col="lastSeenAt"]::before {
    content: attr(data-label) ' · '; color: var(--text-3);
    font-size: var(--t-xs); font-weight: 600;
  }
  .sheet td[data-col="tags"] { grid-column: 1 / -1; order: 5; }
  /* "Came from" is the one column nobody scans on a phone; it is on the
     detail sheet, one tap away. */
  .sheet td[data-col="source"] { display: none; }
  /* A row whose only content is an em-dash is a line of noise on a phone. */
  .sheet td[data-col="address"]:has(.thin),
  .sheet td[data-col="email"]:has(.thin),
  .sheet td[data-col="tags"]:has(.thin) { display: none; }
}

/* A customer's record at a glance (customers.js). Deliberately plain numbers
   rather than a chart — the question is "how many and how often", and four
   figures answer it faster than anything drawn. */
.cust-stats {
  display: flex; flex-wrap: wrap; gap: var(--s4);
  padding: var(--s3) 0; margin-top: var(--s3);
  border-top: 1px solid var(--line); border-bottom: 1px solid var(--line);
  font-size: var(--t-sm);
}
.cust-stats strong { font-size: var(--t-base); }
.cust-stats .muted { color: var(--text-2); }

/* A visit that could not be placed. Flagged rather than hidden and never
   silently booked — it holds no time, and somebody has to give it one. */
.cal-ev.is-clash {
  border-style: dashed;
  border-color: var(--onboarding);
}
.cal-ev.is-clash .cal-ev-who::after { content: " · needs a time"; color: var(--onboarding); }

/* ===== KAMP C13: the Online booking plan's portal (tokens only) =========
   Linked calendars (calendars.js, linked), the booking page block and the
   address field (portal.html). Every colour is a token, so the three themes
   are the tokens' three themes; nothing here moves. */
.field-label { display:block; font-size:var(--t-xs); font-weight:600; color:var(--text-2); margin:0 0 var(--s1); }
.cal-outages { display:flex; flex-direction:column; gap:var(--s2); }
.cal-outages[hidden] { display:none; }

.linked { display:flex; flex-direction:column; gap:var(--s5); }
.linked-part { display:flex; flex-direction:column; gap:var(--s2); }
.linked-part + .linked-part { padding-top:var(--s5); border-top:1px solid var(--line); }
.linked-head { margin:0; font-size:var(--t-base); font-weight:600; color:var(--text); }
.linked-sub { margin:var(--s3) 0 0; font-size:var(--t-sm); font-weight:600; color:var(--text-2); }
.linked-now { display:flex; flex-wrap:wrap; align-items:center; gap:var(--s2); }
.linked-now .crud-row-meta { flex:1 1 100%; }
.linked-actions { display:flex; flex-wrap:wrap; align-items:center; gap:var(--s3) var(--s4); }
.linked-ask { display:flex; flex-wrap:wrap; align-items:center; gap:var(--s2); padding:var(--s3);
              border:1px solid var(--line); border-radius:var(--r-md); font-size:var(--t-sm); color:var(--text); }
.linked-ask > span { flex:1 1 100%; }
.linked-ask-words { font-size:var(--t-sm); color:var(--text); }
.linked-form { display:flex; flex-direction:column; gap:var(--s2); padding:var(--s4);
               border:1px solid var(--line); border-radius:var(--r-md); }
.linked .crud-row-actions { flex-wrap:wrap; }
.state-bad-inline { color:var(--danger); font-weight:600; }
.link-btn.is-destructive { color:var(--danger); }
/* Every control in the panel is a thumb's width tall at any width (WCAG 2.2,
   and 01-ux's 390px walk), not only below 720px. */
.linked .link-btn, .booking-page .link-btn, .bp-logo .link-btn { min-height:44px; display:inline-flex; align-items:center; }
.linked .radio-row, .linked .check-row, .booking-page .radio-row, .cl-field-addressVisibility .radio-row { min-height:44px; }

/* The booking page block, top of Booking Calendar. */
.booking-page { display:flex; flex-direction:column; gap:var(--s3); margin:0 0 var(--s5); padding:var(--s4);
                border:1px solid var(--line); border-radius:var(--r-md); background:var(--panel); }
.booking-page[hidden] { display:none; }
.booking-page-head { margin:0; font-size:var(--t-base); font-weight:600; color:var(--text); }
.booking-page-missing { display:flex; flex-wrap:wrap; align-items:center; gap:var(--s1) var(--s3); color:var(--text); }
.bp-swatches { display:flex; flex-wrap:wrap; gap:var(--s2); }
.bp-swatch { display:flex; align-items:center; gap:var(--s2); min-height:44px; padding:var(--s1) var(--s3) var(--s1) var(--s1);
             border:1px solid var(--line); border-radius:var(--r-md); cursor:pointer; font-size:var(--t-sm); color:var(--text); }
.bp-swatch input { position:absolute; opacity:0; width:1px; height:1px; }
.bp-swatch:has(input:checked) { border-color:var(--accent); box-shadow:inset 0 0 0 1px var(--accent); background:var(--accent-weak); }
.bp-swatch:has(input:focus-visible) { outline:2px solid var(--accent); outline-offset:2px; }
.bp-swatch-chip { display:inline-flex; align-items:flex-end; justify-content:flex-end; width:32px; height:32px;
                  border:1px solid var(--line); border-radius:var(--r-sm); overflow:hidden; }
.bp-swatch-chip > span { width:14px; height:14px; border-radius:var(--r-sm) 0 0 0; }
.bp-logo-row { display:flex; flex-wrap:wrap; align-items:center; gap:var(--s3); }
.bp-logo-img { max-height:38px; max-width:160px; object-fit:contain; }

/* The Plan card on the Online booking plan. */
.booking-plan .kpi-row:empty { display:none; }
.booking-plan .plan-option:disabled { cursor:not-allowed; border-style:dashed; color:var(--text-2); }
.booking-plan .plan-addon.is-off { border-style:dashed; }
.plan-total { margin:var(--s3) 0 0; font-size:var(--t-base); font-weight:600; color:var(--text); font-variant-numeric:tabular-nums; }
.save-row-inline { margin-top:var(--s3); }
.field-hint2.is-locked { color:var(--text); font-weight:600; }
.loc-preview { color:var(--text); }
