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

    :root {
      --navy:       var(--tf-navy);
      --cerulean:   var(--tf-cerulean);
      --ocean:      var(--tf-ocean);
      --aqua-soft:  var(--tf-aqua-050);
      --sand:       var(--tf-sand);
      --saffron:    var(--cta-bg);
      --saffron-dk: var(--cta-bg-hover);
      --bg:         var(--bg-page);
      --text:       var(--text-body);
      /* Deliberately NOT redeclared. brand/tokens.css darkened --text-muted from
         #4d7d93 to #456f83 specifically to pass WCAG AA (4.5:1 on white, sand and
         aqua), and this line was silently undoing that for all 229 uses in this
         app -- every sibling line in this block ALIASES a brand token, which is
         what makes a hardcoded hex here drift rather than a decision. Found by
         measuring the rendered contrast of a control that had just become
         text-only: 4.43:1 on the cream page background, i.e. failing.
         --text-muted now resolves to the brand value. Do not redeclare it here. */
      /* --surface and --muted were USED but never DEFINED. Same class of bug as
         the --text-strong self-reference above, found the same way: deriving the
         token list from the loaded stylesheet and asking which ones resolve to
         nothing. Eight further tokens turned up in that scan and are NOT listed
         here on purpose -- every one of them always carries a fallback
         (var(--tf-saffron) and friends), so they render correctly. These two
         are the only ones with bare uses that silently drop.

         --surface backs .tech-card and .settings-acc; with it empty those cards
         had no background at all. Its one fallback site already said #fff, and
         --app-surface is #ffffff, so defining it changes that site by nothing.

         --muted is the muted-text role. Its bare uses include
         `border: 1px solid var(--muted)`, which an empty token makes INVALID, so
         that control has been rendering with no border. Pointing it at the
         app's own --text-muted also moves its five fallback sites off a
         hand-picked warm grey (#6b6560) onto the brand blue-grey, which is what
         every other muted string in this app already uses. */
      --surface:    var(--app-surface);
      --muted:      var(--text-muted);
      --border:     rgba(6,40,64,0.10);
      /* INTERACTIVE controls -- inputs, selects, textareas, and the white
         secondary button -- need a heavier edge than the 10%-opacity --border
         that draws cards, table rules and dividers. At 10% on a white card an
         input reads as a floating label with no box, and a white button is
         simply not visible. Kept as its OWN token rather than darkening
         --border, because that one is load-bearing for every card and table in
         the app and making those edges 3x darker is a different design change. */
      --border-control:       rgba(6,40,64,0.32);
      --border-control-hover: rgba(6,40,64,0.48);
      --white:      var(--tf-white);
      --red:        var(--status-danger-hover);
      --red-light:  var(--status-danger-soft);
      --shadow-sm:  0 1px 3px rgba(6,40,64,0.07), 0 1px 2px rgba(6,40,64,0.04);
      --shadow-md:  0 6px 20px -4px rgba(11,79,108,0.14), 0 2px 6px -2px rgba(11,79,108,0.08);
      --radius:     10px;
      /* Form-control tokens. Both of these were already the house style before
         they were named: the focus ring was written out as a literal in TEN
         separate rules, and the select chevron in one. A value several surfaces
         have each had to re-type is a missing token, so it gets a name here and
         the copies point at it as they are touched. New form controls should use
         these rather than re-typing the rgba() a eleventh time. */
      --tf-focus-ring: 0 0 0 3px rgba(26,159,196,.12);
      --tf-select-chevron: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='12' height='12' viewBox='0 0 24 24' fill='none' stroke='%234d7d93' stroke-width='2'%3E%3Cpath d='M6 9l6 6 6-6'/%3E%3C/svg%3E");
      /* real, distinct status hues — replaces the old all-blue "red/gold/purple" naming */
      --danger:      var(--status-danger);
      --danger-bg:   var(--status-danger-soft);
      --warning:     var(--status-warning);
      --warning-bg:  var(--status-warning-soft);
      --success:     var(--status-success);
      --success-bg:  var(--status-success-soft);
      --info-bg:     var(--status-info-soft);
    }


    /* --- tap targets (added with the 2026-08-09 type/icon scale) -------------
       These controls are visually small by design (a dismiss x, a caret, a
       round icon button). Growing them to --tap-min with padding would reflow
       the dense rows they live in, so the hit area is an invisible centred
       overlay instead. The glyph stays put; the finger target is 44px. */
    .tb-alertbtn, .dash-priority-dismiss, .dash-drawer-close {
      position: relative;
    }
    .tb-alertbtn::after, .dash-priority-dismiss::after,
    .dash-drawer-close::after {
      content: ''; position: absolute; left: 50%; top: 50%;
      transform: translate(-50%, -50%);
      width: max(100%, var(--tap-min)); height: max(100%, var(--tap-min));
    }
    /* the dismiss x was opacity:0 until row hover — keyboard users never saw it */
    .dash-priority-dismiss:focus-visible { opacity: 1; outline: 2px solid var(--ocean); outline-offset: 2px; }

    body {
      font-family: var(--font-sans);
      background: var(--bg);
      color: var(--text);
      min-height: 100vh;
      -webkit-font-smoothing: antialiased;
    }

    /* ── Sidebar shell (ported from dock/index.html) ───────────────────── */
    [hidden] { display: none !important; }
    #app-view { flex-direction: row; }
    .sidebar {
      width: 228px; flex-shrink: 0; display: flex; flex-direction: column;
      position: sticky; top: 0; height: 100vh;
      background: linear-gradient(180deg, var(--aqua-soft), var(--bg));
      border-right: 1px solid var(--border);
    }
    .sidebar .sb-brand {
      padding: 20px 18px 16px; border-bottom: 1px solid var(--border);
      display: flex; align-items: center; gap: 11px;
    }
    .sidebar .sb-brand .mark {
      width: 34px; height: 34px; border-radius: 9px;
      background: var(--aqua-soft); display: flex; align-items: center;
      justify-content: center; flex-shrink: 0; border: 1px solid var(--border);
    }
    .sidebar .sb-brand .name {
      font-family: var(--font-display); font-weight: 700;
      color: var(--cerulean); font-size: var(--fs-base); line-height: 1.1;
    }
    .sidebar .sb-brand .sub {
      font-size: var(--fs-eyebrow); color: var(--text-muted); font-weight: 700;
      letter-spacing: .13em; text-transform: uppercase;
    }
    .sb-nav { padding: 10px; overflow-y: auto; flex: 1; }
    /* The settings group. flex:0 so it never takes the scroll the run
       needs, and a hairline to read as a separate place rather than the
       last item of the list above it. */
    .sb-nav-foot { flex: 0 0 auto; overflow: visible; border-top: 1px solid var(--border); }
    .sb-nav button {
      display: flex; align-items: center; gap: 11px; width: 100%;
      border: none; background: none; color: var(--cerulean);
      padding: 9px 11px; border-radius: 9px; font-size: var(--fs-xs);
      font-weight: 500; text-align: left; margin-bottom: 2px;
      transition: background .12s; cursor: pointer; font-family: var(--font-sans);
    }
    .sb-nav button svg { width: var(--icon-md); height: var(--icon-md); flex-shrink: 0; opacity: .7; }
    .sb-nav button:hover { background: rgba(255,255,255,.7); }
    .sb-nav button.active { background: var(--ocean); color: #fff; }
    .sb-nav button.active svg { opacity: 1; }
    .sb-nav button .lbl { flex: 1; }
    /* Neutral, not red. A count of things to do is a FACT, not a fault -- under
       the colour law red means broken or money at risk, and a permanently red
       badge on every tab teaches you to ignore red. */
    .sb-nav button .badge {
      background: var(--app-neutral-bg); color: var(--app-text-neutral);
      font-size: var(--fs-xs); font-weight: 700; border-radius: 20px;
      padding: 1px 7px; min-width: 18px; text-align: center; flex-shrink: 0;
    }
    .sb-nav button.active .badge { background: rgba(255,255,255,.28); color: #fff; }
    .sb-divider {
      font-size: var(--fs-eyebrow); font-weight: 700; text-transform: uppercase;
      letter-spacing: .1em; color: var(--text-muted); padding: 12px 11px 4px;
    }

    /* ── Topbar (date + panel title + global search) ── */
    .topbar {
      position: sticky; top: 0; z-index: 15; background: rgba(255,253,249,.92);
      backdrop-filter: blur(8px); border-bottom: 1px solid var(--border);
      padding: 12px 26px; display: flex; align-items: center; gap: 16px;
    }
    .topbar .crumb {
      font-size: var(--fs-eyebrow); font-weight: 700; letter-spacing: .1em; text-transform: uppercase;
      color: var(--text-muted);
    }
    .topbar .tb-title { font-size: var(--fs-lg); font-weight: 700; color: var(--cerulean); font-family: var(--font-display); }
    .topbar .spacer { flex: 1; }
    .tb-search {
      display: flex; align-items: center; gap: 8px; background: #fff;
      border: 1px solid var(--border); border-radius: 10px; padding: 7px 12px;
      width: 300px; color: var(--text-muted); position: relative;
    }
    .tb-search svg { flex-shrink: 0; width: var(--icon-sm); height: var(--icon-sm); }
    /* Global alert controls (new-message sound + desktop notifications) */
    /* Two groups, not five loose circles. The speaker + monitor are one idea
       (how a new customer message reaches you); the bell is another (the office's
       inbox). Equal spacing across all three read as one set of toggles, which is
       what made the bell look like a third alert switch. A hairline and a wider
       gap do the separating that the icons alone could not. */
    .tb-alertctl { display: flex; align-items: center; gap: 6px; margin-right: 12px; }
    .tb-alertctl::after {
      content: ''; width: 1px; height: 20px; margin-left: 12px;
      background: var(--border);
    }
    .tb-alertbtn {
      display: inline-flex; align-items: center; justify-content: center;
      width: 32px; height: 32px; padding: 0; flex-shrink: 0;
      color: var(--text-muted); background: #fff;
      border: 1px solid var(--border); border-radius: 50%; cursor: pointer;
    }
    .tb-alertbtn:hover { border-color: var(--cerulean); color: var(--cerulean); }
    .tb-alertbtn.on { background: var(--aqua-soft); border-color: var(--cerulean); color: var(--cerulean); }
    .tb-alertbtn.off { color: var(--text-muted); opacity: .7; }
    .tb-alertbtn svg { width: var(--icon-md); height: var(--icon-md); }

    /* Office notifications bell. The count element is `hidden` at zero rather
       than rendered as "0" -- a standing badge trains the reader to stop seeing
       it, which is the one rule NOTIFICATIONS-SPEC.md says must not break. */
    .tb-bellwrap { position: relative; margin-right: 14px; }

    /* Pipeline value in the Sent & waiting header. Deliberately quieter than
       .fin-sec-count and in the muted colour, not a money colour: it is not
       money we have, and it must not read like the balance figures above it. */
    .fin-sw-pipeline {
      font-size: var(--fs-xs); font-weight: 600; color: var(--text-muted);
      margin-left: .6rem;
    }
    /* NOT the toggles' on/off language, deliberately. The speaker and the monitor
       are switches, so their colour states mean on and off. The bell is not a
       switch -- it opens a list -- and at rest it was inheriting `.tb-alertbtn`'s
       resting look, which is identical to a toggle that is OFF. So an empty inbox
       read as a disabled control. Dropping the ring at rest makes it a navigation
       affordance instead, and the ring plus the count then mean exactly one thing:
       there is something to read. Same circle, same size, same stroke as its
       neighbours -- it stays in the family, it just does not claim to be a switch. */
    .tb-bellbtn { position: relative; border-color: transparent; }
    .tb-bellbtn:hover { border-color: var(--border-control, rgba(6,40,64,.18)); }
    .tb-bellbtn.has-unread {
      border-color: var(--cerulean); color: var(--cerulean); background: var(--tf-aqua-050);
    }
    .tb-bellcount {
      position: absolute; top: -4px; right: -4px; min-width: 17px; height: 17px;
      padding: 0 4px; border-radius: 9px; background: var(--cerulean); color: #fff;
      font-size: 10px; font-weight: 700; line-height: 17px; text-align: center;
      font-family: var(--font-sans);
    }
    .tb-bellpop {
      position: absolute; top: 40px; right: 0; z-index: 60;
      width: min(380px, calc(100vw - 32px)); max-height: 60vh; overflow-y: auto;
      background: #fff; border: 1px solid var(--border); border-radius: 10px;
      box-shadow: 0 10px 30px rgba(0,0,0,.14); padding: 6px;
    }
    .tb-bellpop .tbn-head {
      padding: 8px 10px 6px; font-size: var(--fs-xs); font-weight: 700;
      color: var(--text-muted); text-transform: uppercase; letter-spacing: .04em;
    }
    .tb-bellpop .tbn-row {
      display: flex; gap: 10px; align-items: flex-start; cursor: pointer;
      padding: 9px 10px; border-radius: 8px; min-height: 44px;
    }
    .tb-bellpop .tbn-row + .tbn-row { border-top: 1px solid var(--border); border-radius: 0; }
    .tb-bellpop .tbn-row.unread { background: var(--aqua-soft); }
    .tb-bellpop .tbn-body { flex: 1; min-width: 0; }
    .tb-bellpop .tbn-title { font-size: var(--fs-xs); font-weight: 600; color: var(--text); }
    .tb-bellpop .tbn-text { font-size: var(--fs-xs); color: var(--text-muted); margin-top: 2px; }
    .tb-bellpop .tbn-when { font-size: 11px; color: var(--text-muted); margin-top: 3px; }
    /* The follow-through. Tier 2 in the same vocabulary as the panels: an outline,
       legible on the row's tinted unread background as well as on white. */

    /* Where a followed link lands. Without it the panel switches and the row is
       somewhere on screen with nothing saying which one you were sent to. */
    .tk-row-flash {
      animation: tkRowFlash 2s ease-out;
      border-color: var(--ocean) !important;
    }
    @keyframes tkRowFlash {
      0%, 30% { background: var(--tf-aqua-050); }
      100%    { background: transparent; }
    }

    .tb-bellpop .tbn-empty {
      padding: 18px 12px; text-align: center;
      font-size: var(--fs-xs); color: var(--text-muted);
    }
    .tb-search input {
      border: none; outline: none; font-size: var(--fs-xs); width: 100%;
      background: none; color: var(--text); font-family: var(--font-sans);
    }
    .tb-search-results {
      display: none; position: absolute; top: calc(100% + 6px); left: 0; right: 0;
      background: #fff; border: 1px solid var(--border); border-radius: 10px;
      box-shadow: var(--shadow-md); max-height: 340px; overflow-y: auto; z-index: 30;
    }
    .tb-search-results .tsr-row {
      padding: 9px 13px; cursor: pointer; font-size: var(--fs-xs); border-bottom: 1px solid var(--border);
    }
    .tb-search-results .tsr-row:last-child { border-bottom: none; }
    .tb-search-results .tsr-row:hover { background: var(--aqua-soft); }
    .tb-search-results .tsr-row .tsr-meta { font-size: var(--fs-xs); color: var(--text-muted); margin-top: 1px; }
    .tb-search-results .tsr-kind {
      font-size: var(--fs-eyebrow); font-weight: 700; text-transform: uppercase; letter-spacing: .05em;
      color: var(--ocean); margin-right: 6px;
    }
    @media (max-width: 860px) { .tb-search { display: none; } }
    .sb-who {
      padding: 13px 16px; border-top: 1px solid var(--border);
      display: flex; align-items: center; gap: 10px; font-size: var(--fs-xs);
    }
    .sb-who .av {
      width: 30px; height: 30px; border-radius: 50%;
      background: var(--saffron); color: #fff;
      display: flex; align-items: center; justify-content: center;
      font-weight: 700; font-size: var(--fs-xs); flex-shrink: 0;
    }
    .sb-who .nm { color: var(--cerulean); font-weight: 600; line-height: 1.3; }
    .sb-who .rl { color: var(--text-muted); font-size: var(--fs-xs); }
    /* A text link, not a button. Signing out is not something anyone is trying
       to do, so it should not carry the visual weight of an action you might
       want -- it just needs to be findable when you do want it. */
    .sb-signout {
      background: none;
      border: 0;
      padding: 0;
      font: inherit;
      font-size: var(--fs-xs);
      color: var(--text-muted);
      text-decoration: underline;
      text-underline-offset: 2px;
      cursor: pointer;
    }
    .sb-signout:hover { color: var(--cerulean); }
    /* ── Panel system ───────────────────────────────────────────────────── */
    .app-main { flex: 1; min-width: 0; overflow-y: auto; }
    .app-panel { display: none; }
    .app-panel.active { display: block; }
    .panel-stub {
      padding: 5rem 2rem; text-align: center;
      color: var(--text-muted); font-size: var(--fs-base);
    }
    .panel-stub strong {
      display: block; font-size: var(--fs-lg); font-weight: 700;
      color: var(--text-strong); margin-bottom: .4rem;
    }
    @media (max-width: 860px) { .sidebar { display: none; } }

    /* ── Goals / Health-signals collapsible section (top of dashboard) ── */
    .goals-block {
      background: #fff; border: 1px solid var(--border); border-radius: var(--radius);
      box-shadow: var(--shadow-sm); margin-bottom: 18px; overflow: hidden;
    }
    .goals-block-head {
      display: flex; align-items: center; gap: 10px; padding: 14px 18px;
      cursor: pointer; user-select: none;
    }
    .goals-block-head svg.chev { width: var(--icon-md); height: var(--icon-md); color: var(--text-muted); transition: transform .15s; margin-left: auto; }
    .goals-block.collapsed .goals-block-head svg.chev { transform: rotate(-90deg); }
    .goals-block-head .gb-title { font-size: var(--fs-sm); font-weight: 800; color: var(--cerulean); display: flex; align-items: center; gap: 8px; }
    .gb-icon {
      display: inline-flex; align-items: center; justify-content: center;
      width: 26px; height: 26px; border-radius: 8px; flex-shrink: 0;
      background: var(--info-bg); color: var(--status-info-text); transition: background .2s, color .2s;
    }
    /* COLOUR LAW (brand/tokens.css). On track is "a holding pattern, nothing to
       do", which is NEUTRAL, not blue -- blue means "an action is needed now"
       and only ONE status may ever carry it. Being behind on goals is ageing
       toward a problem, not broken, so amber rather than red; red stays for
       money at risk and dead rails. This badge was blue on a healthy dashboard,
       i.e. it read as "do something" while saying "on track". */
    .gb-icon.ahead   { background: var(--status-success-soft); color: var(--status-success-text); }
    .gb-icon.ontrack { background: var(--app-neutral-bg);      color: var(--app-text-neutral); }
    .gb-icon.behind  { background: var(--status-warning-soft); color: var(--status-warning-text); }
    .goals-block-head .gb-toggle {
      display: inline-flex; background: var(--aqua-soft); border-radius: 8px; padding: 2px; gap: 2px; margin-left: 8px;
    }
    .goals-block-head .gb-toggle button {
      border: none; background: none; padding: 4px 11px; border-radius: 6px; font-size: var(--fs-xs);
      font-weight: 600; color: var(--text-muted); cursor: pointer; font-family: var(--font-sans);
    }
    .goals-block-head .gb-toggle button.active { background: var(--cerulean); color: #fff; }
    .goals-block-body { padding: 0 18px 18px; display: none; }
    .goals-block:not(.collapsed) .goals-block-body { display: block; }
    /* A TILE SHOULD BE A TILE WHENEVER THERE IS ROOM FOR ONE.
       This was three fixed columns dropping to two at 1100px and one at 700px.
       The panel is not the window: the sidebar takes ~170px, so a perfectly
       ordinary 1280px laptop was already inside the two-column band and a
       narrower window stacked everything full width. A breakpoint cannot know
       how much room the PANEL has; minmax measures it directly.
       auto-FILL rather than auto-fit on purpose. auto-fit collapses the empty
       tracks and stretches the survivors, so a row holding two cards renders
       them half the page wide -- the same full-width look, reached from the
       other direction. auto-fill keeps the tracks, so a card stays card-sized
       however few of them a row happens to hold. */
    .goals-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(255px, 1fr)); gap: 12px; }
    .goal-card {
      border: 1px solid var(--border); border-radius: 10px; padding: 14px 16px;
      transition: box-shadow .15s, transform .15s;
    }
    .goal-card:hover { box-shadow: var(--shadow-md); transform: translateY(-1px); }
    .goal-card .gc-t {
      font-size: var(--fs-eyebrow); font-weight: 700; text-transform: uppercase; letter-spacing: .05em;
      color: var(--text-muted); display: flex; align-items: center; gap: 6px;
    }
    .goal-card .gc-v { font-size: var(--fs-2xl); font-weight: 800; letter-spacing: -.6px; margin-top: 7px; color: var(--cerulean); font-family: var(--font-display); }
    .goal-card .gc-v small { font-size: var(--fs-xs); font-weight: 600; color: var(--text-muted); font-family: var(--font-sans); }
    .goal-card .gc-bar { height: 7px; border-radius: 20px; background: var(--aqua-soft); margin-top: 9px; overflow: hidden; }
    .goal-card .gc-bar i { display: block; height: 100%; border-radius: 20px; background: var(--ocean); }
    .goal-card .gc-bar i.warn { background: var(--warning); }
    .goal-card .gc-bar i.good { background: var(--success); }
    .goal-card .gc-pace {
      font-size: var(--fs-xs); font-weight: 800; margin-top: 8px; display: inline-flex; align-items: center;
      gap: 5px; padding: 2px 8px; border-radius: 20px;
    }
    .goal-card .gc-pace.ahead { color: var(--success); background: var(--success-bg); }
    .goal-card .gc-pace.ontrack { color: var(--status-info-text); background: var(--info-bg); }
    .goal-card .gc-pace.behind { color: var(--danger); background: var(--danger-bg); }
    /* ─── Money row ────────────────────────────────────────────────────────
       Three cards, bigger figure, tinted ground. These are what the page is
       opened for, and ten cards of equal weight meant Revenue read the same as
       Content created. The tint is the only thing separating them; they are not
       a different component. */
    /* The money row is the exception: it holds a small fixed set that is meant
       to fill the width, so auto-fit (which stretches) is right here where
       auto-fill would leave a gap. Its floor is wider because these carry two
       or three lines of context under the figure. */
    .goals-money { display: grid; grid-template-columns: repeat(auto-fit, minmax(305px, 1fr));
                   gap: 12px; margin-bottom: 18px; }
    .money-card {
      border: 1px solid var(--border); border-left: 3px solid var(--cerulean);
      border-radius: 10px; padding: 16px 18px;
      background: linear-gradient(180deg, #fff, var(--aqua-soft));
      transition: box-shadow .15s, transform .15s;
    }
    .money-card:hover { box-shadow: var(--shadow-md); transform: translateY(-1px); }
    .money-card .gc-t { color: var(--cerulean); opacity: .75; }
    .money-card .mc-v {
      font-family: var(--font-display); font-size: 2.4rem; font-weight: 800;
      letter-spacing: -1px; color: var(--cerulean); margin-top: 6px; line-height: 1;
    }
    .money-card .mc-ctx { font-size: var(--fs-xs); color: var(--text-muted); margin-top: 8px; line-height: 1.5; }
    .money-card .mc-ctx b { color: var(--text); font-weight: 700; }
    /* Amber, not red: money already owed is worth chasing today, it is not an
       outage. Red here would compete with the system-health strip, where red
       means something is actually broken. */
    .money-card.alert { border-left-color: var(--warning); background: linear-gradient(180deg, #fff, var(--warning-bg)); }
    .money-card.alert .gc-t, .money-card.alert .mc-v { color: #8a4b06; }

    /* THE TICK. A bar alone answers "how far along am I", which is the wrong
       question on day 17 of a month. The tick marks where the period itself has
       got to, so the bar answers "am I behind" without any words. */
    .gc-bar { position: relative; }
    .gc-bar u { position: absolute; top: -3px; bottom: -3px; width: 2px; background: var(--cerulean); opacity: .45; }

    /* A number with no target is not a fraction of anything, so it gets no bar.
       The old cards drew one at a hardcoded 60%, which looked exactly like data
       and was not. */
    .goal-card.no-bar .gc-v { margin-bottom: 2px; }
    .goal-card .gc-note { font-size: var(--fs-xs); color: var(--text-muted); margin-top: 8px; }
    .goal-card .gc-note b { font-weight: 700; }
    .goal-card .gc-note b.behind { color: var(--warning); }
    .goal-card .gc-note b.ahead  { color: var(--success); }

    /* Anything with no data yet lives here as one line, not a whole card. */
    .goals-quiet {
      margin-top: 14px; border-top: 1px dashed var(--border); padding-top: 12px;
      display: flex; flex-wrap: wrap; gap: 8px 26px;
      font-size: var(--fs-xs); color: var(--text-muted);
    }
    .goals-quiet:empty { display: none; border-top: 0; padding-top: 0; margin-top: 0; }
    .goals-quiet b { color: var(--text); font-weight: 700; }

    .goal-card.pending { border-style: dashed; }
    .goal-card.pending .gc-v { color: var(--text-muted); font-size: var(--fs-lg); }
    .goal-card .gc-edit { margin-top: 8px; display: flex; gap: 6px; }
    .goal-card .gc-edit input {
      width: 70px; border: 1px solid var(--border); border-radius: 7px; padding: 5px 7px; font-size: var(--fs-xs); font-family: var(--font-sans);
    }
    .goal-card .gc-edit button {
      border: 1px solid var(--ocean); background: #fff; color: var(--ocean); border-radius: 7px;
      padding: 5px 10px; font-size: var(--fs-xs); font-weight: 700; cursor: pointer; font-family: var(--font-sans);
    }
    .gb-subnote {
      font-size: var(--fs-xs); color: var(--text-muted); background: var(--info-bg); border: 1px solid var(--border);
      border-radius: 8px; padding: 8px 11px; margin: 2px 0 14px;
    }

    /* ── Login ──────────────────────────────────────────────────────────── */
    #login-view {
      display: flex;
      align-items: center;
      justify-content: center;
      min-height: 100vh;
      padding: 1.5rem;
      background: var(--bg);
    }

    .login-card {
      background: var(--white);
      border-radius: 16px;
      padding: 2.5rem 1.5rem;
      width: 100%;
      max-width: 400px;
      box-shadow: var(--shadow-md);
    }

    .brand {
      display: flex;
      flex-direction: column;
      align-items: center;
      gap: 8px;
      margin-bottom: 1.5rem;
    }
    .login-logo {
      display: block;
      height: 80px;
      max-width: 220px;
      width: auto;
      object-fit: contain;
    }
    @media (min-width: 600px) {
      .login-logo { height: 100px; max-width: 260px; }
    }
    .brand-sub {
      font-size: var(--fs-sm);
      font-weight: 600;
      color: var(--text-muted);
      text-transform: uppercase;
      letter-spacing: .12em;
    }

    /* Password field with eye toggle */
    .pw-wrap { position: relative; display: flex; align-items: center; }
    .pw-wrap input { padding-right: 2.75rem; }
    .pw-eye {
      position: absolute;
      right: .6rem;
      background: none;
      border: none;
      color: var(--text-muted);
      cursor: pointer;
      padding: .25rem;
      display: flex;
      align-items: center;
      justify-content: center;
    }
    .pw-eye:focus-visible { outline: 2px solid var(--ocean); border-radius: var(--radius); }
    .eye-icon { display: block; }

    /* Forgot password */
    .forgot-wrap { text-align: center; margin-top: .85rem; }
    .btn-link {
      background: none;
      border: none;
      color: var(--saffron);
      font-family: var(--font-sans);
      font-size: var(--fs-sm);
      font-weight: 400;
      cursor: pointer;
      padding: 0;
      text-decoration: underline;
    }
    .btn-link:hover { opacity: .85; }
    .forgot-panel {
      display: flex;
      flex-direction: column;
      gap: .5rem;
      margin-top: 1rem;
      padding-top: 1.25rem;
      border-top: 1px solid var(--border);
    }
    .forgot-panel[hidden] { display: none; }
    .forgot-panel label {
      font-size: var(--fs-eyebrow);
      font-weight: 700;
      color: var(--text-muted);
      text-transform: uppercase;
      letter-spacing: .09em;
    }
    .forgot-panel input {
      width: 100%;
      padding: .75rem 1rem;
      border: 1px solid var(--border);
      border-radius: var(--radius);
      font-family: var(--font-sans);
      font-size: var(--fs-base);
      color: var(--text-strong);
      background: var(--white);
      -webkit-appearance: none;
    }
    .forgot-panel input:focus { outline: none; border-color: var(--ocean); box-shadow: 0 0 0 3px rgba(26,159,196,0.12); }
    .forgot-msg { font-size: var(--fs-sm); color: var(--ocean); text-align: center; margin-top: .25rem; }
    .forgot-msg[hidden] { display: none; }

    /* Reset views share the login-view centering */
    #reset-view, #reset-success-view {
      align-items: center;
      justify-content: center;
      min-height: 100vh;
      padding: 1.5rem;
      background: var(--bg);
    }

    .field { margin-bottom: 1.25rem; }
    .field label {
      display: block;
      font-size: var(--fs-eyebrow);
      font-weight: 700;
      color: var(--text-muted);
      text-transform: uppercase;
      letter-spacing: .09em;
      margin-bottom: .45rem;
    }
    /* Radio-option labels inside a .field. `.field label` is (0,1,1) and forces
       uppercase/tiny/muted, which is right for a field CAPTION but wrong for a
       readable choice. Beaten on SPECIFICITY (0,2,1), never by source order —
       an equal-specificity rule placed later would just be re-broken by the
       next rule appended below it. */
    .field label.deposit-opt {
      display: flex;
      align-items: flex-start;
      gap: .5rem;
      font-size: var(--fs-sm);
      font-weight: 500;
      color: var(--text);
      text-transform: none;
      letter-spacing: normal;
      line-height: 1.45;
      margin-bottom: .5rem;
      cursor: pointer;
    }
    /* `.field input` sets appearance:none + padding + border-radius + width:100%.
       That is right for a text box and catastrophic for a checkbox or radio: the
       control renders as an empty rounded box with NO visible checked state, so
       the operator cannot see which option is selected. It affected every
       checkbox/radio inside a .field — including the pay-link kind (catalog vs
       part) and install-plan (add to visit vs new visit) choices, which decide
       what gets charged and when it is scheduled.

       Fixed for the whole class rather than per control, so a checkbox added to
       any future .field inherits a working one instead of repeating this bug. */
    .field input[type="checkbox"],
    .field input[type="radio"] {
      appearance: auto;
      width: 16px;
      height: 16px;
      min-width: 16px;
      padding: 0;
      border: none;
      border-radius: 0;
      flex: none;
      accent-color: var(--cerulean, #1a9fc4);
      box-shadow: none;
      cursor: pointer;
    }

    .field label.deposit-opt input[type="radio"],
    .field label.deposit-opt input[type="checkbox"] { margin: .15rem 0 0; }
    .field label.deposit-opt .deposit-opt-hint {
      display: block;
      font-size: var(--fs-xs);
      font-weight: 400;
      color: var(--text-muted);
      text-transform: none;
      letter-spacing: normal;
      margin-top: .1rem;
    }

    /* `select` and `textarea` were missing from this rule, so every dropdown in
       the app rendered as a raw browser control next to properly-styled text
       inputs -- different height, different font, different border, no token in
       sight. Fixed for the ELEMENT CLASS rather than for the two dropdowns that
       were reported, so a select added to any future .field inherits the right
       styling instead of repeating this. */
    .field input,
    .field select,
    .field textarea {
      width: 100%;
      padding: .75rem 1rem;
      border: 1px solid var(--border-control);
      border-radius: var(--radius);
      font-family: var(--font-sans);
      font-size: var(--fs-base);
      color: var(--text-strong);
      background: var(--white);
      transition: border-color .15s, box-shadow .15s;
      -webkit-appearance: none;
      appearance: none;
      /* Pinned rather than left at `normal`: a select sizes its line box from the
         font metrics while a date input adds room for the native picker glyph, so
         the two sat 2px apart when stacked in the same column. An explicit value
         makes the height a function of padding + font-size only, which is the same
         arithmetic for every control here. */
      line-height: 1.4;
    }
    .field input:hover:not(:disabled),
    .field select:hover:not(:disabled),
    .field textarea:hover:not(:disabled) { border-color: var(--border-control-hover); }

    /* appearance:none removes the native dropdown arrow, so the control would
       otherwise be indistinguishable from a text box. This draws our own as an
       inline SVG data URI -- no network request, and nothing for the CSP or the
       service worker to have an opinion about. The right padding reserves room
       for it so a long service name cannot run underneath the chevron. */
    .field select {
      padding-right: 2.5rem;
      background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='12' height='8' viewBox='0 0 12 8' fill='none'%3E%3Cpath d='M1 1.5L6 6.5L11 1.5' stroke='%23062840' stroke-width='1.75' stroke-linecap='round' stroke-linejoin='round'/%3E%3C/svg%3E");
      background-repeat: no-repeat;
      background-position: right 1rem center;
      cursor: pointer;
    }
    /* Every SINGLE-LINE control gets the same computed height, so a column of
       them lines up. Pinning line-height alone was not enough: a date input
       renders a native picker whose line box is taller than a select's, which
       left the two 2px apart. The value is the same arithmetic the padding
       already implies (1.4em text + .75rem top and bottom + 1px borders), so it
       tracks the font size instead of hard-coding a pixel height.
       Checkbox and radio are unaffected: their own rule above is more specific. */
    .field input:not([type="checkbox"]):not([type="radio"]),
    .field select { height: calc(1.4em + 1.5rem + 2px); }

    /* `.callout` was referenced in three places and DEFINED IN NONE, so every
       capacity note in the New Job drawer -- the multi-service one and the two
       subscription ones that predate it -- rendered as bare unstyled text with
       only an inline colour. A class with no definition fails silently: the
       markup looks intentional, nothing errors, and it reads as a plain
       paragraph jammed against whatever label follows it.
       Inline colours on existing call sites still win, so defining the base
       changes none of their meaning -- it only gives them a box and spacing. */
    .callout {
      display: block;
      padding: .6rem .75rem;
      margin: .1rem 0 1rem;
      background: var(--aqua-soft);
      border-left: 3px solid var(--ocean);
      border-radius: calc(var(--radius) / 2);
      font-size: var(--fs-sm);
      line-height: 1.45;
      color: var(--text-strong);
    }

    /* A select is a fixed-height control; a textarea must stay draggable. */
    .field textarea { resize: vertical; min-height: 5rem; height: auto; line-height: 1.5; }

    .field input:focus,
    .field select:focus,
    .field textarea:focus { outline: none; border-color: var(--ocean); box-shadow: 0 0 0 3px rgba(26,159,196,0.12); }
    .field select:disabled { background-color: var(--bg); cursor: not-allowed; opacity: .7; }

    #login-error {
      margin-top: 1rem;
      font-size: var(--fs-sm);
      color: var(--red);
      text-align: center;
    }

    /* ── Standardized login form rhythm (matches field app reference) ───── */
    .login-card .brand { margin-bottom: 1.6rem; }
    .login-card .field { margin-bottom: 0.85rem; }
    .login-card form .field:last-of-type { margin-bottom: 0; }
    .login-card .field label {
      font-size: var(--fs-sm);
      line-height: 1.2;
      font-weight: 600;
      color: var(--text-strong);
      text-transform: none;
      letter-spacing: normal;
      margin: 0 0 6px;
    }
    .login-card .field input {
      box-sizing: border-box;
      height: 44px;
      padding: 0 14px;
      font-size: var(--fs-md);
      border-radius: 8px;
    }
    .login-card .pw-wrap input { padding-right: 2.75rem; }
    .login-card .btn-signin { margin-top: 0.85rem; }
    .login-card #login-error { margin-top: 0.85rem; min-height: 1.2em; }
    .forgot-wrap { margin-top: 0.85rem; }

    /* ── App shell ──────────────────────────────────────────────────────── */
    #app-view { display: none; }

    header {
      background: var(--white);
      border-bottom: 1px solid var(--border);
      padding: 0 2rem;
      height: 96px;
      display: flex;
      align-items: center;
      justify-content: space-between;
      position: sticky;
      top: 0;
      z-index: 100;
    }

    .header-logo {
      height: 78px;
      width: auto;
      object-fit: contain;
      object-position: left center;
    }

    .header-right {
      display: flex;
      align-items: center;
      gap: 1rem;
    }

    .header-label {
      font-size: var(--fs-eyebrow);
      font-weight: 700;
      text-transform: uppercase;
      letter-spacing: .1em;
      color: var(--text-muted);
    }

    main {
      max-width: 960px;
      margin: 0 auto;
      padding: 2.5rem 1.5rem 4rem;
    }

    /* ── Section header ─────────────────────────────────────────────────── */
    .section-header {
      display: flex;
      align-items: baseline;
      gap: .75rem;
      margin-bottom: 1.25rem;
    }

    .section-title {
      font-family: var(--font-display);
      font-size: var(--fs-xl);
      font-weight: 700;
      color: var(--text-strong);
    }

    .section-count {
      font-size: var(--fs-xs);
      font-weight: 700;
      color: var(--text-muted);
      background: var(--aqua-soft);
      padding: .18rem .6rem;
      border-radius: 999px;
    }

    /* ── Empty / loading ────────────────────────────────────────────────── */
    .state-msg {
      text-align: center;
      padding: 3rem 1rem;
      color: var(--text-muted);
      font-size: var(--fs-sm);
      background: var(--white);
      border-radius: var(--radius);
      border: 1px solid var(--border);
    }

    /* ── Appointment cards ──────────────────────────────────────────────── */
    .appt-list {
      display: flex;
      flex-direction: column;
      gap: 1rem;
      margin-bottom: 3.5rem;
    }

    .appt-card {
      background: var(--white);
      border: 1px solid var(--border);
      border-radius: var(--radius);
      padding: 1.35rem 1.5rem;
      display: grid;
      grid-template-columns: 1fr auto;
      gap: 1rem;
      align-items: start;
      box-shadow: var(--shadow-sm);
      transition: box-shadow .15s;
    }
    .appt-card:hover { box-shadow: var(--shadow-md); }

    .appt-date-pill {
      display: inline-block;
      background: var(--sand);
      color: var(--cerulean);
      font-size: var(--fs-eyebrow);
      font-weight: 700;
      text-transform: uppercase;
      letter-spacing: .08em;
      padding: .2rem .7rem;
      border-radius: 999px;
      margin-bottom: .55rem;
    }

    .appt-top {
      display: flex;
      flex-wrap: wrap;
      align-items: center;
      gap: .5rem;
      margin-bottom: .45rem;
    }

    .appt-service {
      font-family: var(--font-display);
      font-weight: 700;
      font-size: var(--fs-md);
      color: var(--text-strong);
    }

    .status-badge {
      font-size: var(--fs-eyebrow);
      font-weight: 700;
      text-transform: uppercase;
      letter-spacing: .06em;
      padding: .18rem .55rem;
      border-radius: 999px;
    }
    .status-confirmed { background: #d1fae5; color: #065f46; }
    .status-reserved  { background: #fef3c7; color: #78350f; }
    .status-completed { background: #f0f4f8; color: #4d7d93; }
    .status-canceled  { background: var(--red-light); color: var(--red); }
    .status-no_show   { background: #fce7f3; color: #831843; }

    /* Over-capacity warning badge (sql/407/408) — the ONE consistent icon/color
       for "an office user confirmed booking this over the day's hour budget".
       Mirrored 1:1 in field-app (same glyph, same --warning color) so Jeff sees
       the identical signal PoolBooks shows. */
    .capacity-badge {
      display: inline-flex;
      align-items: center;
      gap: .25rem;
      font-size: var(--fs-xs);
      font-weight: 700;
      color: var(--warning);
      background: var(--warning-bg);
      border-radius: 999px;
      padding: .18rem .5rem;
      cursor: default;
    }

    /* Quick Check (sql/495). Deliberately NOT warning-coloured: a quick check is
       a normal, expected stop, not an exception to flag. It sits next to the
       over-capacity badge, so it has to read as neutral information or the board
       gains a second alarm colour that means "everything is fine". */
    .quick-check-badge {
      display: inline-flex;
      align-items: center;
      font-size: var(--fs-xs);
      font-weight: 700;
      color: var(--text-muted);
      background: var(--app-neutral-bg);
      border-radius: 999px;
      padding: .18rem .5rem;
      cursor: default;
      white-space: nowrap;
    }

    .appt-meta {
      font-size: var(--fs-sm);
      color: var(--text-muted);
      line-height: 1.75;
    }
    .appt-meta strong { color: var(--text); font-weight: 600; }
    .appt-meta a { color: var(--ocean); text-decoration: none; }
    .appt-meta a:hover { text-decoration: underline; }

    .appt-divider { height: 1px; background: var(--border); margin: .75rem 0; }

    .appt-money { display: flex; gap: 1.75rem; flex-wrap: wrap; }
    .money-item { font-size: var(--fs-xs); color: var(--text-muted); }
    .money-item strong {
      display: block;
      font-size: var(--fs-md);
      font-weight: 700;
      color: var(--text-strong);
    }

    .appt-actions {
      display: flex;
      flex-direction: column;
      gap: .5rem;
      min-width: 108px;
      justify-self: end;
      align-self: center;
    }

    /* ── Template preview modal shell ────────────────────────────────────── */
    #tpl-modal {
      display: none;
      position: fixed;
      inset: 0;
      background: rgba(6,40,64,.35);
      backdrop-filter: blur(2px);
      z-index: 300;
      align-items: center;
      justify-content: center;
      padding: 1rem;
    }
    #tpl-modal.open { display: flex; }

    /* The card must SCROLL, not overflow off-screen. It never did: with only the
       text templates it happened to fit, so nothing exposed it. Adding the inline
       email render (2026-09-12) made the card ~990px, and on a 596px-tall window
       its top sat at -197px with the render 85px BELOW the viewport and no way to
       reach it -- content that is present, correct, and unreachable. Found by
       Meesha looking at it, after an automated check confirmed only that the
       srcdoc was SET. "The bytes are right" and "a person can see it" are
       different claims, and only the second one is the feature. */
    #tpl-modal .move-card {
      max-height: calc(100vh - 2rem);
      overflow-y: auto;
      overscroll-behavior: contain;
    }

    /* Info icon that opens the message-template preview. Sits inline in a label,
       so it must not inherit the label's pointer/checkbox behaviour. */
    .tpl-peek {
      display: inline-flex; align-items: center; justify-content: center;
      width: 1.05rem; height: 1.05rem; margin-left: .35rem; padding: 0;
      border: 1px solid var(--muted); border-radius: 50%;
      background: transparent; color: var(--muted);
      font-size: var(--fs-xs); font-weight: 700; font-style: italic;
      line-height: 1; cursor: pointer; vertical-align: middle; flex: none;
    }
    .tpl-peek:hover { border-color: var(--text); color: var(--text); }
    .tpl-msg {
      border: 1px solid var(--border);
      border-radius: .6rem; padding: .6rem .7rem; margin: .5rem 0;
      background: rgba(6,40,64,.03);
    }
    .tpl-msg-label {
      font-size: var(--fs-xs); text-transform: uppercase; letter-spacing: .04em;
      color: var(--muted); margin-bottom: .25rem;
    }
    .tpl-msg-body { white-space: pre-wrap; }

    .move-card {
      background: var(--white);
      border-radius: 16px;
      padding: 2.25rem;
      max-width: 400px;
      width: 100%;
      border: 1px solid var(--border);
      box-shadow: var(--shadow-md);
    }

    .move-title {
      font-family: var(--font-display);
      font-size: var(--fs-lg);
      font-weight: 700;
      color: var(--text-strong);
      margin-bottom: 1.5rem;
    }

    .move-card select {
      width: 100%;
      padding: .75rem 1rem;
      border: 1px solid var(--border);
      border-radius: var(--radius);
      font-family: var(--font-sans);
      font-size: var(--fs-base);
      color: var(--text-strong);
      background: var(--white);
      -webkit-appearance: none;
      transition: border-color .15s;
    }
    .move-card select:focus { outline: none; border-color: var(--ocean); box-shadow: 0 0 0 3px rgba(26,159,196,0.12); }

    .move-actions { display: flex; gap: .75rem; margin-top: 1.75rem; }
    #move-error { margin-top: .75rem; font-size: var(--fs-sm); color: var(--red); text-align: center; }

    .move-notify-row { display: flex; align-items: flex-start; gap: .6rem; margin: 1rem 0 .25rem; font-size: var(--fs-sm); color: var(--text); cursor: pointer; }
    .move-notify-row input { width: auto; margin-top: .15rem; flex: none; }
    .move-card #move-reason { width: 100%; padding: .8rem .9rem; border: 1px solid var(--border); border-radius: 10px; font: inherit; }
    .move-card #move-reason:focus { outline: none; border-color: var(--ocean); box-shadow: 0 0 0 3px rgba(26,159,196,0.12); }
    #move-correction-note { margin-top: .75rem; font-size: var(--fs-xs); line-height: 1.4; color: var(--text-body); background: var(--aqua-soft); border-radius: 10px; padding: .7rem .85rem; }

    /* ── Job cards ──────────────────────────────────────────────────────── */
    .jobs-section { margin-top: 0; }

    .job-list {
      display: flex;
      flex-direction: column;
      gap: 1rem;
      margin-bottom: 3.5rem;
    }

    .job-card {
      background: var(--white);
      border: 1px solid var(--border);
      border-left: 3px solid var(--cerulean);
      border-radius: var(--radius);
      padding: 1.35rem 1.5rem;
      display: grid;
      grid-template-columns: 1fr auto;
      gap: 1rem;
      align-items: start;
      box-shadow: var(--shadow-sm);
    }
    .job-card.status-scheduled { border-left-color: #10b981; }

    /* Parts-slip state (sql/423). A slipped job is scheduled, so it would otherwise
       render with the calm green "scheduled" rail. Specificity, not source order,
       is what makes this win over .job-card.status-scheduled — both are (0,2,0), so
       relying on order would silently break the next time a rule is appended. */
    .job-card.job-slipped.job-slipped { border-left-color: var(--danger); }
    .job-card > .job-slip-banner,
    .job-card > .job-parts-line { grid-column: 1 / -1; }
    .job-slip-banner {
      margin-top: .6rem;
      padding: .7rem .85rem;
      border-radius: calc(var(--radius) / 1.5);
      background: var(--danger-bg);
      color: var(--danger);
      font-size: var(--fs-xs);
      line-height: 1.5;
    }

    .job-category-pill {
      display: inline-block;
      background: var(--sand);
      color: var(--cerulean);
      font-size: var(--fs-eyebrow);
      font-weight: 700;
      text-transform: uppercase;
      letter-spacing: .08em;
      padding: .2rem .7rem;
      border-radius: 999px;
      margin-bottom: .55rem;
    }

    .job-title {
      font-family: var(--font-display);
      font-weight: 700;
      font-size: var(--fs-md);
      color: var(--text-strong);
      margin-bottom: .35rem;
    }

    .job-desc {
      font-size: var(--fs-sm);
      color: var(--text-muted);
      line-height: 1.65;
    }
    .job-desc strong { color: var(--text); font-weight: 600; }

    .job-divider { height: 1px; background: var(--border); margin: .75rem 0; }

    .job-money { display: flex; gap: 1.75rem; flex-wrap: wrap; }

    .job-status-badge {
      font-size: var(--fs-eyebrow);
      font-weight: 700;
      text-transform: uppercase;
      letter-spacing: .06em;
      padding: .18rem .55rem;
      border-radius: 999px;
    }
    .job-status-deposit_paid { background: #fef3c7; color: #78350f; }
    .job-status-scheduled    { background: #d1fae5; color: #065f46; }

    /* Who's-ordering radios in the Parts modal (jobs.parts_ordered_by, sql/489).
       Same trap as .deposit-opt above, and beaten the same way: `.field label`
       is (0,1,1) and forces uppercase/tiny/muted, which is right for a field
       CAPTION and wrong for a readable choice. `.field label.parts-owner-opt`
       is (0,2,1) so it wins on SPECIFICITY, never on source order — an equal
       rule placed later is just re-broken by the next rule appended below it.
       Caught on sight in the browser: the whole group rendered in caps. */
    .field label.parts-owner-opt {
      display: flex; align-items: flex-start; gap: .6rem; cursor: pointer;
      border: 1px solid var(--border); border-radius: 9px; padding: .6rem .75rem;
      font-size: var(--fs-sm); font-weight: 500; color: var(--text);
      text-transform: none; letter-spacing: normal; line-height: 1.45;
      margin-bottom: 0;
    }
    .field label.parts-owner-opt:hover { border-color: var(--ocean); }
    .field label.parts-owner-opt input { margin-top: .2rem; flex-shrink: 0; }
    .field label.parts-owner-opt b { display: block; font-size: var(--fs-sm); font-weight: 600;
      color: var(--text-strong); text-transform: none; letter-spacing: normal; }
    .field label.parts-owner-opt em { display: block; font-size: var(--fs-xs); font-style: normal;
      font-weight: 400; color: var(--text-muted); line-height: 1.45; margin-top: .1rem;
      text-transform: none; letter-spacing: normal; }
    .field label.parts-owner-opt:has(input:checked) { border-color: var(--ocean); background: var(--aqua-soft); }

    /* ── Quote cards (Estimates panel) ─────────────────────────────────── */
    .quote-panel-header {
      display: flex; align-items: center; justify-content: space-between;
      margin-bottom: 1.5rem;
    }
    /* --border is a 10%-alpha hairline. On the cream page background a
       transparent chip behind it is very nearly invisible, so the whole filter
       row read as plain text. Unselected chips get the neutral surface the
       tertiary buttons use; the selected one is a FILLED ocean chip rather than
       white-with-an-ocean-outline, so which view you are in is legible at a
       glance instead of by comparing outlines. */
    /* A filter is not an action, so it must not be shaped like one. Six outlined
       pills sitting above a card of outlined buttons read as one undifferentiated
       field of things to press. Collapsing them onto a single tinted track makes
       them one CONTROL with a current value -- the same move a segmented control
       makes -- and the borders come off, because the track already groups them. */
    .quote-filter-tabs {
      display: inline-flex; gap: 2px; padding: 3px; flex-wrap: wrap;
      background: var(--tf-aqua-050); border-radius: 999px;
    }
    .quote-filter-tab {
      padding: .35rem .85rem; border-radius: 999px; font-size: var(--fs-xs);
      font-weight: 600; border: 1px solid transparent;
      background: transparent; color: var(--tf-ocean-800);
      cursor: pointer; font-family: var(--font-sans); transition: all .12s;
    }
    .quote-filter-tab:hover {
      background: #fff; border-color: transparent; color: var(--cerulean);
    }
    .quote-filter-tab.active {
      background: var(--ocean); color: #fff;
      border-color: var(--ocean); box-shadow: var(--shadow-sm);
    }
    .quote-filter-tab.active:hover {
      background: var(--ocean); color: #fff; border-color: var(--ocean);
      filter: brightness(1.06);
    }
    .quote-list { display: flex; flex-direction: column; gap: 1rem; margin-bottom: 3.5rem; }
    .quote-card {
      background: var(--white); border: 1px solid var(--border);
      border-left: 3px solid var(--cerulean);
      border-radius: var(--radius); padding: 1.2rem 1.4rem;
      display: grid; grid-template-columns: 1fr;
      gap: .75rem 1rem; align-items: start; box-shadow: var(--shadow-sm);
    }
    /* The card stripe stays per-status on purpose: it is the only thing that
       separates Sent from Opened at a glance, and tone collapses those two.
       `draft` is deliberately unstriped (neutral); pending_approval is striped
       because it is waiting on a person. */
    .quote-card.status-pending_approval { border-left-color: #c9a96e; }
    .quote-card.status-sent     { border-left-color: #f59e0b; }
    .quote-card.status-viewed   { border-left-color: #3b82f6; }
    .quote-card.status-accepted { border-left-color: #10b981; }
    .quote-card.status-declined { border-left-color: #ef4444; }
    .quote-card.status-expired  { border-left-color: var(--border); opacity: .7; }
    .quote-title {
      font-family: var(--font-display); font-weight: 700;
      font-size: var(--fs-md); color: var(--text-strong); margin-bottom: .25rem;
    }
    .quote-meta { font-size: var(--fs-xs); color: var(--text-muted); line-height: 1.6; }
    .quote-meta strong { color: var(--text); font-weight: 600; }
    .quote-money { display: flex; gap: 1.5rem; flex-wrap: wrap; margin-top: .5rem; }
    /* Every badge carries a border now. Draft's near-white fill on a white card
       read as a word rather than a chip, which is the one status where knowing
       "this has not gone out yet" matters most. Borders come from the status
       token families rather than fresh hex, so a badge cannot drift from the
       card border and the button of the same meaning. */
    .quote-status-badge {
      font-size: var(--fs-eyebrow); font-weight: 700; text-transform: uppercase;
      letter-spacing: .06em; padding: .18rem .55rem; border-radius: 999px;
      white-space: nowrap; border: 1px solid transparent;
    }
    /* Four tones, not seven statuses. A per-status rule means every new status
       needs someone to remember a matching class, and `pending_approval` is the
       proof nobody does: it had no rule and rendered unstyled. Tone comes from
       the shared vocabulary, so a status can never arrive without a colour. */
    /* Geoff's cost and markup, under the line description on a field card. */
    .fq-basis { display:block; font-size: var(--fs-xs); color: var(--text-muted); margin-top:.1rem; }

    .cq-markup-row { display:flex; align-items:center; gap:.4rem; }
    .cq-markup-row input { width: 5.5rem; }
    .cq-markup-pct { color: var(--text-muted); font-size: var(--fs-sm); margin-right:.3rem; }

    /* ── Create/Edit Estimate modal rhythm ──────────────────────────────
       The modal had one flat spacing value from the customer field to the
       internal notes, so eleven controls of quite different weight (a price, a
       narrative, a deposit override) read as one undifferentiated column and
       nothing announced where one concern ended and the next began. These heads
       name the three questions the form actually asks, and the spacing steps up
       at each one so the grouping is visible before it is read. */
    .cq-section-head {
      font-size: var(--fs-xs); font-weight: 700; letter-spacing: .08em;
      text-transform: uppercase; color: var(--text-muted);
      margin: 1.9rem 0 .7rem; padding-top: 1.15rem;
      border-top: 1px solid var(--border);
    }
    #create-quote-modal .field { margin-bottom: 1.15rem; }
    #create-quote-modal .quote-totals { margin: 1.5rem 0 1.25rem; }
    #create-quote-modal .quote-line-items { margin-bottom: .55rem; }
    #create-quote-modal .move-actions { margin-top: 1.9rem; padding-top: 1.15rem; border-top: 1px solid var(--border); }
    #create-quote-modal textarea { line-height: 1.5; }

    /* The options control, folded away until wanted. `list-style:none` plus the
       webkit pseudo-element is what actually removes the default triangle on
       both engines; one alone leaves it on the other. */
    .cq-disclosure { border-top: 1px solid var(--border); padding-top: 1.15rem; margin-top: 1.9rem; }
    /* Row, not column. As a flex COLUMN the ::before marker became its own line
       above the title -- a bare "+" floating over the heading, which reads as a
       stray character rather than a control. The marker is now a real element in
       a row beside the text, which also gives it somewhere to be vertically
       centred. */
    .cq-disclosure > summary {
      cursor: pointer; list-style: none;
      display: flex; flex-direction: row; align-items: flex-start; gap: .5rem;
    }
    .cq-disclosure > summary::-webkit-details-marker { display: none; }
    .cq-disclosure-mark {
      flex: none; width: 1.1rem; text-align: center;
      font-weight: 700; color: var(--text-muted); line-height: 1.35;
    }
    .cq-disclosure[open] .cq-disclosure-mark::after { content: ''; }
    .cq-disclosure-text { display: flex; flex-direction: column; gap: .15rem; min-width: 0; }
    .cq-disclosure-label { font-size: var(--fs-sm); font-weight: 600; color: var(--text-strong); }
    .cq-disclosure > summary:hover .cq-disclosure-label { color: var(--text-link); }
    .cq-disclosure-hint { font-size: var(--fs-xs); font-weight: 400; color: var(--text-muted); }
    .cq-disclosure .deposit-opt { margin-top: .9rem; }

    /* Geoff's review, on the estimate card beside Send. Three states, and the
       colour is the whole point: a concern must read differently at a glance
       from an approval, on a card whose next action spends money. */
    .qreview {
      display: flex; flex-wrap: wrap; align-items: center; gap: .5rem;
      margin-top: .6rem; padding: .55rem .75rem; border-radius: 6px;
      font-size: var(--fs-xs); line-height: 1.45;
    }
    .qreview-waiting { background: var(--app-neutral-bg); color: var(--text-muted); }
    .qreview-ok      { background: #d1fae5; color: var(--status-success-text); }
    .qreview-concern { background: #fee2e2; color: var(--status-danger-text); }
    /* Money the customer has already paid that this estimate has not credited.
       Saffron, not red: nothing is broken, there is just money on the table. */
    .qreview-diag    { background: var(--tf-sand); color: var(--text-strong); }
    .qreview-head    { font-weight: 600; }
    .qreview-note    { flex-basis: 100%; color: var(--text); }

    .qbadge-warn     { background: #fef3c7; color: var(--status-warning-text); border-color: var(--status-warning-border); }
    .qbadge-open     { background: #dbeafe; color: var(--status-info-text);    border-color: var(--status-info-border); }
    .qbadge-done     { background: #d1fae5; color: var(--status-success-text); border-color: var(--status-success-border); }
    .qbadge-dead     { background: var(--app-neutral-bg); color: #6b7280;      border-color: var(--app-neutral-border); }
    /* Actions sit in one row across the BOTTOM of the card, not stacked down the
       right edge. A right-hand column grew a vertical stack of up to five
       buttons that pushed the card taller than its own content and read as a
       pile rather than a set of choices. A single row also puts them in a real
       order: what to do first is leftmost, and the way out is pushed to the far
       right where a dismissive action belongs. */
    .quote-actions {
      display: flex; flex-direction: row; flex-wrap: wrap;
      align-items: center; gap: .5rem;
      border-top: 1px solid var(--border);
      padding-top: .85rem;
    }
    /* Retiring an estimate is not a peer of sending one. Away from the doing
       buttons, and it only takes on danger colouring when reached for. */
    .quote-actions .btn-quote-close { margin-left: auto; }

    /* ── Create Quote modal ──────────────────────────────────────────────── */
    /* ═══ New Estimate: a DRAWER, not a centred modal (2026-09-26) ═══
       New Job made this exact move already, and this is the same shell it uses:
       a right-hand panel you move through, not a box you dismiss.

       Done as a PRESENTATION change only. The open/close/Escape/backdrop
       handlers in 13-estimates.js all key off `.open` on this same element and
       are untouched, so nothing about how the form behaves had to be re-proved
       to change how it sits on the screen.

       The scrim stays on the container and the panel is the child, which is why
       clicking outside still closes it -- that handler compares e.target to the
       container itself. */
    #create-quote-modal {
      display: none; position: fixed; inset: 0;
      background: rgba(6,40,64,.35); backdrop-filter: blur(2px);
      z-index: 300; align-items: stretch; justify-content: flex-end;
      padding: 0; overflow: hidden;
    }
    #create-quote-modal.open { display: flex; }
    #create-quote-modal .modal-card {
      max-width: 560px; width: 100%;
      border-radius: 0; border: none; border-left: 1px solid var(--border);
      max-height: none; height: 100%;
      padding: 1.75rem 1.75rem 2.5rem;
      overflow-y: auto;
      /* Slides in rather than appearing, so it reads as arriving from the side.
         Respects the reduced-motion preference like everything else here. */
      animation: cqDrawerIn .26s cubic-bezier(.2,.8,.2,1);
    }
    @keyframes cqDrawerIn { from { transform: translateX(100%); } to { transform: translateX(0); } }
    @media (prefers-reduced-motion: reduce) {
      #create-quote-modal .modal-card { animation: none; }
    }
    @media (max-width: 640px) {
      #create-quote-modal .modal-card { max-width: 100vw; border-left: none; }
    }
    .quote-line-items { display: flex; flex-direction: column; gap: .75rem; margin: .5rem 0 1rem; }
    .quote-line-row {
      display: grid; grid-template-columns: 1fr 70px 100px auto;
      gap: .5rem; align-items: center;
    }
    .quote-line-row input {
      width: 100%; padding: .55rem .75rem; border: 1px solid var(--border);
      border-radius: 8px; font: inherit; font-size: var(--fs-sm);
    }
    .quote-line-row input:focus { outline: none; border-color: var(--ocean); box-shadow: 0 0 0 3px rgba(26,159,196,0.12); }
    .btn-remove-line {
      background: none; border: none; cursor: pointer;
      color: var(--text-muted); font-size: var(--fs-lg); padding: 0 .25rem;
      line-height: 1;
    }
    .btn-remove-line:hover { color: var(--red); }
    .btn-add-line {
      background: none; border: 1px dashed var(--border-control); border-radius: 8px;
      color: var(--text-muted); font-size: var(--fs-xs); padding: .5rem; width: 100%;
      cursor: pointer; font-family: var(--font-sans); transition: all .12s;
    }
    .btn-add-line:hover { border-color: var(--ocean); color: var(--ocean); background: var(--aqua-soft); }
    /* ── Column header above a .quote-line-items / option grid ──────────────
       The Line Items header row was four <span>s carrying the SAME inline style
       repeated in the markup, and adding the options grid would have made it
       eight. Extracted so the caption styling lives once and any future grid in
       this modal inherits it. Column template is set by the row class it sits
       above (see .quote-line-row below), so this only owns the caption look. */
    /* Action row on a from-the-field card. Was an inline flex repeated in the
       markup; extracted so the two states (review / cleared) line up identically. */
    .fq-actions { display: flex; gap: .5rem; margin-top: .75rem; align-items: center; }


    /* Pushes "Show cleared" to the far end of the section header, away from the
       count it must not be mistaken for. */
    .fq-cleared-toggle { margin-left: auto; }

    .quote-line-head {
      display: grid; gap: .5rem;
      margin-bottom: .4rem; padding: 0 .1rem;
      grid-template-columns: 1fr 70px 100px auto;   /* matches .quote-line-row */
    }
    .quote-line-head > span {
      font-size: var(--fs-eyebrow);
      font-weight: 700;
      color: var(--text-muted);
      text-transform: uppercase;
      letter-spacing: .09em;
    }

    /* ── Pick-one option rows (sql/052 selectable_options) ──────────────────
       A MODIFIER on .quote-line-row, not a new component: it is the same control
       (text + number + remove) with one column swapped, so it inherits the input
       styling, the focus ring and the remove button. Only the track widths
       differ -- qty becomes price, and price becomes the "shown first" radio. */
    .quote-line-row.opt-row,
    .quote-line-head.opt-head { grid-template-columns: 1fr 110px 96px auto; }

    /* The radio that marks which option the customer sees selected.
       `.field label.opt-default-pick`, NOT a bare `.opt-default-pick`: these rows
       sit inside a .field, and `.field label` is (0,1,1) while a lone class is
       (0,1,0), so the bare selector LOSES on specificity no matter where it is
       placed in the file. Measured, not assumed -- the first version of this rule
       rendered the radio label as an uppercase letter-spaced block caption.
       Same fix, and the same reason, as .field label.deposit-opt above. */
    .field label.opt-default-pick {
      display: flex; align-items: center; gap: .4rem;
      font-size: var(--fs-xs); font-weight: 400; color: var(--text-muted);
      /* text-transform and letter-spacing must be RESET, not merely left unset:
         winning on specificity only overrides the properties this rule actually
         declares, so an omitted one keeps inheriting `.field label`'s uppercase
         caption treatment. Measured after the specificity fix, which is the only
         reason this was caught -- display was correct while the text was still
         UPPERCASE AND LETTER-SPACED. */
      text-transform: none; letter-spacing: normal;
      margin-bottom: 0;
      cursor: pointer; white-space: nowrap;
    }
    /* .field input[type=radio] only applies inside a .field; these rows are not
       one, so the same approved 16px/appearance:auto treatment is restated for
       this control rather than left to the browser default. Same values, so the
       two cannot drift into different-looking radios. */
    .field label.opt-default-pick input[type="radio"] {
      appearance: auto; width: 16px; height: 16px; min-width: 16px;
      padding: 0; border: none; border-radius: 0; flex: none;
      accent-color: var(--cerulean, #1a9fc4); box-shadow: none; cursor: pointer;
    }

    /* A muted note under a control group. Used for the options explainer and the
       "Showing X, customer picks A to B" line under the totals. */
    .quote-hint { font-size: var(--fs-xs); color: var(--text-muted); line-height: 1.5; margin-top: .5rem; }
    .quote-hint.align-end { text-align: right; margin-top: .35rem; }

    /* Inline form error. #cq-error carried these exact declarations as an inline
       style; both now share one class so an error looks the same wherever it is
       raised in this modal. */
    .form-err { color: var(--red); font-size: var(--fs-sm); margin-top: .45rem; }

    /* A group of controls separated from what precedes it by a rule, rather than
       by a hand-typed margin + border on the element itself. */
    .field-group-sep { margin-top: 1rem; border-top: 1px solid var(--border); padding-top: .85rem; }

    .quote-totals { background: var(--aqua-soft); border-radius: 10px; padding: .85rem 1rem; margin: .75rem 0; }
    .quote-total-row { display: flex; justify-content: space-between; font-size: var(--fs-sm); padding: .15rem 0; }
    .quote-total-row.total { font-weight: 700; font-size: var(--fs-md); color: var(--text-strong); }

    /* Payment terms: a segmented control, not three loose buttons. Built on the
       same tinted-track idiom as .quote-filter-tab rather than inventing a
       fourth switch shape (the app already has .quote-filter-tabs and
       .queue-viewtog; a fifth would be the drift those two are evidence of).
       The radios stay in the DOM and stay focusable -- hidden with opacity, not
       display:none -- so arrow keys work with no JS and an agent can read and
       operate the control without knowing our class names. */
    .field > .label {
      display: block; font-size: var(--fs-eyebrow); font-weight: 700;
      color: var(--text-muted); text-transform: uppercase;
      letter-spacing: .09em; margin-bottom: .45rem;
    }
    .cq-terms {
      display: inline-flex; gap: 2px; padding: 3px; flex-wrap: wrap;
      background: var(--tf-aqua-050); border-radius: var(--radius-pill);
    }
    .field label.cq-term, .cq-term {
      position: relative; display: inline-flex; align-items: center;
      padding: .4rem .9rem; border-radius: var(--radius-pill);
      font-size: var(--fs-xs); font-weight: 600; font-family: var(--font-sans);
      color: var(--tf-ocean-800); cursor: pointer; transition: all .12s;
      min-height: 32px; text-transform: none; letter-spacing: normal;
      margin-bottom: 0;
    }
    .cq-term input {
      position: absolute; inset: 0; width: 100%; height: 100%;
      margin: 0; opacity: 0; cursor: pointer;
    }
    .field label.cq-term:hover, .cq-term:hover { background: #fff; color: var(--cerulean); }
    /* The selected state is painted from a CLASS the JS maintains, not from
       :has(input:checked). Two earlier cuts tried the pure-CSS version and both
       were measured wrong: the first put .is-on at the same specificity as the
       :has() rule, so source order quietly won; the second, with .is-on moved
       safely behind @supports, left the fill lagging exactly ONE CLICK behind
       the radio, reproducibly, 250ms after the transition had settled. A
       segmented control that shows the previous answer is worse than one that
       shows none. So: one mechanism, the same one .quote-filter-tab.active and
       .queue-viewtog already use here -- a class set by the one state-setter --
       rather than a third pattern invented for this drawer. syncCqTermsUi()
       owns it and runs on every change, on Edit and on reset. */
    .field label.cq-term.is-on, .cq-term.is-on {
      background: var(--ocean); color: #fff; box-shadow: var(--shadow-sm);
    }
    .field label.cq-term.is-on:hover, .cq-term.is-on:hover {
      background: var(--ocean); color: #fff; filter: brightness(1.06);
    }
    .cq-term:focus-within { outline: 2px solid var(--cerulean); outline-offset: 2px; }
    /* PoolBooks is used on a phone, so the pills have to be a real tap target
       there. 32px is fine for a mouse and under the 44px minimum for a thumb. */
    @media (max-width: 640px) {
      .field label.cq-term, .cq-term { min-height: var(--tap-min); padding: .5rem 1rem; }
    }


    /* ── Task cards ─────────────────────────────────────────────────────── */
    .tasks-section { margin-top: 0; }

    /* ── Tasks panel v2: triage tiles, filters, urgency lanes ──────────────
       All scoped to #panel-tasks so nothing bleeds into the other panels. */
    /* auto-fit, not a hardcoded count: the lane registry in 02-financial.js is
       meant to be the ONE place a lane is added, and a fixed repeat(3) quietly
       made it two places. It was already wrong before "From the field" landed,
       because "Missing info to type in" was a 4th lane dropping onto its own
       row. Track the count here and it never needs touching again. */
    #panel-tasks .tk-triage { display:grid; grid-template-columns:repeat(auto-fit, minmax(min(100%, 11rem), 1fr)); gap:.75rem; margin-bottom:1.25rem; }
    #panel-tasks .tk-tri {
      border:1px solid var(--border); border-radius:12px; padding:.8rem .9rem;
      background:var(--white); cursor:pointer; text-align:left;
      font-family:var(--font-sans); transition:border-color .12s, box-shadow .12s;
    }
    #panel-tasks .tk-tri:hover { border-color: var(--cerulean); }
    #panel-tasks .tk-tri-n { font-size: var(--fs-xl); font-weight:700; line-height:1; color:var(--text-strong); }
    /* Brand cerulean, not a danger/warn colour: a field task is not an alarm,
       it is a person waiting. It leads the row by position, not by shouting. */
    #panel-tasks .tk-tri.field .tk-tri-n { color:var(--cerulean); }
    #panel-tasks .tk-tri.money .tk-tri-n { color:var(--status-danger-text); }
    #panel-tasks .tk-tri.time  .tk-tri-n { color:#a05a00; }
    #panel-tasks .tk-tri-l   { font-size: var(--fs-xs); color:var(--text-muted); margin-top:.3rem; font-weight:600; }
    #panel-tasks .tk-tri-sub { font-size: var(--fs-xs); color:var(--text-muted); margin-top:.15rem; }

    #panel-tasks .tk-filters { display:flex; gap:.5rem; flex-wrap:wrap; align-items:center; margin-bottom:.6rem; }
    #panel-tasks .tk-search {
      flex:1 1 240px; min-width:180px; padding:.45rem .7rem; border:1px solid var(--border);
      border-radius:8px; font-family:var(--font-sans); font-size: var(--fs-sm); background:var(--white); color:var(--text);
    }
    #panel-tasks .tk-select {
      padding:.45rem .6rem; border:1px solid var(--border); border-radius:8px;
      font-family:var(--font-sans); font-size: var(--fs-xs); background:var(--white); color:var(--text); cursor:pointer;
    }
    #panel-tasks .tk-resultline { font-size: var(--fs-xs); color:var(--text-muted); margin-bottom:1rem; min-height:1.1em; }

    #panel-tasks .tk-group { margin-bottom:1.5rem; }
    /* Lane header. Section-label treatment per the design system (11px / 700 /
       .10em / uppercase). Every collapsible section on this panel uses THIS
       header and THIS caret — the review-approval section used to carry a 1.35rem
       serif title and a text "Show/Hide" button, which read as a different kind
       of thing on the same page. */
    #panel-tasks .tk-group-head {
      display:flex; align-items:center; gap:.6rem;
      padding:.5rem .75rem; margin-bottom:.4rem;
      cursor:pointer; user-select:none;
      border-radius:10px; transition:background .12s;
    }
    #panel-tasks .tk-group-head:hover { background:var(--app-border-soft, #f0ece2); }
    #panel-tasks .tk-group-head h3 {
      font-size: var(--fs-eyebrow); letter-spacing:.10em; text-transform:uppercase;
      font-weight:700; margin:0;
    }
    #panel-tasks .tk-group-count {
      font-size: var(--fs-xs); font-weight:700; border-radius:999px; padding:.1rem .5rem;
    }

    /* Lane colour. Status tokens only — never hand-picked hex (design-system.md).
       The -text variants are used for words: the base --status-* colours are
       fill/icon only and fail AA on their own -soft background. */
    #panel-tasks #tk-g-money   .tk-group-head h3     { color:var(--status-danger-text); }
    #panel-tasks #tk-g-money   .tk-group-count       { color:var(--status-danger-text); background:var(--status-danger-soft); }
    #panel-tasks #tk-g-money   .tk-group-body        { border-left:3px solid var(--status-danger); }
    #panel-tasks #tk-g-time    .tk-group-head h3     { color:var(--status-warning-text, #9a5b00); }
    #panel-tasks #tk-g-time    .tk-group-count       { color:var(--status-warning-text, #9a5b00); background:var(--status-warning-soft); }
    #panel-tasks #tk-g-time    .tk-group-body        { border-left:3px solid var(--status-warning); }
    #panel-tasks #tk-g-routine .tk-group-head h3     { color:var(--tf-ocean-700); }
    #panel-tasks #tk-g-routine .tk-group-count       { color:var(--tf-ocean-800); background:var(--app-bg-blue, #e6f3f8); }
    #panel-tasks #tk-g-routine .tk-group-body        { border-left:3px solid var(--ocean); }
    /* Backfill reads as calm, not alarming: a blank column is somebody's ten
       seconds on the phone, never money or a waiting customer. Muted rather than
       a fourth status hue, so the three urgency colours keep their meaning. */
    #panel-tasks #tk-g-backfill .tk-group-head h3    { color:var(--text-muted); }
    #panel-tasks #tk-g-backfill .tk-group-count      { color:var(--text-muted); background:var(--app-border-soft, #f0ece2); }
    #panel-tasks #tk-g-backfill .tk-group-body       { border-left:3px solid var(--border); }

    /* One caret, one size, everywhere. The old ▾ glyph rendered at ~9px and
       differed from the review section's text toggle. */
    #panel-tasks .tk-chev {
      margin-left:auto; display:grid; place-items:center;
      width:16px; height:16px; flex:0 0 16px;
      color:var(--text-muted); transition:transform .15s;
    }
    #panel-tasks .tk-chev svg { width: var(--icon-md); height: var(--icon-md); stroke-width:2; fill:none; stroke-linecap:round; stroke-linejoin:round; }
    #panel-tasks .tk-group.collapsed .tk-chev { transform:rotate(-90deg); }
    #panel-tasks .tk-group-head:hover .tk-chev { color:var(--text); }
    #panel-tasks .tk-group.collapsed .tk-group-body { display:none; }
    #panel-tasks .tk-group-body { border:1px solid var(--border); border-radius:12px; background:var(--white); overflow:hidden; }

    /* Restored 2026-09-23. c6577e8 deleted .tk-row/.tk-body/.tk-acts as "dead
       CSS ... rendered by nothing", but taskRowHTML() in 02-financial.js builds
       this markup as a JS template STRING, so nothing in any .html file
       references the classes. Losing display:flex turned every task row into
       three stacked blocks with the buttons on their own lines -- the whole
       Tasks panel, for eleven days. `grep -rl tk-acts admin/` returns the
       renderer immediately, so the "verified zero renderers" in that commit
       was checkable and wrong. A sweep for unused CSS in this codebase must
       search the JS, because that is where the markup lives. */
    #panel-tasks .tk-row { display:flex; align-items:flex-start; gap:.85rem; padding:.8rem 1rem; border-bottom:1px solid var(--border); }
    #panel-tasks .tk-row:last-child { border-bottom:0; }
    #panel-tasks .tk-row.is-dismissed { opacity:.62; }
    #panel-tasks .tk-ico { width:34px; height:34px; border-radius:9px; display:grid; place-items:center; flex:0 0 34px; margin-top:1px; }
    #panel-tasks .tk-ico svg { width: var(--icon-md); height: var(--icon-md); stroke-width:2; fill:none; stroke-linecap:round; stroke-linejoin:round; }
    #panel-tasks .tk-ico.danger { background:var(--status-danger-soft); color:var(--status-danger-text); }
    #panel-tasks .tk-ico.warn   { background:#fef3e0; color:#9a5b00; }
    #panel-tasks .tk-ico.ok     { background:#e6f5ee; color:#11692f; }
    #panel-tasks .tk-ico.info   { background:#e6f3f8; color:#0f6d8a; }

        #panel-tasks .tk-title { font-size: var(--fs-base); font-weight:650; color:var(--text-strong); display:flex; align-items:center; gap:.45rem; flex-wrap:wrap; }
    #panel-tasks .tk-desc  { font-size: var(--fs-xs); color:var(--text-muted); margin-top:.15rem; line-height:1.45; white-space:pre-wrap; }
    #panel-tasks .tk-meta  { font-size: var(--fs-xs); color:var(--text-muted); margin-top:.3rem; display:flex; gap:.5rem; align-items:center; flex-wrap:wrap; }

    /* Type-level "why this exists", declared in TASK_TYPES.hint. Collapsed by
       default and opened from the small i beside the title. Colours match
       .tk-ico.info so the icon and the panel it opens read as one thing. */
    #panel-tasks .tk-hint-ico { width:1.05rem; height:1.05rem; flex:none; border:0; border-radius:999px;
      background:#e6f3f8; color:#0f6d8a; font-size:.7rem; font-weight:800; font-style:italic;
      line-height:1.05rem; padding:0; cursor:pointer; font-family:Georgia, 'Times New Roman', serif; }
    #panel-tasks .tk-hint-ico:hover,
    #panel-tasks .tk-hint-ico.is-open { background:#0f6d8a; color:#fff; }
    #panel-tasks .tk-hint-text { font-size: var(--fs-xs); color:var(--text-muted); line-height:1.5;
      margin-top:.4rem; padding:.5rem .6rem; border-radius:8px; background:#e6f3f8;
      border-left:3px solid #0f6d8a; }

    #panel-tasks .tk-pill { font-size: var(--fs-xs); font-weight:700; padding:.1rem .4rem; border-radius:999px; }
    #panel-tasks .tk-pill.money { background:var(--status-danger-soft); color:var(--status-danger-text); }
    #panel-tasks .tk-pill.age   { background:#fef3e0; color:#9a5b00; }
    #panel-tasks .tk-pill.quiet { background:var(--border); color:var(--text-muted); }

    /* flex:0 0 auto sized this column to its own CONTENT and never let it shrink,
       so a task type with six buttons pushed .tk-body down to one word per line
       while every existing two-button type looked fine. Same family as the grid
       trap in root CLAUDE.md: a track that sizes to content silently outgrows its
       container, and the damage shows up on whichever row grows first rather than
       where the rule is written. Wrap instead, cap the share of the row the
       buttons may take, and give the text a real minimum. */
    #panel-tasks .tk-body  { flex:1 1 320px; min-width:0; }
    #panel-tasks .tk-acts { display:flex; align-items:center; justify-content:flex-end; gap:.4rem; flex:0 1 auto; flex-wrap:wrap; max-width:52%; padding-top:2px; }

    #panel-tasks .tk-empty { padding:1.6rem; text-align:center; color:var(--text-muted); font-size: var(--fs-sm); }

    /* The pile expander is the same caret as the lane header, rotated the same
       way — one disclosure control on the panel, not three. */
    #panel-tasks .tk-pile-btn { display:inline-flex; align-items:center; gap:.35rem; }
    /* Same 16px caret as the lane header — one size on the panel, not two. */
    #panel-tasks .tk-chev-inline { margin-left:0; }
    #panel-tasks .tk-pile-btn:not(.is-open) .tk-chev-inline { transform:rotate(-90deg); }

    /* Editable review-text draft, inside a normal task row */
    /* The draft grows to fit its text. The fixed height clipped the last line of
       every message — the Google review URL, i.e. the entire point of the text —
       and a scrollbar in a 3-line box is not a fix, because the operator would be
       approving a message they cannot fully see.

       `field-sizing: content` makes the browser do this natively. That matters
       more than saving a few lines of JS: the JS version has to MEASURE, and a
       measurement taken before the panel is laid out returns a wrapped-at-20px
       scrollHeight in the thousands, which then gets written to style.height and
       sticks. The browser has no such failure mode. The JS below stays as a
       fallback for engines without it, guarded so it can never write a bad value. */
    #panel-tasks .tk-draft {
      width:100%; margin-top:.4rem; min-height:3.4rem; resize:vertical; overflow:hidden;
      field-sizing:content;
      padding:.5rem .65rem; border:1px solid var(--border); border-radius:8px;
      font-family:var(--font-sans); font-size: var(--fs-xs); line-height:1.45;
      color:var(--text); background:var(--white);
    }
    #panel-tasks .tk-draft:focus { outline:2px solid var(--ocean); outline-offset:-1px; }

    /* Collapsed pile of repeated tasks of one type */
    #panel-tasks .tk-pile { border-bottom:1px solid var(--border); }
    #panel-tasks .tk-pile:last-child { border-bottom:0; }
    #panel-tasks .tk-pile .tk-pile-head { background:#fcfbf8; border-bottom:0; }
    #panel-tasks .tk-pile-kids { border-top:1px solid var(--border); background:#fdfcfa; }
    #panel-tasks .tk-pile-kids .tk-row { padding-left:3.6rem; background:transparent; }
    #panel-tasks .tk-pile-kids .tk-row:last-child { border-bottom:0; }

    /* Inline picker for attaching an unmatched payment to a booking */
    #panel-tasks .tk-slot { margin-top:.6rem; }

    /* Backfill form, mounted inline in .tk-slot (see openBackfillForm). Sits
       inside an already-bordered row, so it is a tinted panel rather than a
       second card — a card in a card reads as a different kind of thing. */
    #panel-tasks .tk-bf {
      border:1px solid var(--border); border-radius:10px;
      background:var(--app-border-soft, #f7f5ef); padding:.75rem .85rem;
    }
    #panel-tasks .tk-bf-head { font-size: var(--fs-sm); font-weight:650; color:var(--text-strong); }
    #panel-tasks .tk-bf-sub  { font-size: var(--fs-xs); color:var(--text-muted); margin:.15rem 0 .7rem; line-height:1.45; }
    #panel-tasks .tk-bf-lbl  {
      display:block; font-size: var(--fs-eyebrow); letter-spacing:.06em; text-transform:uppercase;
      font-weight:700; color:var(--text-muted); margin:.65rem 0 .3rem;
    }
    #panel-tasks .tk-bf-input {
      width:100%; box-sizing:border-box; font-family:var(--font-sans); font-size: var(--fs-sm);
      padding:.45rem .6rem; border:1px solid var(--border); border-radius:8px;
      background:var(--white); color:var(--text);
    }
    #panel-tasks textarea.tk-bf-input { resize:vertical; line-height:1.5; }
    #panel-tasks .tk-bf-note   { font-size: var(--fs-xs); color:var(--text-muted); margin-top:.35rem; line-height:1.45; }
    #panel-tasks .tk-bf-bar    { display:flex; align-items:center; gap:.6rem; flex-wrap:wrap; margin-top:.85rem; }
    #panel-tasks .tk-bf-status { font-size: var(--fs-xs); color:var(--text-muted); }
    #panel-tasks .tk-bf-status.err { color:var(--status-danger-text); }
    #panel-tasks .tk-bf-loading, #panel-tasks .tk-bf-err { font-size: var(--fs-xs); color:var(--text-muted); padding:.5rem 0; }
    #panel-tasks .tk-bf-err { color:var(--status-danger-text); }
    #panel-tasks .tk-picker { border:1px solid var(--border); border-radius:10px; padding:.6rem; background:#fdfcfa; }
    #panel-tasks .tk-picker-search {
      width:100%; padding:.45rem .7rem; border:1px solid var(--border); border-radius:8px;
      font-family:var(--font-sans); font-size: var(--fs-sm); background:var(--white); color:var(--text);
    }
    #panel-tasks .tk-picker-results { margin-top:.5rem; max-height:230px; overflow-y:auto; }
    #panel-tasks .tk-picker-hint { font-size: var(--fs-xs); color:var(--text-muted); padding:.4rem .2rem; }
    #panel-tasks .tk-picker-row {
      display:flex; flex-direction:column; gap:.1rem; width:100%; text-align:left;
      padding:.45rem .6rem; border:1px solid transparent; border-radius:8px;
      background:none; cursor:pointer; font-family:var(--font-sans);
    }
    #panel-tasks .tk-picker-row:hover { background:var(--white); border-color:var(--border); }
    #panel-tasks .tk-picker-row:disabled { opacity:.5; cursor:default; }
    #panel-tasks .tk-picker-who  { font-size: var(--fs-sm); font-weight:650; color:var(--text-strong); }
    #panel-tasks .tk-picker-meta { font-size: var(--fs-xs); color:var(--text-muted); }

    /* ── Job schedule picker (mountJobSchedulePicker, 04-jobs-balances.js) ──
       Deliberately NOT scoped to #panel-tasks: the same component mounts in the
       Tasks panel and in the appointment panel, and a scoped rule would leave
       the second one unstyled. Uses var(--text) rather than var(--text-strong)
       for anything that must stay readable -- --text-strong resolves to empty
       in this app, so it silently falls back to inherited colour. */
    .jsp {
      border:1px solid var(--border); border-radius:10px;
      background:var(--app-border-soft, #f7f5ef); padding:.75rem .85rem;
    }
    .jsp-h   { font-size: var(--fs-sm); font-weight:650; color:var(--text); }
    .jsp-job { font-size: var(--fs-xs); color:var(--text-muted); margin:.15rem 0 .6rem; line-height:1.45; }
    .jsp-sec { margin-top:.7rem; }
    .jsp-sec-h {
      font-size: var(--fs-eyebrow); letter-spacing:.06em; text-transform:uppercase;
      font-weight:700; color:var(--text-muted); margin-bottom:.35rem;
    }
    .jsp-note { font-size: var(--fs-xs); color:var(--text-muted); line-height:1.45; margin:.2rem 0 .45rem; }
    .jsp-opt {
      display:flex; gap:.55rem; align-items:center; padding:.4rem .5rem;
      font-size:.88rem; color:var(--text); cursor:pointer;
      border:1px solid transparent; border-radius:8px;
    }
    .jsp-opt:hover { background:var(--white); border-color:var(--border); }
    /* Radios and checkboxes ONLY. This was written as a bare `input` when a
       radio was the only input a .jsp-opt row could contain, so the moment the
       parts block put a date field on the same row it was sized to 18x18 and
       rendered as a second empty circle beside the radio -- unreadable, and
       unclickable as a date picker. Narrowed to what the rule always meant.
       Safe by construction: every pre-existing .jsp-opt input is a radio, so
       this changes nothing that already worked. */
    .jsp-opt input[type="radio"], .jsp-opt input[type="checkbox"] { width:18px; height:18px; flex:none; }
    .jsp-opt input[type="date"] { flex:0 1 auto; min-width:9.5rem; }
    /* The past-visit block is visually separated because picking one COMPLETES
       the job immediately -- a different act from scheduling future work, and
       it must not read as one more row in the same list. */
    .jsp-past {
      border:1px solid var(--status-warning-border); border-radius:8px;
      background:var(--status-warning-soft); padding:.5rem .55rem;
    }
    /* The parts question sits ABOVE the visit list and is tinted, not boxed in
       warning colours: it is a question to answer, not a refusal to read. The
       visits below stay fully pickable while it is open -- the block informs
       the choice, it does not gate it. */
    .jsp-parts {
      border:1px solid var(--status-info-border); border-radius:8px;
      background:var(--status-info-soft); padding:.5rem .55rem;
    }
    .jsp-parts input[type="date"], .jsp-parts input[type="text"] {
      font-family:var(--font-sans); font-size: var(--fs-sm);
      padding:.35rem .55rem; border:1px solid var(--border); border-radius:8px;
      background:var(--white); color:var(--text);
    }
    .jsp-parts input[type="text"] { flex:1 1 100%; }
    .jsp-parts-known { font-style:normal; }
    /* Flagged, never hidden and never disabled: booking before the part is a
       real thing the office sometimes means, and the rule is "say why", not
       "you may not". */
    .jsp-early { color:var(--status-warning-text); font-style:normal; }
    .jsp-new { display:flex; flex-wrap:wrap; gap:.5rem; padding:.2rem .5rem .1rem 2rem; }
    .jsp-new input[type="date"], .jsp-new input[type="text"], .jsp-new select {
      font-family:var(--font-sans); font-size: var(--fs-sm);
      padding:.45rem .6rem; border:1px solid var(--border); border-radius:8px;
      background:var(--white); color:var(--text);
    }
    .jsp-check {
      display:flex; gap:.5rem; align-items:center; flex:1 1 100%;
      font-size: var(--fs-xs); color:var(--text-muted); cursor:pointer;
    }
    .jsp-check input { width:16px; height:16px; flex:none; }
    .jsp-acts { display:flex; gap:.6rem; flex-wrap:wrap; margin-top:.85rem; }
    .jsp-loading, .jsp-err { font-size: var(--fs-xs); color:var(--text-muted); padding:.5rem 0; }
    .jsp-err { color:var(--status-danger-text); }

    
    
    .task-desc { font-size: var(--fs-sm); color: var(--text); line-height: 1.55; }
    .task-date { font-size: var(--fs-xs); color: var(--text-muted); margin-top: .35rem; }

    .task-omw-actions { display: flex; gap: .5rem; margin-top: .75rem; }

    /* ── Review-text approval cards ────────────────────────────────────────── */
    .review-approval-card {
      background: var(--white);
      border: 1px solid var(--border);
      border-left: 3px solid var(--cerulean);
      border-radius: var(--radius);
      padding: 1.1rem 1.5rem;
      margin-bottom: .85rem;
      box-shadow: var(--shadow-sm);
    }
    .ra-meta { font-size: var(--fs-xs); color: var(--text-muted); margin-bottom: .4rem; }
    .ra-text-box {
      width: 100%; box-sizing: border-box; font-family: var(--font-sans); font-size: var(--fs-sm);
      line-height: 1.5; color: var(--text); border: 1px solid var(--border); border-radius: 8px;
      padding: .65rem .8rem; resize: vertical; min-height: 70px;
    }
    .ra-actions { display: flex; gap: .5rem; margin-top: .7rem; flex-wrap: wrap; }

    /* ── Section header row (title + actions) ──────────────────────────────── */
    .section-header-row {
      display: flex;
      align-items: center;
      justify-content: space-between;
      flex-wrap: wrap;
      gap: .75rem;
      margin-bottom: 1.25rem;
    }
    .section-header-row .section-header { margin-bottom: 0; }

    /* ── Filter tabs ───────────────────────────────────────────────────────── */
    .filter-tabs {
      display: inline-flex;
      gap: .25rem;
      background: var(--aqua-soft);
      padding: .25rem;
      border-radius: 999px;
      margin-bottom: 1.25rem;
    }
    .filter-tab {
      padding: .4rem 1.1rem;
      border: none;
      background: transparent;
      border-radius: 999px;
      font-family: var(--font-sans);
      font-size: var(--fs-xs);
      font-weight: 600;
      color: var(--text-muted);
      cursor: pointer;
      transition: all .15s;
    }
    .filter-tab.active { background: var(--white); color: var(--cerulean); box-shadow: var(--shadow-sm); }

    /* ── Generic modal (Create Appointment, Review panel) ──────────────────── */
    .modal {
      display: none;
      position: fixed;
      inset: 0;
      background: rgba(6,40,64,.35);
      backdrop-filter: blur(2px);
      z-index: 300;
      align-items: center;
      justify-content: center;
      padding: 1rem;
    }
    .modal.open { display: flex; }
    .modal-card {
      background: var(--white);
      border-radius: 16px;
      padding: 2.25rem;
      max-width: 440px;
      width: 100%;
      border: 1px solid var(--border);
      box-shadow: var(--shadow-md);
      max-height: 90vh;
      overflow-y: auto;
    }
    .modal-title {
      font-family: var(--font-display);
      font-size: var(--fs-lg);
      font-weight: 700;
      color: var(--text-strong);
      margin-bottom: 1.5rem;
    }
    .modal-card select,
    .modal-card input[type="text"],
    .modal-card input[type="email"],
    .modal-card input[type="tel"],
    .modal-card input[type="date"],
    .modal-card input[type="search"] {
      width: 100%;
      padding: .75rem 1rem;
      border: 1px solid var(--border);
      border-radius: var(--radius);
      font-family: var(--font-sans);
      font-size: var(--fs-base);
      color: var(--text-strong);
      background: var(--white);
      -webkit-appearance: none;
      transition: border-color .15s;
    }
    .modal-card select:focus,
    .modal-card input:focus { outline: none; border-color: var(--ocean); box-shadow: 0 0 0 3px rgba(26,159,196,0.12); }
    .modal-actions { display: flex; gap: .75rem; margin-top: 1.75rem; }
    .modal-error { margin-top: .75rem; font-size: var(--fs-sm); color: var(--red); text-align: center; }

    /* ── New Job drawer (NEW-JOB-DRAWER-SPEC.md) ────────────────────────────
       Right-side slide-out, replacing the old centered "Create Appointment"
       modal. Reuses PoolDock's #scrim/.panel slide-in mechanism (dock/index.html
       — see DOCK-SPEC.md Refunds card) rather than inventing a new pattern. */
    /* These rules were originally written as #create-scrim / #create-modal.drawer
       — scoped to the New Job drawer BY ID. #sub-checkout-modal carries the same
       class="drawer" but matched none of them, so it inherited no positioning and
       no hidden state and rendered inline at the bottom of every panel as a stray
       "Take payment" ribbon. Nothing errored; it just sat there.

       Now keyed on the CLASS, so every drawer that exists today or is added later
       inherits hiding by construction. Add class="scrim" + class="drawer" and
       toggle .open — there is nothing per-drawer to remember. Any drawer needing a
       different width overrides ONLY width, by id, below. */
    .scrim {
      position: fixed; inset: 0;
      background: rgba(6,40,64,.35);
      z-index: 300;
      opacity: 0; pointer-events: none;
      transition: opacity .2s ease;
    }
    .scrim.open { opacity: 1; pointer-events: auto; }

    .drawer {
      position: fixed; top: 0; right: 0; bottom: 0;
      width: 560px; max-width: 100vw;
      background: var(--white);
      box-shadow: var(--shadow-md);
      transform: translateX(100%);
      transition: transform .26s cubic-bezier(.2,.8,.2,1);
      z-index: 301;
      display: flex; flex-direction: column;
      border-radius: 0;
      padding: 0;
      max-height: none;
    }
    .drawer.open { transform: translateX(0); }
    @media (max-width: 640px) { .drawer { width: 100vw; } }

    .drawer-header {
      padding: 1.25rem 1.5rem;
      border-bottom: 1px solid var(--border);
      display: flex; align-items: center; justify-content: space-between;
      flex-shrink: 0;
    }
    .drawer-title { font-family: var(--font-display); font-size: var(--fs-lg); color: var(--navy); font-weight: 700; }
    /* A 44px square target with a glyph you can actually see. It was a
       --fs-lg character with .25rem of padding, which is both hard to hit and
       hard to spot in the corner of a full-height drawer. 44px is the usual
       minimum for a comfortable target, and it matters here because this is
       the control someone reaches for when they have opened the wrong thing. */
    .drawer-close {
      background: none; border: none; cursor: pointer;
      font-size: 1.75rem; color: var(--text-muted); line-height: 1;
      width: 44px; height: 44px; flex: none;
      display: inline-flex; align-items: center; justify-content: center;
      border-radius: var(--radius); transition: background .12s, color .12s;
    }
    .drawer-close:hover { background: var(--sand); color: var(--text-strong); }
    .drawer-close:focus-visible { outline: 2px solid var(--cerulean); outline-offset: 2px; }

    .drawer-stepper { display: flex; padding: 1rem 1.5rem; border-bottom: 1px solid var(--border); gap: .5rem; flex-shrink: 0; }
    .drawer-stepper .step-pill { flex: 1; text-align: center; font-size: var(--fs-xs); font-weight: 600; letter-spacing: .02em; color: var(--text-muted); padding-bottom: .6rem; border-bottom: 3px solid var(--border); cursor: default; }
    .drawer-stepper .step-pill.active { color: var(--cerulean); border-color: var(--ocean); }
    .drawer-stepper .step-pill.done { color: #1b8a5a; border-color: #1b8a5a; cursor: pointer; }
    .drawer-stepper .step-pill .step-num { display: inline-block; width: 16px; height: 16px; border-radius: 50%; background: var(--sand); color: var(--text-muted); font-size: var(--fs-xs); line-height: 16px; margin-right: 4px; }
    .drawer-stepper .step-pill.active .step-num { background: var(--ocean); color: var(--white); }
    .drawer-stepper .step-pill.done .step-num { background: #1b8a5a; color: var(--white); }

    .drawer-body { flex: 1; overflow-y: auto; padding: 1.5rem; }
    .drawer-panel { display: none; }
    .drawer-panel.active { display: block; }

    .drawer-footer { padding: 1rem 1.5rem; border-top: 1px solid var(--border); display: flex; align-items: center; flex-wrap: wrap; gap: .75rem; flex-shrink: 0; }
    .drawer-footer .modal-error { margin: 0; text-align: left; flex-basis: 100%; order: 3; }

    .review-block { border: 1px solid var(--border); border-radius: var(--radius); margin-bottom: .85rem; overflow: hidden; }
    .review-block-header { display: flex; align-items: center; justify-content: space-between; padding: .7rem 1rem; background: var(--sand); font-size: var(--fs-xs); font-weight: 700; color: var(--cerulean); text-transform: uppercase; letter-spacing: .03em; }
    .review-edit-link { font-size: var(--fs-xs); font-weight: 600; color: var(--ocean); text-transform: none; letter-spacing: 0; background: none; border: none; cursor: pointer; }
    .review-edit-link:hover { text-decoration: underline; }
    .review-block-body { padding: .85rem 1rem; }
    .review-row { display: flex; justify-content: space-between; font-size: var(--fs-xs); padding: .3rem 0; gap: 1rem; }
    .review-row .k { color: var(--text-muted); }
    .review-row .v { color: var(--text-strong); font-weight: 600; text-align: right; }
    .money-callout { background: #fef3e0; border: 1px solid rgba(160,90,0,0.2); border-radius: var(--radius); padding: .85rem 1rem; font-size: var(--fs-xs); color: #a05a00; margin-top: .5rem; font-weight: 600; }

    /* customer search results in Create modal */
    .cust-results {
      border: 1px solid var(--border);
      border-radius: var(--radius);
      margin-top: .5rem;
      max-height: 180px;
      overflow-y: auto;
    }
    .cust-results:empty { display: none; }
    .cust-result {
      padding: .6rem .9rem;
      cursor: pointer;
      font-size: var(--fs-sm);
      border-bottom: 1px solid var(--border);
    }
    .cust-result:last-child { border-bottom: none; }
    .cust-result:hover { background: var(--aqua-soft); }
    .cust-result strong { color: var(--text-strong); }
    .cust-result span { color: var(--text-muted); font-size: var(--fs-xs); }
    .cust-selected {
      background: var(--aqua-soft);
      border: 1px solid var(--ocean);
      border-radius: var(--radius);
      padding: .7rem .9rem;
      margin-top: .5rem;
      font-size: var(--fs-sm);
      display: flex;
      justify-content: space-between;
      align-items: center;
    }
    .cust-selected button {
      background: transparent; border: none; color: var(--ocean);
      font-family: var(--font-sans); font-size: var(--fs-xs); font-weight: 700; cursor: pointer;
    }
    .new-cust-fields { margin-top: .75rem; }
    .toggle-row {
      display: flex; align-items: center; gap: .6rem;
      margin-top: 1rem; font-size: var(--fs-sm); color: var(--text);
    }
    .toggle-row input[type="checkbox"] { width: 18px; height: 18px; accent-color: var(--ocean); cursor: pointer; }

    /* ── Skill-flag toggle switches (settings) ─────────────────────────────── */
    .skill-switch { display: inline-flex; align-items: center; gap: .5rem; cursor: pointer; user-select: none; }
    .skill-switch input { position: absolute; opacity: 0; width: 0; height: 0; }
    .skill-switch .track {
      position: relative; width: 38px; height: 22px; border-radius: 11px;
      background: var(--border); transition: background .15s ease; flex-shrink: 0;
    }
    .skill-switch .track::after {
      content: ''; position: absolute; top: 2px; left: 2px; width: 18px; height: 18px;
      border-radius: 50%; background: #fff; box-shadow: 0 1px 2px rgba(0,0,0,.25);
      transition: transform .15s ease;
    }
    .skill-switch input:checked + .track { background: var(--ocean); }
    .skill-switch input:checked + .track::after { transform: translateX(16px); }
    .skill-switch input:disabled + .track { opacity: .5; cursor: default; }
    .skill-switch .sw-label { font-size: var(--fs-sm); color: var(--text-strong); }

    /* ─── Field-sendable service rows ─────────────────────────────────────── */
    .sendable-row {
      display: flex; align-items: center; gap: .75rem;
      padding: .6rem .75rem; border: 1px solid var(--border); border-radius: 8px;
      background: var(--white);
    }
    .sendable-row.blocked { background: var(--bg); opacity: .75; }
    .sendable-name { font-size: var(--fs-sm); font-weight: 600; color: var(--text-strong); }
    .sendable-price { font-size: var(--fs-xs); color: var(--text-muted); margin-left: auto; padding-left: .5rem; white-space: nowrap; }
    .sendable-why { font-size: var(--fs-xs); color: var(--text-muted); line-height: 1.35; margin-top: .15rem; }
    .sendable-warn {
      display: inline-block; font-size: var(--fs-xs); font-weight: 700; letter-spacing: .03em;
      color: #8a5a00; background: #fdf1d6; border: 1px solid #f0d9a3;
      border-radius: 999px; padding: .05rem .45rem; margin-left: .4rem; vertical-align: middle;
    }

    /* ─── Field send (office visibility) rows ─────────────────────────────── */
    .send-card {
      border: 1px solid var(--border); border-left: 3px solid var(--border);
      border-radius: 8px; background: var(--white); padding: .75rem .85rem;
    }
    /* Unpaid and aging is the whole point of this list — escalate it visually. */
    .send-card.unpaid       { border-left-color: #f0b429; }
    .send-card.unpaid-aging { border-left-color: #c0392b; background: #fdf4f3; }
    .send-card.paid         { border-left-color: #1b8a5a; }
    .send-card.closed       { opacity: .6; }
    .send-head { display: flex; align-items: baseline; gap: .5rem; flex-wrap: wrap; }
    .send-customer { font-size: var(--fs-sm); font-weight: 700; color: var(--text-strong); }
    .send-amount { font-size: var(--fs-sm); font-weight: 700; color: var(--text-strong); margin-left: auto; }
    .send-what { font-size: var(--fs-xs); color: var(--text-strong); margin-top: .2rem; line-height: 1.4; }
    .send-meta { font-size: var(--fs-xs); color: var(--text-muted); margin-top: .3rem; }
    .send-pill {
      font-size: var(--fs-eyebrow); font-weight: 700; text-transform: uppercase; letter-spacing: .04em;
      border-radius: 999px; padding: .1rem .5rem;
    }
    .send-pill.paid   { color: #10633f; background: #dff3e8; }
    .send-pill.unpaid { color: #8a5a00; background: #fdf1d6; }
    .send-pill.aging  { color: #8f2018; background: #fbe3e0; }
    .send-pill.dead   { color: var(--text-muted); background: var(--bg); }
    .send-link-row { display: flex; gap: .4rem; align-items: center; margin-top: .55rem; }
    .send-link-url {
      flex: 1; padding: .32rem .5rem; font-size: var(--fs-xs); border: 1px solid var(--border);
      border-radius: 5px; font-family: var(--font-sans); color: var(--text-muted);
      background: var(--bg); overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
    }
    .send-quote {
      margin-top: .6rem; padding-top: .55rem; border-top: 1px dashed var(--border);
    }
    .send-quote-label {
      font-size: var(--fs-eyebrow); font-weight: 700; text-transform: uppercase; letter-spacing: .05em;
      color: var(--text-muted); margin-bottom: .35rem;
    }
    .send-quote-item {
      display: flex; justify-content: space-between; gap: .75rem;
      font-size: var(--fs-xs); color: var(--text-strong); padding: .1rem 0;
    }
    .send-quote-item span:last-child { color: var(--text-muted); white-space: nowrap; }
    .send-photos { display: flex; gap: .35rem; margin-top: .45rem; flex-wrap: wrap; }
    .send-photos img {
      width: 54px; height: 54px; object-fit: cover; border-radius: 5px;
      border: 1px solid var(--border); cursor: pointer; background: var(--bg);
    }

    /* ── Collapsible technician profile cards ──────────────────────────────── */
    .tech-card { background: var(--surface); border: 1px solid var(--border); border-radius: 10px; overflow: hidden; }
    .tech-card-head {
      display: flex; align-items: center; gap: .6rem; width: 100%;
      padding: .85rem 1.1rem; background: none; border: none; cursor: pointer;
      font-family: var(--font-sans); text-align: left;
    }
    .tech-card-head:hover { background: rgba(0,0,0,.02); }
    .tech-card-head .chev {
      width: 16px; height: 16px; flex-shrink: 0; color: var(--text-muted);
      transition: transform .15s ease;
    }
    .tech-card.open .tech-card-head .chev { transform: rotate(90deg); }
    .tech-card-name { flex: 1; font-size: var(--fs-sm); font-weight: 700; color: var(--text-strong); }
    .tech-card-meta { font-size: var(--fs-xs); font-weight: 600; color: var(--text-muted); }
    .tech-card-body { display: none; padding: 0 1.1rem 1.1rem; }
    .tech-card.open .tech-card-body { display: block; }
    .tech-card-body .field-label {
      font-size: var(--fs-xs); font-weight: 700; text-transform: uppercase; letter-spacing: .05em;
      color: var(--text-muted); display: block; margin: .75rem 0 .4rem;
    }

    /* ── Collapsible Settings sections (native <details>) ──────────────────────
       Every Settings section is a collapsible accordion so the tab reads as one
       consistent, scannable stack (Meesha, 2026-07-17). Built on native <details>
       — no JS, so there is no listener to re-stack across tab re-entries. */
    .settings-acc {
      margin-bottom: 1rem; border: 1px solid var(--border); border-radius: 10px;
      background: var(--surface); padding: 0 1.1rem;
    }
    .settings-acc > summary {
      cursor: pointer; list-style: none; display: flex; align-items: center; gap: .55rem;
      padding: .95rem 0; font-size: var(--fs-xs); font-weight: 700; text-transform: uppercase;
      letter-spacing: .08em; color: var(--text-muted);
    }
    .settings-acc > summary::-webkit-details-marker { display: none; }
    .settings-acc > summary::before {
      content: ''; width: 7px; height: 7px; flex-shrink: 0;
      border-right: 2px solid var(--text-muted); border-bottom: 2px solid var(--text-muted);
      transform: rotate(-45deg); transition: transform .15s ease; margin-right: .1rem;
    }
    .settings-acc[open] > summary::before { transform: rotate(45deg); }
    .settings-acc > summary .acc-count { font-weight: 600; text-transform: none; letter-spacing: 0; }
    .settings-acc-body { padding: 0 0 1.25rem; }

    /* ── Per-service intake-policy cards (reuse .tech-card look) ────────────── */
    .intake-qlist { margin-top: .4rem; display: flex; flex-direction: column; gap: .55rem; }
    .intake-q {
      border: 1px solid var(--border); border-radius: 8px; background: var(--bg);
      padding: .55rem .7rem;
    }
    .intake-q-label { font-size: var(--fs-xs); font-weight: 700; color: var(--text-strong); }
    .intake-q-ask { font-size: var(--fs-xs); color: var(--text-muted); line-height: 1.4; margin-top: .2rem; }
    .intake-empty { font-size: var(--fs-xs); color: var(--text-muted); line-height: 1.45; }
    .intake-helper { font-size: var(--fs-xs); color: var(--text-muted); line-height: 1.45; margin: .1rem 0 .9rem; }
    .intake-toggle-row { display: flex; align-items: center; gap: .6rem; margin: .5rem 0; }
    .intake-toggle-row .sw-label { font-size: var(--fs-sm); color: var(--text-strong); font-weight: 600; }
    .intake-toggle-note { font-size: var(--fs-xs); color: var(--text-muted); margin-left: auto; }

    /* ── Services & Parts rows (sql/818) ───────────────────────────────────
       A .tech-card with no head and no body: the whole row IS the control, so
       there is nothing to expand into. Reuses .tech-card and .skill-switch
       rather than defining a second card that merely looks like them. */
    /* ── The cost summary in the New Estimate drawer (2026-09-29) ─────────
       Sits inside the existing markup block, so it inherits that block's own
       show/hide. Quiet by design: this is a reference figure the office reads
       while pricing, not a control. */
    .cq-cost-summary { border: 1px solid var(--border); border-radius: 8px;
      padding: .55rem .7rem; margin-top: .6rem; background: var(--app-neutral-bg, #f7fafb); }
    .cq-cost-row { display: flex; justify-content: space-between; align-items: baseline;
      font-size: var(--fs-sm); color: var(--text-muted); padding: .15rem 0; }
    .cq-cost-row span:last-child { font-weight: 700; color: var(--text-strong);
      font-variant-numeric: tabular-nums; }
    .cq-cost-total { border-top: 1px solid var(--border); margin-top: .35rem; padding-top: .4rem; }
    .cq-cost-thin span:last-child { color: var(--status-warning-text); }

    .parts-row { display: flex; align-items: center; gap: .7rem; padding: .75rem 1.1rem; }
    .parts-row .sw-label { flex: 1; font-size: var(--fs-sm); color: var(--text-strong); font-weight: 600; }
    .parts-row .tech-card-meta { min-width: 8.5rem; text-align: right; }

    /* ── Numeric settings fields (Escalation Alerts timings) ───────────────── */
    .settings-num-row { display: flex; flex-direction: column; gap: .4rem; }
    .settings-num-row .sw-label { font-size: var(--fs-sm); color: var(--text-strong); font-weight: 600; }
    .settings-num-input {
      width: 6.5rem; padding: .45rem .6rem; font: inherit; font-size: var(--fs-sm);
      color: var(--text-strong); background: var(--surface, #fff);
      border: 1px solid var(--border, #e5e0d8); border-radius: 8px;
    }
    /* A type="time" input renders "10:00 AM" plus the browser's own clock picker,
       which does not fit the 6.5rem above — right for a 2-3 digit number, too
       narrow for a time, which clipped to "10:00 A". Scoped to type="time" so the
       numeric settings rows are untouched. Lives HERE, beside its sibling, rather
       than next to the rule it was widening: the first version of this sat above
       the .pm-* block and was deleted as collateral when that block was removed
       the same day. Put a rule where its SELECTOR belongs, not where you happened
       to be editing. */
    input[type="time"].settings-num-input { width: 9.5rem; }
    .settings-num-input:disabled { opacity: .55; cursor: not-allowed; }
    .settings-num-input.is-invalid { border-color: var(--status-danger); }
    .settings-num-help { font-size: var(--fs-xs); color: var(--text-muted); line-height: 1.45; }
    .settings-num-status { font-size: var(--fs-xs); line-height: 1.45; min-height: 1rem; }

    /* ── Financial snapshot ────────────────────────────────────────────────── */
    .fin-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); gap: 1rem; margin-bottom: 1.25rem; }
    .fin-card {
      background: var(--white);
      border: 1px solid var(--border);
      border-radius: var(--radius);
      padding: 1.25rem 1.4rem;
      box-shadow: var(--shadow-sm);
    }
    .fin-label { font-size: var(--fs-eyebrow); font-weight: 700; text-transform: uppercase; letter-spacing: .08em; color: var(--text-muted); margin-bottom: .4rem; }
    .fin-value { font-size: var(--fs-xl); font-weight: 700; color: var(--text-strong); font-family: var(--font-display); }
    .fin-pending { font-size: var(--fs-xs); color: var(--text-muted); font-style: italic; }
    .fin-tx-wrap { margin-top: 1.25rem; }
    .fin-tx-title { font-size: var(--fs-eyebrow); font-weight: 700; text-transform: uppercase; letter-spacing: .08em; color: var(--text-muted); margin-bottom: .5rem; }
    .fin-tx-row { display: flex; justify-content: space-between; align-items: center; gap: 1rem; padding: .6rem .25rem; border-bottom: 1px solid var(--border, #e5e0d8); }
    .fin-tx-row:last-child { border-bottom: none; }
    .fin-tx-desc { font-size: var(--fs-sm); color: var(--text-strong); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
    .fin-tx-status { font-size: var(--fs-eyebrow); text-transform: uppercase; letter-spacing: .04em; color: #b4541f; font-weight: 700; }
    .fin-tx-meta { display: flex; align-items: baseline; gap: .75rem; flex-shrink: 0; }
    .fin-tx-amt { font-size: var(--fs-sm); font-weight: 700; color: var(--text-strong); }
    .fin-tx-when { font-size: var(--fs-xs); color: var(--text-muted); }

    /* ── Financial: worklist rebuild (FINANCIAL-SPEC.md §1) ──────────────────
       Financial is a WORKLIST, not a report. Two zones: full-width worklists on
       the left for anything actionable, a narrow rail on the right for short
       reference panels. Balance rows carry customer, service, visit date, age,
       amount, paid-so-far and two buttons — two even columns crowd all of that
       into wrapping, which is why the rail is ~1/3 and not 1/2. */
    #panel-financial main { max-width: 1200px; margin: 0 auto; padding: 1.5rem; }

    .fin-zones { display: grid; grid-template-columns: minmax(0, 2fr) minmax(0, 1fr);
      gap: 1.5rem; align-items: start; }
    /* minmax(0,…) not 1fr: `1fr` means minmax(auto,1fr), and a nowrap child
       (a money figure, a phone number) would silently push the track wider than
       its column instead of shrinking. */
    /* Two columns, held far longer than the usual breakpoint. Collapsing to one
       stacks Financial into a page too long to scan, which is the complaint this
       answers (Meesha, 2026-08-26) -- the rail's cards ended up hundreds of
       pixels below the worklists they are meant to sit beside. 560px is a phone
       in portrait, where two columns genuinely cannot work; every laptop, tablet
       and half-width desktop window now keeps both. */
    @media (max-width: 560px) { .fin-zones { grid-template-columns: minmax(0, 1fr); } }
    .fin-rail { display: flex; flex-direction: column; gap: 1rem; position: sticky; top: 1rem; }
    @media (max-width: 900px) { .fin-rail { position: static; } }

    /* Coverage header — full width, above both zones. Says what was HANDLED. */
    .fin-cov { display: flex; align-items: center; gap: .9rem; flex-wrap: wrap;
      background: var(--white); border: 1px solid var(--border);
      border-left: 4px solid var(--success); border-radius: var(--radius);
      padding: .9rem 1.1rem; box-shadow: var(--shadow-sm); margin-bottom: 1.25rem; }
    .fin-cov.is-stuck   { border-left-color: var(--danger); }
    .fin-cov.is-unknown { border-left-color: var(--text-muted); }
    .fin-cov-main { font-size: var(--fs-md); font-weight: 700; color: var(--text-strong); letter-spacing: -.01em; }
    .fin-cov-main b { font-variant-numeric: tabular-nums; }
    .fin-cov-sub { font-size: var(--fs-xs); color: var(--text-muted); line-height: 1.5; flex-basis: 100%; }
    .fin-cov-pills { display: flex; gap: .4rem; flex-wrap: wrap; margin-left: auto; }
    .fin-pill { font-size: var(--fs-xs); font-weight: 700; border-radius: 999px; padding: .15rem .6rem;
      background: var(--status-info-soft); color: var(--status-info-text); white-space: nowrap; }
    .fin-pill.warn { background: var(--warning-bg); color: var(--status-warning-text, #9a5b00); }
    .fin-pill.bad  { background: var(--danger-bg);  color: var(--status-danger-text); }

    .fin-sec { margin-bottom: 1.75rem; }
    .fin-sec:last-child { margin-bottom: 0; }
    .fin-sec-head { display: flex; align-items: baseline; gap: .6rem; margin-bottom: .2rem; }
    .fin-sec-head h2 { font-size: var(--fs-base); font-weight: 700; color: var(--text-strong); }
    .fin-sec-count { font-size: var(--fs-xs); font-weight: 700; border-radius: 999px; padding: .1rem .5rem;
      background: var(--status-info-soft); color: var(--status-info-text); }
    .fin-sec-sub { font-size: var(--fs-xs); color: var(--text-muted); line-height: 1.5;
      margin-bottom: .8rem; max-width: 74ch; }

    /* Needs Attention — hidden ENTIRELY when empty so it is never noise. */
    #fin-attention { background: var(--danger-bg); border: 1px solid rgba(192,57,43,.22);
      border-left: 4px solid var(--danger); border-radius: var(--radius); padding: 1rem 1.1rem 1.1rem; }
    #fin-attention .fin-sec-head h2 { color: var(--status-danger-text); }
    #fin-attention .fin-sec-count { background: rgba(255,255,255,.7); color: var(--status-danger-text); }
    #fin-attention .fin-sec-sub { color: var(--status-danger-text); opacity: .9; }

    /* Same shape as .quote-actions on the Estimates panel: content on top,
       ONE wrapping row of actions across the BOTTOM above a rule. A right-hand
       stack sizes to its own content and pushes the card taller than the text
       it describes -- the reason that panel moved, and the same reason here. */
    .fin-row { background: var(--white); border: 1px solid var(--border); border-radius: 8px;
      padding: .8rem .95rem; margin-bottom: .5rem; display: grid;
      grid-template-columns: 1fr auto; gap: .5rem .9rem; align-items: start; }
    .fin-row:last-child { margin-bottom: 0; }
    .fin-row-main { flex: 1; min-width: min(280px, 100%); }
    .fin-row-t { font-size: var(--fs-sm); font-weight: 700; color: var(--text-strong); line-height: 1.35; }
    .fin-row-d { font-size: var(--fs-xs); color: var(--text-muted); line-height: 1.55; margin-top: .2rem; }
    .fin-row-amt { text-align: right; white-space: nowrap; }
    .fin-row-amt b { display: block; font-size: var(--fs-md); font-weight: 800; color: var(--text-strong);
      font-variant-numeric: tabular-nums; }
    .fin-row-amt span { display: block; font-size: var(--fs-xs); color: var(--text-muted); margin-top: .1rem; }
    .fin-row-acts { grid-column: 1 / -1; display: flex; gap: .5rem; flex-wrap: wrap;
      align-items: center; border-top: 1px solid var(--border); padding-top: .75rem;
      margin-top: .15rem; }
    /* The way out is not a peer of the doing buttons. */
    .fin-row-acts .btn-quiet { margin-left: auto; }

    /* Internal notes on a job row (sql/556). The hint is not decoration: it is
       the entire reason this control is distinguishable from the old free-text
       box whose contents got texted to a customer. Keep it visible at rest --
       a warning that only appears once the box is open comes too late to
       change what someone types into it. */
    .fin-notes { grid-column: 1 / -1; margin-top: .5rem; border-top: 1px solid var(--border); padding-top: .5rem; }
    .fin-notes-toggle { display: inline-flex; align-items: baseline; gap: .45rem;
      background: none; border: 0; padding: 0; cursor: pointer; font: inherit;
      font-size: var(--fs-xs); color: var(--text-muted); }
    .fin-notes-toggle:hover .fin-notes-label { text-decoration: underline; }
    .fin-notes-label { font-weight: 600; color: var(--ocean); }
    .fin-notes-hint { font-size: var(--fs-xs); color: var(--text-muted); font-style: italic; }
    .fin-notes-body { margin-top: .45rem; }
    .fin-notes-ta { width: 100%; box-sizing: border-box; font: inherit; font-size: var(--fs-xs);
      padding: .45rem .55rem; border: 1px solid var(--border); border-radius: 6px; resize: vertical; }
    .fin-notes-acts { display: flex; align-items: center; gap: .5rem; margin-top: .35rem; }
    .fin-notes-status { font-size: var(--fs-xs); color: var(--text-muted); }
    .fin-note-list { display: flex; flex-direction: column; gap: .4rem; margin-bottom: .5rem; }
    .fin-note { border-left: 2px solid var(--border); padding-left: .5rem; }
    .fin-note-meta { font-size: var(--fs-xs); color: var(--text-muted); }
    .fin-note-body { font-size: var(--fs-xs); white-space: pre-wrap; }
    .fin-notes-empty { font-size: var(--fs-xs); color: var(--text-muted); font-style: italic; margin: 0 0 .5rem; }
    .fin-notes-foot { font-size: var(--fs-xs); color: var(--text-muted); margin: .35rem 0 0; }

    .fin-tag { display: inline-block; font-size: var(--fs-xs); font-weight: 700; padding: .1rem .45rem;
      border-radius: 5px; background: var(--status-info-soft); color: var(--status-info-text); vertical-align: 2px; }
    .fin-tag.warn { background: var(--warning-bg); color: var(--status-warning-text, #9a5b00); }
    .fin-tag.bad  { background: var(--danger-bg);  color: var(--status-danger-text); }
    .fin-tag.good { background: var(--success-bg); color: var(--status-success-text, #157a3a);
      border: 1px solid var(--status-success-border); }
    .fin-tag.mute { background: rgba(6,40,64,.06); color: var(--text-muted);
      border: 1px solid var(--border-control); }
    /* A capability (you can charge this customer), NOT money received. `good`
       stays for money that has actually landed -- the two were both green and
       said the same thing about two different facts. */
    .fin-tag.info { background: var(--status-info-soft); color: var(--status-info-text);
      border: 1px solid var(--status-info-border); }
    .fin-age { font-weight: 700; color: var(--status-warning-text, #9a5b00); }
    /* The quote the customer actually agreed to. A REFERENCE, so it lives in
       the context line -- never in .fin-row-acts, where every other control
       moves money and a misclick has a dollar value. Colour + weight mark it as
       interactive without competing with the row's primary button; the phone
       link beside it stays plain. `.fin-noquote` holds the same slot when there
       is no quote, so the eye always lands in one place. */
    .fin-quote { color: var(--cerulean); font-weight: 600; text-decoration: none;
      border-bottom: 1px solid rgba(11,79,108,.35); }
    .fin-quote:hover { border-bottom-color: var(--cerulean); }

    /* @button-component:start — every button rule lives between these
       markers. tools/check-buttons.py fails the build on any base button rule
       defined outside them. */
    /* ╔══════════════════════════════════════════════════════════════════════╗
       ║  BUTTONS — the one button component            (unified 2026-09-01)  ║
       ╚══════════════════════════════════════════════════════════════════════╝
       Everything a button looks like is decided in THIS block and nowhere else.

       Why the long selector lists. 17 of these class names are JS behaviour
       hooks -- `querySelectorAll('.btn-action')` disables a row, `.btn-ra`
       toggles during an approve, `.dash-charge-btn` carries a click handler.
       A purpose name here is doing double duty: style hook AND behaviour hook.
       Renaming them in the markup would break behaviour, not just appearance,
       so the legacy names stay and are listed as ALIASES of the canonical
       modifier instead. `.tk-btn` and `.btn.btn-sm` are now literally the same
       declarations, so markup can migrate to the canonical names later, one
       call site at a time, with zero visual change on either side of the swap.

       New code writes `btn` + a size + a role. Nothing else.

         size   .btn-xs  .btn-sm  (default)  .btn-lg
         role   .btn-primary (cyan)   .btn-commit (gold)   .btn-soft
                .btn-ghost   .btn-quiet   .btn-danger
         shape  .btn-pill        layout  .btn-block  .btn-flex

       On the two primaries. This app already had three fill colours calling
       themselves primary, and the split was not random: gold landed on New
       Job, the modal footer confirm, the appointment-panel confirm, Sign In
       and Save & clear flag -- every one of them THE BUTTON THAT FINISHES THE
       THING. Cyan landed on inline actions inside a panel. That rule was real
       and undocumented; `--commit` and `--primary` are it, written down.
       (Cerulean was on two buttons only and was drift; both are now cyan.)
       ────────────────────────────────────────────────────────────────────── */

    .btn,
    .tk-btn, .btn-fin, .btn-ra, .btn-task-done, .btn-action, .camp-btn,
    .sms-action-btn, .send-btn, .btn-signout, .btn-signin, .btn-relay,
    .dash-btn-sm, .dash-btn-primary, .dash-btn-secondary,
    .ap-btn, .pf-btn, .tb-new-job-btn, .camp-replied-btn,
    .dash-charge-btn, .dash-writeoff-btn,
    .settings-save-addr-btn,
    #msg-viewer .btn-mv-primary, #msg-viewer .btn-mv-ghost {
      display: inline-flex; align-items: center; justify-content: center; gap: .4rem;
      padding: .5rem 1rem;
      font-family: var(--font-sans); font-size: var(--fs-sm); font-weight: 600;
      border-radius: 8px; border: 1px solid var(--border-control);
      background: var(--white); color: var(--text-strong);
      cursor: pointer; white-space: nowrap;
      transition: filter .12s, border-color .12s, background .12s, color .12s;
    }
    .btn:hover,
    .tk-btn:hover, .btn-fin:hover, .btn-ra:hover, .btn-task-done:hover,
    .btn-action:hover, .camp-btn:hover, .sms-action-btn:hover, .send-btn:hover,
    .btn-signout:hover, .btn-relay:hover, .dash-btn-sm:hover,
    .ap-btn:hover, .camp-replied-btn:hover,
    .dash-charge-btn:hover:hover, .dash-writeoff-btn:hover,
    .settings-save-addr-btn:hover,
    #msg-viewer .btn-mv-ghost:hover {
      border-color: var(--ocean); color: var(--cerulean);
    }
    .btn:focus-visible,
    .tk-btn:focus-visible, .btn-fin:focus-visible, .btn-ra:focus-visible,
    .btn-action:focus-visible, .camp-btn:focus-visible, .ap-btn:focus-visible,
    .pf-btn:focus-visible, .btn-signin:focus-visible, .dash-btn-primary:focus-visible,
    .dash-btn-secondary:focus-visible {
      outline: 2px solid var(--cerulean); outline-offset: 2px;
    }
    .btn:disabled,
    .tk-btn:disabled, .btn-fin:disabled, .btn-ra:disabled, .btn-action:disabled,
    .camp-btn:disabled, .sms-action-btn:disabled, .send-btn:disabled,
    .ap-btn:disabled, .pf-btn:disabled, .btn-signin:disabled,
    .dash-btn-primary:disabled, .dash-btn-secondary:disabled {
      opacity: .5; cursor: not-allowed; pointer-events: none;
    }

    /* ── sizes ─────────────────────────────────────────────────────────── */
    .btn-xs, .pf-btn, .dash-btn-sm { padding: .22rem .6rem; font-size: var(--fs-xs); }
    .btn-sm,
    .tk-btn, .btn-fin, .btn-ra, .btn-task-done, .sms-action-btn, .send-btn,
    .btn-signout, .tb-new-job-btn, .camp-replied-btn,
    .dash-charge-btn, .dash-writeoff-btn,
    .settings-save-addr-btn {
      padding: .38rem .8rem; font-size: var(--fs-xs);
    }
    .btn-lg,
    .ap-btn, .dash-btn-primary, .dash-btn-secondary { padding: .7rem 1.25rem; }

    /* ── roles ─────────────────────────────────────────────────────────── */
    .btn-primary, #msg-viewer .btn-mv-primary,
    .btn-fin.primary, .tk-btn-primary, .camp-btn.primary, .dash-btn-sm.ocean,
    .sms-action-btn-primary, .pf-btn, .btn-quote-send, .btn-quote-charge {
      /* --tf-ocean-700, not --ocean. White on --ocean (#1a9fc4) measures
         3.09:1, below the 4.5 AA minimum for text; on --tf-ocean-700
         (#0f7a9a) it measures 4.91:1. The TOKEN is deliberately unchanged:
         --ocean is also a border, icon and accent colour where 3:1 is the
         right bar, so darkening it globally would be a far larger change to
         fix a problem that exists only where white text sits on the fill. */
      background: var(--tf-ocean-700); border-color: var(--tf-ocean-700); color: #fff;
    }
    .btn-primary:hover,
    .btn-fin.primary:hover, .tk-btn-primary:hover, .camp-btn.primary:hover,
    .dash-btn-sm.ocean:hover, .sms-action-btn-primary:hover, .pf-btn:hover,
    .btn-quote-send:hover, .btn-quote-charge:hover {
      filter: brightness(1.06); color: #fff; border-color: var(--ocean);
    }

    .btn-commit,
    .ap-btn-primary, .dash-btn-primary, .btn-signin, .btn-nr-save, .tb-new-job-btn {
      background: var(--saffron); border-color: var(--saffron); color: var(--white);
    }
    .btn-commit:hover,
    .ap-btn-primary:hover, .dash-btn-primary:hover, .btn-signin:hover,
    .btn-nr-save:hover, .tb-new-job-btn:hover {
      background: var(--saffron-dk); border-color: var(--saffron-dk); color: var(--white);
    }

    .btn-soft,
    .ap-btn-secondary, .ap-btn-mini {
      background: #eaf4f8; border-color: #b9d9e5; color: var(--cerulean);
    }
    .btn-soft:hover,
    .ap-btn-secondary:hover, .ap-btn-mini:hover {
      background: var(--aqua-soft); border-color: var(--ocean); color: var(--cerulean);
    }

    /* Tier 2 on the money panels: an outline, not a pale fill. #eaf4f8 on #fff is
       a 1.06:1 difference, so `.btn-soft` on a white card was the "white button on
       a white background" -- present, but you had to look for its edge. Shares the
       exact treatment the Estimates panel uses, because a secondary action should
       not change appearance depending on which tab you are standing in. */
    .btn-fin.btn-outline {
      background: #fff; border-color: var(--ocean); color: var(--tf-ocean-800);
      font-weight: 600;
    }
    .btn-fin.btn-outline:hover {
      background: var(--tf-aqua-050); border-color: var(--ocean); color: var(--cerulean);
    }
    .btn-fin.btn-tertiary:hover {
      background: var(--tf-aqua-050); border-color: var(--border-control-hover);
      color: var(--cerulean);
    }

    .btn-ghost, .tk-btn-ghost {
      background: transparent; border-color: transparent; color: var(--text-muted);
    }
    .btn-ghost:hover, .tk-btn-ghost:hover {
      background: var(--app-neutral-bg); border-color: transparent; color: var(--text);
    }

    /* The TERTIARY role: transparent fill, muted text. On --border at 10% it was
       effectively borderless on a white panel -- Back, Dismiss, Cancel and Sign
       out all read as floating text rather than buttons. It keeps the quiet
       treatment (no fill, muted label); it just has an edge you can see now.
       `.btn-ghost` is deliberately left alone: that role is border-color
       transparent ON PURPOSE and is the one true borderless button. */
    .btn-quiet,
    .btn-task-done, .btn-signout, .btn-action.btn-nr-cancel, .pf-btn.pf-quiet {
      background: transparent; border-color: var(--border-control); color: var(--text-muted);
    }
    .btn-quiet:hover,
    .btn-task-done:hover, .btn-signout:hover, .btn-action.btn-nr-cancel:hover,
    .pf-btn.pf-quiet:hover {
      background: var(--aqua-soft); border-color: var(--ocean); color: var(--cerulean);
    }

    .btn-danger, .btn-fin.danger {
      background: transparent; border-color: rgba(192,57,43,.4); color: var(--status-danger-text);
    }
    .btn-danger:hover, .btn-fin.danger:hover {
      background: var(--danger-bg); border-color: var(--status-danger-text);
      color: var(--status-danger-text);
    }
    .btn-danger-solid, .ap-btn-danger {
      background: var(--status-danger-text); border-color: var(--status-danger-text);
      color: var(--white);
    }
    .btn-danger-solid:hover, .ap-btn-danger:hover {
      filter: brightness(1.06); color: var(--white);
    }

    /* ── shape + layout ────────────────────────────────────────────────── */
    .btn-pill, .pf-btn { border-radius: 999px; }
    .btn-block, .btn-signin { width: 100%; }
    .btn-flex, .dash-btn-primary, .dash-btn-secondary { flex: 1; }

    /* ── local trivia: the few genuinely one-off declarations ──────────── */
    /* Login is its own visual surface -- a 17px label on a full-width button.
       Everything else about it (gold, radius, font) comes from the component. */
    .btn-signin { font-size: 17px; line-height: 1; padding: .69rem 1.25rem; }
    .btn-signin:active { transform: scale(0.97); }
    /* Narrow flex:1 pick control -- the only button allowed to wrap. */
    /* Inline control sitting in a run of text. Smaller than its row, but still a 44px target. */
    .ap-btn-mini { padding: .25rem .9rem; font-size: .9375rem; min-height: 44px; }
    /* Filter pills read as a set, so they carry the heavier weight. */
    .pf-btn { font-weight: 700; }
    /* Escalation control: outlined in deep cerulean, never a filled action. */
    .btn-relay { background: transparent; border-color: var(--cerulean); color: var(--cerulean); }
    .btn-relay:hover { background: var(--aqua-soft); border-color: var(--cerulean); color: var(--cerulean); }
    /* Review-approve keeps its own semantic green / red, not the danger role. */
    .btn-ra-approve { color: #1a7a4c; border-color: #bfe3cf; background: transparent; }
    .btn-ra-approve:hover { background: #eaf7f0; border-color: #1a7a4c; color: #1a7a4c; }
    .btn-ra-skip { color: #9a3b3b; border-color: #f0c9c9; background: transparent; }
    .btn-ra-skip:hover { background: #fbeaea; border-color: #9a3b3b; color: #9a3b3b; }
    /* Icon-bearing toolbar button. */
    .tb-new-job-btn svg, .btn-relay svg { width: var(--icon-md); height: var(--icon-md); }
    /* The customer record (#money-drawer.cr-mode) is used from an iPhone: every action button in it is a
       44px target at a readable size, whichever component drew it. Scoped to the mode so the money and
       KPI drawers, which share this element, are untouched. */
    #money-drawer.cr-mode .btn, #money-drawer.cr-mode .btn-sm { min-height: 44px; font-size: .9375rem; }
    /* @button-component:end */

    /* @tf-component:start — queue, status and record components.
       ╔══════════════════════════════════════════════════════════════════════╗
       ║  IA COMPONENTS                    (INFORMATION-ARCHITECTURE-SPEC.md) ║
       ╚══════════════════════════════════════════════════════════════════════╝
       Introduced 2026-09-16 with the verb/noun split. These live HERE and not in
       brand/components.css on purpose: that file is the CUSTOMER-DOCUMENT layer
       (inspection reports, proposals) and speaks the brand token vocabulary,
       while these are internal-tool components. Same reasoning that put the
       boundary there in the first place.

       Approved design: prototypes/queue-and-financial-v2.html (Meesha, 2026-09-16).
       ────────────────────────────────────────────────────────────────────── */

    /* ── tf-status: THE status pill. One colour per state, and the SAME colour
       on every screen. A thing does not change colour because you opened a
       different tab. See tokens.css "COLOUR LAW" for which role means what.
       GUARDRAIL: only ONE status may ever be blue. "Needs an action" owns it. */
    .tf-status {
      display: inline-flex; align-items: center; gap: .3rem;
      font-size: var(--fs-eyebrow); font-weight: 700; letter-spacing: .04em;
      text-transform: uppercase; padding: 2px 8px; white-space: nowrap;
      border-radius: var(--radius-pill); border: 1px solid transparent;
    }
    .tf-status::before { content: ""; width: 6px; height: 6px; border-radius: 50%; background: currentColor; }
    .tf-status-ok    { background: var(--status-success-soft); color: var(--status-success-text); border-color: var(--status-success-border); }
    .tf-status-act   { background: var(--status-info-soft);    color: var(--status-info-text);    border-color: var(--status-info-border); }
    .tf-status-wait  { background: var(--app-neutral-bg);      color: var(--app-text-neutral);    border-color: var(--app-neutral-border); }
    .tf-status-warn  { background: var(--status-warning-soft); color: var(--status-warning-text); border-color: var(--status-warning-border); }
    .tf-status-alert { background: var(--status-danger-soft);  color: var(--status-danger-text);  border-color: var(--status-danger-border); }

    /* ── tf-queue-card: ONE ROW PER KIND of work, never per task. On 2026-09-16
       there were 49 open office tasks and 42 of them were one type; a flat list
       buries seven kinds of work under one. The group is the unit of scanning,
       the item is the unit of working. */
    .tf-queue-list { display: flex; flex-direction: column; }
    .tf-queue-card {
      display: grid; grid-template-columns: 34px minmax(0,1fr) auto auto;
      align-items: center; column-gap: .75rem; width: 100%; text-align: left;
      padding: .8rem 1rem; border: 0; border-bottom: 1px solid var(--border);
      background: var(--white); cursor: pointer; font: inherit; color: inherit;
      transition: background .12s;
    }
    .tf-queue-list .tf-queue-card:last-child { border-bottom: 0; }
    .tf-queue-card:hover { background: var(--aqua-soft); }
    .tf-queue-card:focus-visible { outline: 2px solid var(--cerulean); outline-offset: -2px; }
    .tf-queue-ico {
      width: 32px; height: 32px; border-radius: 6px; display: flex;
      align-items: center; justify-content: center; font-weight: 700;
      font-size: var(--fs-sm); background: var(--aqua-soft); color: var(--ocean);
    }
    .tf-queue-card[data-u="high"] .tf-queue-ico { background: var(--red-light); color: var(--status-danger-text); }
    .tf-queue-card[data-u="med"]  .tf-queue-ico { background: var(--status-warning-soft); color: var(--status-warning-text); }
    .tf-queue-txt { min-width: 0; }
    .tf-queue-label {
      font-size: var(--fs-sm); font-weight: 600; color: var(--text-strong);
      line-height: 1.3; display: flex; align-items: center; gap: .4rem; flex-wrap: wrap;
    }
    .tf-queue-sub { font-size: var(--fs-xs); color: var(--text-muted); margin-top: 2px; line-height: 1.4; }
    .tf-queue-n {
      font-size: var(--fs-lg); font-weight: 700; line-height: 1;
      color: var(--text-strong); font-variant-numeric: tabular-nums;
    }
    .tf-queue-card[data-u="high"] .tf-queue-n { color: var(--status-danger-text); }
    .tf-queue-chev { color: var(--muted); font-size: var(--fs-md); line-height: 1; }
    .tf-queue-act { display: none; }

    /* ── tf-record: a collapsible record table. <details> deliberately, so it
       works with no JS and an AI agent can read the contents whether or not it
       is open (root CLAUDE.md: agent crawlability is a global constraint). */
    .tf-record {
      background: var(--white); border: 1px solid var(--border);
      border-radius: 14px; box-shadow: var(--shadow-sm);
      margin-bottom: 1rem; overflow: hidden;
    }
    .tf-record > summary {
      list-style: none; cursor: pointer; padding: .875rem 1.25rem;
      display: flex; align-items: center; gap: .6rem;
      font-size: var(--fs-xs); font-weight: 600; color: var(--text-strong);
      text-transform: uppercase; letter-spacing: .06em;
    }
    .tf-record > summary::-webkit-details-marker { display: none; }
    .tf-record > summary::after { content: "\25BE"; margin-left: auto; color: var(--muted); font-size: var(--fs-sm); transition: transform .15s; }
    .tf-record:not([open]) > summary::after { transform: rotate(-90deg); }
    .tf-record > summary .meta { font-weight: 400; text-transform: none; letter-spacing: 0; color: var(--text-muted); font-size: var(--fs-xs); }
    .tf-record-body { border-top: 1px solid var(--border); }
    .tf-tbl { width: 100%; border-collapse: collapse; }
    .tf-tbl th {
      position: sticky; top: 0; text-align: left; font-size: var(--fs-eyebrow);
      letter-spacing: .05em; text-transform: uppercase; color: var(--text-muted);
      font-weight: 700; padding: .6rem 1.25rem; background: var(--tf-ink-050);
      border-bottom: 1px solid var(--border);
    }
    .tf-tbl td { padding: .6rem 1.25rem; border-bottom: 1px solid var(--border); font-size: var(--fs-sm); }
    .tf-tbl tbody tr:last-child td { border-bottom: 0; }
    .tf-tbl tbody tr:hover { background: var(--aqua-soft); }
    .tf-tbl .num { text-align: right; font-variant-numeric: tabular-nums; white-space: nowrap; font-weight: 600; }
    .tf-tbl .who { font-weight: 600; color: var(--text-strong); }
    .tf-scrollbox { max-height: 320px; overflow-y: auto; }

    /* ── THE KPI BOARD (28-kpi-board.js) ───────────────────────────────────
       Colour means PACE and nothing else, on four steps plus stone for a
       metric with no target. The number itself carries the verdict and the
       card sits on that verdict's tint; the palette is the Command Center's,
       with every -text member derived to clear 4.5:1 on its own tint AND on
       white (see admin/prototypes/kpi-color-system.html for the figures). */
    /* The --kpi-* palette is CANONICAL in brand/tokens.css (loaded before
       this file) and is defined there, not here. */
    .kpi-bar-top { display: flex; gap: 6px; justify-content: flex-end; margin-bottom: 4px; }

    details.kpi-stage { margin-top: 14px; }
    details.kpi-stage > summary { list-style: none; cursor: pointer; display: flex;
      align-items: center; gap: 9px; padding: 5px 0; border-bottom: 1px solid var(--border); }
    details.kpi-stage > summary::-webkit-details-marker { display: none; }
    .kpi-caret { width: 8px; height: 8px; border-right: 2px solid var(--kpi-stone);
      border-bottom: 2px solid var(--kpi-stone); transform: rotate(-45deg);
      transition: transform .15s; flex: none; }
    details.kpi-stage[open] > summary .kpi-caret { transform: rotate(45deg); }
    details.kpi-stage > summary h4 { font-size: 11px; text-transform: uppercase;
      letter-spacing: 1.2px; margin: 0; font-weight: 700; color: var(--text); }
    .kpi-hint { font-size: var(--fs-xs); color: var(--muted); }
    /* One dot per card. This is what makes collapsing safe: shut every row and
       you can still see which stage is red. */
    .kpi-dots { display: flex; gap: 3px; margin-left: auto; }
    .kpi-dots i { width: 7px; height: 7px; border-radius: 50%; display: block; }

    .kpi-row { display: grid; gap: 10px; padding-top: 10px;
      grid-template-columns: repeat(auto-fill, minmax(196px, 1fr)); }
    .kpi-c { border: 1px solid var(--kc-bd, var(--border)); border-radius: 9px;
      background: var(--kc-soft, var(--white)); padding: 11px 12px 10px; cursor: pointer; }
    .kpi-c:hover { box-shadow: var(--shadow-sm); }
    .kpi-c:focus-visible { outline: 2px solid var(--kpi-onpace); outline-offset: 2px; }
    .kpi-c-t { font-size: 10px; text-transform: uppercase; letter-spacing: .9px;
      font-weight: 600; color: var(--muted); white-space: nowrap; overflow: hidden;
      text-overflow: ellipsis; }
    .kpi-c-v { font-family: var(--font-display); font-size: 1.8rem; font-weight: 800;
      line-height: 1; letter-spacing: -.9px; margin: 6px 0 0; color: var(--kc-txt, var(--text)); }
    .kpi-c-v u { font-size: .85rem; font-weight: 700; letter-spacing: -.2px;
      text-decoration: none; color: var(--muted); }
    .kpi-tgt { cursor: pointer; border-bottom: 1px dashed var(--kpi-stone); }
    .kpi-tgt:hover { color: var(--text); border-bottom-style: solid; }
    .kpi-c-bar { position: relative; height: 4px; border-radius: 999px;
      background: var(--kpi-track); margin-top: 8px; }
    .kpi-c-bar i { display: block; height: 100%; border-radius: 999px; background: var(--kc-bar); }
    .kpi-c-bar u { position: absolute; top: -3px; width: 2px; height: 10px;
      background: var(--text); opacity: .34; border-radius: 1px; }
    /* One row, clamped in CSS -- a cap that relies on the writer fails on the
       first long customer name. */
    .kpi-c-s { margin-top: 7px; font-size: 11.5px; line-height: 1.35; color: var(--muted);
      white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }

    .kpi-c.s-ahead  { --kc-soft: var(--kpi-ahead-soft);  --kc-txt: var(--kpi-ahead-text);
                      --kc-bar: var(--kpi-ahead);  --kc-bd: #d5e6d7; }
    .kpi-c.s-onpace { --kc-soft: var(--kpi-onpace-soft); --kc-txt: var(--kpi-onpace-text);
                      --kc-bar: var(--kpi-onpace); --kc-bd: #c8e0ea; }
    .kpi-c.s-watch  { --kc-soft: var(--kpi-watch-soft);  --kc-txt: var(--kpi-watch-text);
                      --kc-bar: var(--kpi-watch);  --kc-bd: #ecdcb6; }
    .kpi-c.s-behind { --kc-soft: var(--kpi-behind-soft); --kc-txt: var(--kpi-behind-text);
                      --kc-bar: var(--kpi-behind); --kc-bd: #ecd2ce; }
    .kpi-c.s-none   { --kc-soft: var(--white); --kc-txt: var(--text);
                      --kc-bar: var(--kpi-stone); --kc-bd: var(--border); }
    .kpi-c.s-unknown{ --kc-soft: transparent; --kc-txt: var(--kpi-stone-text);
                      --kc-bar: var(--kpi-stone); --kc-bd: var(--kpi-stone);
                      border-style: dashed; }
    .kpi-c.s-unknown .kpi-c-v { font-size: 1rem; letter-spacing: -.2px; }

    .kpi-dir { font-size: 9px; font-weight: 700; text-transform: uppercase;
      letter-spacing: .5px; padding: 2px 6px; border-radius: 999px;
      background: var(--kpi-stone-soft); color: var(--kpi-stone-text); white-space: nowrap; }
    .kpi-dim { color: var(--muted); font-size: 11px; line-height: 1.4; }
    .kpi-tin { width: 90px; font: inherit; font-size: var(--fs-xs); padding: 3px 6px;
      text-align: right; border: 1px solid var(--border-control); border-radius: 5px;
      background: var(--white); }
    .kpi-tin.ok  { border-color: var(--kpi-ahead); }
    .kpi-tin.bad { border-color: var(--kpi-behind); }
    .tf-tbl tr.hl td { background: var(--kpi-watch-soft); }

    /* ── KPI DRILL-DOWN (27-kpi-detail.js) ─────────────────────────────────
       Reuses .tf-tbl / .tf-scrollbox / .state-msg rather than inventing a
       second table. Only the three section wrappers and the trend bars are
       new, and every custom property named here is one tokens.css actually
       defines -- a var() with a plausible fallback renders fine and is how a
       fourth almost-right grey gets into a stylesheet that has three. */
    .kpi-head { padding: 0 1.25rem 1rem; }
    .kpi-head-v { font-size: 2rem; font-weight: 700; color: var(--navy); }
    .kpi-head-v small { font-size: var(--fs-sm); font-weight: 600; color: var(--muted); }
    .kpi-head-why { display: block; margin-top: .35rem; font-size: var(--fs-sm); color: var(--muted); }
    .kpi-sec { padding: 1rem 1.25rem; border-top: 1px solid var(--border); }
    .kpi-sec > h4 { margin: 0 0 .6rem; font-size: var(--fs-sm); text-transform: uppercase;
      letter-spacing: .06em; color: var(--muted); display: flex; justify-content: space-between;
      align-items: center; gap: 1rem; }
    .kpi-plain { margin: 0 0 .6rem; font-size: var(--fs-sm); line-height: 1.5; color: var(--text); }
    .kpi-warn { margin: .6rem 0 0; font-size: var(--fs-sm); line-height: 1.5; color: var(--text);
      background: var(--warning-bg); border-left: 3px solid var(--warning);
      padding: .55rem .75rem; border-radius: var(--radius); }
    .kpi-tech { margin-top: .5rem; font-size: var(--fs-sm); }
    .kpi-tech summary { cursor: pointer; color: var(--ocean); font-weight: 600; }
    .kpi-tech p { margin: .5rem 0 0; color: var(--muted); line-height: 1.5; }
    .kpi-tables code { background: var(--sand); padding: 1px 5px; border-radius: 3px;
      font-size: var(--fs-xs); }
    .kpi-toggle { display: flex; gap: .25rem; }
    .kpi-toggle button { border: 1px solid var(--border-control); background: var(--white);
      color: var(--muted); font: inherit; font-size: var(--fs-xs); text-transform: none;
      letter-spacing: 0; padding: .2rem .6rem; border-radius: var(--radius); cursor: pointer; }
    .kpi-toggle button.on { background: var(--ocean); border-color: var(--ocean); color: var(--white); }
    .kpi-trend-row { display: grid; grid-template-columns: 4.5rem 1fr 6rem; align-items: center;
      gap: .6rem; padding: .18rem 0; }
    .kpi-trend-m { font-size: var(--fs-xs); color: var(--muted); }
    /* stretch, not end: an item sized to its own content overflows its track
       the moment the text is wider than the column. */
    .kpi-trend-v { justify-self: stretch; text-align: right; font-size: var(--fs-sm);
      font-weight: 600; font-variant-numeric: tabular-nums; }
    .kpi-trend-bar { background: var(--sand); border-radius: 3px; height: 14px; overflow: hidden; }
    .kpi-trend-bar i { display: block; height: 100%; background: var(--ocean); border-radius: 3px; }
    .kpi-trend-bar i.neg { background: var(--danger); }
    .kpi-blind { margin: 0; padding-left: 1.1rem; font-size: var(--fs-sm); color: var(--muted);
      line-height: 1.6; }
    .tf-tbl tr.kpi-total td { font-weight: 700; color: var(--navy); background: var(--aqua-soft); }


    /* ── tf-sumline: the "count that links out". IA spec rule 2 — a fact lives
       in FULL in one place and as a COUNT in at most one other. */
    .tf-sumline {
      display: flex; align-items: center; gap: .75rem; width: 100%; text-align: left;
      background: var(--white); border: 1px solid var(--border); border-radius: 14px;
      box-shadow: var(--shadow-sm); padding: .875rem 1.25rem;
      font: inherit; color: inherit; cursor: pointer; margin-bottom: .6rem;
    }
    .tf-sumline:hover { border-color: var(--ocean); background: var(--aqua-soft); }
    .tf-sumline .lead { font-weight: 700; color: var(--text-strong); font-size: var(--fs-sm); }
    .tf-sumline .rest { color: var(--text-muted); font-size: var(--fs-xs); }
    .tf-sumline .go {
      margin-left: auto; font-size: var(--fs-eyebrow); font-weight: 600;
      color: var(--ocean); text-transform: uppercase; letter-spacing: .05em; white-space: nowrap;
    }

    /* ── tf-cov: "checked N of M". A dead feed and a genuinely clear day render
       identically without this. Never state "0 failed" — a zero-failure count
       and a system that ran zero times are indistinguishable. */
    .tf-cov { display: inline-flex; align-items: center; gap: .4rem; font-size: var(--fs-xs); color: var(--text-muted); }
    .tf-cov .dot { width: 7px; height: 7px; border-radius: 50%; background: var(--status-success); }

    /* Phone: cards stack their actions, tables become cards. */
    @media (max-width: 767px) {
      .tf-queue-card {
        grid-template-columns: 32px minmax(0,1fr) auto;
        grid-template-areas: "ico txt n" "act act act";
        padding: .8rem .9rem;
      }
      .tf-queue-ico { grid-area: ico; } .tf-queue-txt { grid-area: txt; } .tf-queue-n { grid-area: n; }
      .tf-queue-chev { display: none; }
      .tf-queue-act {
        grid-area: act; display: flex; gap: .4rem; margin-top: .7rem;
        padding-top: .6rem; border-top: 1px solid var(--border);
      }
      .tf-queue-act .btn { flex: 1; min-height: var(--tap-min); }
      .tf-queue-sub { white-space: normal; }
      .tf-tbl, .tf-tbl tbody, .tf-tbl tr, .tf-tbl td { display: block; width: 100%; }
      .tf-tbl thead { display: none; }
      .tf-tbl tr { padding: .75rem 1rem; border-bottom: 1px solid var(--border); }
      .tf-tbl td { border: 0; padding: 2px 0; display: flex; justify-content: space-between; gap: 1rem; }
      .tf-tbl td::before { content: attr(data-l); color: var(--text-muted); font-size: var(--fs-xs); }
      .tf-tbl td.who::before { display: none; }
      .tf-scrollbox { max-height: none; }
    }
    /* ── tf-deck: a literal DECK. ONE card on top — the single most important
       thing you could be doing right now — with the next ones stacked behind it.
       Meesha 2026-09-16: "I want the cards stacked like a deck of cards."
       The card is SELF-SUFFICIENT: you act on it here, no drawer, because a
       drawer is what made the old list a list. The drawer survives only for a
       card that needs a real editor (a field estimate). */
    .tf-deck{position:relative;padding:0 1rem 1rem}
    /* The TOP card is in flow and therefore SIZES the stack; the two behind are
       absolutely pinned to its exact box. Previously the stack had a fixed 330px
       min-height while card content varies wildly (a reply card carries a
       textarea), so a taller card behind protruded past the shorter one in front
       and tall cards collided with the action row underneath. Sizing from the live
       card makes both impossible rather than unlikely. */
    .tf-deck-stack{position:relative}
    .tf-deck-card[data-behind]{position:absolute;inset:0;overflow:hidden;
      transition:transform .22s var(--ease-standard),opacity .22s}
    .tf-deck-card[data-behind="1"]{transform:translateY(12px) scale(.97);opacity:.5;box-shadow:var(--shadow-sm)}
    .tf-deck-card[data-behind="2"]{transform:translateY(24px) scale(.94);opacity:.28;box-shadow:none}
    .tf-deck-card:not([data-behind]){position:relative;z-index:2}
    /* The tile is tinted by the type's TONE, the same four tints the Tasks panel
       uses (.tk-ico). Every tile being one blue was the earlier call and it was
       wrong twice over: it threw away information the registry already carries,
       and it clashed with the tinted icons sitting beside it in SMS Inbox. */
    .tf-deck-ico{width:30px;height:30px;border-radius:8px;display:grid;place-items:center;
      background:var(--app-neutral-bg);color:var(--app-text-neutral);flex:0 0 30px}
    .tf-deck-ico svg{width:16px;height:16px;stroke-width:2;fill:none}
    .tf-deck-ico.danger{background:var(--status-danger-soft); color:var(--status-danger-text)}
    .tf-deck-ico.warn  {background:var(--status-warning-soft);color:var(--status-warning-text)}
    .tf-deck-ico.ok    {background:var(--status-success-soft);color:var(--status-success-text)}
    .tf-deck-ico.info  {background:var(--status-info-soft);   color:var(--status-info-text)}
    /* Age is quiet text beside the label, not a second pill. One chip per card
       at most; the tile already carries urgency. */
    /* ONE tile colour for every kind. The icon says WHAT the work is; urgency is
       carried by the age pill, and a second urgency channel here would duplicate
       and contradict it. That is how "field estimates" ended up red for no reason. */
    .tf-deck-ico svg{width:16px;height:16px}
    .tf-deck-who{font-family:var(--font-display);font-size:var(--fs-xl);color:var(--cerulean);line-height:1.2}
    .tf-deck-what{font-size:var(--fs-sm);color:var(--text-muted);margin-top:.2rem}
    .tf-deck-why{font-size:var(--fs-sm);color:var(--text);margin-top:.7rem;line-height:1.5}
    .tf-deck-edit{width:100%;min-height:96px;font:inherit;font-size:var(--fs-sm);line-height:1.5;
      padding:.7rem;border:1px solid var(--border-control);border-radius:10px;
      background:var(--white);color:var(--text);resize:vertical;margin-top:.7rem;display:block}
    .tf-deck-edit:focus{outline:2px solid var(--ocean);outline-offset:-1px;border-color:var(--ocean)}
    /* Actions sit BELOW the stack, never over it. Primary first, then a secondary
       only where there is a genuine either/or; "Do all N" is a quiet third. */
    .tf-deck-acts{display:flex;gap:.5rem;flex-wrap:wrap;margin-top:1.1rem}
    .tf-deck-nav{display:flex;align-items:center;gap:.6rem;margin-bottom:.7rem;
      font-size:var(--fs-xs);color:var(--text-muted)}
    .tf-deck-nav .bar{flex:1;height:4px;border-radius:2px;background:var(--border);overflow:hidden}
    .tf-deck-nav .bar i{display:block;height:100%;background:var(--ocean);transition:width .22s}
    /* The nav row must not move between cards. Three things were shifting it:
       the position text changing width ("1 of 66" vs "27 of 66"), the bar
       reflowing to absorb that, and "Skip all N" appearing only for some cards
       -- so the Next button sat somewhere different on almost every card and
       you had to re-aim to press the same thing twice. Now: fixed-width
       position, fixed-width skip slot that stays reserved when empty, and the
       arrows pinned to the ends. */
    .tf-deck-pos{white-space:nowrap;font-variant-numeric:tabular-nums;
      min-width:6.5ch;text-align:center;flex:0 0 auto}
    .tf-deck-skipall{white-space:nowrap;flex:0 0 auto}
    .tf-deck-skipslot{flex:0 0 9rem;display:flex;justify-content:flex-end}
    .tf-deck-nav > .btn{flex:0 0 auto}
    .tf-deck-skipall{white-space:nowrap}
    /* Preview, not the whole story: two lines then an expando, so the buttons stay
       above the fold instead of sitting under a wall of text. */
    .tf-deck-note{margin:.6rem 0 0;font-size:var(--fs-sm);line-height:1.5;color:var(--text-muted)}
    /* A card with no resolvable customer leads with its detail, so that text
       carries the weight the name would have. */
    .tf-deck-note-lead{margin-top:.5rem;font-size:var(--fs-md);color:var(--text-strong);-webkit-line-clamp:3}
    .tf-deck-note[data-clamp]{display:-webkit-box;-webkit-line-clamp:2;-webkit-box-orient:vertical;overflow:hidden}
    /* "Why am I seeing this?" -- the type-level hint, ported from the Tasks
       panel 2026-09-24 so retiring that tab does not take the only copy that
       explains a task type with it. Same shape as .tf-deck-more below: a text
       disclosure, not a button, which is also why the name carries no "btn"
       (check-buttons.py matches any class containing it). */
    .tf-deck-why{background:none;border:0;padding:.4rem 0 0;font:inherit;font-size:var(--fs-xs);
      font-weight:600;color:var(--status-info-text);text-decoration:underline;cursor:pointer}
    .tf-deck-why.is-open{text-decoration:none}
    .tf-deck-why-text{margin-top:.4rem;padding:.5rem .6rem;border-radius:8px;
      background:var(--status-info-soft);border-left:3px solid var(--status-info-text);
      font-size:var(--fs-xs);line-height:1.5;color:var(--text-muted)}
    .tf-deck-more{background:none;border:0;padding:.25rem 0 0;font:inherit;font-size:var(--fs-xs);
      font-weight:600;color:var(--ocean);cursor:pointer}
    .tf-deck-more:hover{text-decoration:underline}
    /* Both disclosures are bare inline-block buttons with no margin, so when a
       card renders BOTH they sit flush against each other and read as one
       run-on link ("LessWhy am I seeing this?"). Neither rule was wrong alone;
       the gap only exists between them, which is why it appeared the first time
       a type declared a hint AND a description long enough to truncate. */
    .tf-deck-more + .tf-deck-why{margin-left:.85rem}
    /* A grouped card lists its instances as chips: one problem, many dates. */
    .tf-deck-dates{display:flex;flex-wrap:wrap;gap:.3rem;margin-top:.6rem}
    .tf-deck-done{text-align:center;padding:2.5rem 1rem;color:var(--text-muted)}
    .tf-deck-done h3{font-family:var(--font-display);font-weight:400;font-size:var(--fs-xl);
      color:var(--cerulean);margin:0 0 .4rem}

    /* Phone: one peek card only, and every control goes full width so nothing
       wraps into a ragged row. */
    @media (max-width:767px){
      .tf-deck{padding:0 0 1rem}
      .tf-deck-card[data-behind="2"]{display:none}
      .tf-deck-acts{flex-direction:column;align-items:stretch}
      .tf-deck-acts .btn{width:100%}
      /* Every verb on this card is a thing you press with a thumb. .btn-sm is
         30px, which is below the tap minimum the rest of the app holds to --
         measured on a 375px viewport. The deck's buttons were never brought up
         to it because the phone rule only ever set their WIDTH. */
      .tf-deck-acts .btn{min-height:var(--tap-min)}
      .tf-deck-who{font-size:var(--fs-lg)}
      .m-qamt{font-size:var(--fs-xl)}

      /* THE PARTS WIZARD, by thumb. Measured on a 375px viewport: every
         control in it came out 25-28px tall -- the date inputs worst of all,
         and this is a flow that has to be finishable on a phone. Scoped to
         .pf-step rather than to the deck, so the Financial rail's copy of the
         same control gets the same treatment; there is one implementation of
         "who is ordering" and there should be one answer to how big it is.
         The date and mode fields go full width because a date input with a
         native picker behind it is a poor thing to aim at when it is 132px in
         a row with something else. */
      .pf-step{gap:.5rem}
      .pf-step .pf-btn,
      .pf-step .pf-date,
      .pf-step .pf-mode,
      .pf-step .pf-day{min-height:var(--tap-min)}
      .pf-step .pf-date,
      .pf-step .pf-mode,
      .pf-step .pf-days,
      .pf-step .pf-save{flex:1 1 100%;width:100%}
      .pf-days .pf-day{flex:1 1 8rem}
      .pf-q1,.pf-q2,.pf-q3{gap:.5rem}
      .pf-q1 .pf-btn,.pf-q2 .pf-btn{flex:1 1 auto}

      /* THE NAV ROW, on a phone. On desktop the "Skip all N" slot is a fixed
         9rem that stays reserved when empty, so the arrows never move between
         cards. At 375px that same 9rem is 144px of a 293px row: measured, it
         pushed its own right edge to 409px against a panel ending at 351, and
         squeezed the progress bar to ZERO width. Reserving space to stop a
         row shifting is worth it on a mouse surface and nonsense on a phone,
         so here the slot drops to its own line and gives the width back. */
      .tf-deck-nav{flex-wrap:wrap;gap:.4rem}
      /* The progress bar goes. Four 44px targets, the position text and the
         bar do not fit 293px, and what wrapped was the LAST jump button --
         stranded alone on a second row. The bar and "1 of 9" say the same
         thing; the text is the one that still reads at this size. */
      .tf-deck-nav .bar{display:none}
      .tf-deck-skipslot{flex:1 0 100%;order:9;justify-content:stretch;margin-top:.5rem}
      .tf-deck-skipslot:empty{display:none}   /* no reserved blank row */
      .tf-deck-skipall{width:100%;min-height:var(--tap-min)}
      /* The four jumps are the primary way through the deck on a phone --
         there is no keyboard here, so J/K and the arrow keys do not exist. */
      .tf-deck-nav > .btn{min-height:var(--tap-min);min-width:var(--tap-min)}
    }

    /* Cards / Table switch on the queue panel. Matches the segmented control
       the app already uses elsewhere (.quote-filter-tab family) rather than
       introducing a new switch shape. */
    .queue-viewtog {
      margin-left: auto; display: inline-flex; overflow: hidden;
      border: 1px solid var(--border-control); border-radius: var(--radius-pill);
      background: var(--white);
    }
    .queue-viewtog button {
      border: 0; background: transparent; font: inherit; cursor: pointer;
      font-size: var(--fs-eyebrow); font-weight: 700; letter-spacing: .05em;
      text-transform: uppercase; padding: 5px 12px; color: var(--text-muted);
    }
    .queue-viewtog button[aria-pressed="true"] { background: var(--cerulean); color: #fff; }

    /* Meesha 2026-09-16: the queue must NOT own the page. Two thirds / one
       third, so Inbox and Reviews stay visible beside it; Schedule keeps its
       full width on its own row below. */
    /* stretch, not start: the two columns stand level. The QUEUE decides the
       height -- it is the work, and it should not grow to match a message list.
       The messages panel is taken out of the height calculation entirely
       (absolute inside a relative column) so it cannot push the row taller,
       then scrolls inside whatever the queue leaves it. */
    .dash-grid { grid-template-columns: 2fr 1fr; align-items: stretch; }
    .dash-grid > .dash-panel-col { position: relative; }
    .dash-grid > .dash-panel-col > .dash-panel-stretch {
      position: absolute; inset: 0; display: flex; flex-direction: column;
    }
    @media (max-width: 1100px) {
      .dash-grid { grid-template-columns: 1fr; align-items: start; }
      /* Single column: nothing to stand level WITH, and an absolute panel in a
         stacked layout would collapse to nothing. */
      .dash-grid > .dash-panel-col { position: static; }
      .dash-grid > .dash-panel-col > .dash-panel-stretch {
        position: static; max-height: 32rem;
      }
    }

    /* ── Sidebar: narrower by default, collapsible to icons ──────────────
       Meesha 2026-09-16. 228px -> 200px expanded, 62px collapsed. The labels
       are hidden with a width/opacity transition rather than display:none so
       the rail animates instead of snapping. */
    .sidebar { width: 200px; transition: width .16s var(--ease-standard); }
    /* `display: block` here is DEAD and kept only to be honest about it: the
       earlier `.sidebar .sb-brand` rule is (0,2,0) and wins, so the brand is a
       flex row in both states. What this rule actually contributes is
       `position: relative` (the containing block for the absolute caret) and
       the right padding that reserves room for it. Do not add layout here
       expecting it to apply. */
    .sb-brand { position: relative; gap: 0; padding-right: 36px; }
    .sb-logo  { width: 100%; max-width: 128px; height: auto; display: block; }
    /* wave-mark.PNG, not the .svg. The two are different crops of the same mark:
       the SVG's viewBox is 4079x869, a 4.69:1 ribbon, so at 38px wide it renders
       8px tall -- a hairline in a 62px rail, which is what "the collapsed rail is
       broken" was. The PNG is 593x534, near square, and is the crop that belongs
       at icon size. Sized in a square box so the rail's geometry does not depend
       on the asset's aspect ratio again. */
    .sb-logo-mark { display: none; width: 34px; height: 34px; object-fit: contain; }
    .sb-collapse {
      position: absolute; top: 50%; right: 8px; transform: translateY(-50%);
      width: 24px; height: 24px; display: grid; place-items: center; padding: 0;
      border: 1px solid transparent; border-radius: 7px; cursor: pointer;
      background: transparent; color: var(--text-muted);
    }
    .sb-collapse:hover { background: rgba(255,255,255,.7); color: var(--cerulean); }
    .sb-collapse:focus { outline: none; }
    .sb-collapse:focus-visible { outline: 2px solid var(--ocean); outline-offset: 1px; }
    .sb-collapse svg { width: 14px; height: 14px; transition: transform .16s var(--ease-standard); }
    /* flex-basis, not just width. The sidebar is a flex item, and a declared
       width loses to flex sizing -- so .collapsed hid the labels while the rail
       stayed 200px, leaving icons floating in a full-width bar. That is the
       whole bug: it looked like the collapse "didn't work" when in fact only
       the narrowing failed. */
    .sidebar.collapsed { width: 62px; flex: 0 0 62px; }
    .sidebar.collapsed .sb-collapse { position: static; transform: none; margin: 0; }
    .sidebar.collapsed .sb-collapse svg { transform: rotate(180deg); }
    /* Collapsed, the mark and the caret stack and BOTH centre by the same
       mechanism. They used to centre by two different ones -- `margin: 0 auto`
       on the mark, `text-align: center` for the caret -- and neither worked,
       because the winning `.sidebar .sb-brand` rule above makes this a flex
       ROW: auto margins cannot centre a flex item that has a sibling beside it,
       and `text-align` never centres a block-level box at all. The caret ended
       up 26px right of the rail's centre line and 3px outside the rail. One
       rule for both children is what stops them drifting apart again. */
    .sidebar.collapsed .sb-brand {
      padding: 14px 0 12px;
      flex-direction: column; align-items: center; justify-content: center;
      gap: .45rem;
    }
    .sidebar.collapsed .sb-logo { display: none; }
    .sidebar.collapsed .sb-logo-mark { display: block; margin: 0; }
    /* The footer cramps at 200px: name, role and Sign out fight for one row. */
    .sb-who { flex-wrap: wrap; row-gap: .4rem; }
    .sb-who .nm { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }

    .sidebar.collapsed .sb-nav button .lbl,
    .sidebar.collapsed .sb-who > div,
    .sidebar.collapsed .sb-signout,
    .sidebar.collapsed .sb-divider { display: none; }
    .sidebar.collapsed .sb-nav button { justify-content: center; padding: 9px 0; position: relative; }
    /* The badge must survive collapsing: hiding a count when the rail shuts
       removes the one thing the rail is there for. */
    .sidebar.collapsed .sb-nav button .badge {
      position: absolute; transform: translate(12px, -10px);
      font-size: 9px; padding: 0 5px; min-width: 14px;
    }
    .sidebar.collapsed .sb-who { justify-content: center; padding: 10px 0; }

    /* ── The health-signals explainer, now an info icon ─────────────────── */
    .gb-subnote-row { display: flex; align-items: center; gap: .4rem; margin: .9rem 0 .5rem; }
    .gb-subnote-label {
      font-size: var(--fs-eyebrow); font-weight: 700; letter-spacing: .08em;
      text-transform: uppercase; color: var(--text-muted);
    }
    .tf-info {
      width: 15px; height: 15px; border-radius: 50%; display: inline-grid;
      place-items: center; font-size: 10px; font-weight: 700; cursor: help;
      background: var(--app-neutral-bg); color: var(--app-text-neutral);
      border: 1px solid var(--app-neutral-border); font-style: normal;
    }
    .tf-info:hover, .tf-info:focus-visible { border-color: var(--ocean); color: var(--cerulean); }

    /* ── Queue header ─────────────────────────────────────────────────────
       Three things shared one flex row here -- title, count badge and the
       "updated N mins ago" line -- and the age line is the one that grows.
       At anything under a wide desktop it wrapped to three lines, pushed the
       title over and clipped the Cards/Table control to "TABI". The row is now
       two fixed things: a title block that may shrink, and the toggle, which
       may not. The age moved UNDER the title where a timestamp belongs, at
       the smallest size in the app.                                          */
    .queue-head-l { min-width: 0; flex: 1 1 auto; }   /* may shrink; the toggle may not */
    .queue-headtext { display: flex; flex-direction: column; gap: 1px; min-width: 0; }
    .queue-headline { display: flex; align-items: center; gap: .5rem; min-width: 0; }
    .queue-title {
      /* Truncates rather than wrapping. A second title row would reflow the
         whole head, which is the failure this section exists to end. */
      white-space: nowrap; overflow: hidden; text-overflow: ellipsis; min-width: 0;
    }
    /* THE COUNT. It was a .dash-panel-badge.neutral -- aqua on aqua, the same
       blue as the panel icon, the toggle and every heading beside it, so the
       one number on the header that says how much work is waiting read as
       decoration. It is now the only warm thing in the row, and the numeral
       carries the weight while the word stays small. */
    .queue-count {
      display: inline-flex; align-items: baseline; gap: .25rem; flex: 0 0 auto;
      padding: 2px 9px; border-radius: var(--radius-pill);
      background: var(--status-warning-soft); color: var(--status-warning-text);
      font-size: var(--fs-xs); font-weight: 600; white-space: nowrap;
    }
    .queue-count b { font-size: var(--fs-md); font-weight: 800; font-variant-numeric: tabular-nums; }
    .queue-count:empty { display: none; }
    /* Clear day: nothing is waiting, so the pill stops shouting. */
    .queue-count.clear { background: var(--status-success-soft); color: var(--status-success-text); }
    /* The age is a footnote, not a column. Smallest type in the app and it
       never wraps -- if the panel is too narrow for it, it goes away rather
       than taking a second row. */
    .queue-age {
      font-size: 11px; line-height: 1.3; white-space: nowrap;
      overflow: hidden; text-overflow: ellipsis;
    }
    /* ── Queue TABLE view ─────────────────────────────────────────────────
       The deck's flat twin, and until now the unusable one. Five auto-sized
       columns in a 2fr panel meant the sentence column won the width fight and
       everything else wrapped to one word per line. Fixed layout plus a
       colgroup ends that: Detail is elastic, the rest are declared. */
    .tf-tblwrap { width: 100%; overflow-x: auto; -webkit-overflow-scrolling: touch; }
    /* min-width measured against the real panel, which is 534px at a 1440
       desktop -- so the table fits without a horizontal scroller at the width
       it actually gets, and the scroller is a genuine last resort rather than
       the normal case. */
    .tf-tbl-queue { table-layout: fixed; min-width: 430px; }
    /* Every column that is not Detail is as narrow as its content genuinely
       needs and no narrower -- the point is to hand width to the sentence, not
       to crush four columns so a fifth can sprawl. */
    .tf-tbl-queue col.c-item   { width: 9rem; }
    .tf-tbl-queue col.c-detail { width: auto; }
    .tf-tbl-queue col.c-wait   { width: 4.5rem; }
    .tf-tbl-queue col.c-amt    { width: 5rem; }
    /* The age chip is a glance, not a headline. Shrinking it here rather than
       shrinking .tf-status everywhere, because the same chip does heavier work
       on other surfaces. */
    .tf-tbl-queue .tf-status {
      font-size: 9px; padding: 1px 5px; letter-spacing: .02em; line-height: 1.5;
    }
    .tf-tbl-queue th, .tf-tbl-queue td { padding: .55rem .75rem; vertical-align: top; }
    .tf-tbl-queue th:first-child, .tf-tbl-queue td:first-child { padding-left: 1rem; }
    .tf-tbl-queue th:last-child,  .tf-tbl-queue td:last-child  { padding-right: 1rem; }
    /* Two lines of the sentence, then it stops. A worklist row is a thing you
       SCAN; the full text is one click away on the card, so letting one row
       grow to six lines costs every other row its place on screen. */
    .tf-tbl-queue td.detail { color: var(--text-muted); line-height: 1.45; }
    /* Two and a half lines, via max-height rather than line-clamp: a clamp cuts
       cleanly at a line boundary, which looks like the end of the sentence. A
       half line showing is what says "there is more" without a word. */
    .tf-tbl-queue td.detail > span {
      display: block; overflow: hidden; max-height: calc(1.45em * 2.5);
    }
    .tf-tbl-queue td.detail.is-open > span { max-height: none; }
    /* The expando. A text button, not a chevron: it sits inside a dense row of
       words and a lone glyph there reads as punctuation. */
    .tf-tbl-queue .q-more {
      background: none; border: 0; padding: .1rem 0 0; font: inherit;
      font-size: var(--fs-xs); font-weight: 600; color: var(--ocean);
      cursor: pointer; display: block;
    }
    .tf-tbl-queue .q-more:hover { text-decoration: underline; }
    .tf-tbl-queue .q-more:focus-visible { outline: 2px solid var(--ocean); outline-offset: 2px; }
    /* Name over kind, the way the card stacks them. */
    .tf-tbl-queue td.who { white-space: normal; line-height: 1.3; font-weight: 400; }
    .tf-tbl-queue .q-name { display: block; font-weight: 700; color: var(--text-strong); }
    .tf-tbl-queue .q-kind { display: block; font-size: var(--fs-xs); color: var(--text-muted); }
    .tf-tbl-queue tbody tr { cursor: pointer; }
    .tf-tbl-queue tbody tr:focus-visible { outline: 2px solid var(--ocean); outline-offset: -2px; }
    /* Phone: the table stops being a table (the shared .tf-tbl rules below
       already stack it) so the horizontal scroller and the fixed minimum must
       both stand down, or every row would sit inside a scrollbar it does not
       need. The clamp goes too -- stacked, the sentence IS the row. */
    @media (max-width: 767px) {   /* must match the .tf-tbl stacking breakpoint above */
      .tf-tblwrap { overflow-x: visible; }
      .tf-tbl-queue { min-width: 0; table-layout: auto; }
      /* Stacked, the sentence IS the row -- nothing to clamp and nothing to
         expand, so the expando is removed rather than left as a no-op. */
      .tf-tbl-queue td.detail > span { max-height: none; display: inline; }
      .tf-tbl-queue .q-more { display: none; }
      .tf-tbl-queue td.detail { display: flex; }
      /* The shared stacking rule turns every cell into a space-between flex
         row, which laid the kind out BESIDE the name instead of under it.
         This one cell opts back out. */
      .tf-tbl-queue td.who { display: block; margin-bottom: .2rem; }
      .tf-tbl-queue .q-kind { font-size: var(--fs-xs); }
      /* No money on this row: drop the line rather than label a blank. */
      .tf-tbl-queue td.num.is-blank { display: none; }
      .tf-tbl-queue td, .tf-tbl-queue th { padding: 2px 0; }
      .tf-tbl-queue tbody tr { padding: .7rem 1rem; }
    }

    .queue-viewtog { flex: 0 0 auto; }
    .queue-viewtog button { padding: 5px 14px; white-space: nowrap; }
    /* Below ~1280px the panel head runs out of room. The toggle keeps its full
       labels now (it is no longer competing with a three-line age block) and
       only tightens its padding. */
    @media (max-width: 1280px) {
      .queue-viewtog button { padding: 5px 10px; letter-spacing: .03em; }
    }
    @media (max-width: 560px) {
      .queue-age { display: none; }   /* a phone header has no room for a footnote */
      .queue-viewtog button { padding: 5px 8px; font-size: 10px; }
      /* The head gets TWO rows on a phone. Icon, title, count and the toggle
         fighting over 303px left the title 77px wide -- it rendered as
         "Wha...", which reads as a broken panel rather than a tight one.
         Given its own row the title fits whole, and the toggle keeps a real
         tap target underneath instead of being squeezed to fit beside it. */
      .dash-panel-wide > .dash-panel-head { flex-wrap: wrap; row-gap: .5rem; }
      /* The lightning glyph goes. It decorates a heading that is about to say
         the same thing in words, and it was costing the title 38px of the 303
         the row has -- enough that "What needs you now" still rendered as
         "What needs you n...". Decoration loses to the sentence. */
      .dash-panel-wide > .dash-panel-head .dash-panel-icon { display: none; }
      .dash-panel-wide > .dash-panel-head > .queue-head-l { flex: 1 1 100%; }
      .dash-panel-wide > .dash-panel-head > .queue-viewtog { margin-left: auto; }
      .queue-viewtog button { min-height: var(--tap-min); padding: 5px 16px; }
    }
    /* The queue and Schedule were sitting flush against each other. */
    .dash-grid { margin-bottom: 1rem; }

    /* Goal-card action: a small right-aligned link, never a button. A button
       here competes with the number the card exists to show. */
    .gc-action {
      display: block; margin-top: .45rem; text-align: right;
      font-size: var(--fs-xs); font-weight: 600; color: var(--ocean);
      text-decoration: none;
    }
    .gc-action:hover { text-decoration: underline; }
    /* A second action stacks under the first, tighter than the first sits under
       the card. Two links is the maximum a goal card carries: one verb, one
       record. A third would turn the card into a menu. */
    .gc-action + .gc-action { margin-top: .25rem; }
    /* The awaiting-check note has to fit ONE row inside its card. */
    .gc-pace-note {
      font-style: italic; white-space: nowrap; overflow: hidden;
      text-overflow: ellipsis; max-width: 100%;
    }


    /* Financial, as a RECORD. The coverage line is a reassurance in the page
       header rather than a card; the refund groups gather a customer's payments
       together because that is how you think about a refund. */
    .fin-covline { padding: 0 0 .75rem; }
    .fin-covline .tf-cov.bad { color: var(--status-danger-text); }
    .fin-covline .tf-cov.bad .dot { display: none; }
    .fin-unknown { color: var(--text-muted); font-style: italic; }
    .fin-rf-group { margin-bottom: 1rem; }
    .fin-rf-who {
      font-size: var(--fs-sm); font-weight: 700; color: var(--text-strong);
      padding: .4rem 0 .35rem; display: flex; align-items: baseline; gap: .5rem;
    }
    .fin-rf-who .rest { font-weight: 400; font-size: var(--fs-xs); color: var(--text-muted); }
    .searchrow { display: flex; gap: .5rem; }
    .searchrow input {
      flex: 1; font: inherit; font-size: var(--fs-sm); padding: .55rem .875rem;
      border: 1px solid var(--border-control); border-radius: 10px;
      background: var(--white); color: var(--text); min-height: var(--tap-min);
    }
    .searchrow input:focus { outline: 2px solid var(--ocean); outline-offset: -1px; border-color: var(--ocean); }
    .tf-deck-more-acts { display: contents; }
    /* Data age. Quiet by default; it only raises its voice once the screen is
       older than two refresh intervals, which means the refresh stopped rather
       than that the data is merely a little old. */
    .queue-age { color: var(--text-muted); }
    .queue-age.stale { color: var(--status-warning-text); font-weight: 600; }
    /* Undo sits INSIDE the toast, so the confirmation and the way back are the
       same object. A separate control would be another thing to find. */
    .queue-undo-btn {
      margin-left: .6rem; background: none; border: 0; padding: 0;
      font: inherit; font-weight: 700; color: var(--tf-aqua);
      text-decoration: underline; cursor: pointer;
    }
    .queue-undo-btn:hover { color: #fff; }
    .fin-covline { display: flex; align-items: center; gap: .75rem; }
    .fin-covline #fin-density-btn { margin-left: auto; }
    /* Compact trims the row box, not the type: shrinking the text is how a
       dense table becomes an unreadable one. */
    #panel-financial.is-compact .tf-tbl td { padding: .3rem 1.25rem; }
    #panel-financial.is-compact .tf-tbl th { padding: .35rem 1.25rem; }
    #panel-financial.is-compact .tf-scrollbox { max-height: 460px; }
    /* The parts decision inside a deck card. The control is the panel's own, so
       only its container needs placing. */
    .tf-deck-parts { margin-top: .8rem; padding-top: .7rem; border-top: 1px solid var(--border); }
    .tf-deck-parts .pf-step { display: flex; flex-wrap: wrap; gap: .4rem; align-items: center; }
    /* The follow-up note. Sits in the card's own slot so it appears where you
       are looking, not in a dialog over the top of it. */
    .queue-noteform { display: flex; flex-wrap: wrap; gap: .4rem; align-items: center;
      margin-top: .8rem; padding-top: .7rem; border-top: 1px solid var(--border); }
    .queue-noteform label { width: 100%; font-size: var(--fs-xs); font-weight: 600; color: var(--text-muted); }
    .queue-noteform input { flex: 1 1 16rem; font: inherit; font-size: var(--fs-sm);
      padding: .5rem .7rem; border: 1px solid var(--border-control); border-radius: 10px;
      background: var(--white); color: var(--text); min-height: var(--tap-min); }
    .queue-noteform input:focus { outline: 2px solid var(--ocean); outline-offset: -1px; border-color: var(--ocean); }
    /* @tf-component:end */
    /* The money closes on a charge card (Write off / Already paid) reuse the
       note form and add a select, a short amount field and a status line. */
    .queue-noteform select { font: inherit; font-size: var(--fs-sm); padding: .5rem .7rem;
      border: 1px solid var(--border-control); border-radius: 10px; background: var(--white);
      color: var(--text); min-height: var(--tap-min); }
    .queue-noteform select:focus { outline: 2px solid var(--ocean); outline-offset: -1px; border-color: var(--ocean); }
    .queue-noteform input.queue-money-amt { flex: 0 0 8rem; }
    .queue-noteform .queue-money-say { width: 100%; font-size: var(--fs-xs); color: var(--text-muted); margin: 0; }
    .queue-noteform .queue-money-say.bad { color: var(--status-danger); font-weight: 600; }

    .fin-state { font-size: var(--fs-xs); color: var(--text-muted); padding: .85rem; background: var(--white);
      border: 1px dashed var(--border); border-radius: 8px; line-height: 1.55; }
    /* "Could not read" must never render like "nothing here" — an RLS-filtered
       read returns [] with error === null, so the two are indistinguishable
       unless the UI says which one it is. */
    .fin-state.err { color: var(--status-danger-text); background: var(--danger-bg);
      border-color: rgba(192,57,43,.3); border-style: solid; }
    /* Result of a Hold/Release that returned false. The RPCs return a boolean
       precisely because a hold that silently did nothing looks identical to one
       that worked — so the false path gets its own visible line, never a toast. */
    .fin-msg { font-size: var(--fs-xs); line-height: 1.5; margin-top: .45rem; padding: .4rem .6rem;
      border-radius: 6px; flex-basis: 100%; }
    .fin-msg.ok { background: var(--success-bg); color: var(--status-success-text, #0f7a3d); }
    .fin-msg.no { background: var(--warning-bg); color: var(--status-warning-text, #9a5b00); }

    /* ── Balances due, v2 (.fin-bd-*) ──────────────────────────────────────
       ONE number meaning every dollar owed, split into the half that can be
       acted on and the half that cannot.

       WHY: the panel used to count COMPLETED work only, because charge-balance
       hard-blocks on `completed` and a Charge button on anything else could only
       fail. That is right for the BUTTON and wrong for the COUNT: it read
       "Balances due 0" while $74.50 was owed and visible two panels away in
       Expected, and the Dashboard called the same money "cash to collect". Three
       surfaces, three definitions. The count now answers "what are we owed"; the
       button still only appears where it can succeed.

       Every value below is a brand token. No literals, no fallbacks — the
       fallback pattern (`var(--x, #hex)`) is how a hand-picked colour gets in
       and then outlives the token it was standing in for. */
    .fin-bd-total { padding: var(--space-4) var(--space-5) var(--space-3); }
    .fin-bd-total .fin-bd-lbl { font-size: var(--fs-eyebrow); text-transform: uppercase;
      letter-spacing: .07em; font-weight: 700; color: var(--app-text-secondary); }
    .fin-bd-total .fin-bd-amt { font-family: var(--font-display); font-size: var(--fs-3xl);
      line-height: var(--lh-tight); color: var(--app-text-primary); margin-top: var(--space-1); }
    .fin-bd-total .fin-bd-sub { font-size: var(--fs-sm); color: var(--app-text-secondary);
      margin-top: var(--space-1); line-height: var(--lh-snug); }

    .fin-bd-split { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr));
      border-top: 1px solid var(--app-border-soft); }
    .fin-bd-split > div { padding: var(--space-3) var(--space-5); }
    .fin-bd-split > div + div { border-left: 1px solid var(--app-border-soft); }
    .fin-bd-split .fin-bd-lbl { display: flex; align-items: center; gap: var(--space-1);
      font-size: var(--fs-xs); font-weight: 700; color: var(--app-text-secondary);
      text-transform: uppercase; letter-spacing: .05em; }
    .fin-bd-split .fin-bd-amt { font-size: var(--fs-xl); font-weight: 700;
      color: var(--app-text-primary); margin-top: var(--space-1); }
    .fin-bd-split .fin-bd-n { font-size: var(--fs-xs); color: var(--app-text-secondary); }

    /* Info icon. A real button with a real hit target, because the definitions
       it carries are the thing that stops the two halves being confused.

       `.settings-info` is an ALIAS, not a copy. The Settings panel needs the same
       icon and the `.fin-` prefix would be a lie there, but renaming the three
       existing finance call sites to a neutral class would be churn with a real
       downside: a class name can be a behaviour hook as well as a style hook, and
       a rename that misses one leaves a raw unstyled button with no error. Adding
       the alias leaves those three byte-identical and lets Settings use a name
       that means something. Retire `.fin-info` later, one call site at a time. */
    .fin-info, .settings-info { appearance: none; background: none; border: 0; padding: 0; cursor: pointer;
      width: var(--icon-md); height: var(--icon-md); min-width: var(--icon-md);
      color: var(--app-text-secondary); display: inline-grid; place-items: center;
      border-radius: var(--radius-pill); position: relative; }
    .fin-info:hover, .fin-info:focus-visible,
    .settings-info:hover, .settings-info:focus-visible { color: var(--tf-ocean-700);
      background: var(--tf-aqua-050); outline: none; }
    .fin-info svg, .settings-info svg { width: var(--icon-sm); height: var(--icon-sm); }
    .fin-info::after, .settings-info::after { content: attr(data-tip); position: absolute; left: 0;
      top: calc(100% + var(--space-1)); width: 250px; background: var(--surface-deepest);
      color: var(--text-on-dark); font-size: var(--fs-xs); font-weight: 400;
      line-height: var(--lh-snug); text-transform: none; letter-spacing: 0;
      padding: var(--space-2); border-radius: var(--radius-sm); opacity: 0;
      pointer-events: none; transition: opacity var(--duration-base) var(--ease-standard);
      z-index: 5; box-shadow: var(--shadow-md); }
    /* Hover for mouse, focus for keyboard, aria-expanded for TOUCH. A hover-only
       tooltip cannot be opened on a tablet at all. */
    .fin-info:hover::after, .fin-info:focus-visible::after,
    .fin-info[aria-expanded="true"]::after,
    .settings-info:hover::after, .settings-info:focus-visible::after,
    .settings-info[aria-expanded="true"]::after { opacity: 1; }

    /* A setting shown for reference only. It is deliberately NOT an input: these
       four values are live compliance/pacing behaviour, and a box you can type in
       is an invitation to type in it. Rendered as text so "visible" and
       "changeable" stay separate decisions (Meesha, 2026-09-13). */
    .settings-ro-row { display: flex; align-items: baseline; gap: var(--space-2);
      flex-wrap: wrap; }
    .settings-ro-label { font-size: var(--fs-sm); color: var(--app-text-secondary); }
    .settings-ro-value { font-size: var(--fs-sm); font-weight: 600;
      color: var(--app-text-primary); font-variant-numeric: tabular-nums; }
    .settings-ro-value.is-error { font-weight: 400; color: var(--status-danger-text); }

    /* Campaign grouping in Sent & waiting. Eight identical quotes from one send
       are one decision, and listed flat they bury the estimates that have been
       waiting weeks. */
    .fin-grp { border: 1px dashed var(--app-border); border-radius: var(--radius-md);
      margin: var(--space-2) 0; }
    .fin-grp > summary { list-style: none; cursor: pointer; display: flex; align-items: center;
      gap: var(--space-2); padding: var(--space-2) var(--space-3); min-height: var(--tap-min); }
    .fin-grp > summary::-webkit-details-marker { display: none; }
    /* Flex children default to shrink:1, so a fixed-size pill and the chevron
       give way before a long title does — the pill collapsed to icon width in
       testing. Pin them and let the TITLE be what ellipses. */
    .fin-grp > summary .fin-tag, .fin-grp > summary .fin-chev { flex: none; }
    .fin-grp > summary .fin-grp-t { min-width: 0; overflow: hidden;
      text-overflow: ellipsis; white-space: nowrap; font-weight: 700; }
    .fin-grp > summary .fin-grp-meta { min-width: 0; font-size: var(--fs-xs);
      color: var(--app-text-secondary); }
    .fin-chev { width: var(--icon-md); height: var(--icon-md); margin-left: auto;
      color: var(--app-text-secondary); transition: transform var(--duration-base) var(--ease-standard); }
    details[open] > summary .fin-chev { transform: rotate(180deg); }
    .fin-grp-body { padding: 0 var(--space-3) var(--space-2); }
    .fin-tag.campaign { background: var(--status-info-soft); color: var(--status-info-text); }

    /* Collapsible section header, for Sent & waiting / Subscriptions / Collected.
       Scoped to details so the existing static .fin-sec-head is untouched. */
    details.fin-sec > summary { list-style: none; cursor: pointer; display: flex;
      align-items: center; gap: var(--space-2); min-height: var(--tap-min); }
    details.fin-sec > summary::-webkit-details-marker { display: none; }
    details.fin-sec > summary h2 { margin: 0; }
    details.fin-sec > summary .fin-sum-hint { font-size: var(--fs-xs);
      color: var(--app-text-secondary); }

    /* Right-rail cards */
    .rail-card { background: var(--white); border: 1px solid var(--border);
      border-radius: var(--radius); box-shadow: var(--shadow-sm); overflow: hidden; }
    .rail-card > summary { list-style: none; cursor: pointer; padding: .8rem .95rem; }
    .rail-card > summary::-webkit-details-marker { display: none; }
    .rail-head { display: flex; align-items: center; gap: .5rem; margin-bottom: .25rem; }
    .rail-head h3 { font-size: var(--fs-xs); font-weight: 800; text-transform: uppercase;
      letter-spacing: .06em; color: var(--text-muted); }
    .rail-caret { margin-left: auto; font-size: var(--fs-xs); color: var(--tf-ocean-700); font-weight: 600; }
    .rail-card[open] .rail-caret::after      { content: " \25B4"; }
    .rail-card:not([open]) .rail-caret::after { content: " \25BE"; }
    .rail-line { font-size: var(--fs-sm); color: var(--text-strong); font-weight: 600; line-height: 1.45; }
    .rail-line b { font-variant-numeric: tabular-nums; }
    .rail-body { padding: 0 .95rem .95rem; border-top: 1px solid var(--border); }
    .rail-plain { padding: .8rem .95rem; }

    .exp-row { padding: .55rem 0; border-bottom: 1px solid var(--border); }
    .exp-row:last-of-type { border-bottom: none; }
    .exp-t { font-size: var(--fs-xs); font-weight: 700; color: var(--text-strong); line-height: 1.35; }
    .exp-d { font-size: var(--fs-xs); color: var(--text-muted); margin-top: .1rem; }
    .exp-foot { font-size: var(--fs-xs); color: var(--text-muted); line-height: 1.5; margin-top: .7rem;
      padding-top: .6rem; border-top: 1px dashed var(--border); }

    /* Parts decision flow — the control that answers whichever question the
       row is actually on. Laid out with flex + gap so a wrapped second line
       keeps its spacing; no per-element margins to collapse. */
    .pf-step { display: flex; flex-wrap: wrap; align-items: center; gap: .35rem; margin-top: .45rem; }
    .pf-q1, .pf-q2, .pf-q3 { display: flex; flex-wrap: wrap; align-items: center; gap: .35rem; }
    /* [hidden] loses to the display rule above on its own -- the sitewide
       [hidden]{display:none!important} covers it, and these repeat it locally
       because each step depends on staying hidden to stay unanswerable. */
    .pf-q2[hidden], .pf-q3[hidden] { display: none; }
    /* Question 3 is a row of its own under the two buttons that revealed it,
       not a continuation of them. */
    .pf-q3 { flex-basis: 100%; padding-top: .35rem; }
    /* Which owner is selected has to survive the reveal: the buttons stay on
       screen underneath question 3, and two identical-looking buttons with no
       state is a form that cannot tell you what it is about to save.

       A ring ALONE was not enough and was actively misleading. Both buttons
       keep their filled treatment, so on a phone the ringed one read as merely
       focused while the UNPICKED one, being solid and unqualified, looked like
       the chosen answer -- the form was saying the opposite of what it meant.
       So the ring marks the pick AND the other answer steps back. */
    .pf-btn.is-on {
      box-shadow: inset 0 0 0 2px var(--text-strong);
      font-weight: 800;
    }
    .pf-q2:has(.is-on) .pf-btn:not(.is-on) {
      background: var(--white); color: var(--text-muted);
      border: 1px solid var(--border-control); box-shadow: none;
    }
    .pf-lbl { font-size: var(--fs-xs); color: var(--text-muted); font-weight: 600; }
    .pf-date {
      font-family: var(--font-sans); font-size: var(--fs-xs); padding: .18rem .4rem;
      border: 1px solid var(--border); border-radius: 8px; background: #fff; color: var(--text-strong); }
    .pf-date:focus-visible {
      outline: 2px solid var(--cerulean); outline-offset: 1px; }
    /* Consequence line. Takes the full row so it reads as a sentence under the
       control rather than trailing off the end of it — this is the text that
       says a customer is about to be messaged. */
    .pf-note { flex: 1 0 100%; font-size: var(--fs-xs); color: var(--text-muted); line-height: 1.45; }
    /* Mount for the shared job schedule picker. Full row inside .pf-step's flex,
       so the picker is not squeezed alongside the buttons that opened it. */
    .pf-slot { flex: 1 0 100%; margin-top: .5rem; }

    .fin-line { display: flex; justify-content: space-between; gap: .8rem; padding: .5rem 0;
      border-bottom: 1px solid var(--border); font-size: var(--fs-xs); align-items: baseline; }
    .fin-line:last-child { border-bottom: none; }
    .fin-line b { font-variant-numeric: tabular-nums; color: var(--text-strong); white-space: nowrap; }
    .fin-line .sub { display: block; font-size: var(--fs-xs); color: var(--text-muted); margin-top: .05rem; }
    .fin-sub-h { font-size: var(--fs-xs); font-weight: 800; text-transform: uppercase; letter-spacing: .05em;
      color: var(--text-muted); margin: 1rem 0 .35rem; }

    /* Message-template viewer */
    .fin-tpl-intro { font-size: var(--fs-xs); color: var(--text-muted); line-height: 1.5; margin: .7rem 0; }
    .fin-tpl { border: 1px solid var(--border); border-radius: 8px; margin-bottom: .4rem; }
    .fin-tpl > summary { list-style: none; cursor: pointer; padding: .5rem .65rem; font-size: var(--fs-xs);
      color: var(--text-strong); display: flex; align-items: center; gap: .5rem; }
    .fin-tpl > summary::-webkit-details-marker { display: none; }
    .fin-tpl > summary b { flex: 1; font-weight: 600; }
    .fin-tpl-body { padding: 0 .65rem .65rem; }
    .fin-tpl-trigger { font-size: var(--fs-xs); color: var(--text-muted); line-height: 1.5; margin-bottom: .5rem; }
    /* Shaped like an SMS bubble on purpose: the point of the viewer is seeing
       what lands on a customer's phone, and three bubbles reads as three buzzes
       in a way three paragraphs does not. */
    .fin-tpl-msg { background: var(--aqua-soft); border-radius: 10px; padding: .45rem .65rem;
      font-size: var(--fs-xs); line-height: 1.5; color: var(--text-strong); margin-bottom: .3rem;
      white-space: pre-wrap; word-break: break-word; }
    .fin-tpl-src { font-size: var(--fs-xs); color: var(--text-muted); font-family: ui-monospace, monospace;
      margin-top: .4rem; word-break: break-all; }

    /* ── "+ New" front door drawer (FINANCIAL-SPEC.md §3) ────────────────── */
    .fd-scrim { position: fixed; inset: 0; background: rgba(6,40,64,.42); z-index: 140; }
    .fd-drawer { position: fixed; top: 0; right: 0; height: 100%; width: min(480px, 100%);
      background: var(--white); box-shadow: var(--shadow-md); z-index: 150;
      display: flex; flex-direction: column; }
    .fd-head { padding: 1.1rem 1.35rem; border-bottom: 1px solid var(--border);
      display: flex; align-items: center; gap: .7rem; }
    .fd-title { font-family: var(--font-display); font-size: var(--fs-md); color: var(--text-strong); }
    /* Same 44px target as .drawer-close, for the same reason. */
    .fd-close { margin-left: auto; background: none; border: none; font-size: 1.75rem; line-height: 1;
      color: var(--text-muted); cursor: pointer;
      width: 44px; height: 44px; flex: none;
      display: inline-flex; align-items: center; justify-content: center;
      border-radius: var(--radius); transition: background .12s, color .12s; }
    .fd-close:hover { background: var(--sand); color: var(--text-strong); }
    .fd-close:focus-visible { outline: 2px solid var(--cerulean); outline-offset: 2px; }
    .fd-back { background: none; border: 1px solid var(--border-control); border-radius: 7px;
      padding: .25rem .6rem; font-size: var(--fs-xs); color: var(--text-muted); cursor: pointer;
      font-family: var(--font-sans); }
    .fd-body { padding: 1.35rem; overflow-y: auto; flex: 1; }
    .fd-q { font-size: var(--fs-md); font-weight: 700; color: var(--text-strong); margin: 0 0 .3rem; }
    .fd-qh { font-size: var(--fs-xs); color: var(--text-muted); margin: 0 0 1.1rem; line-height: 1.5; }
    .fd-srch { width: 100%; padding: .65rem .8rem; font-size: var(--fs-base); border: 1px solid var(--border);
      border-radius: 9px; font-family: var(--font-sans); color: var(--text-strong); }
    .fd-srch:focus { outline: none; border-color: var(--ocean); box-shadow: 0 0 0 3px rgba(26,159,196,.14); }
    .fd-res { margin-top: .5rem; border: 1px solid var(--border); border-radius: 9px; overflow: hidden; }
    .fd-res button { display: block; width: 100%; text-align: left; background: none; border: none;
      border-bottom: 1px solid var(--border); padding: .6rem .8rem; cursor: pointer; font-family: var(--font-sans); }
    .fd-res button:last-child { border-bottom: none; }
    .fd-res button:hover { background: var(--aqua-soft); }
    .fd-rn { font-size: var(--fs-sm); font-weight: 600; color: var(--text-strong); }
    .fd-rm { font-size: var(--fs-xs); color: var(--text-muted); }
    .fd-newp { margin-top: .9rem; background: none; border: 1px dashed var(--border); border-radius: 9px;
      width: 100%; padding: .6rem; font-size: var(--fs-sm); color: var(--text-muted); cursor: pointer;
      font-family: var(--font-sans); }
    .fd-newp:hover { border-color: var(--ocean); color: var(--ocean); }
    .fd-chip { display: flex; align-items: center; gap: .6rem; background: var(--aqua-soft);
      border: 1px solid var(--border); border-radius: 10px; padding: .6rem .8rem; margin-bottom: 1.25rem; }
    .fd-chip-n { flex: 1; font-size: var(--fs-sm); font-weight: 600; color: var(--text-strong); }
    .fd-chip-c { background: none; border: none; font-size: var(--fs-xs); color: var(--ocean); cursor: pointer;
      font-weight: 600; font-family: var(--font-sans); text-decoration: underline; }
    /* Inline create-a-customer form, added 2026-09-12. "Someone new" used to
       collect nothing, so the New Job drawer asked again and the money routes
       had to refuse the click. See NEW-JOB-DRAWER-SPEC.md §9. */
    /* ── New Job drawer: what's on file (2026-09-12) ──────────────────────
       Replaces the amber/rose card stack. Present facts are quiet reference in
       named sections; only a MISSING fact gets a colour, because that is the
       one thing the office can act on while the customer is on the phone.
       See NEW-JOB-DRAWER-SPEC.md §9.3. */
    .create-strip { padding: .8rem 1.25rem; background: var(--aqua-soft);
      border-bottom: 1px solid var(--border); }
    .create-strip-top { display: flex; align-items: flex-start; justify-content: space-between; gap: .75rem; }
    .create-strip-name { font-size: var(--fs-base); font-weight: 800; color: var(--text-strong); }
    .create-strip-meta { font-size: var(--fs-xs); color: var(--text-muted); margin-top: 1px; }
    .create-strip-change { background: none; border: none; padding: 0; flex: none;
      font-family: var(--font-sans); font-size: var(--fs-xs); font-weight: 700;
      color: var(--ocean); cursor: pointer; text-decoration: underline; }

    .of-toggle { display: block; margin-top: .5rem; padding: 0; background: none; border: none;
      font-family: var(--font-sans); font-size: var(--fs-xs); font-weight: 700;
      color: var(--ocean); cursor: pointer; }
    .of-toggle:hover { text-decoration: underline; }
    .of-det { margin-top: .35rem; }
    .of-r { width: 100%; text-align: left; background: none; border: none;
      border-top: 1px solid var(--border); padding: .55rem .1rem; cursor: pointer;
      font-family: var(--font-sans); display: flex; justify-content: space-between;
      align-items: center; gap: .6rem; }
    .of-r:first-child { border-top: none; }
    .of-t { display: block; font-size: var(--fs-sm); font-weight: 700; color: var(--text-strong); }
    .of-s { display: block; font-size: var(--fs-xs); color: var(--text-muted); margin-top: .1rem; }
    .of-car { font-size: 10px; color: var(--text-muted); flex: none; }
    .of-b { padding: .1rem .1rem .5rem .9rem; }
    .of-kv { padding: .45rem 0; border-bottom: 1px solid var(--border); }
    .of-kv:last-child { border-bottom: none; }
    .of-kv b { display: block; font-size: 10.5px; font-weight: 800; text-transform: uppercase;
      letter-spacing: .05em; color: var(--text-muted); margin-bottom: .12rem; }
    .of-kv span { font-size: var(--fs-sm); line-height: 1.5; color: var(--text-strong); }
    .of-gap { color: #7c3aed; font-weight: 700; font-style: normal; }
    .of-hint { display: block; font-size: var(--fs-xs); color: var(--text-muted);
      font-style: italic; margin-top: .15rem; line-height: 1.4; }
    .of-edit { background: none; border: none; padding: 0; margin-top: .2rem;
      font-family: var(--font-sans); font-size: var(--fs-xs); font-weight: 700;
      color: var(--ocean); cursor: pointer; }
    .of-edit:hover { text-decoration: underline; }
    .of-ask { background: #f5f3ff; border: 1px solid #ddd6fe; border-radius: 10px;
      padding: .6rem .75rem; margin-top: .6rem; }
    .of-ask-l { display: flex; justify-content: space-between; align-items: center;
      font-size: 10.5px; font-weight: 800; text-transform: uppercase; letter-spacing: .055em;
      color: #7c3aed; margin-bottom: .3rem; }
    .of-ask-l span:last-child { font-weight: 700; text-transform: none; letter-spacing: 0; opacity: .7; }
    .of-ask-q { font-size: var(--fs-sm); font-weight: 600; color: #4c1d95; line-height: 1.45; }
    .of-ask-in { margin-top: .45rem; display: flex; gap: .4rem; }
    .of-ask-in input { flex: 1; font-family: var(--font-sans); font-size: var(--fs-sm);
      padding: .45rem .6rem; border: 1px solid #ddd6fe; border-radius: 8px; background: var(--white); }
    .of-ask-in input:focus { outline: none; border-color: #7c3aed; box-shadow: 0 0 0 3px #ede9fe; }
    .of-ask-in button { flex: none; background: #7c3aed; color: var(--white); border: none;
      border-radius: 8px; padding: .45rem .8rem; font-family: var(--font-sans);
      font-size: var(--fs-xs); font-weight: 700; cursor: pointer; }
    .of-ask-in button:disabled { opacity: .6; cursor: not-allowed; }

    /* "Is this visit doing work you've already quoted?" — replaced the
       Link-to-an-existing-job toggle + search (NEW-JOB-DRAWER-SPEC.md §9.5). */
    .jl-list { display: flex; flex-direction: column; gap: .4rem; margin-top: .4rem; }
    .jl { display: flex; align-items: flex-start; gap: .55rem; text-align: left; width: 100%;
      background: var(--white); border: 1px solid var(--border-control, var(--border));
      border-radius: 10px; padding: .6rem .75rem; cursor: pointer; font-family: var(--font-sans); }
    .jl:hover { border-color: var(--ocean); background: var(--aqua-soft); }
    .jl.on { border-color: var(--ocean); background: var(--aqua-soft);
      box-shadow: 0 0 0 1px var(--ocean) inset; }
    .jl-dot { flex: none; width: 15px; height: 15px; border-radius: 50%; margin-top: 1px;
      border: 1.5px solid var(--border-control, var(--border)); background: var(--white); }
    .jl.on .jl-dot { border-color: var(--ocean); border-width: 5px; }
    .jl-t { display: block; font-size: var(--fs-sm); font-weight: 700; color: var(--text-strong); }
    .jl-s { display: block; font-size: var(--fs-xs); color: var(--text-muted); margin-top: .1rem; line-height: 1.4; }

    .fd-newform { margin-top: .9rem; padding-top: .9rem; border-top: 1px dashed var(--border); }
    .fd-nl { display: block; font-size: var(--fs-xs); font-weight: 700; color: var(--text-strong);
      margin: .6rem 0 .25rem; }
    .fd-nl:first-child { margin-top: 0; }
    .fd-ni { width: 100%; font-family: var(--font-sans); font-size: var(--fs-sm);
      padding: .55rem .7rem; border: 1px solid var(--border-control, var(--border));
      border-radius: 8px; background: var(--white); color: var(--text-strong); }
    .fd-ni:focus { outline: none; border-color: var(--ocean); box-shadow: 0 0 0 3px var(--aqua-soft); }
    .fd-nrow { display: grid; grid-template-columns: 1fr 1fr; gap: .7rem; }
    .fd-nsave { margin-top: .9rem; width: 100%; background: var(--ocean); color: var(--white);
      border: none; border-radius: 9px; padding: .6rem; font-family: var(--font-sans);
      font-size: var(--fs-sm); font-weight: 700; cursor: pointer; }
    .fd-nsave:hover:not(:disabled) { background: var(--tf-ocean-700); }
    .fd-nsave:disabled { background: var(--border); color: var(--text-muted); cursor: not-allowed; }
    .fd-nhint { font-size: var(--fs-xs); color: var(--text-muted); line-height: 1.45; margin-top: .4rem; }

    /* The actions stay on screen and dim, rather than living behind a second
       step. An operator can read where this is going before picking anyone. */
    .fd-locked { opacity: .4; pointer-events: none; }

    .fd-grp { margin-bottom: 1.5rem; }
    .fd-grp-l { font-size: var(--fs-xs); font-weight: 800; text-transform: uppercase; letter-spacing: .06em;
      color: var(--text-muted); margin-bottom: .55rem; }
    .fd-opt { display: block; width: 100%; text-align: left; background: var(--white);
      border: 1px solid var(--border); border-radius: 11px; padding: .75rem .9rem; margin-bottom: .5rem;
      cursor: pointer; font-family: var(--font-sans); transition: border-color .12s, box-shadow .12s; }
    .fd-opt:hover { border-color: var(--ocean); box-shadow: 0 3px 12px rgba(6,40,64,.07); }
    .fd-opt-t { display: block; font-size: var(--fs-sm); font-weight: 700; color: var(--text-strong); }
    .fd-opt-d { display: block; font-size: var(--fs-xs); color: var(--text-muted); line-height: 1.45; margin-top: .15rem; }
    /* The moved action panels sit open inside step 3 — they are the destination
       here, not a list to browse, so their accordion chrome is suppressed. */
    #fd-action-host .settings-acc > summary { display: none; }
    #fd-action-host .settings-acc { border: none; padding: 0; }
    #fd-action-host .settings-acc-body { padding: 0; }

    /* ── Toast ──────────────────────────────────────────────────────────── */
    /* REBUILT 2026-08-19. Three faults, all of which landed together the first
       time a toast had to carry a long Stripe error:

       1. `white-space: nowrap` meant a long message could never wrap. Combined
          with the centring translateX(-50%), it grew past the viewport in BOTH
          directions at once, so the start and end of the very messages that
          matter most were the parts off-screen.
       2. The timeout was a flat 3500ms regardless of length, so the longest
          message got the same reading time as "Saved" — i.e. the amount of text
          and the time to read it moved in opposite directions.
       3. Errors painted a full bright-red fill via --red, which reads as an
          alarm for what is often a routine refusal.

       The danger tokens below already existed for exactly this and the toast
       simply predated them; see brand/tokens.css on why a missing token member
       generates its own drift. An error now reads as a navy toast with a danger
       rule and marker rather than a red slab. */
    #toast {
      position: fixed;
      bottom: 1.75rem;
      left: 50%;
      transform: translateX(-50%) translateY(10px);
      background: var(--navy);
      color: var(--white);
      padding: .75rem 1.15rem;
      border-radius: var(--radius-md);
      font-size: var(--fs-sm);
      font-weight: 600;
      opacity: 0;
      transition: opacity .2s, transform .2s;
      pointer-events: none;
      /* Above the visit panel (240) and every sheet (260 and up), or an error toast raised while
         one is open is drawn BEHIND it and never seen. */
      z-index: 400;
      /* Never wider than the viewport, and free to wrap. min() rather than a
         bare vw so it stays readable on a phone and does not sprawl on a
         desktop. */
      /* width:max-content + a cap, rather than the cap alone. A fixed-position
         block shrink-to-fits, and on a narrow screen that resolved to ~188px of
         a 375px viewport — not overflowing, but half the available width and
         twice as tall as it needed to be. max-content asks for the full single
         line and lets max-width do the clamping, so the toast uses the room it
         actually has at every size. */
      width: max-content;
      max-width: min(32rem, calc(100vw - 2rem));
      white-space: pre-wrap;
      overflow-wrap: anywhere;
      text-align: left;
      line-height: 1.45;
      box-shadow: var(--shadow-md);
    }
    #toast.show { opacity: 1; transform: translateX(-50%) translateY(0); }
    /* Dismissible, so a long error can be read at the operator's pace and then
       cleared — which is only possible with pointer events back on. */
    #toast.show.dismissible { pointer-events: auto; cursor: pointer; }
    #toast.error {
      background: var(--navy);
      border-left: 4px solid var(--status-danger);
      padding-left: calc(1.15rem - 4px);
    }
    #toast .toast-hint {
      display: block;
      margin-top: .4rem;
      font-size: .75rem;
      font-weight: 500;
      opacity: .7;
    }

    /* ── Dashboard panel ───────────────────────────────────────────────────── */
    #panel-dashboard { padding: 0; }
    .dash-wrap { max-width: 1200px; margin: 0 auto; padding: 1.5rem 1.5rem 4rem; }

    .dash-priority-strip { background: var(--white); border: 1px solid var(--border); border-radius: 14px; box-shadow: var(--shadow-sm); margin-bottom: 1.25rem; overflow: hidden; }
    .dash-priority-header { display: flex; align-items: center; justify-content: space-between; padding: .875rem 1.25rem; border-bottom: 1px solid var(--border); }
    .dash-priority-title { display: flex; align-items: center; gap: .5rem; font-size: var(--fs-xs); font-weight: 600; color: var(--text-strong); text-transform: uppercase; letter-spacing: .06em; }
    .dash-priority-dot { width: 7px; height: 7px; border-radius: 50%; background: var(--red); box-shadow: 0 0 0 2px rgba(181,43,43,.2); }
    .dash-priority-count { font-size: var(--fs-xs); color: var(--text-muted); font-weight: 400; }
    .dash-priority-items { display: flex; flex-wrap: wrap; }
    .dash-priority-item { display: flex; align-items: flex-start; gap: .625rem; padding: .875rem 1.25rem; border-right: 1px solid var(--border); flex: 1; min-width: 240px; cursor: pointer; transition: background .12s; }
    .dash-priority-item:last-child { border-right: none; }
    .dash-priority-item:hover { background: var(--aqua-soft); }
    .dash-priority-icon { width: 32px; height: 32px; border-radius: 6px; display: flex; align-items: center; justify-content: center; flex-shrink: 0; }
    .dash-priority-icon.red { background: #fdeaea; color: var(--status-danger-text); }
    .dash-priority-icon.amber { background: #fef3e0; color: #a05a00; }
    .dash-priority-icon.blue { background: var(--aqua-soft); color: var(--ocean); }
    .dash-priority-icon svg { width: var(--icon-sm); height: var(--icon-sm); }
    .dash-priority-text { flex: 1; min-width: 0; }
    .dash-priority-label { font-size: var(--fs-xs); font-weight: 600; color: var(--text-strong); line-height: 1.3; }
    .dash-priority-sub { font-size: var(--fs-xs); color: var(--text-muted); margin-top: 2px; line-height: 1.4; }
    .dash-priority-cta { font-size: var(--fs-eyebrow); font-weight: 600; color: var(--ocean); white-space: nowrap; align-self: center; text-transform: uppercase; letter-spacing: .05em; }
    .dash-priority-count-badge {
      display: inline-flex; align-items: center; justify-content: center;
      min-width: 26px; height: 26px; padding: 0 6px; border-radius: 8px;
      font-size: var(--fs-xs); font-weight: 800; align-self: center; flex-shrink: 0;
    }
    .dash-priority-count-badge.red   { background: #fdeaea; color: var(--status-danger-text); }
    .dash-priority-count-badge.amber { background: #fef3e0; color: #a05a00; }
    .dash-priority-count-badge.blue  { background: var(--aqua-soft); color: var(--ocean); }
    .dash-priority-count-badge.green { background: var(--success-bg); color: var(--success); }

    .dash-grid { display: grid; grid-template-columns: 1.5fr 1.5fr; gap: 1rem; }
    @media (max-width: 1024px) { .dash-grid { grid-template-columns: 1fr 1fr; } }
    .dash-grid > * { min-width: 0; }
    .dash-panel-wide { grid-column: span 1; }
    .dash-panel-full { grid-column: 1 / -1; }
    @media (max-width: 640px) {
      .dash-grid { grid-template-columns: 1fr; }
      .dash-panel-wide, .dash-panel-full { grid-column: span 1; }
      .dash-priority-item { min-width: 100%; border-right: none; border-bottom: 1px solid var(--border); }
      .dash-priority-item:last-child { border-bottom: none; }
    }

    .dash-panel { background: var(--white); border: 1px solid var(--border); border-radius: 14px; box-shadow: var(--shadow-sm); display: flex; flex-direction: column; overflow: hidden; }
    .dash-panel-head { display: flex; align-items: center; justify-content: space-between; padding: .875rem 1.25rem; border-bottom: 1px solid var(--border); }
    .dash-panel-head-left { display: flex; align-items: center; gap: .5rem; }
    .dash-panel-icon { width: 28px; height: 28px; border-radius: 6px; display: flex; align-items: center; justify-content: center; background: var(--aqua-soft); color: var(--ocean); flex-shrink: 0; }
    .dash-panel-icon svg { width: var(--icon-sm); height: var(--icon-sm); }

    /* The messages panel fills its column so it stands level with the queue
       beside it. It was roughly half the height, which read as the smaller
       concern -- and it is not: an unanswered customer is the most expensive
       thing on this screen. */
    .dash-panel-col { display: flex; flex-direction: column; }
    .dash-panel-stretch { flex: 1; min-height: 0; }
    .dash-panel-stretch .dash-panel-body { flex: 1; min-height: 0; overflow-y: auto; }
    /* Only email is called out. Text is the norm here, so labelling every SMS
       would be noise on the majority to inform about the minority. */
    /* ── System health ──────────────────────────────────────────────────
       Borrows .goals-block wholesale so the two collapsibles on this page are
       the same control, not two that merely look alike. Set apart only by
       sitting last and by a heavier top rule -- it is the same app, not a
       monitoring tool bolted on. */
    .sys-health { margin-top: var(--space-8, 32px); border-top-width: 2px; }
    /* A LIST, not a wrapped pill row (2026-09-27). Rows are ranked worst-first
       and read down a numeral column, which a flex-wrap grid cannot express. */
    .sys-health .sys-health-body { display: none; padding: .35rem 0 0; }
    .sys-health.open .sys-health-body { display: block; }
    .sys-health .chev { transition: transform var(--duration-base, .15s); }
    .sys-health.open .chev { transform: rotate(180deg); }

    /* The verdict, readable while collapsed. */
    .sys-health-summary { font-size: var(--fs-xs); font-weight: 700; margin-left: auto;
                          margin-right: .5rem; }
    .sys-health-summary.ok      { color: var(--status-success-text); }
    .sys-health-summary.never   { color: var(--app-text-secondary, var(--text-muted)); }
    .sys-health-summary.warn    { color: var(--status-warning-text); }
    .sys-health-summary.bad     { color: var(--status-danger-text); }
    .sys-health-summary.unknown { color: var(--status-warning-text); }

    .sys-dot { width: .45rem; height: .45rem; border-radius: 999px; flex: 0 0 auto; }
    /* 'never' is NEUTRAL, not amber. A job waiting on its first trigger is a
       fact, not a fault, and crying wolf is how a person learns to skip the
       strip entirely. */
    .sys-dot.ok      { background: var(--status-success); }
    .sys-dot.never   { background: var(--app-text-secondary, #7a8a95); }
    .sys-dot.warn    { background: var(--status-warning); }
    .sys-dot.bad     { background: var(--status-danger); }
    .sys-dot.unknown { background: var(--status-warning); }

    /* ONE ROW PER PROBLEM. The severity is carried by a left rule rather than a
       filled pill: eight tinted pills made a busy day look like confetti, and
       the rule lines up down the left edge so the worst block is one shape. */
    .sh-row {
      display: flex; align-items: baseline; gap: .75rem; width: 100%;
      text-align: left; padding: .7rem 1rem; background: none; border: 0;
      border-left: 3px solid transparent; font: inherit; cursor: pointer;
    }
    .sh-row:hover { background: var(--app-neutral-bg); }
    .sh-row.bad     { border-left-color: var(--status-danger); }
    .sh-row.warn    { border-left-color: var(--status-warning); }
    .sh-row.never   { border-left-color: #c3ced6; }
    .sh-row.unknown { border-left-color: var(--status-warning); }

    /* Right-aligned and tabular so the figures line up as a column you can read
       straight down without reading the sentences at all. */
    .sh-num { flex: 0 0 2.6rem; text-align: right; font-size: 1.35rem;
              font-weight: 700; font-variant-numeric: tabular-nums; line-height: 1.1; }
    .sh-row.bad     .sh-num { color: var(--status-danger-text); }
    .sh-row.warn    .sh-num { color: var(--status-warning-text); }
    .sh-row.never   .sh-num,
    .sh-row.unknown .sh-num { color: var(--app-text-neutral); }

    /* No count to show. Drawn rather than typed: the obvious character for
       "no value" is an em dash, which the UI voice lint forbids in anything a
       person reads -- and an HTML entity would render identically while
       walking straight past the lint's literal match, which is the trap the
       entity rule exists for. A rule stays enforceable when the thing it bans
       never enters the string in the first place. */
    .sh-num-none::before { content: ''; display: inline-block; width: 1rem;
                           height: 2px; border-radius: 1px;
                           background: var(--app-border-soft, var(--border));
                           vertical-align: middle; }

    .sh-copy { flex: 1 1 auto; line-height: 1.4; min-width: 0; }
    .sh-say  { font-size: var(--fs-sm); font-weight: 600; display: block;
               color: var(--app-text-primary); }
    .sh-sub  { font-size: var(--fs-xs); color: var(--text-muted); display: block;
               margin-top: 1px; }
    .sh-go   { flex: 0 0 auto; font-size: var(--fs-xs); color: var(--app-text-secondary);
               opacity: 0; transition: opacity var(--duration-base, .15s); }
    .sh-row:hover .sh-go, .sh-row:focus-visible .sh-go { opacity: 1; }

    /* The healthy checks, collapsed to one line. */
    .sh-clear {
      display: flex; align-items: center; gap: .5rem; width: 100%; text-align: left;
      padding: .6rem 1rem .65rem; background: none;
      border: 0; border-top: 1px solid var(--app-border-soft, var(--border));
      font: inherit; font-size: var(--fs-xs); color: var(--app-text-secondary);
      cursor: pointer;
    }
    .sh-clear:hover { color: var(--app-text-primary); }
    /* On an all-clear day this line IS the block, so it carries the verdict's
       colour and sits alone with no rule above it. */
    .sh-allclear { border-top: 0; padding: .9rem 1rem; font-size: var(--fs-sm);
                   font-weight: 600; color: var(--status-success-text); }
    .sh-allclear:hover { color: var(--status-success-text); }

    .sh-rest { display: none; padding: .15rem 0 .35rem;
               border-top: 1px solid var(--app-border-soft, var(--border)); }
    .sh-rest.open { display: block; }
    .sh-okrow {
      display: flex; align-items: baseline; gap: .75rem; width: 100%;
      text-align: left; padding: .45rem 1rem; background: none; border: 0;
      font: inherit; font-size: var(--fs-xs); color: var(--app-text-neutral);
      cursor: pointer;
    }
    .sh-okrow:hover { background: var(--app-neutral-bg); }
    .sh-okrow .sys-dot { transform: translateY(-1px); }
    .sh-okname { flex: 1 1 auto; }
    .sh-okval  { color: var(--app-text-secondary); font-weight: 600; }

    /* Detail, in the drawer every other record opens in. */
    #money-drawer .sys-detail-head { display: flex; align-items: center; gap: .5rem;
                                     margin-bottom: var(--space-3, 12px); }
    #money-drawer .sys-detail-state { font-weight: 700; }
    #money-drawer .sys-detail-body { font-size: var(--fs-sm);
                                     line-height: var(--lh-base, 1.55);
                                     margin-bottom: var(--space-4, 16px); }

    .dash-sms-chan {
      font-size: var(--fs-eyebrow); font-weight: 700; letter-spacing: .04em;
      text-transform: uppercase; padding: .05rem .35rem; border-radius: 999px;
      background: var(--status-info-soft); color: var(--status-info-text);
    }
    .dash-panel-title { font-size: var(--fs-md); font-weight: 800; color: var(--cerulean); font-family: var(--font-display); letter-spacing: -.2px; }
    .dash-panel-badge { display: inline-flex; align-items: center; justify-content: center; min-width: 20px; height: 20px; padding: 0 6px; border-radius: 10px; font-size: var(--fs-xs); font-weight: 700; background: #fdeaea; color: var(--status-danger-text); }
    .dash-panel-badge.green { background: #e6f5ee; color: #1b8a5a; }
    .dash-panel-badge.amber { background: #fef3e0; color: #a05a00; }
    .dash-panel-badge.blue { background: var(--aqua-soft); color: var(--ocean); }
    .dash-panel-badge.neutral { background: var(--aqua-soft); color: var(--cerulean); }
    .dash-panel-action { font-size: var(--fs-xs); font-weight: 600; color: var(--ocean); text-decoration: none; background: none; border: none; cursor: pointer; padding: 0; font-family: var(--font-sans); }
    .dash-panel-action:hover { color: var(--cerulean); }
    .dash-panel-body { flex: 1; }
    .dash-empty-state { padding: 2rem 1.25rem; text-align: center; color: var(--text-muted); font-size: var(--fs-xs); }

    .dash-visit-row { display: flex; align-items: flex-start; gap: .875rem; padding: .875rem 1.25rem; border-bottom: 1px solid var(--border); transition: background .12s; }
    .dash-visit-row:last-child { border-bottom: none; }
    .dash-visit-row:hover { background: var(--aqua-soft); }
    .dash-visit-time { font-size: var(--fs-eyebrow); font-weight: 700; color: var(--text-muted); text-transform: uppercase; letter-spacing: .04em; min-width: 32px; padding-top: 2px; }
    .dash-visit-info { flex: 1; min-width: 0; }
    .dash-visit-name { font-size: var(--fs-sm); font-weight: 600; color: var(--text-strong); }
    .dash-visit-service { font-size: var(--fs-xs); color: var(--text-muted); margin-top: 2px; }
    .dash-visit-address { font-size: var(--fs-xs); color: var(--text-muted); margin-top: 1px; }
    .dash-visit-email { font-size: var(--fs-xs); color: var(--text-muted); margin-top: 1px; }
    .dash-visit-actions { display: flex; gap: .375rem; margin-top: .5rem; }
    .dash-visit-status { font-size: var(--fs-xs); font-weight: 600; padding: 3px 8px; border-radius: 4px; white-space: nowrap; align-self: flex-start; }
    .dash-visit-status.pending, .dash-visit-status.reserved { background: #fef3e0; color: #a05a00; }
    .dash-visit-status.confirmed { background: #e6f5ee; color: #1b8a5a; }
    .dash-visit-status.completed { background: var(--aqua-soft); color: var(--cerulean); }
    .dash-visit-status.canceled { background: #fdeaea; color: var(--status-danger-text); }
    /* ── Visits tab (happy status + disposition) ─────────────────────────── */
    .dash-visit-outcome-btns { display: flex; gap: .375rem; margin-top: .25rem; }
    .dash-outcome-happy-set { display: flex; align-items: center; gap: .5rem; flex-wrap: wrap; }

    .dash-upcoming-row { display: flex; align-items: center; gap: .875rem; padding: .75rem 1.25rem; border-bottom: 1px solid var(--border); }
    .dash-upcoming-row:last-child { border-bottom: none; }
    .dash-upcoming-date { font-size: var(--fs-xs); font-weight: 700; color: var(--text-muted); min-width: 64px; }
    .dash-upcoming-name { font-size: var(--fs-xs); font-weight: 500; color: var(--text-strong); flex: 1; min-width: 0; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
    .dash-upcoming-service { font-size: var(--fs-xs); color: var(--text-muted); white-space: nowrap; }

    .dash-balance-row { display: flex; align-items: center; gap: .875rem; padding: .875rem 1.25rem; border-bottom: 1px solid var(--border); cursor: pointer; transition: background .12s; }
    .dash-balance-row:last-child { border-bottom: none; }
    .dash-balance-row:hover { background: var(--aqua-soft); }
    .dash-balance-info { flex: 1; min-width: 0; }
    .dash-balance-name { font-size: var(--fs-sm); font-weight: 600; color: var(--text-strong); }
    .dash-balance-service { font-size: var(--fs-xs); color: var(--text-muted); margin-top: 2px; }
    .dash-balance-amount { font-size: var(--fs-lg); font-weight: 700; color: #c2610c; font-variant-numeric: tabular-nums; white-space: nowrap; }
    .dash-balance-right { display: flex; flex-direction: column; align-items: flex-end; }

    .dash-task-row { display: flex; align-items: flex-start; gap: .75rem; padding: .875rem 1.25rem; border-bottom: 1px solid var(--border); }
    .dash-task-row:last-child { border-bottom: none; }
    .dash-task-type-dot { width: 8px; height: 8px; border-radius: 50%; margin-top: 5px; flex-shrink: 0; }
    .dash-task-type-dot.red { background: var(--status-danger); }
    .dash-task-type-dot.amber { background: #a05a00; }
    .dash-task-type-dot.blue { background: var(--ocean); }
    .dash-task-info { flex: 1; min-width: 0; }
    .dash-task-label { font-size: var(--fs-xs); font-weight: 500; color: var(--text-strong); line-height: 1.4; }
    .dash-task-meta { font-size: var(--fs-xs); color: var(--text-muted); margin-top: 2px; }
    .dash-task-done-btn { font-size: var(--fs-xs); font-weight: 600; color: var(--ocean); background: none; border: none; cursor: pointer; font-family: var(--font-sans); padding: 0; white-space: nowrap; align-self: center; }
    .dash-task-done-btn:hover { color: var(--cerulean); }

    .dash-sms-row { display: flex; align-items: flex-start; gap: .875rem; padding: .875rem 1.25rem; border-bottom: 1px solid var(--border); cursor: pointer; transition: background .12s; }
    .dash-sms-row:last-child { border-bottom: none; }
    .dash-sms-row:hover { background: var(--aqua-soft); }
    .dash-sms-avatar { width: 34px; height: 34px; border-radius: 50%; background: var(--aqua-soft); color: var(--cerulean); font-size: var(--fs-xs); font-weight: 700; display: flex; align-items: center; justify-content: center; flex-shrink: 0; }
    .dash-sms-info { flex: 1; min-width: 0; }
    .dash-sms-header { display: flex; align-items: baseline; justify-content: space-between; gap: .5rem; }
    .dash-sms-name { font-size: var(--fs-xs); font-weight: 600; color: var(--text-strong); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; min-width: 0; }
    .dash-sms-time { font-size: var(--fs-xs); color: var(--text-muted); white-space: nowrap; }
    .dash-sms-preview { font-size: var(--fs-xs); color: var(--text-muted); margin-top: 3px; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
    .sms-equip-form { margin-top: .625rem; }
    .sms-equip-form-inner { background: var(--aqua-soft); border-radius: 10px; padding: .75rem; }
    .sms-equip-input { width: 100%; box-sizing: border-box; font-size: var(--fs-xs); padding: .35rem .55rem; border: 1px solid var(--border); border-radius: 6px; background: var(--white); color: var(--text-strong); }

    .dash-fin-strip { display: grid; grid-template-columns: repeat(3, 1fr); }
    .dash-fin-cell { padding: .875rem 1.25rem; border-right: 1px solid var(--border); text-align: center; }
    .dash-fin-cell:last-child { border-right: none; }
    .dash-fin-label { font-size: var(--fs-eyebrow); font-weight: 600; color: var(--text-muted); text-transform: uppercase; letter-spacing: .06em; }
    .dash-fin-value { font-size: var(--fs-xl); font-weight: 700; color: var(--text-strong); margin-top: .25rem; font-variant-numeric: tabular-nums; }
    .dash-fin-sub { font-size: var(--fs-xs); color: var(--text-muted); margin-top: .25rem; }

    /* ── Dashboard: financial hero bar ──────────────────────────────────── */
    .dash-fin-hero { background: var(--white); border: 1px solid var(--border); border-radius: 14px; box-shadow: var(--shadow-sm); margin-bottom: 1rem; display: grid; grid-template-columns: repeat(3, 1fr); overflow: hidden; }
    .dash-fin-hero-cell { padding: 1rem 1.25rem; border-right: 1px solid var(--border); text-align: center; }
    .dash-fin-hero-cell:last-child { border-right: none; }
    .dash-fin-hero-cell.clickable { cursor: pointer; transition: background .12s; }
    .dash-fin-hero-cell.clickable:hover { background: var(--aqua-soft); }
    .dash-fin-hero-label { font-size: var(--fs-eyebrow); font-weight: 600; color: var(--text-muted); text-transform: uppercase; letter-spacing: .06em; }
    .dash-fin-hero-value { font-size: var(--fs-xl); font-weight: 700; color: var(--text-strong); margin-top: .25rem; font-variant-numeric: tabular-nums; }
    .dash-fin-hero-sub { font-size: var(--fs-xs); color: var(--text-muted); margin-top: .25rem; }
    .dash-fin-hero-drill { font-size: var(--fs-eyebrow); font-weight: 600; color: var(--ocean); margin-top: .3rem; text-transform: uppercase; letter-spacing: .05em; }

    /* ── Dashboard: priority strip ─────────────────────────────────────────── */
    .dash-priority-strip-right { display: flex; align-items: center; gap: .75rem; }
    .dash-priority-caught-up { padding: .875rem 1.25rem; font-size: var(--fs-xs); color: #1b8a5a; font-weight: 500; display: flex; align-items: center; gap: .5rem; }
    .dash-priority-caught-up svg { width: var(--icon-sm); height: var(--icon-sm); }
    /* Dismiss X on hover */
    .dash-priority-item { position: relative; }
    .dash-priority-dismiss { position: absolute; top: .5rem; right: .5rem; background: none; border: none; cursor: pointer; color: var(--aqua-soft); padding: .125rem; display: flex; align-items: center; opacity: 0; transition: opacity .12s; }
    .dash-priority-item:hover .dash-priority-dismiss { opacity: 1; }
    .dash-priority-dismiss:hover { color: var(--text-muted); }
    .dash-priority-dismiss svg { width: var(--icon-md); height: var(--icon-md); }

    /* ── Dashboard: tab bar ──────────────────────────────────────────────── */

    /* ── Dashboard: right column stacking ───────────────────────────────── */
    .dash-panel-col { display: flex; flex-direction: column; gap: 1rem; }
    .dash-upcoming-row { cursor: pointer; transition: background .12s; }
    .dash-upcoming-row:hover { background: var(--aqua-soft); }

    /* ── Dashboard: drawers ──────────────────────────────────────────────── */
    .dash-drawer-overlay { position: fixed; inset: 0; background: rgba(6,40,64,.35); z-index: 200; display: flex; justify-content: flex-end; align-items: stretch; }
    .dash-drawer { width: min(480px, 96vw); background: var(--white); box-shadow: var(--shadow-md); display: flex; flex-direction: column; overflow: hidden; }
    .dash-drawer-head { display: flex; align-items: center; justify-content: space-between; padding: 1.25rem 1.5rem; border-bottom: 1px solid var(--border); }
    .dash-drawer-title { font-size: var(--fs-md); font-weight: 600; color: var(--text-strong); }
    .dash-drawer-close { background: none; border: none; cursor: pointer; color: var(--text-muted); padding: .25rem; display: flex; align-items: center; }
    .dash-drawer-close svg { width: var(--icon-md); height: var(--icon-md); }
    .dash-drawer-body { flex: 1; overflow-y: auto; padding: 1.25rem 1.5rem; }
    .dash-drawer-footer { border-top: 1px solid var(--border); padding: 1rem 1.5rem; display: flex; gap: .75rem; }
    .dash-d-field { margin-bottom: 1rem; }
    .dash-d-label { display: block; font-size: var(--fs-eyebrow); font-weight: 700; color: var(--text-muted); text-transform: uppercase; letter-spacing: .08em; margin-bottom: .4rem; }
    .dash-d-value { font-size: var(--fs-sm); color: var(--text-strong); font-weight: 500; }
    .dash-d-input { width: 100%; padding: .65rem .9rem; border: 1px solid var(--border); border-radius: 6px; font-family: var(--font-sans); font-size: var(--fs-sm); color: var(--text-strong); background: var(--white); }
    .dash-d-input:focus { outline: none; border-color: var(--ocean); box-shadow: 0 0 0 3px rgba(26,159,196,.12); }
    .dash-d-select { appearance: none; background-image: var(--tf-select-chevron); background-repeat: no-repeat; background-position: right .75rem center; padding-right: 2rem; }
    .dash-d-divider { height: 1px; background: var(--border); margin: 1.25rem 0; }
    .dash-d-note { font-size: var(--fs-xs); color: var(--text-muted); line-height: 1.5; padding: .75rem 1rem; background: var(--aqua-soft); border-radius: 6px; border-left: 3px solid var(--ocean); }
    .dash-decision-prompt { display: flex; flex-direction: column; gap: .625rem; margin: 1rem 0; }
    .dash-decision-icon { width: 32px; height: 32px; border-radius: 6px; display: flex; align-items: center; justify-content: center; flex-shrink: 0; background: var(--aqua-soft); color: var(--cerulean); }
    .dash-decision-icon svg { width: var(--icon-sm); height: var(--icon-sm); }
    .dash-decision-label { font-size: var(--fs-sm); font-weight: 600; color: var(--text-strong); }
    .dash-decision-sub { font-size: var(--fs-xs); color: var(--text-muted); margin-top: 2px; line-height: 1.4; }
    .dash-overbook-warn { background: #fef3e0; border: 1px solid rgba(160,90,0,.2); border-radius: var(--radius); padding: .875rem 1rem; display: flex; gap: .75rem; align-items: flex-start; margin-top: 1rem; }
    .dash-overbook-warn svg { width: var(--icon-md); height: var(--icon-md); color: #a05a00; flex-shrink: 0; margin-top: 1px; }
    .dash-overbook-warn-text { font-size: var(--fs-xs); color: #a05a00; line-height: 1.5; }
    .dash-overbook-confirm { display: flex; align-items: center; gap: .5rem; margin-top: .625rem; }
    .dash-overbook-confirm input[type="checkbox"] { width: 16px; height: 16px; accent-color: #a05a00; cursor: pointer; }
    .dash-overbook-confirm label { font-size: var(--fs-xs); font-weight: 600; color: #a05a00; cursor: pointer; }
    .dash-resync-row { display: flex; align-items: center; justify-content: space-between; padding: .75rem 1rem; background: var(--aqua-soft); border-radius: 6px; margin-top: 1rem; }
    .dash-resync-text { font-size: var(--fs-xs); color: var(--text-strong); }
    .dash-resync-text strong { font-weight: 600; }
    .dash-bal-amount-large { font-size: var(--fs-2xl); font-weight: 700; color: #c2610c; font-variant-numeric: tabular-nums; }
    .dash-bal-locked-note { background: #fef3e0; border: 1px solid rgba(160,90,0,.2); border-radius: var(--radius); padding: .75rem 1rem; font-size: var(--fs-xs); color: #a05a00; line-height: 1.5; }

    /* ── Responsive ─────────────────────────────────────────────────────── */
    @media (max-width: 520px) {
      .appt-card { grid-template-columns: 1fr; }
      .appt-actions { flex-direction: row; justify-self: start; flex-wrap: wrap; }
      header { padding: 0 1rem; }
      main { padding: 1.75rem 1rem 3rem; }
      .header-label { display: none; }
      .nr-card { grid-template-columns: 1fr; }
      .nr-actions { flex-direction: row; flex-wrap: wrap; min-width: 0; }
    }

    /* ── Flagged Customers (needs_review) ──────────────────────────────────── */
    .nr-card {
      background: var(--white);
      border: 1px solid var(--border);
      border-left: 3px solid var(--saffron);
      border-radius: var(--radius);
      padding: 1.1rem 1.5rem;
      margin-bottom: .85rem;
      box-shadow: var(--shadow-sm);
      display: grid;
      grid-template-columns: 1fr auto;
      gap: 1rem;
      align-items: start;
    }
    .nr-pair-group { margin-bottom: 1.25rem; }
    .nr-pair-label {
      font-size: var(--fs-eyebrow);
      font-weight: 700;
      text-transform: uppercase;
      letter-spacing: .1em;
      color: var(--red);
      margin-bottom: .5rem;
      padding-left: .25rem;
    }
    .nr-pair-group .nr-card { border-left-color: var(--red); margin-bottom: .5rem; }
    .nr-pair-group .nr-card:last-child { margin-bottom: 0; }

    .nr-top { display: flex; align-items: center; gap: .6rem; margin-bottom: .6rem; flex-wrap: wrap; }
    .nr-cust-name {
      font-family: var(--font-display);
      font-weight: 700;
      font-size: var(--fs-md);
      color: var(--text-strong);
    }
    .nr-flag-pill {
      display: inline-block;
      font-size: var(--fs-eyebrow);
      font-weight: 700;
      text-transform: uppercase;
      letter-spacing: .07em;
      padding: .18rem .6rem;
      border-radius: 999px;
    }
    .nr-flag-phone-merge      { background: #fef3c7; color: #78350f; }
    .nr-flag-stripe-collision { background: var(--red-light); color: var(--red); }
    .nr-flag-no-contact       { background: #f3f4f6; color: #374151; }
    /* sql/828 split one guessed label into five real ones. An orphaned Stripe
       collision (twin already resolved) is not the alarm a live pair is, so it
       reads neutral rather than red. 'unknown' and 'manual' are deliberately
       the quietest: they ask for a glance, not a decision. */
    .nr-flag-stripe-orphan    { background: #f3f4f6; color: #374151; }
    .nr-flag-manual           { background: #f3f4f6; color: #374151; }
    .nr-flag-unknown          { background: #f3f4f6; color: #6b7280; }
    /* One line of plain English under the pill, saying what is being asked for.
       Sized below the field rows so it reads as a caption on the card rather
       than competing with the customer's name. */
    .nr-reason-help { font-size: .78rem; line-height: 1.45; color: var(--text-muted);
                      margin: .3rem 0 .55rem; }

    .nr-fields { display: flex; flex-direction: column; gap: .2rem; font-size: var(--fs-xs); }
    .nr-field-row { display: flex; gap: .6rem; line-height: 1.55; }
    .nr-field-label {
      color: var(--text-muted);
      min-width: 72px;
      flex-shrink: 0;
      font-weight: 600;
      font-size: var(--fs-xs);
      text-transform: uppercase;
      letter-spacing: .04em;
      padding-top: .05rem;
    }
    .nr-field-val { color: var(--text); word-break: break-all; }
    .nr-stripe-ids { font-size: var(--fs-xs); color: var(--text-muted); font-family: monospace; }

    .nr-actions { display: flex; flex-direction: column; gap: .5rem; min-width: 100px; align-self: start; }

    .nr-inline-edit {
      margin-top: .85rem;
      padding-top: .85rem;
      border-top: 1px solid var(--border);
    }
    .nr-edit-field { margin-bottom: .6rem; }
    .nr-edit-label {
      display: block;
      font-size: var(--fs-xs);
      font-weight: 600;
      color: var(--text-muted);
      margin-bottom: .25rem;
      text-transform: uppercase;
      letter-spacing: .04em;
    }
    .nr-edit-input {
      width: 100%;
      padding: .5rem .75rem;
      border: 1px solid var(--border);
      border-radius: 8px;
      font-family: var(--font-sans);
      font-size: var(--fs-sm);
      color: var(--text-strong);
      background: var(--bg);
      transition: border-color .15s;
    }
    .nr-edit-input:focus { outline: none; border-color: var(--ocean); box-shadow: 0 0 0 3px rgba(26,159,196,0.12); }
    .nr-edit-actions { display: flex; gap: .6rem; margin-top: .75rem; }
    .nr-edit-error { margin-top: .5rem; font-size: var(--fs-xs); color: var(--red); }

    /* ── Dispatch calendar board ────────────────────────────────────────── */
    /* The board used to be `height: 100vh; overflow: hidden`, which pinned it to
       exactly one screen and CLIPPED anything past it. That was survivable while
       the day route was always open at a fixed size, and became a real bug once
       it collapsed: expanding the route added ~380px that simply could not be
       reached — the board refused to grow and its own overflow was hidden, so the
       scrollable ancestor (.app-main) never received anything to scroll.

       Now: min-height fills the screen so a light week still looks right, but the
       board GROWS past it and .app-main scrolls normally. */
    .dispatch-board {
      display: flex; flex-direction: column; min-height: 100vh;
      background: var(--bg);
    }
    .dispatch-topbar {
      display: flex; align-items: center; justify-content: space-between;
      padding: 14px 20px 12px; background: var(--white);
      border-bottom: 1px solid var(--border); flex-shrink: 0; gap: 12px; flex-wrap: wrap;
    }
    .dispatch-topbar-left {
      display: flex; align-items: center; gap: 8px;
    }
    .dispatch-nav-btn {
      width: 32px; height: 32px; border-radius: 8px; border: 1px solid var(--border);
      background: var(--white); color: var(--cerulean); font-size: var(--fs-md);
      cursor: pointer; display: flex; align-items: center; justify-content: center;
      transition: all .12s; flex-shrink: 0;
    }
    .dispatch-nav-btn:hover { background: var(--aqua-soft); border-color: var(--ocean); }
    .dispatch-week-label { text-align: center; min-width: 140px; }
    .dispatch-week-of {
      display: block; font-size: var(--fs-eyebrow); font-weight: 700; letter-spacing: .12em;
      text-transform: uppercase; color: var(--text-muted); line-height: 1.2;
    }
    .dispatch-week-range {
      font-family: var(--font-display); font-weight: 700; font-size: var(--fs-sm);
      color: var(--text-strong); line-height: 1.3;
    }
    .dispatch-topbar-right { display: flex; align-items: center; gap: 12px; }
    .dispatch-view-toggle {
      display: flex; background: var(--aqua-soft); border-radius: 8px;
      padding: 3px; gap: 2px;
    }
    .dispatch-view-btn {
      padding: 4px 12px; border-radius: 6px; border: none; background: transparent;
      font-family: var(--font-sans); font-size: var(--fs-xs); font-weight: 600;
      color: var(--text-muted); cursor: pointer; transition: all .12s;
    }
    .dispatch-view-btn.active {
      background: var(--white); color: var(--cerulean);
      box-shadow: 0 1px 3px rgba(6,40,64,.1);
    }
    .dispatch-unassigned-badge {
      display: inline-flex; align-items: center; gap: 6px;
      padding: 5px 12px; border-radius: 20px; border: none;
      font-family: var(--font-sans); font-size: var(--fs-xs); font-weight: 700;
      cursor: pointer; transition: all .12s; flex-shrink: 0;
      background: #d97706; color: #fff;
    }
    .dispatch-unassigned-badge:hover { filter: brightness(1.08); }
    .dispatch-unassigned-badge.all-clear {
      background: var(--aqua-soft); color: var(--text-muted); cursor: default;
      font-weight: 600;
    }
    .dispatch-unassigned-badge.all-clear:hover { filter: none; }

    .dispatch-tech-bar {
      display: flex; align-items: center; gap: 10px;
      padding: 8px 20px; background: var(--white); border-bottom: 1px solid var(--border);
      flex-shrink: 0;
    }
    .dispatch-tech-bar-label {
      font-size: var(--fs-eyebrow); font-weight: 700; letter-spacing: .1em; text-transform: uppercase;
      color: var(--text-muted); flex-shrink: 0;
    }
    .dispatch-tech-tabs {
      display: flex; gap: 4px;
    }
    .dispatch-tech-tab {
      padding: 5px 12px; border-radius: 20px; border: 1px solid var(--border);
      background: var(--white); font-family: var(--font-sans); font-size: var(--fs-xs);
      font-weight: 600; color: var(--text-muted); cursor: pointer; transition: all .12s;
    }
    .dispatch-tech-tab.active {
      /* Same white-on-ocean 3.09:1 as the primary button above, same fix. */
      background: var(--tf-ocean-700); border-color: var(--tf-ocean-700); color: #fff;
    }
    .dispatch-tech-tab:hover:not(.active) { background: var(--aqua-soft); border-color: var(--ocean); }

    /* Day view: navigation strip. Shown whenever day view is active, on any
       screen -- the view is a real view, not a mobile hack. */
    .dispatch-daynav {
      display: flex; align-items: center; gap: 10px;
      padding: 10px 14px; background: var(--white);
      border-bottom: 1px solid var(--border); flex-shrink: 0;
    }
    .dispatch-daynav-label {
      flex: 1; min-width: 0; text-align: center; line-height: 1.2;
    }
    .dispatch-daynav-label .dn-day {
      display: block; font-size: var(--fs-eyebrow); font-weight: 700;
      text-transform: uppercase; letter-spacing: .08em; color: var(--text-muted);
    }
    .dispatch-daynav-label .dn-date {
      display: block; font-size: var(--fs-base); font-weight: 700; color: var(--text-strong);
      font-family: var(--font-display);
    }
    .dispatch-daynav-label .dn-count {
      display: block; font-size: var(--fs-xs); color: var(--text-muted); font-weight: 600;
    }
    /* The Day BUTTON is phone-only: on a desktop the week grid is the better
       default and a third toggle would just be clutter. */
    .dispatch-view-day { display: none; }

    /* ONE ROW, never two. A wrapped legend pushes the calendar grid down by a
       whole line, and it wrapped on a desktop as soon as a week carried five or
       six services. nowrap plus overflow-x means a very narrow window scrolls
       the strip sideways instead of stealing a row from the calendar, and the
       items themselves never break mid-label. */
    .dispatch-legend {
      display: flex; align-items: center; gap: 16px; flex-wrap: nowrap;
      overflow-x: auto; scrollbar-width: none;
      padding: 6px 20px; background: var(--white); border-top: 1px solid var(--border);
      border-bottom: 1px solid var(--border); flex-shrink: 0;
    }
    .dispatch-legend::-webkit-scrollbar { display: none; }
    .dispatch-legend-item {
      display: inline-flex; align-items: center; gap: 5px; flex: 0 0 auto;
      white-space: nowrap;
      font-family: var(--font-sans); font-size: var(--fs-xs); font-weight: 600;
      color: var(--text-muted);
    }
    .dispatch-legend-dot {
      width: 8px; height: 8px; border-radius: 50%; flex-shrink: 0;
    }

    /* Calendar grid */
    /* Calendar is capped at roughly 9 appointment rows; the day detail below
       gets the rest of the viewport. */
    /* No max-height: a busy week used to scroll inside a 396px window, so the
       bottom of a full day was invisible and the day route below it got pushed
       off-screen entirely. The grid now shows in full and the page scrolls. */
    .dispatch-grid-wrap {
      flex: 0 0 auto; overflow-x: auto;
    }
    .dispatch-grid {
      display: grid;
      /* Sunday is a HALF track. We do not schedule work on the Lord's Day, so the
         column only has to announce itself — it does not need room for stops. The
         width it gives back is spread across the six working days, which is what
         stops customer names truncating at this viewport. minmax(0,..) on the six
         is deliberate: a bare 1fr means minmax(auto,1fr) and text-overflow never
         fires on a nowrap child. */
      grid-template-columns: minmax(64px, .5fr) repeat(6, minmax(112px, 1fr));
      gap: 0; min-height: 180px;
    }
    .dispatch-grid.dm-month { display: block; }
    .dispatch-day-col {
      border-right: 1px solid var(--border); display: flex; flex-direction: column;
      border-top: 3px solid transparent;
    }
    .dispatch-day-col:last-child { border-right: none; }
    .dispatch-day-col.today { background: rgba(26,159,196,.06); border-top-color: var(--ocean); }
    .dispatch-day-col.selected { background: rgba(201,169,110,.10); }
    .dispatch-day-col.today.selected { background: rgba(26,159,196,.13); }

    /* ── Sunday / the Lord's Day ──────────────────────────────────────────
       A core value, so the board should state it rather than quietly leaving a
       column empty. Warm sand + saffron reads as "set apart", where a grey
       disabled column would read as "broken" or "no data loaded". */
    .dispatch-day-col.sabbath {
      /* Deepened 2026-09-14 (Meesha): the original two-stop #fdf6e9 -> --sand was
         so close to the page ground that the column read as washed out rather
         than set apart, which is the opposite of the point. Same warm family on
         purpose — the intent above is right, it was only pitched too quietly —
         but a third gradient stop for depth, a true saffron top rule at 4px, and
         faint inset edges so it reads as a deliberate panel with a left and a
         right, not as an empty gap before Monday. */
      background: linear-gradient(180deg, #fbf0dc 0%, #f2e0c2 55%, #ecd5ae 100%);
      border-top-color: #b8893f;
      border-top-width: 4px;
      box-shadow: inset -1px 0 0 rgba(184,137,63,.30), inset 1px 0 0 rgba(184,137,63,.30);
    }
    .dispatch-day-col.sabbath .dispatch-day-header { border-bottom-color: rgba(184,137,63,.55); }
    .dispatch-day-col.sabbath .dispatch-day-header:hover { background: rgba(184,137,63,.20); }
    .dispatch-day-col.sabbath .dispatch-day-name,
    .dispatch-day-col.sabbath .dispatch-day-num { color: #6b4a18; }
    .dispatch-sabbath-note {
      flex: 1; display: flex; flex-direction: column; align-items: center;
      justify-content: center; gap: 4px; padding: 14px 6px; text-align: center;
    }
    .dispatch-sabbath-note .mark {
      width: var(--icon-lg); height: var(--icon-lg); border-radius: 50%;
      display: grid; place-items: center; color: #6b4a18;
      background: rgba(255,255,255,.92); border: 1px solid rgba(184,137,63,.65);
      box-shadow: 0 1px 3px rgba(107,74,24,.18);
    }
    .dispatch-sabbath-note .mark svg { width: var(--icon-sm); height: var(--icon-sm); }
    .dispatch-sabbath-note .lbl {
      font-size: var(--fs-eyebrow); font-weight: 700; text-transform: uppercase;
      letter-spacing: .09em; color: #6b4a18; line-height: var(--lh-tight);
    }
    .dispatch-sabbath-note .sub {
      font-size: var(--fs-xs); color: #8a6a35; line-height: var(--lh-snug);
    }
    /* If a stop ever DOES land on a Sunday the lanes render as normal and this
       banner says so — the column must never hide real work. */
    .dispatch-sabbath-warn {
      margin: 6px; padding: 6px 8px; border-radius: var(--radius-sm);
      background: var(--status-warning-soft); border: 1px solid var(--status-warning-border);
      color: var(--status-warning-text); font-size: var(--fs-xs);
      font-weight: 700; line-height: var(--lh-snug); text-align: center;
    }
    .dispatch-day-header {
      padding: 8px 10px 6px; border-bottom: 1px solid var(--border);
      text-align: center; background: transparent; position: sticky; top: 0; z-index: 2;
      cursor: pointer; transition: background .1s;
    }
    .dispatch-day-header:hover { background: var(--aqua-soft); }
    .dispatch-day-name {
      font-size: var(--fs-eyebrow); font-weight: 700; text-transform: uppercase;
      letter-spacing: .1em; color: var(--text-muted); line-height: 1;
    }
    .dispatch-day-num {
      font-family: var(--font-display); font-size: var(--fs-lg); font-weight: 700;
      color: var(--text-strong); line-height: 1.2; margin-top: 1px;
    }
    .dispatch-day-header.today .dispatch-day-num {
      color: var(--ocean);
    }
    /* ── AM / PM lanes ────────────────────────────────────────────────────
       A day is TWO booking windows, not one list. Each lane carries its own tag,
       its own remaining-hours readout and its own fill bar, because "is Thursday
       full?" is never a question about the day — it is a question about a block.
       The PM lane takes a faint wash so the split reads at a glance.

       Capacity is measured in HOURS (appointments.slot_cost), not appointment
       count: Pool School and Green Pool Cleanup each cost 2, so three bookings
       can fill a four-hour window. Counting rows gives the wrong answer. */
    .dispatch-lane { border-bottom: 1px solid var(--border); }
    .dispatch-lane:last-child { border-bottom: none; }
    .dispatch-lane.pm { background: rgba(6,40,64,.022); }
    .dispatch-lane-hdr { display: flex; align-items: center; gap: 6px; padding: 5px 8px 4px; }
    .dispatch-lane-tag {
      font-size: var(--fs-xs); font-weight: 800; letter-spacing: .07em; color: var(--text-muted);
      background: rgba(6,40,64,.06); border-radius: 4px; padding: 1px 5px;
    }
    .dispatch-lane.pm .dispatch-lane-tag { background: rgba(6,40,64,.10); }
    .dispatch-lane-room { margin-left: auto; font-size: var(--fs-xs); font-weight: 700; color: #0f7a3d; }
    .dispatch-lane-room.full { color: var(--status-warning-text); }
    /* A block we do not staff (Saturday afternoon). Reads as muted and inert
       rather than green-and-available, because the green free-count is what
       invited someone to treat it as a normal open window. Placed after .full
       so a closed lane can never also paint as full -- equal specificity is
       decided by source order. */
    .dispatch-lane-room.closed {
      color: var(--text-muted); font-weight: 700; text-transform: lowercase;
    }
    .dispatch-lane.isclosed { background: rgba(6,40,64,.045); }
    .dispatch-lane.isclosed .dispatch-lane-tag { opacity: .65; }
    .dispatch-lane.isclosed .dispatch-lane-bar { background: #e9eef1; }
    .dispatch-lane-bar {
      height: 3px; border-radius: 999px; background: #e3ecf0; margin: 0 8px 5px; overflow: hidden;
    }
    .dispatch-lane-fill { height: 100%; border-radius: 999px; background: var(--ocean); }
    .dispatch-lane.isfull .dispatch-lane-fill { background: var(--warning); }
    .dispatch-lane-body {
      padding: 0 6px 7px; display: flex; flex-direction: column; gap: 4px; min-height: 26px;
    }
    .dispatch-lane-none {
      font-size: var(--fs-xs); color: var(--text-muted); opacity: .55; padding: 1px 3px 4px; font-style: italic;
    }

    .dispatch-pill {
      display: flex; align-items: center; gap: 5px; padding: 5px 7px;
      border-radius: 7px; background: #0e8fa0; color: #fff; position: relative;
      border-left: 3px solid transparent;
      font-size: var(--fs-xs); line-height: 1.3; cursor: pointer;
      transition: opacity .15s, filter .15s, transform .1s;
    }
    .dispatch-pill:hover { filter: brightness(1.07); transform: translateX(1px); }
    /* Live field status no longer REPLACES the service colour — it would throw
       away the only thing that says what the job is. Completed dims, in-progress
       and on-my-way get a ring, so both facts survive on one pill. */
    .dispatch-pill.status-completed   { opacity: .5; }
    .dispatch-pill.status-in-progress { box-shadow: 0 0 0 2px var(--success); }
    .dispatch-pill.status-omw         { box-shadow: 0 0 0 2px var(--warning); }
    /* min-width, not width. This was a fixed 17px square sized for its only two
       possible contents, "•" and "2h". A quick check's "15m" is three characters
       and overflowed the square onto the customer name — visible as the badge
       colliding with the text. Growing horizontally keeps the square shape for
       the short cases and simply widens for the longer one. */
    .dispatch-pill-badge {
      min-width: 17px; height: 17px; border-radius: 5px; background: rgba(255,255,255,.28);
      font-size: var(--fs-xs); font-weight: 700; display: flex; align-items: center;
      justify-content: center; flex-shrink: 0; padding: 0 3px;
    }
    /* display:block matters — these are <div>s here but were <span>s in the
       prototype; keeping it explicit so min-width/ellipsis cannot silently stop
       working if the tag ever changes. */
    .dispatch-pill-info { display: block; min-width: 0; flex: 1; overflow: hidden; }
    .dispatch-pill-name {
      display: block; font-weight: 600; white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
    }
    .dispatch-pill-svc {
      display: block; font-size: var(--fs-xs); opacity: .82; white-space: nowrap; overflow: hidden;
      text-overflow: ellipsis;
    }
    /* The typed reason is deliberately NOT a third line on the week pill. Tried
       it: every follow-up pill grew from ~41px to ~61px, which changes row
       density across the whole board to explain a minority of stops. It rides in
       the pill's title tooltip instead, and shows in full on the three surfaces
       with room for it — day view, the appointments card, and the detail drawer. */
    .dispatch-stop-reason {
      font-size: var(--fs-xs); color: var(--text-muted); font-style: italic;
      margin: .1rem 0 .15rem;
    }
    .dispatch-pill-due {
      position: absolute; top: -4px; right: -4px; background: var(--warning); color: #fff;
      font-size: var(--fs-xs); font-weight: 800; border-radius: 999px; padding: 1px 5px;
      box-shadow: var(--shadow-sm); border: 1.5px solid var(--white);
    }
    /* Marks the BOOKING that was confirmed over the block's hour budget.
       Sits opposite the balance chip so the two can never overlap on one pill.
       Deliberately louder than the muted palette: an override is a decision
       someone made on purpose, and the whole point is that it stopped being
       visible the moment it was made. */
    .dispatch-pill-over {
      position: absolute; top: -4px; left: -4px; background: var(--danger, #c0392b); color: #fff;
      font-size: var(--fs-xs); font-weight: 800; line-height: 1; border-radius: 999px;
      min-width: 14px; height: 14px; display: flex; align-items: center; justify-content: center;
      box-shadow: var(--shadow-sm); border: 1.5px solid var(--white);
    }
    /* Focus mode: the recurring route recedes so the non-routine work is what
       your eye lands on. 24 of 26 upcoming visits are the same weekly route, so
       colouring everything at equal strength just rebuilds Google Calendar.

       The recede is done by TINTING the fill in pillHTML (JS), not by fading the
       pill here — a faded pill takes its own text down with it and made the
       customer names the least readable thing on the board. Do not reintroduce
       an opacity/filter rule on .routine; it would undo that and fight the tint. */
    .dispatch-focus .dispatch-pill.routine:hover { filter: brightness(.96); }
    .dispatch-focus .dispatch-pill.exception { box-shadow: 0 0 0 2px rgba(6,40,64,.16); }
    .dispatch-pill.search-miss { opacity: .12 !important; filter: grayscale(1) !important; }
    .dispatch-day-empty {
      padding: 10px 8px; font-size: var(--fs-xs); color: var(--text-muted);
      text-align: center;
    }

    /* Day detail + map row */
    /* This row SIZES TO ITS CONTENT and lets the page scroll. It used to be
       `flex: 1 1 auto; min-height: 380px; overflow: hidden`, which is three locks
       at once: it took its height from the flex parent rather than its own stops,
       it could never be shorter than 380px, and it CLIPPED anything taller. A
       seven-stop day therefore scrolled inside a fixed box instead of extending
       the page — which is what "the board doesn't fully grow" was. */
    .dispatch-detail-row {
      display: flex; gap: 0; border-top: 2px solid var(--border);
      background: var(--white); flex: 0 0 auto; align-items: stretch;
    }
    /* The map has no intrinsic height (its canvas is position:absolute inset:0),
       so it needs one declared or it collapses to zero once the row stops being
       stretched by the parent. Sticky so it stays put while a long stop list
       scrolls past it. */
    .dispatch-live-map {
      flex: 0 0 42%; min-width: 340px; border-right: 1px solid var(--border);
      position: relative; overflow: hidden;
      height: 460px; position: sticky; top: 0; align-self: flex-start;
    }
    #dispatch-map-canvas { width: 100%; height: 100%; }
    .dispatch-map-loading {
      position: absolute; inset: 0; display: flex; align-items: center;
      justify-content: center; background: #d8e8ec;
      font-size: var(--fs-xs); color: #4d7d93; font-style: italic;
    }
    /* Grows with its stops; the PAGE scrolls, not this box. Was overflow-y:auto,
       which hid a long route inside a fixed-height panel. */
    .dispatch-day-detail { flex: 1; min-width: 0; padding: 14px 18px; }
    .dispatch-detail-header {
      display: flex; align-items: baseline; gap: 10px; margin-bottom: 12px; flex-wrap: wrap;
    }
    .dispatch-detail-date {
      font-family: var(--font-display); font-size: var(--fs-base); font-weight: 700;
      color: var(--text-strong);
    }
    .dispatch-detail-meta {
      font-size: var(--fs-xs); color: var(--text-muted);
    }
    .dispatch-tech-cols { display: flex; gap: 18px; }
    .dispatch-tech-col { flex: 1; min-width: 0; }
    .dispatch-tech-col-name {
      font-size: var(--fs-eyebrow); font-weight: 700; text-transform: uppercase; letter-spacing: .1em;
      color: var(--ocean); margin-bottom: 8px;
      display: flex; align-items: center; justify-content: space-between; gap: 6px;
    }
    /* Was 10px in --ocean (#1a9fc4) on --aqua-soft (#e4f7fc) — cyan on pale cyan,
       about 2.3:1, which is barely legible. Now 12.5px in --cerulean on the same
       wash (~7:1) with real padding, so it reads as a button instead of a hint. */
    .dispatch-optimize-btn {
      font-family: var(--font-sans);
      font-size: var(--fs-xs); font-weight: 700; letter-spacing: .01em;
      background: var(--aqua-soft); color: var(--cerulean);
      border: 1px solid var(--ocean);
      border-radius: 7px; padding: 6px 14px; cursor: pointer; white-space: nowrap;
      text-transform: none; transition: background .15s, border-color .15s;
    }
    .dispatch-optimize-btn:hover:not(:disabled) { background: #c8e8f0; border-color: var(--cerulean); }
    /* Disabled must still be READABLE, just clearly inactive — a 50%-opacity
       button on a light background is neither. Muted ink on a flat grey instead. */
    .dispatch-optimize-btn:disabled {
      background: #eef2f4; color: #4e6675; border-color: var(--border);
      cursor: default; opacity: 1;
    }
    .dispatch-block { margin-bottom: 12px; }
    .dispatch-block-label {
      font-size: var(--fs-eyebrow); font-weight: 700; text-transform: uppercase; letter-spacing: .08em;
      color: var(--text-muted); padding: 3px 0 5px;
      border-bottom: 1px solid var(--border); margin-bottom: 6px;
    }
    .dispatch-stop {
      display: flex; align-items: flex-start; gap: 8px; padding: 6px 8px;
      border-bottom: 1px solid rgba(6,40,64,.05); border-radius: 8px;
      cursor: pointer; transition: background .1s;
    }
    .dispatch-stop:hover { background: var(--aqua-soft); }
    .dispatch-stop:last-child { border-bottom: none; }
    /* Drag-to-reorder. grab only on rows that are actually draggable: an
       unassigned stop has no technician, and save_route_order refuses a set of
       stops that does not share one, so offering the affordance there would be
       offering a gesture that cannot succeed. */
    .dispatch-stop[draggable="true"] { cursor: grab; }
    .dispatch-stop[draggable="true"]:active { cursor: grabbing; }
    .dispatch-stop.dragging { opacity: .45; cursor: grabbing; background: var(--aqua-soft); }
    /* Live progress state, per appointment (mirrors the calendar legend) */
    .dispatch-stop-live {
      display: inline-flex; align-items: center; gap: 4px; margin-left: 6px;
      font-size: var(--fs-eyebrow); font-weight: 700; text-transform: uppercase; letter-spacing: .05em;
      color: var(--text-muted); vertical-align: middle;
    }
    .dispatch-stop-live-dot {
      width: 7px; height: 7px; border-radius: 50%; flex-shrink: 0;
    }
    .dispatch-stop-num {
      width: 22px; height: 22px; border-radius: 50%; background: var(--ocean);
      color: #fff; font-size: var(--fs-xs); font-weight: 700; display: flex;
      align-items: center; justify-content: center; flex-shrink: 0; margin-top: 1px;
    }
    .dispatch-stop-body { flex: 1; min-width: 0; }
    .dispatch-stop-svc { font-weight: 600; font-size: var(--fs-xs); color: var(--text-strong); }
    .dispatch-stop-customer { font-size: var(--fs-xs); color: var(--text-muted); }
    .dispatch-stop-addr { font-size: var(--fs-xs); color: var(--text-muted); opacity: .75; }
    .dispatch-stop-email { font-size: var(--fs-xs); color: var(--text-muted); opacity: .75; }
    .dispatch-stop-status {
      font-size: var(--fs-eyebrow); font-weight: 700; text-transform: uppercase; letter-spacing: .05em;
      padding: 2px 6px; border-radius: 999px; margin-top: 3px; display: inline-block;
    }
    .dispatch-status-confirmed { background: #d1fae5; color: #065f46; }
    .dispatch-status-reserved  { background: #fef3c7; color: #78350f; }
    .dispatch-status-completed { background: #f0f4f8; color: #4d7d93; }
    .dispatch-status-recurring { background: #e0f2fe; color: #0369a1; }
    .dispatch-block-empty {
      font-size: var(--fs-xs); color: var(--text-muted); padding: 4px 0; font-style: italic;
    }
    .dispatch-stop-assign {
      display: flex; align-items: center; gap: 5px; margin-top: 4px;
    }
    .dispatch-stop-assign label {
      font-size: var(--fs-eyebrow); font-weight: 700; text-transform: uppercase; letter-spacing: .06em;
      color: var(--text-muted);
    }
    .dispatch-assign-select {
      font-family: var(--font-sans); font-size: var(--fs-xs); font-weight: 600;
      padding: 2px 20px 2px 7px; border-radius: 999px; border: 1px solid var(--border);
      background: var(--white); color: var(--text-strong); cursor: pointer;
      appearance: none; -webkit-appearance: none;
      background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='8' height='8' viewBox='0 0 8 8'%3E%3Cpath fill='%234d7d93' d='M0 2l4 4 4-4z'/%3E%3C/svg%3E");
      background-repeat: no-repeat; background-position: right 6px center;
    }
    .dispatch-assign-select:disabled { opacity: .6; cursor: default; }
    .dispatch-tech-col-name .dispatch-tech-dot,
    .dispatch-stop-tech-dot {
      display: inline-block; width: 8px; height: 8px; border-radius: 50%;
      margin-right: 5px; vertical-align: middle;
    }

    /* ── Today strip ──────────────────────────────────────────────────────
       Answers "what is happening today" without a click. The live line says
       "no activity recorded" rather than "not started", because Geoff not
       tapping PoolCrew and Geoff not having started look identical from here —
       299 field_appointment_state rows against far fewer recorded starts — and
       reporting them as the same thing would be a confident wrong answer. */
    .dispatch-today-strip {
      display: flex; align-items: center; gap: 15px; flex-wrap: wrap;
      background: linear-gradient(100deg, var(--cerulean), #0d6080); color: #fff;
      padding: 13px 17px; font-size: var(--fs-xs);
    }
    .dispatch-ts-day { font-family: var(--font-display); font-weight: 700; font-size: var(--fs-base); }
    .dispatch-ts-sum { opacity: .92; }
    .dispatch-ts-live {
      display: flex; align-items: center; gap: 7px; background: rgba(255,255,255,.14);
      border-radius: 999px; padding: 5px 12px; font-size: var(--fs-xs); font-weight: 600;
    }
    .dispatch-ts-dot { width: 8px; height: 8px; border-radius: 50%; background: rgba(255,255,255,.5); }
    .dispatch-ts-dot.active { background: #7ee2a8; }
    .dispatch-ts-next { margin-left: auto; font-size: var(--fs-xs); opacity: .9; }

    /* ── Focus toggle + Today button + search ─────────────────────────────── */
    .dispatch-focus-toggle {
      display: flex; align-items: center; gap: 8px; font-size: var(--fs-xs);
      color: var(--text-muted); cursor: pointer; user-select: none;
    }
    .dispatch-switch {
      width: 36px; height: 20px; border-radius: 20px; background: #cbd9e0;
      position: relative; transition: background .18s; flex-shrink: 0;
    }
    .dispatch-switch::after {
      content: ""; position: absolute; top: 2px; left: 2px; width: 16px; height: 16px;
      border-radius: 50%; background: #fff; box-shadow: var(--shadow-sm); transition: transform .18s;
    }
    .dispatch-focus-toggle.on .dispatch-switch { background: var(--ocean); }
    .dispatch-focus-toggle.on .dispatch-switch::after { transform: translateX(16px); }
    .dispatch-today-btn {
      padding: 0 12px; height: 30px; background: var(--white); border: 1px solid var(--border);
      border-radius: 8px; cursor: pointer; font-family: var(--font-sans);
      font-size: var(--fs-xs); font-weight: 600; color: var(--cerulean);
    }
    .dispatch-today-btn:hover { background: var(--aqua-soft); border-color: var(--ocean); }
    .dispatch-search { position: relative; }
    .dispatch-search input {
      width: 200px; padding: 7px 10px 7px 30px; border: 1px solid var(--border);
      border-radius: 8px; font-family: var(--font-sans); font-size: var(--fs-xs);
      color: var(--text); background: var(--white);
    }
    .dispatch-search input:focus {
      outline: none; border-color: var(--ocean); box-shadow: 0 0 0 3px rgba(26,159,196,.12);
    }
    .dispatch-search svg { position: absolute; left: 9px; top: 8px; color: var(--text-muted); }

    /* ── Month grid ───────────────────────────────────────────────────────
       One AM row and one PM row of colour bars per day, each bar sized to the
       share of the block's hour budget it actually consumes. Bars must NEVER
       flex to fill the track, or a single one-hour visit renders exactly as
       busy as a full four-hour morning. */
    .dispatch-mgrid { display: grid; grid-template-columns: repeat(7, minmax(0, 1fr)); }
    .dispatch-mhead {
      padding: 8px 4px; text-align: center; font-size: var(--fs-eyebrow); font-weight: 700;
      letter-spacing: .08em; text-transform: uppercase; color: var(--text-muted);
      border-bottom: 1px solid var(--border); background: #fbfdfe;
    }
    .dispatch-mcell {
      border-right: 1px solid var(--border); border-bottom: 1px solid var(--border);
      min-height: 104px; padding: 5px 6px 7px; cursor: pointer;
      display: flex; flex-direction: column; gap: 3px;
    }
    .dispatch-mcell:nth-child(7n+7) { border-right: none; }
    .dispatch-mcell:hover { background: var(--aqua-soft); }
    .dispatch-mcell.out { background: #fbfbfa; opacity: .5; }
    .dispatch-mcell.today { background: rgba(26,159,196,.07); }
    .dispatch-mcell.selected { background: rgba(201,169,110,.13); }
    .dispatch-mnum { font-family: var(--font-display); font-weight: 700; font-size: var(--fs-xs); color: var(--navy); }
    .dispatch-mcell.today .dispatch-mnum { color: var(--ocean); }
    .dispatch-mrow { display: flex; align-items: center; gap: 4px; }
    .dispatch-mtag {
      font-size: var(--fs-xs); font-weight: 800; letter-spacing: .05em;
      color: var(--text-muted); width: 15px; flex-shrink: 0;
    }
    .dispatch-mdots {
      display: flex; gap: 2px; flex: 1; background: #eef3f5;
      border-radius: 999px; height: 5px; overflow: hidden;
    }
    .dispatch-mdot { height: 5px; border-radius: 999px; flex: 0 0 auto; }
    .dispatch-mfull {
      font-size: var(--fs-xs); font-weight: 800; color: var(--status-warning-text); flex-shrink: 0;
    }
    .dispatch-mdue {
      margin-top: auto; font-size: var(--fs-xs); font-weight: 800; color: var(--status-warning-text);
      background: var(--warning-bg); border-radius: 5px; padding: 1px 5px; align-self: flex-start;
    }

    /* ── Charge confirm, inside the appointment panel's money card ────────
       The .appt-balance-box / -amt / -lbl / -acts / .appt-deposit-acts rules
       that used to live here went with the old modal: the side panel's money
       card (.ap-money) does that job now. Only the confirm box survives,
       because the confirm survives. */
    .appt-balance-confirm {
      margin-top: .7rem; background: var(--white); border: 1px solid #f0d08a;
      border-radius: 8px; padding: .7rem .8rem; font-size: var(--fs-xs);
    }
    .appt-balance-confirm .appt-confirm-acts {
      display: flex; gap: .5rem; margin-top: .6rem; align-items: center; }

    /* ── Appointment side panel ───────────────────────────────────────────
       Replaces the old #appt-detail-modal centre-screen dialog. Approved
       design: prototypes/appt-panel-v4.html (2026-08-19). Three things it
       fixes that the modal could not:
         * the six actions that each opened their OWN modal now open in place
           as accordion sections, so acting on a visit never loses the visit;
         * money is a permanently-expanded card at the top instead of a row
           buried in a list of facts;
         * amber is spent ONLY on genuinely overdue money. Painting merely
           expected money amber is what made the real warnings invisible.
       Ports the prototype's raw hex onto the app's own tokens so a brand
       token change reaches this panel like everything else. */
    .ap-stage { position: fixed; inset: 0; z-index: 240; display: none; justify-content: flex-end; }
    .ap-stage.open { display: flex; }
    .ap-scrim { position: absolute; inset: 0; background: rgba(6,40,64,.45); }
    .ap-panel {
      position: relative; width: min(480px, 100%); height: 100%; background: var(--bg);
      display: flex; flex-direction: column; box-shadow: -8px 0 32px rgba(6,40,64,.25);
    }
    /* The customer is the subject of this panel, so the header reads like one. */
    .ap-head {
      flex: none; padding: 1.4rem 1.4rem 1.1rem; border-bottom: 1px solid var(--border);
      display: flex; align-items: flex-start; gap: 1rem;
    }
    .ap-head-tx { min-width: 0; }
    .ap-title {
      margin: 0 0 .3rem; font-family: var(--font-display); font-size: 1.6rem;
      line-height: 1.2; color: var(--navy); font-weight: 700;
    }
    .ap-meta { font-size: .9rem; color: var(--text-muted); }
    /* The close button is the IconButton look (css/m-components.css .m-iconbtn): 44px, tinted. */
    .ap-x {
      flex: none; width: 44px; height: 44px; border-radius: 12px; border: 1px solid var(--tf-ink-200);
      background: var(--tf-aqua-050); color: var(--tf-ocean-800); cursor: pointer;
      display: flex; align-items: center; justify-content: center;
    }
    .ap-x:hover { background: var(--tf-aqua-soft); }
    .ap-x svg { width: 22px; height: 22px; stroke: currentColor; stroke-width: 2.2; fill: none; stroke-linecap: round; }
    .ap-pill {
      display: inline-block; margin-top: .5rem; font-size: .7rem; font-weight: 700;
      letter-spacing: .05em; text-transform: uppercase; padding: .22rem .55rem;
      border-radius: 999px; background: #e6f5ee; border: 1px solid #a3d9b8; color: #157a3a;
    }
    .ap-pill.warn { background: var(--status-warning-soft); border-color: #f0d08a; color: var(--status-warning-text); }
    .ap-pill.grey { background: var(--sand); border-color: var(--border); color: var(--text-muted); }
    /* In progress. Green because it is the one state that means someone is
       at the pool right now; the other three are all "not currently being
       worked". Same colour Dispatch uses for the same word. */
    .ap-pill.live { background: #e8f5ee; border-color: #bcdcca; color: #16653c; }
    .ap-body { flex: 1; overflow: auto; padding: 1.2rem 1.4rem 2rem; }

    /* Money card — the number is the point, so it is the biggest thing here. */
    .ap-money {
      border: 1px solid #cfdde4; background: #f4f8fa; border-radius: 14px;
      padding: 1rem 1.1rem; margin-bottom: 1.4rem;
    }
    .ap-money.overdue { border-color: #f0d08a; background: var(--status-warning-soft); }
    .ap-amt { font-family: var(--font-display); font-size: 2rem; line-height: 1; color: var(--navy); font-weight: 700; }
    .ap-money.overdue .ap-amt { color: var(--status-warning-text); }
    .ap-sub { font-size: .86rem; color: var(--text-muted); margin-top: .35rem; }
    .ap-money.overdue .ap-sub { color: var(--status-warning-text); opacity: .9; }

    /* The facts that used to be the whole modal body. Still here, still
       scannable, but no longer competing with the actions for attention. */
    .ap-facts {
      margin-bottom: 1.5rem; border: 1px solid var(--border); border-radius: 14px;
      background: var(--white); padding: .3rem 1rem;
    }
    .ap-fact {
      display: flex; justify-content: space-between; gap: 1rem; padding: .42rem 0;
      border-bottom: 1px solid var(--border); font-size: .86rem;
    }
    .ap-fact:last-child { border-bottom: none; }
    .ap-fact-k { color: var(--text-muted); flex: none; }
    .ap-fact-v { text-align: right; font-weight: 600; color: var(--navy); min-width: 0; word-break: break-word; }

    /* The action list is a ListRow (css/m-components.css), each row opening its own stacked
       sheet. These two rules are the only appointment-specific part: the row icons are
       drawn as strokes (AP_ICON in 05-appointment-panel.js), and a group label reads as a heading. */
    .ap-sheet .m-row-ic svg, .ap-sheet .m-btn svg, #appt-detail-actionbar .m-btn svg, .m-chip-btn svg:not(.m-row-chev) {
      stroke: currentColor; fill: none; stroke-width: 2; stroke-linecap: round; stroke-linejoin: round;
    }
    .ap-item.danger .m-row-t { color: var(--tf-coral-text); }

    /* A step's form. It used to expand in place under its row; it now fills a sheet, so there is
       no card chrome left to draw. Padding and the pinned Save row come from .m-sheet-body. */
    .ap-exp { background: transparent; }

    /* Change the service — the money summary and the SMS draft. Everything else
       in this row reuses the existing .ap-lbl / .ap-callout / .ap-row controls. */
    .ap-cv-math { background: var(--app-neutral-bg); border-radius: 8px; padding: .8rem .95rem; margin: .55rem 0 .2rem; }
    .ap-cv-row { display: flex; justify-content: space-between; align-items: baseline; font-size: var(--fs-sm); padding: .22rem 0; color: var(--text-body); }
    .ap-cv-row span:last-child { font-variant-numeric: tabular-nums; font-weight: 600; }
    .ap-cv-total { border-top: 1px solid var(--app-border-soft); margin-top: .35rem; padding-top: .45rem; font-weight: 700; color: var(--text-strong); }
    .ap-cv-msg { position: relative; padding-left: 1.75rem; margin-top: .6rem; }
    .ap-cv-n { position: absolute; left: 0; top: .45rem; width: 1.3rem; height: 1.3rem; border-radius: 50%; background: var(--ocean); color: var(--white); font-size: .68rem; font-weight: 700; display: flex; align-items: center; justify-content: center; }
    .ap-cv-foot { font-size: var(--fs-xs); color: var(--text-muted); margin-top: .2rem; }
    .ap-exp label.ap-lbl {
      display: block; font-size: .82rem; font-weight: 700; color: var(--navy);
      margin: 1rem 0 .35rem; line-height: 1.3;
    }
    .ap-exp .ap-lbl:first-child { margin-top: .6rem; }
    .ap-hint { font-size: .82rem; color: var(--text-muted); line-height: 1.45; margin: 0 0 .55rem; }

    /* ── The visit's recorded outcome ──────────────────────────────────────
       Sits at the top of the "How did the visit go?" row, above the two
       actions, because it is the ANSWER to that row's question and the actions
       below it only make sense once you have read it. */
    .ap-outcome { margin: 0 0 .9rem; }
    .ap-outcome-line { display: flex; align-items: center; gap: .5rem; flex-wrap: wrap;
                       margin-bottom: .45rem; }
    .ap-happy-chip { font-size: var(--fs-xs); font-weight: 700; padding: .22rem .6rem;
                     border-radius: var(--radius-pill); border: 1px solid; }
    .ap-happy-chip.happy      { background: var(--status-success-soft);
                                color: var(--status-success-text);
                                border-color: var(--status-success-border); }
    .ap-happy-chip.in_between { background: var(--status-warning-soft);
                                color: var(--status-warning-text);
                                border-color: var(--status-warning-border); }
    .ap-happy-chip.unhappy    { background: var(--status-danger-soft);
                                color: var(--status-danger-text);
                                border-color: var(--status-danger-border); }
    /* Neutral, not amber. An unrated visit is a FACT, not a fault -- the same
       rule System Health applies to a job that has never run. */
    .ap-happy-chip.unset      { background: var(--app-neutral-bg);
                                color: var(--app-text-neutral);
                                border-color: var(--border); }
    .ap-outcome-note { font-size: var(--fs-xs); color: var(--app-text-neutral);
                       background: var(--app-neutral-bg); border-radius: var(--radius-sm);
                       padding: .5rem .65rem; margin: 0 0 .55rem; line-height: 1.5; }
    .ap-exp input[type="text"], .ap-exp input[type="date"], .ap-exp input[type="number"],
    .ap-exp input[type="time"],
    .ap-exp select, .ap-exp textarea {
      /* 1rem, not .92rem: iOS Safari zooms the whole page when a field under 16px takes
         focus, and stays zoomed. 48px tall for a thumb. Same on desktop, one look. */
      width: 100%; font: inherit; font-size: 1rem; padding: .7rem .8rem; min-height: 48px; box-sizing: border-box;
      border: 1px solid var(--tf-ink-300); border-radius: 12px; background: var(--white);
      color: var(--text-strong); font-family: var(--font-sans);
      transition: border-color .15s;
    }
    .ap-exp textarea { min-height: 96px; }
    .ap-exp input:focus, .ap-exp select:focus, .ap-exp textarea:focus {
      outline: none; border-color: var(--ocean); box-shadow: var(--tf-focus-ring);
    }
    /* Native select chrome is the one control that does NOT inherit the house
       look from padding and borders alone -- without appearance:none it keeps the
       OS arrow, which is what made these read as unstyled next to the dashboard's
       own drawers. Same chevron, same offsets, now from the shared token. */
    .ap-exp select {
      appearance: none; -webkit-appearance: none;
      background-image: var(--tf-select-chevron);
      background-repeat: no-repeat; background-position: right .75rem center;
      padding-right: 2rem;
    }
    /* The app's checkbox accent is scoped to .field, which this panel does not
       use, so its boxes were rendering in the browser default blue. */
    .ap-exp input[type="checkbox"], .ap-exp input[type="radio"],
    .ap-check input, .ap-cov input[type="checkbox"] { accent-color: var(--ocean); cursor: pointer; }
    .ap-exp textarea { resize: vertical; line-height: 1.5; }
    /* Static notes in a form. Same look as the shared .m-callout (css/m-components.css), written
       here because these are authored as markup strings, not built with MCallout. A small "i" or
       "!" marks the tone; .warn is the one that means "you have something to do". */
    .ap-callout {
      display: flex; gap: .7rem; align-items: flex-start; margin: .9rem 0; padding: .75rem .9rem;
      border: 1px solid var(--status-info-border); border-radius: 14px;
      background: var(--status-info-soft); color: var(--status-info-text);
      font-size: .9375rem; line-height: 1.45;
    }
    .ap-callout::before {
      content: "i"; flex: none; width: 24px; height: 24px; border-radius: 50%;
      display: flex; align-items: center; justify-content: center;
      background: var(--status-info); color: var(--white); font-weight: 800; font-size: .85rem; font-style: normal;
    }
    .ap-callout.warn { border-color: var(--status-warning-border); background: var(--status-warning-soft); color: var(--status-warning-text); }
    .ap-callout.warn::before { content: "!"; background: var(--status-warning); }
    .ap-unlinked { margin-bottom: .35rem; }
    .ap-uslot { margin-top: .5rem; }
    .ap-row { display: flex; gap: .55rem; flex-wrap: wrap; align-items: center; }
    .ap-row .ap-btn { flex: 1 1 auto; }
    /* Save, pinned. The first top-level row in a step that holds a primary or danger button is
       marked .ap-pin by openApptDetail, and stays at the bottom of the sheet while the form
       scrolls, so the button is never below the fold on a phone. */
    .ap-exp .ap-row.ap-pin {
      position: sticky; bottom: 0; z-index: 1; margin: 1rem -16px 0; padding: .75rem 16px;
      padding-bottom: calc(.75rem + env(safe-area-inset-bottom, 0px));
      background: var(--white); border-top: 1px solid var(--tf-ink-100);
    }
    /* A status is a banner, not a footnote. Anything written by apSay() shows as one, red when it
       went wrong, and sits ABOVE the buttons so it is read before the next tap. It collapses when empty. */
    .ap-status {
      order: -1; flex: 1 1 100%; display: block; padding: .65rem .8rem; border-radius: 12px;
      border: 1px solid var(--status-info-border); background: var(--status-info-soft); color: var(--status-info-text);
      font-size: .9375rem; line-height: 1.4;
    }
    .ap-status:empty { display: none; }
    .ap-status.bad { border-color: var(--status-danger-border); background: var(--status-danger-soft); color: var(--status-danger-text); font-weight: 600; }
    .ap-check { display: flex; gap: .7rem; align-items: center; min-height: 44px; padding: .4rem 0; font-size: .9375rem; cursor: pointer; }
    .ap-check input { width: 24px; height: 24px; flex: none; }

    /* ── Drawer buttons: 48px tall, one colour per meaning (same palette on every width) ──
       primary = ocean, secondary = aqua wash, money = navy, danger = coral. These win over the
       shared button block near the top of this file by coming later; every .ap-btn is in this drawer. */
    .ap-btn { min-height: 48px; border-radius: 12px; font-size: 1rem; white-space: normal; text-align: center; }
    .ap-btn-primary { background: var(--tf-ocean-800); border-color: var(--tf-ocean-800); color: var(--white); }
    .ap-btn-primary:hover { background: var(--tf-ocean-700); border-color: var(--tf-ocean-700); color: var(--white); }
    .ap-btn-secondary { background: var(--tf-aqua-050); border-color: var(--tf-ink-200); color: var(--tf-ocean-800); }
    .ap-btn-secondary:hover { background: var(--tf-aqua-soft); border-color: var(--tf-ocean-800); color: var(--tf-ocean-800); }
    .ap-btn-money { background: var(--tf-navy); border-color: var(--tf-navy); color: var(--white); }
    .ap-btn-money:hover { background: var(--tf-ink-700); border-color: var(--tf-ink-700); color: var(--white); }
    .ap-btn-danger { background: var(--tf-coral-bg); border-color: var(--tf-coral); color: var(--tf-coral-text); }
    .ap-btn-danger:hover { background: var(--tf-coral-bg); border-color: var(--tf-coral-text); color: var(--tf-coral-text); }
    /* Tap-for-definition. The label says it in the customer's words; the ⓘ
       carries the sentence that explains which message it actually is. */
    .ap-cov { display: flex; gap: .7rem; align-items: center; min-height: 44px; padding: .4rem 0; font-size: .9375rem; }
    .ap-cov input[type="checkbox"] { width: 24px; height: 24px; flex: none; }
    .ap-cov .ap-i {
      flex: none; width: 44px; height: 44px; border-radius: 50%; border: 1px solid var(--tf-ink-200);
      background: var(--tf-aqua-050); color: var(--tf-ocean-800); font-size: .9rem; font-weight: 700;
      cursor: pointer; padding: 0; font-family: var(--font-sans);
    }
    .ap-cov .ap-def {
      display: none; font-size: .8rem; color: var(--text-muted); line-height: 1.45;
      margin-top: .35rem; padding: .5rem .65rem; background: #f3f7f9; border-radius: 8px;
    }
    .ap-cov.open .ap-def { display: block; }
    .ap-cov-t { font-size: .88rem; font-weight: 600; color: var(--navy); cursor: pointer; }
    .ap-notes-list { display: flex; flex-direction: column; gap: .5rem; }
    .ap-note {
      padding: .55rem .7rem; background: var(--sand); border-radius: 8px;
      font-size: .84rem; white-space: pre-wrap; color: var(--text-strong);
    }
    .ap-note-hd { color: var(--text-muted); font-size: .76rem; margin-bottom: .2rem; }
    /* Under 861px the visit panel is a bottom sheet, like every sheet it opens (css/m-components.css).
       From 861px up it stays the right-hand drawer, and the sheets it opens are right-hand
       drawers too, so desktop and phone read as one design. */
    @media (max-width: 860px) {
      .ap-stage { align-items: flex-end; }
      .ap-panel {
        width: 100%; height: auto; min-height: 40dvh; max-height: 92dvh;
        border-radius: 20px 20px 0 0; box-shadow: var(--shadow-md);
        animation: ap-rise .26s cubic-bezier(.32, .72, 0, 1);
      }
      .ap-panel::before { content: ""; flex: none; width: 40px; height: 5px; border-radius: 3px; background: var(--tf-ink-200); margin: 10px auto 0; }
      .ap-head { padding: .7rem 1rem .8rem; }
      .ap-title { font-size: 1.35rem; }
      .ap-body { padding: .9rem 1rem 1rem; }
    }
    @keyframes ap-rise { from { transform: translateY(100%); } to { transform: none; } }
    @media (prefers-reduced-motion: reduce) { .ap-panel { animation: none; } }
    /* The pinned bar under the visit panel's body (Collect, Reschedule, More). */
    #appt-detail-actionbar:empty { display: none; }

    /* ── Collapsible day-route header ─────────────────────────────────────── */
    .dispatch-detail-hdr {
      display: flex; align-items: center; gap: 12px; padding: 12px 16px;
      border-top: 2px solid var(--border); background: var(--white);
    }
    .dispatch-detail-date {
      font-family: var(--font-display); font-weight: 700; font-size: var(--fs-sm); color: var(--navy);
    }
    .dispatch-detail-count { font-size: var(--fs-xs); color: var(--text-muted); }
    /* The map is REMOVED, not greyed, when a day has no stops — an empty grey
       panel reading "No stops today" was spending 380px to say nothing. */
    .dispatch-live-map.no-stops { display: none; }
    .dispatch-block-room { font-weight: 700; color: #0f7a3d; letter-spacing: 0; text-transform: none; }
    .dispatch-block-room.full { color: var(--status-warning-text); }
    .dispatch-stop-due {
      font-size: var(--fs-xs); font-weight: 700; color: var(--status-warning-text);
      background: var(--warning-bg); border-radius: 6px; padding: 2px 7px;
      white-space: nowrap; margin-left: auto;
    }

    /* ══════════════════════════════════════════════════════════════════
       MOBILE SHELL  (<=860px)
       Below 860px the sidebar is display:none, which left a phone with no
       navigation at all. This block supplies the phone chrome: a bottom tab
       bar, a + FAB, a More sheet, and full-width panels.

       Everything here is INSIDE the media query -- the desktop layout above
       is untouched. Approved prototype: prototypes/mobile-shell-v1.html
       ══════════════════════════════════════════════════════════════════ */
    .mb-only { display: none; }

    @media (max-width: 860px) {
      :root { --mb-tabbar-h: 56px; --mb-safe-b: env(safe-area-inset-bottom, 0px); }

      .mb-only { display: block; }

      /* Give the scrolling panel area room for the fixed bar + FAB. */
      .app-main { padding-bottom: calc(var(--mb-tabbar-h) + var(--mb-safe-b)); }

      /* Compact topbar. The + New button is replaced by the FAB, so it would
         otherwise be a second entrance to the same six flows. */
      .topbar { padding: 12px 14px; gap: 10px; }
      .topbar .tb-title { font-size: var(--fs-base); }
      .tb-new-job-btn { display: none; }

      /* Any slide-over becomes full-screen once the sidebar is gone. These
         two already did so at 640px; 860 is where the desktop layout ends,
         so a tablet no longer gets a half-width panel. */
      .drawer { width: 100vw; }
      .ap-panel { width: 100vw; }

      /* ── Bottom tab bar ── */
      .mb-tabbar {
        position: fixed; left: 0; right: 0; bottom: 0; z-index: 60;
        height: calc(var(--mb-tabbar-h) + var(--mb-safe-b));
        padding-bottom: var(--mb-safe-b);
        background: rgba(255, 253, 249, .94);
        backdrop-filter: saturate(180%) blur(14px);
        -webkit-backdrop-filter: saturate(180%) blur(14px);
        border-top: 1px solid var(--border);
        display: grid; grid-template-columns: repeat(6, 1fr);
      }
      .mb-tab {
        border: none; background: none; cursor: pointer; font-family: var(--font-sans);
        display: flex; flex-direction: column; align-items: center; justify-content: center;
        gap: 2px; color: var(--text-muted); padding: 6px 0 4px; position: relative; min-width: 0;
      }
      .mb-tab svg { width: 21px; height: 21px; }
      .mb-tab .mb-lbl {
        font-size: 9.5px; font-weight: 700;
        white-space: nowrap; overflow: hidden; text-overflow: ellipsis; max-width: 100%;
      }
      .mb-tab.active { color: var(--ocean); }
      .mb-tab .mb-badge {
        position: absolute; top: 3px; left: calc(50% + 6px);
        min-width: 16px; height: 16px; padding: 0 4px; border-radius: 8px;
        background: var(--status-danger); color: #fff; font-size: 10px; font-weight: 800;
        display: flex; align-items: center; justify-content: center;
      }

      /* ── FAB: one entrance to the same six flows the + New button opens ── */
      .mb-fab {
        position: fixed; right: 16px; z-index: 61;
        bottom: calc(var(--mb-tabbar-h) + var(--mb-safe-b) + 16px);
        width: 56px; height: 56px; border-radius: 50%; border: none; cursor: pointer;
        background: var(--saffron); color: #fff; box-shadow: var(--shadow-md);
        display: flex; align-items: center; justify-content: center;
      }
      .mb-fab svg { width: 26px; height: 26px; }

      /* ── More sheet ── */
      .mb-scrim {
        position: fixed; inset: 0; z-index: 70; background: rgba(6, 40, 64, .42);
        opacity: 0; pointer-events: none; transition: opacity .2s ease;
      }
      .mb-scrim.open { opacity: 1; pointer-events: auto; }
      .mb-sheet {
        position: fixed; left: 0; right: 0; bottom: 0; z-index: 71;
        background: #fff; border-radius: 20px 20px 0 0;
        padding-bottom: calc(var(--mb-safe-b) + 10px);
        transform: translateY(102%); transition: transform .26s cubic-bezier(.32,.72,0,1);
        max-height: 82%; overflow-y: auto;
      }
      .mb-sheet.open { transform: translateY(0); }
      .mb-grab { width: 38px; height: 4px; border-radius: 2px; background: var(--border); margin: 10px auto 4px; }
      .mb-sheet-title {
        font-size: var(--fs-eyebrow); font-weight: 700; color: var(--text-muted);
        padding: 8px 16px 4px; text-transform: uppercase; letter-spacing: .05em;
      }
      .mb-sheet-row {
        display: flex; align-items: center; gap: 11px; padding: 12px 16px; min-height: 56px;
        width: 100%; border: none; border-bottom: 1px solid var(--border);
        background: #fff; text-align: left; cursor: pointer;
        font-family: var(--font-sans); font-size: var(--fs-sm); color: var(--text);
      }
      .mb-sheet-row:last-child { border-bottom: none; }
      .mb-sheet-row:active { background: var(--aqua-soft); }
      .mb-sheet-row .mb-ic {
        width: 36px; height: 36px; flex: 0 0 36px; border-radius: 9px;
        background: var(--aqua-soft); color: var(--cerulean);
        display: flex; align-items: center; justify-content: center;
      }
      .mb-sheet-row .mb-ic svg { width: 18px; height: 18px; }
      .mb-sheet-row .mb-nm { flex: 1; min-width: 0; font-weight: 600; color: var(--text-strong); }
      .mb-sheet-row .mb-ct {
        font-size: var(--fs-xs); font-weight: 700; color: var(--text-muted);
      }

      /* ── Panels whose desktop layout does not survive 390px ──────────
         Found by measuring each element's right edge against .app-main, not
         by eye: document.scrollWidth cannot detect these because the app
         clips overflow at a container, so the page never widens. ─────────── */

      /* Dashboard: a fixed 3-column money strip forced ~69px past the edge. */
      .dash-fin-hero { grid-template-columns: 1fr; }
      .dash-fin-hero-cell { border-right: none; border-bottom: 1px solid var(--border); }
      .dash-fin-hero-cell:last-child { border-bottom: none; }

      /* Schedule: the topbar's right cluster (search + view toggle + optimise)
         ran ~137px over. The topbar already wraps; its children did not. */
      .dispatch-topbar { padding: 12px 14px; }
      .dispatch-topbar-right { flex-wrap: wrap; width: 100%; gap: 8px; }
      .dispatch-topbar-right .dispatch-search { flex: 1 1 100%; }
      .dispatch-view-toggle { flex: 0 0 auto; }
      .dispatch-optimize-btn { flex: 1 1 auto; }

      /* Day view is the phone default, so its toggle button appears here. */
      .dispatch-view-day { display: block; }
      /* Stops FIRST, map after. The stop list is what the day is for; a
         420px map above it pushed every stop below the fold. column-reverse
         because the map is the first child in source order (desktop wants it
         on the left). */
      .dispatch-detail-row { flex-direction: column-reverse; }
      .dispatch-live-map { width: 100%; height: 240px; min-height: 0; }
      .dispatch-day-detail { padding: 12px 14px; }
      /* The day-nav strip already states the date and stop count, so the
         detail header directly beneath it said the same thing twice. */
      .dispatch-daynav:not([hidden]) ~ .dispatch-detail-hdr { display: none; }

      /* Reputation: bulk actions were pinned right with margin-left:auto and
         could not wrap, so the buttons ran ~78px over. */
      #panel-reputation .rp-bulk-actions { margin-left: 0; flex-wrap: wrap; width: 100%; }

      @media (prefers-reduced-motion: reduce) {
        .mb-sheet, .mb-scrim { transition: none; }
      }
    }


    /* ── Campaign panel (js/17-campaign.js) ───────────────────────────────────
       Reuses .settings-acc for the collapsible stack, so the tab reads as the
       same object as Settings. What is defined here is only what the verdict,
       the rate cards and the tables need. */
    .camp-head { display: flex; justify-content: flex-end; margin-bottom: 1rem; }
    #camp-select {
      padding: .5rem .7rem; font-size: var(--fs-sm); font-weight: 600;
      border: 1px solid var(--border); border-radius: 8px; background: var(--white);
      color: var(--app-text-primary); font-family: var(--font-sans); max-width: 100%;
    }

    /* The verdict is a sentence, not a colour. The colour is a second channel on
       top of it, so the panel still reads correctly to someone who cannot
       distinguish the two amber states. */
    .camp-verdict {
      border: 1px solid var(--border); border-left-width: 5px; border-radius: var(--radius);
      background: var(--white); box-shadow: var(--shadow-sm);
      padding: 1.1rem 1.25rem; margin-bottom: 1rem;
    }
    .camp-verdict.ok      { border-left-color: var(--ocean); }
    .camp-verdict.warn    { border-left-color: var(--warning); }
    .camp-verdict.danger  { border-left-color: var(--danger); }
    .camp-verdict.unknown { border-left-color: var(--border); }
    .camp-verdict .cv-title {
      font-size: var(--fs-md); font-weight: 800; color: var(--app-text-primary);
      display: flex; align-items: center; gap: .6rem; flex-wrap: wrap;
    }
    .camp-verdict .cv-sub {
      margin-top: .4rem; font-size: var(--fs-sm); color: var(--text-muted); line-height: 1.5;
    }

    .camp-status {
      font-size: var(--fs-xs); font-weight: 700; text-transform: uppercase;
      letter-spacing: .06em; padding: .18rem .55rem; border-radius: 999px;
      background: var(--bg); color: var(--text-muted); border: 1px solid var(--border);
    }
    .camp-status.running { background: var(--ocean); color: #fff; border-color: var(--ocean); }
    .camp-status.paused  { background: var(--warning-bg); color: var(--warning); border-color: transparent; }
    .camp-status.done    { background: var(--bg); color: var(--text-muted); }

    .camp-rates {
      display: grid; grid-template-columns: repeat(auto-fit, minmax(240px, 1fr));
      gap: 1rem; margin-bottom: 1rem;
    }
    .camp-rate-card {
      border: 1px solid var(--border); border-radius: var(--radius); background: var(--white);
      box-shadow: var(--shadow-sm); padding: 1rem 1.1rem;
    }
    .camp-rate-card .crc-label {
      font-size: var(--fs-xs); font-weight: 700; text-transform: uppercase;
      letter-spacing: .08em; color: var(--text-muted);
    }
    .camp-rate-card .crc-value { font-size: 2rem; font-weight: 800; line-height: 1.15; margin: .3rem 0 .15rem; }
    .crc-value.ok     { color: var(--ocean); }
    .crc-value.warn   { color: var(--warning); }
    .crc-value.danger { color: var(--danger); }
    .crc-value.muted  { color: var(--text-muted); }
    .camp-rate-card .crc-note { font-size: var(--fs-xs); font-weight: 600; color: var(--text-muted); }
    .camp-rate-card .crc-help {
      margin-top: .5rem; font-size: var(--fs-xs); color: var(--text-muted); line-height: 1.45;
    }

    .camp-help, .camp-note {
      font-size: var(--fs-xs); color: var(--text-muted); line-height: 1.5; margin: 0 0 1rem;
    }
    .camp-note { margin: .75rem 0 0; }

    .camp-status-row {
      display: flex; align-items: center; justify-content: space-between;
      gap: 1rem; flex-wrap: wrap; margin-bottom: 1.2rem;
    }
    .camp-btn-row { display: flex; gap: .5rem; flex-wrap: wrap; }

    .camp-grid {
      display: grid; grid-template-columns: repeat(auto-fit, minmax(220px, 1fr));
      gap: 1rem; margin-bottom: 1.1rem;
    }
    .camp-grid label {
      display: flex; flex-direction: column; gap: .3rem;
      font-size: var(--fs-xs); font-weight: 700; color: var(--app-text-primary);
    }
    .camp-grid .camp-wide { grid-column: 1 / -1; }
    .camp-grid input, .camp-grid textarea {
      padding: .5rem .7rem; font-size: var(--fs-sm); font-family: var(--font-sans);
      border: 1px solid var(--border); border-radius: 8px; background: var(--white);
      color: var(--app-text-primary); width: 100%;
    }
    .camp-grid textarea { font-family: ui-monospace, SFMono-Regular, Menlo, monospace; resize: vertical; }
    .camp-grid .camp-fh { font-size: var(--fs-xs); font-weight: 500; color: var(--text-muted); line-height: 1.4; }

    .camp-funnel-rows { display: flex; flex-direction: column; gap: .55rem; }
    .cf-row { display: grid; grid-template-columns: minmax(120px, 200px) 1fr auto; gap: .75rem; align-items: center; }
    .cf-l { font-size: var(--fs-sm); font-weight: 700; color: var(--app-text-primary); }
    .cf-l .cf-help { display: block; font-size: var(--fs-xs); font-weight: 500; color: var(--text-muted); line-height: 1.35; }
    .cf-track { height: 10px; border-radius: 20px; background: var(--bg); overflow: hidden; }
    .cf-track i { display: block; height: 100%; background: var(--ocean); }
    .cf-n { font-size: var(--fs-sm); font-weight: 800; color: var(--app-text-primary); white-space: nowrap; }
    .cf-n .cf-drop { font-size: var(--fs-xs); font-weight: 700; color: var(--danger); margin-left: .4rem; }

    /* Wide tables scroll inside their own box; the panel itself never scrolls
       sideways. */
    .camp-table-wrap { overflow-x: auto; }
    .camp-table { width: 100%; border-collapse: collapse; font-size: var(--fs-sm); }
    .camp-table th {
      text-align: left; font-size: var(--fs-xs); font-weight: 700; text-transform: uppercase;
      letter-spacing: .06em; color: var(--text-muted); padding: .5rem .6rem;
      border-bottom: 1px solid var(--border); white-space: nowrap;
    }
    .camp-table td { padding: .55rem .6rem; border-bottom: 1px solid var(--border); vertical-align: top; }
    .camp-table td.camp-best { color: var(--ocean); font-weight: 800; }
    .camp-table td.camp-warn { color: var(--warning); font-weight: 700; }
    .camp-table td.camp-bad  { color: var(--danger);  font-weight: 700; }
    .cp-em { font-size: var(--fs-xs); color: var(--text-muted); word-break: break-word; }
    .cp-ct { font-size: var(--fs-xs); color: var(--text-muted); }
    .cp-pill {
      display: inline-block; font-size: var(--fs-xs); font-weight: 700;
      padding: .15rem .5rem; border-radius: 999px; background: var(--bg);
      color: var(--text-muted); white-space: nowrap;
    }
    .cp-pill.good { background: var(--ocean); color: #fff; }
    .cp-pill.warn { background: var(--warning-bg); color: var(--warning); }
    .cp-pill.bad  { background: var(--danger-bg);  color: var(--danger); }

    .camp-people-bar {
      display: flex; gap: .6rem; flex-wrap: wrap; align-items: center; margin-bottom: 1rem;
    }
    .camp-filters { display: flex; gap: .35rem; flex-wrap: wrap; }
    .camp-fbtn {
      padding: .35rem .8rem; font-size: var(--fs-xs); font-weight: 600;
      border: 1px solid var(--border); border-radius: 999px; background: var(--white);
      color: var(--app-text-primary); cursor: pointer; font-family: var(--font-sans);
    }
    .camp-fbtn.active { background: var(--ocean); color: #fff; border-color: var(--ocean); }
    #camp-people-search {
      flex: 1; min-width: 200px; padding: .5rem .7rem; font-size: var(--fs-xs);
      border: 1px solid var(--border); border-radius: 8px; font-family: var(--font-sans);
    }

    @media (max-width: 640px) {
      .cf-row { grid-template-columns: 1fr auto; }
      .cf-track { grid-column: 1 / -1; }
    }

    /* ── Campaign charts ──────────────────────────────────────────────────────
       Inline SVG plus a few flex/grid bar rigs. Every chart is one series, so
       colour here is a MAGNITUDE ramp (the brand ocean steps, monotonic in
       lightness) and never an identity code. The status hues appear only as
       threshold zones and breached points. Nothing requires telling two hues
       apart, which is what keeps these readable in greyscale and to a
       colourblind reader. */
    .viz { display: block; width: 100%; height: auto; overflow: visible; }
    .viz-bars { width: 100%; }

    .camp-cards {
      display: grid; grid-template-columns: repeat(auto-fit, minmax(260px, 1fr));
      gap: 1rem; margin-bottom: 1rem;
    }
    .camp-card {
      border: 1px solid var(--border); border-radius: var(--radius); background: var(--white);
      box-shadow: var(--shadow-sm); padding: 1rem 1.1rem;
    }
    .camp-card-wide { margin-bottom: 1.5rem; }
    .camp-card .cc-h {
      font-size: var(--fs-xs); font-weight: 700; text-transform: uppercase;
      letter-spacing: .08em; color: var(--text-muted); margin-bottom: .7rem;
    }
    .camp-card .cc-n {
      font-size: var(--fs-xs); color: var(--text-muted); line-height: 1.5; margin-top: .6rem;
    }

    .viz-meter .vm-track {
      height: 12px; border-radius: 20px; background: #e4f7fc; overflow: hidden;
    }
    .viz-meter .vm-track i { display: block; height: 100%; border-radius: 20px; }
    .viz-meter .vm-cap {
      margin-top: .45rem; font-size: var(--fs-sm); color: var(--text-muted);
    }
    .viz-meter .vm-cap strong { font-size: 1.35rem; font-weight: 800; color: var(--app-text-primary); }
    .viz-meter .vm-cap span { font-size: var(--fs-xs); }

    .camp-vgrid {
      display: grid; grid-template-columns: repeat(auto-fit, minmax(280px, 1fr));
      gap: 1rem; margin-bottom: 1rem;
    }
    .camp-vcard {
      border: 1px solid var(--border); border-radius: var(--radius); background: var(--white);
      box-shadow: var(--shadow-sm); padding: 1rem 1.1rem;
    }
    .camp-vcard .cc-h {
      font-size: var(--fs-xs); font-weight: 700; text-transform: uppercase;
      letter-spacing: .08em; color: var(--text-muted); margin-bottom: .8rem;
    }

    /* Variant bars. The value is direct-labelled above every column, so the
       chart is never the only place the number exists. */
    .camp-vbars { display: flex; gap: 1rem; align-items: flex-end; }
    .cvb { flex: 1; text-align: center; min-width: 0; }
    .cvb-col {
      height: 90px; display: flex; align-items: flex-end; justify-content: center;
      background: linear-gradient(var(--border), var(--border)) bottom / 100% 1px no-repeat;
    }
    .cvb-col i { display: block; width: 70%; max-width: 54px; border-radius: 4px 4px 0 0; }
    .cvb-v { margin-top: .4rem; font-size: var(--fs-sm); font-weight: 700; color: var(--text-muted); }
    .cvb-v.win { color: var(--app-text-primary); font-weight: 800; }
    .cvb-l { font-size: var(--fs-xs); font-weight: 800; color: var(--app-text-primary); }
    .cvb-n { font-size: var(--fs-xs); color: var(--text-muted); }

    /* Daily volume: accepted drawn in front of attempted, same hue two steps,
       so the shortfall reads as the pale remainder rather than a second series
       anyone has to decode. */
    .camp-vol {
      display: flex; gap: 3px; align-items: flex-end; overflow-x: auto; padding-bottom: .2rem;
    }
    .cvol { flex: 1 0 26px; text-align: center; min-width: 26px; }
    .cvol-col {
      height: 96px; display: flex; flex-direction: column; justify-content: flex-end;
      align-items: center; gap: 2px;   /* 2px surface gap between stacked segments */
    }
    .cvol-col i { display: block; width: 76%; border-radius: 3px; }
    .cvol-col i:last-child { border-radius: 0 0 3px 3px; }
    .cvol-col i:first-child { border-radius: 3px 3px 0 0; }
    .cvol-acc { background: #1a9fc4; }
    .cvol-ref { background: #d9694e; }
    .cvol-d {
      margin-top: .3rem; font-size: 10px; color: var(--text-muted); white-space: nowrap;
      overflow: hidden; text-overflow: ellipsis;
    }
    .camp-legend {
      display: flex; gap: 1rem; flex-wrap: wrap; margin-top: .7rem;
      font-size: var(--fs-xs); color: var(--text-muted);
    }
    .camp-legend span { display: inline-flex; align-items: center; gap: .35rem; }
    .camp-legend i { width: 11px; height: 11px; border-radius: 3px; display: inline-block; }

    .camp-subacc { margin-top: 1rem; }
    .camp-subacc > summary {
      cursor: pointer; font-size: var(--fs-xs); font-weight: 700; color: var(--ocean);
      padding: .4rem 0;
    }

    /* Status glyphs. The traffic-light triad does not survive a colourblind
       check as a SET, so every status reading on this panel carries a glyph and
       a word as well as a hue. Never remove these to tidy the layout. */
    .cv-glyph, .crc-glyph {
      display: inline-flex; align-items: center; justify-content: center;
      width: 1.15em; height: 1.15em; border-radius: 50%; margin-right: .4rem;
      font-size: .72em; font-weight: 900; color: #fff; flex: none;
    }
    .camp-verdict.ok      .cv-glyph { background: #16a34a; }
    .camp-verdict.warn    .cv-glyph { background: #d97706; }
    .camp-verdict.danger  .cv-glyph { background: #dc2626; }
    .camp-verdict.unknown .cv-glyph { background: #4d7d93; }
    .crc-value.ok     .crc-glyph { background: #16a34a; }
    .crc-value.warn   .crc-glyph { background: #d97706; }
    .crc-value.danger .crc-glyph { background: #dc2626; }
    .crc-value.muted  .crc-glyph { background: #a8c8d8; }
    .crc-value { display: flex; align-items: center; }

    /* The verdict band earns a tinted surface, not just a left rule. The brand
       is explicit that the palette is vibrant and should not be muted. */
    .camp-verdict.ok      { background: #e6f5ee; }
    .camp-verdict.warn    { background: #fef3e0; }
    .camp-verdict.danger  { background: #fee2e2; }
    .camp-verdict.unknown { background: #f2fafc; }
    .viz-meter .vm-cap .vm-note {
      display: block; font-size: var(--fs-xs); color: var(--text-muted); margin-top: .1rem;
    }

    /* ── Seed-inbox placement ─────────────────────────────────────────────────
       Status again, so again never colour alone: each chip states the provider
       and the placement in words, and the colour only reinforces them. */
    .camp-place-row { display: flex; flex-wrap: wrap; gap: .6rem; margin-bottom: 1rem; }
    .camp-place {
      flex: 1 1 150px; border: 1px solid var(--border); border-left-width: 4px;
      border-radius: 8px; padding: .6rem .75rem; background: var(--white);
    }
    .camp-place.ok     { border-left-color: #16a34a; }
    .camp-place.warn   { border-left-color: #d97706; }
    .camp-place.danger { border-left-color: #dc2626; }
    .camp-place.stale  { border-left-color: #a8c8d8; }
    .camp-place .cpl-p {
      font-size: var(--fs-xs); font-weight: 700; text-transform: uppercase;
      letter-spacing: .07em; color: var(--text-muted);
    }
    .camp-place .cpl-v { font-size: var(--fs-sm); font-weight: 800; color: var(--app-text-primary); margin: .15rem 0; }
    .camp-place.ok     .cpl-v { color: #157a3a; }
    .camp-place.danger .cpl-v { color: #b91c1c; }
    .camp-place.stale  .cpl-v { color: var(--text-muted); }
    .camp-place .cpl-d, .camp-place .cpl-n { font-size: var(--fs-xs); color: var(--text-muted); line-height: 1.4; }
    .camp-place .cpl-n { margin-top: .2rem; font-style: italic; }

    .camp-place-form {
      display: flex; flex-wrap: wrap; gap: .6rem; align-items: flex-end;
      padding-top: .9rem; border-top: 1px solid var(--border);
    }
    .camp-place-form label {
      display: flex; flex-direction: column; gap: .25rem;
      font-size: var(--fs-xs); font-weight: 700; color: var(--app-text-primary);
    }
    .camp-place-form .camp-place-note-f { flex: 1; min-width: 180px; }
    .camp-place-form select, .camp-place-form input {
      padding: .45rem .6rem; font-size: var(--fs-xs); font-family: var(--font-sans);
      border: 1px solid var(--border); border-radius: 8px; background: var(--white);
      color: var(--app-text-primary);
    }

    /* Deep-link landing flash: the Tasks panel sends the office here, so the card
       has to announce itself or they arrive at a list and hunt. */
    .send-card.fq-flash {
      animation: fqFlash 2s ease-out;
    }
    @keyframes fqFlash {
      0%, 30% { box-shadow: 0 0 0 3px var(--saffron); }
      100%    { box-shadow: 0 0 0 0 transparent; }
    }
    /* Tertiary buttons used to be white on a white card behind a 10%-alpha
       border, which is close to invisible and made the office hunt for them.
       The token set already carries a neutral surface family — use it rather
       than inventing a sixth button colour. */
    /* ─────────────────────────────────────────────────────────────────────
       ESTIMATES: three tiers, and never two tier-1s in one row.

       The panel had six differently-NAMED button classes that resolved to three
       near-identical neutral fills, so every card offered three controls of equal
       weight and the eye had nothing to land on. A button colour is not decoration,
       it is a claim about what to do next, and when everything claims it, nothing does.

         tier 1  ocean fill      the one action that moves the job forward, and it is
                                 always about money: Charge card on file, Send to
                                 Customer, Build the estimate. Exactly one per card.
         tier 2  ocean outline   a useful side door that costs nothing: Copy Link,
                                 Edit, Preview. Legible on white, which the old
                                 #eaf4f8-on-#fff fill was not.
         tier 3  text, no box    the way out: Not moving forward, Won't do, Show
                                 won't-do. These are not buttons, and drawing a box
                                 around them is what made them compete with the
                                 action beside them.

       Tier 3 deliberately has no border. The previous note here argued the opposite
       -- that a transparent text button "reads as absent" -- and that is true when it
       sits ALONE. It does not sit alone: it sits next to a filled primary, and next to
       a filled primary a bordered neutral reads as a second, equal option rather than
       as a way out. Position (pushed to the far edge) plus a hover state carries it. */
    .btn-quote-edit, .btn-quote-preview, .btn-quote-copy {
      background: #fff; border-color: var(--ocean); color: var(--tf-ocean-800);
      font-weight: 600;
    }
    .btn-quote-edit:hover, .btn-quote-preview:hover, .btn-quote-copy:hover {
      background: var(--tf-aqua-050); border-color: var(--ocean);
      color: var(--cerulean);
    }
    /* Same neutral surface as the other tertiary buttons. A transparent text
       button in the corner of a white card reads as absent rather than as quiet
       — its POSITION already carries "this is the way out", so it does not also
       need to be faint to say so. It still turns red only on hover, so the
       dismissive meaning arrives when it is reached for and never before. */
    /* Tier 3 is a BUTTON with a visible edge, not naked text. It briefly was not,
       and the button is the better call: every one of these opens a confirm and
       changes a record, and a control that changes state should look pressable.
       Naked text reads as navigation, so it under-promises what the click does.
       What made the ORIGINAL treatment wrong was never the border -- it was the
       neutral FILL, which gave it the same visual mass as the primary beside it.
       A bordered button with no fill and muted text is a third weight, which is
       what was missing. True navigation stays text (Put it back, Add internal
       notes), and so do view filters (Show won't-do) -- those change nothing. */
    .fq-cleared-toggle, .fq-cleared-toggle:hover {
      background: transparent; border-color: transparent;
      color: var(--text-muted); font-weight: 600;
      text-decoration: underline; text-underline-offset: 3px;
    }
    .fq-cleared-toggle:hover { color: var(--cerulean); }

    .btn-quote-close, .btn-quote-tertiary, .btn-fin.btn-tertiary {
      background: transparent; border-color: var(--border-control);
      color: var(--text-muted); font-weight: 600;
    }
    /* Neutral, not danger. "Not moving forward" deletes nothing, sends nothing
       and keeps the estimate with its price -- it records that a customer passed,
       which is an ordinary business outcome, not a destructive act. Red reads as
       "careful" and earns nothing here.
       It also mattered for a second reason: .btn-quote-tertiary is shared, so
       "Ask Geoff to check" inherited the danger hover and looked alarming for
       asking a colleague to read something (Meesha, 2026-09-15). */
    .btn-quote-close:hover, .btn-quote-tertiary:hover {
      background: var(--app-neutral-bg);
      border-color: var(--text-muted);
      color: var(--text-strong);
    }

    /* Modal header row: title left, close affordance right. Cancel lives at the
       bottom of a scrolling modal, so a long estimate had no visible way out. */
    .modal-title-row { display:flex; align-items:flex-start; gap:.75rem; }
    .modal-title-row .modal-title { flex:1 1 auto; }
    .modal-x {
      flex:0 0 auto; background:none; border:0; cursor:pointer;
      font-size:1.6rem; line-height:1; padding:0 .15rem;
      color: var(--text-muted); border-radius:6px;
    }
    .modal-x:hover  { color: var(--text-strong); }
    .modal-x:focus-visible { outline:2px solid var(--ocean, #1a9fc4); outline-offset:2px; }

    /* ══ Create a Pay Link: the day + charge controls (sql/704) ════════════
       Tokens only -- no invented colours. The disabled state uses a muted
       foreground on a muted surface rather than opacity, which fails WCAG on
       text (measured 5.12:1 on the sibling control in the prototype). */
    .pl-dopt { display:flex; gap:.55rem; align-items:flex-start; padding:.5rem .6rem;
      border:1px solid var(--border); border-radius:8px; cursor:pointer; background:var(--white);
      font-size: var(--fs-sm); line-height:1.35; }
    .pl-dopt:hover { border-color: var(--border-control-hover, var(--ocean)); }
    .pl-dopt:has(input:checked) { border-color: var(--ocean); background: var(--aqua-soft); }
    .pl-dopt input { margin-top:.2rem; flex:0 0 auto; }
    .pl-dsub { font-size: var(--fs-xs); color: var(--text-muted); }

    .pl-cal { border:1px solid var(--border); border-radius:10px; overflow:hidden; background:var(--white); }
    .pl-cal-head { display:flex; align-items:center; justify-content:space-between;
      padding:.4rem .55rem; background:var(--bg);
      font-size: var(--fs-sm); font-weight:700; }
    .pl-cal-head button { border:none; background:transparent; font:inherit; font-size:1rem;
      cursor:pointer; padding:.1rem .45rem; border-radius:5px; color:var(--text-strong); }
    .pl-cal-head button:hover:not(:disabled) { background:var(--sand); }
    .pl-cal-head button:disabled { color:var(--text-muted); cursor:not-allowed; }
    .pl-cal-grid { display:grid; grid-template-columns:repeat(7,1fr); gap:1px; background:var(--border); }
    .pl-cal-dow { background:var(--white); text-align:center; font-size:.62rem; padding:.28rem 0;
      color:var(--text-muted); font-weight:700; text-transform:uppercase; }
    .pl-cal-day { background:var(--white); border:none; font:inherit; font-size: var(--fs-xs);
      padding:.42rem 0; cursor:pointer; color:var(--text-strong); }
    .pl-cal-day:hover:not(:disabled) { background:var(--aqua-soft); }
    .pl-cal-day:disabled { color:var(--text-muted); cursor:default; background:var(--bg); }
    .pl-cal-day[aria-pressed="true"] { background:var(--ocean); color:var(--white); font-weight:700; }
    .pl-cal-empty { background:var(--bg); }

    /* The rest of the pay-link panel's own controls. These were written as
       INLINE style= attributes when the panel shipped, which is how a hardcoded
       #e4f7fc and a JS-assigned background got in -- Meesha caught it. Classes
       here, tokens only, so the panel is restyled from one place like every
       other surface in this app. */
    .pl-narrow { max-width: 110px; }
    .pl-hint {
      font-size: var(--fs-xs);
      color: var(--text-muted);
      margin: .3rem 0 0;
      min-height: 1rem;
      line-height: 1.45;
    }
    .pl-hint.is-error { color: var(--status-danger-text); }
    .pl-stack { display: flex; flex-direction: column; gap: .4rem; }

    /* Segmented control. The selected state is a CLASS, not a background
       assigned from JS -- that is what made it impossible to restyle without
       editing the handler, and what put a colour outside the token set. */
    /* The money readout on the pay-link drawer (sql/773). Both numbers, always,
       so a wrong split is visible rather than inferred from a button state. */
    /* Backdrop for the charge-card-on-file drawer. z-index sits just under
       .fd-drawer's 150 so the drawer stays above its own overlay. */
    /* Field-quote photos inside the create-estimate modal. Small enough to scan,
       clickable to open full size in a tab -- the office is checking what the
       part looks like, not admiring it. */
    .cq-photos { display: flex; flex-wrap: wrap; gap: .4rem; }
    .cq-photos a { display: block; line-height: 0; }
    .cq-photos img { width: 84px; height: 84px; object-fit: cover;
      border-radius: var(--radius); border: 1px solid var(--border); }

    /* Estimate photos picker in the create-estimate modal (13-estimates.js, sql/892). */
    .cq-ep-count { font-weight: 400; color: var(--text-muted); font-size: var(--fs-xs); }
    .cq-ep-row { display: flex; align-items: center; gap: .5rem; margin-bottom: .5rem; }
    .cq-ep-row img { width: 56px; height: 56px; object-fit: cover; border-radius: var(--radius); border: 1px solid var(--border); flex: none; background: var(--app-neutral-bg); }
    .cq-ep-row input { flex: 1; min-width: 0; min-height: 44px; }
    @media (max-width: 520px) { .cq-ep-row { flex-wrap: wrap; } .cq-ep-row img { margin-right: auto; } .cq-ep-row input { order: 5; flex: 1 0 100%; } }
    .cq-ep-ib { width: 44px; height: 44px; border: 0; background: none; color: var(--tf-cerulean); display: inline-flex; align-items: center; justify-content: center; flex: none; cursor: pointer; padding: 0; }
    .cq-ep-ib:disabled { opacity: .25; cursor: default; }
    .cq-ep-add { display: inline-flex; align-items: center; gap: .4rem; min-height: 44px; padding: 0 .9rem; border: 2px dashed var(--tf-ocean); border-radius: 22px; background: var(--tf-aqua-soft); color: var(--tf-cerulean); font: inherit; cursor: pointer; }
    .cq-ep-tray { border: 2px solid var(--tf-ocean); border-radius: 14px; padding: .6rem; margin-top: .6rem; }
    .cq-ep-tray[hidden] { display: none; }
    .cq-ep-note { font-size: var(--fs-xs); color: var(--text-muted); margin: 0 0 .5rem; }
    .cq-ep-grid { display: grid; grid-template-columns: repeat(3, 1fr); gap: .5rem; }
    .cq-ep-cell { position: relative; aspect-ratio: 1; padding: 0; border: 3px solid transparent; border-radius: 12px; overflow: hidden; background: var(--app-neutral-bg); cursor: pointer; }
    .cq-ep-cell.sel { border-color: var(--tf-ocean); }
    .cq-ep-cell img { position: absolute; inset: 0; width: 100%; height: 100%; object-fit: cover; }
    .cq-ep-badge { position: absolute; top: 6px; right: 6px; width: 24px; height: 24px; border-radius: 50%; background: var(--tf-ocean); color: #fff; font-size: .8rem; font-weight: 700; display: none; align-items: center; justify-content: center; }
    .cq-ep-cell.sel .cq-ep-badge { display: flex; }
    .cq-ep-chip { position: absolute; left: 4px; bottom: 4px; font-size: .7rem; border-radius: 8px; padding: 1px 6px; background: var(--tf-sand); color: var(--tf-cerulean); }
    .cq-ep-tray-foot { display: flex; align-items: center; gap: .5rem; margin-top: .6rem; }
    .cq-ep-tray-foot .cq-ep-status { flex: 1; font-size: var(--fs-xs); color: var(--text-muted); }

    /* Visit photos + add-a-photo in the appointment drawer (12-visit-report.js, sql/892). */
    .dp-grid { display: grid; grid-template-columns: repeat(3, 1fr); gap: .5rem; margin-bottom: .5rem; }
    .dp-ph { position: relative; display: block; aspect-ratio: 1; border-radius: 10px; overflow: hidden; background: var(--app-neutral-bg); }
    .dp-ph img { position: absolute; inset: 0; width: 100%; height: 100%; object-fit: cover; }
    .dp-tag { position: absolute; left: 0; right: 0; bottom: 0; background: rgba(6, 40, 64, .7); color: #fff; font-size: .7rem; text-align: center; padding: 1px 0; }
    .dp-add { display: flex; align-items: center; gap: .75rem; flex-wrap: wrap; }
    .dp-tog { display: inline-flex; align-items: center; gap: .5rem; min-height: 44px; font-size: var(--fs-xs); cursor: pointer; }
    .dp-tog span { white-space: nowrap; }
    .dp-tog input { width: 20px; height: 20px; flex: none; }

    /* The charge-card-on-file drawer opens ON TOP OF another drawer -- it is
       reached from the + New drawer's mint result as well as from Estimates --
       so it cannot share .fd-drawer's z-index of 150. It did, and because it
       sits EARLIER in the DOM, equal z-index meant source order decided and the
       + New drawer painted over it. The overlay darkened the screen, the drawer
       was there, and it was unreachable: the confirm promise never settled and
       the button read "Checking..." forever, twice, on a real charge.
       Two stacked surfaces need two stacking levels, not one shared class. */
    /* 318 and 319, above the mobile sheets (260 and up), the .drawer family (301), the New Estimate modal (300)
       and the visit panel (240), and below #toast (400). On a phone Charge sits in an estimate SHEET, so the
       confirmation it raises has to clear the sheet or it opens underneath it where nobody can answer it:
       Charge would show "Checking..." and never complete. Found by review before it shipped. */
    .cof-overlay { position: fixed; inset: 0; background: rgba(0,0,0,.45); z-index: 318; }
    #cof-drawer { z-index: 319; }

    .pl-money { display: flex; gap: 1.4rem; margin-top: .7rem;
      padding: .6rem .8rem; background: var(--sand); border-radius: var(--radius); }
    .pl-money > div { display: flex; flex-direction: column; }
    .pl-money-k { font-size: var(--fs-eyebrow); font-weight: 700;
      text-transform: uppercase; letter-spacing: .08em; color: var(--text-muted); }
    .pl-money-v { font-size: var(--fs-md); font-weight: 700;
      color: var(--text-strong); font-variant-numeric: tabular-nums; }
    .pl-money-v.is-zero { color: var(--text-muted); font-weight: 600; }

    .pl-seg {
      display: flex;
      border: 1px solid var(--border);
      border-radius: 8px;
      overflow: hidden;
    }
    .pl-seg-btn {
      flex: 1;
      border: none;
      padding: .55rem .5rem;
      font: inherit;
      font-size: var(--fs-sm);
      font-weight: 600;
      cursor: pointer;
      background: var(--white);
      color: var(--text-muted);
    }
    .pl-seg-btn + .pl-seg-btn { border-left: 1px solid var(--border); }
    .pl-seg-btn:hover:not([aria-pressed="true"]) { background: var(--bg); }
    .pl-seg-btn[aria-pressed="true"] { background: var(--ocean); color: var(--white); }
    .pl-seg-btn:focus-visible { outline: 2px solid var(--ocean); outline-offset: -2px; }

    /* ── The money record drawer (js/20-money-record.js) ───────────────────
       Layout only. Every colour, radius and size comes from a token, and the
       status pills reuse .tf-status rather than defining a sixth vocabulary. */
    .mr-sub { font-size: var(--fs-xs); color: var(--text-muted); margin-top: .15rem; }
    .mr-sums { display: grid; grid-template-columns: repeat(auto-fit, minmax(140px, 1fr)); gap: .6rem; margin-bottom: 1rem; }
    .mr-sum { background: var(--sand); border: 1px solid var(--border); border-radius: var(--radius-sm, 8px); padding: .6rem .7rem; }
    .mr-sum span { display: block; font-size: var(--fs-xs); color: var(--text-muted); }
    .mr-sum b { font-size: var(--fs-md); color: var(--text-strong); }
    .mr-h { font-size: var(--fs-sm); color: var(--text-strong); margin: 1.1rem 0 .5rem; }
    .mr-amt { text-align: right; white-space: nowrap; font-variant-numeric: tabular-nums; }
    /* A row's qualifier sits UNDER the thing it qualifies, never appended to it:
       "credited into an estimate" read inline looks like part of the job title. */
    .mr-note { display: block; font-size: var(--fs-xs); color: var(--text-muted); margin-top: .15rem; }
    .mr-foot { font-size: var(--fs-xs); color: var(--text-muted); line-height: 1.5; margin-top: .7rem; }

    /* A card's hold note: why this one is deliberately NOT being acted on.
       Warm-grey ground, because a holding pattern is a real state and not a
       warning -- nothing here is going wrong. (IA spec §6.1, the five states.) */
    .tf-deck-hold {
      margin-top: .55rem; padding: .55rem .6rem;
      background: var(--app-neutral-bg); border: 1px solid var(--app-neutral-border);
      border-radius: var(--radius-sm, 8px);
    }
    .tf-deck-hold p {
      margin: .35rem 0 0; font-size: var(--fs-xs); line-height: 1.5;
      color: var(--text-muted);
    }

    /* ═══ THE CUSTOMER RECORD DRAWER ══════════════════════════════════════
       Owner doc: CUSTOMER-RECORD-SPEC.md. Scoped to #money-drawer on purpose.

       WHY SCOPED, AND WHAT IT QUIETLY FIXES. `.job-row`, `.job-row-name`,
       `.job-row-sub` and `.tk-pill` are used by the refund list that renders in
       this drawer, and NONE of them is defined in any linked stylesheet:
       `.tk-pill` exists only as `#panel-tasks .tk-pill`, which cannot reach
       here, and `.job-row` is defined nowhere at all. Those rows have been
       drawing as bare unstyled divs since the refund doorway moved into the
       drawer. Defining them unscoped would restyle every other surface using
       the same dead class names, which is a change nobody asked for; defining
       them here fixes both doorways and can regress nothing outside this
       element. `#panel-tasks .tk-pill` still wins in Tasks on specificity.   */

    #money-drawer .tk-pill {
      font-size: var(--fs-xs); font-weight: 700; padding: .1rem .45rem;
      border-radius: 999px; background: var(--border); color: var(--text-muted);
      white-space: nowrap;
    }
    #money-drawer .tk-pill.money {
      background: var(--status-danger-soft); color: var(--status-danger-text);
    }

    #money-drawer .cr-prov,
    #money-drawer .cr-tags { font-size: var(--fs-xs); color: var(--text-muted); }
    #money-drawer .cr-tags { margin-top: .3rem; display: flex; gap: .3rem; flex-wrap: wrap; }
    #money-drawer .cr-notes {
      background: var(--sand); border: 1px solid var(--border);
      border-radius: var(--radius-sm, 8px); padding: .6rem .7rem;
      margin-bottom: .6rem; font-size: var(--fs-xs);
    }
    #money-drawer .cr-notes b { display: block; color: var(--text-muted); margin-bottom: .2rem; }

    /* The summary is a SENTENCE, not two bordered cards. Boxes carry the visual
       weight of a control, and the only controls on this screen that matter are
       the Refund buttons in the list below. Stated in words it reads at a
       glance and gives the emphasis back to the rows. */
    #money-drawer .cr-summary {
      margin: 0 0 .9rem; padding-bottom: .9rem;
      border-bottom: 1px solid var(--border);
      font-size: var(--fs-xs); color: var(--text-muted);
    }
    #money-drawer .cr-summary b { color: var(--text); font-size: var(--fs-sm, 1rem); }

    /* ─── THE PROPERTY SECTION (22-pool-property.js) ─────────────────────
       Scoped to #money-drawer for the reason recorded above: an unscoped class
       is a new global, and equal-specificity rules here are decided by source
       order. Colours are tokens only -- an earlier pass used var(--cta-bg) for
       inline text actions and they rendered GOLD, because that token is the
       primary button's BACKGROUND, not a link colour. */

    /* A tag beside the NAME. Deliberately NOT .cr-tag: that class already exists
       further down this file and is used by the ledger rows, so recolouring it
       would change things this work never looked at. Two rules with the same
       selector and equal specificity are decided by source order, which is how
       the first attempt silently lost -- the rule was correct and simply came
       first. Grey also read as "disabled" here; a subscriber is the most
       valuable state a customer has, so it takes the informational ink. */
    #money-drawer .cr-idtag {
      display: inline-block; vertical-align: middle; margin-left: .45rem;
      font-size: var(--fs-xs); font-weight: 700; letter-spacing: .01em;
      padding: .1rem .5rem; border-radius: var(--radius-pill, 999px);
      background: var(--status-info-soft); color: var(--status-info-text);
      border: 1px solid var(--status-info-border);
    }

    /* Space is what groups. Sections previously sat ~20px apart, which is about
       the gap INSIDE a section, so the drawer read as one continuous column. */
    #money-drawer .cd-sec { border-top: 1px solid var(--border);
                            margin-top: var(--space-10, 40px);
                            padding-top: var(--space-6, 24px); }
    #money-drawer .cr-summary + .cd-sec { border-top: 0; margin-top: 0; padding-top: 0; }

    /* A heading is a landmark, not a caption. Grey uppercase micro-text is what
       this app uses for FIELD LABELS, so using it for headings too gave
       "Property" and "Shape" the same weight and left nothing to anchor on. */
    #money-drawer .cd-h { font-size: var(--fs-md, 1.0625rem); font-weight: 700;
                          letter-spacing: -0.01em; line-height: var(--lh-snug, 1.3);
                          color: var(--app-text-primary, var(--text));
                          margin-bottom: var(--space-4, 16px);
                          display: flex; gap: var(--space-3, 12px);
                          align-items: center; justify-content: space-between; }
    #money-drawer .cd-h-sub { font-size: var(--fs-sm, .9375rem); font-weight: 700;
                              color: var(--app-text-primary, var(--text));
                              margin: var(--space-6, 24px) 0 var(--space-3, 12px);
                              display: flex; align-items: center;
                              justify-content: space-between; gap: var(--space-3, 12px); }
    #money-drawer .cd-sub { font-size: var(--fs-xs); color: var(--app-text-secondary, var(--text-muted)); }
    #money-drawer .cd-strong { font-weight: 600; }
    #money-drawer .cd-none { color: var(--status-danger-text); font-weight: 600; }
    #money-drawer .cd-stack > * + * { margin-top: var(--space-4, 16px); }

    #money-drawer .cd-link { background: none; border: 0; padding: 0; font: inherit;
                             font-size: var(--fs-xs); font-weight: 600;
                             color: var(--status-info-text); cursor: pointer; }
    #money-drawer .cd-link:hover { text-decoration: underline; }

    #money-drawer .cd-prop + .cd-prop { margin-top: var(--space-8, 32px); }
    #money-drawer .cd-prop.multi { border: 1px solid var(--border);
                                   border-radius: var(--radius-md, 10px);
                                   padding: var(--space-4, 16px); }
    #money-drawer .cd-prop-head { display: flex; align-items: flex-start;
                                  gap: var(--space-3, 12px); justify-content: space-between; }
    #money-drawer .cd-addr { font-weight: 600; font-size: var(--fs-sm); flex: 1; min-width: 0; }

    /* A body of water sits under its property: the address is where you drive,
       the pool is what you service. */
    #money-drawer .cd-wb { margin-top: var(--space-5, 20px);
                           padding-left: var(--space-3, 12px);
                           border-left: 2px solid var(--status-info-border); }
    #money-drawer .cd-wb-name { font-weight: 600; font-size: var(--fs-sm);
                                display: flex; align-items: baseline; gap: var(--space-2, 8px); }
    #money-drawer .cd-wb .tf-fact-grid { margin-top: var(--space-2, 8px);
                                         grid-template-columns: repeat(auto-fill, minmax(104px, 1fr));
                                         gap: var(--space-2, 8px); }
    #money-drawer .cd-wb .tf-fact { padding: var(--space-2, 8px) var(--space-3, 12px); }

    /* A MISSING fact is an empty slot, not an alarm. Amber here means "needs
       attention now", and an unrecorded pool surface does not. */
    #money-drawer .tf-fact.cd-gap { background: transparent; border-style: dashed;
                                    border-color: var(--border); }
    #money-drawer .cd-gapbtn { background: none; border: 0; padding: 0; font: inherit;
                               font-size: var(--fs-xs); font-weight: 600;
                               color: var(--status-info-text); cursor: pointer; }
    #money-drawer .cd-gapbtn:hover { text-decoration: underline; }

    /* Equipment is a register of physical things -- a different kind of content
       from the prose around it, so it reads as a table. */
    #money-drawer .cd-table { border: 1px solid var(--border);
                              border-radius: var(--radius-md, 10px); overflow: hidden; }
    #money-drawer .cd-tr { display: flex; align-items: center; gap: var(--space-3, 12px);
                           padding: var(--space-4, 16px); font-size: var(--fs-xs); }
    #money-drawer .cd-tr + .cd-tr { border-top: 1px solid var(--border); }
    #money-drawer .cd-tr:nth-child(odd) { background: var(--app-neutral-bg, transparent); }
    #money-drawer .cd-td-main { flex: 1; min-width: 0; }
    #money-drawer .cd-td-kind { font-weight: 600; text-transform: capitalize; }
    #money-drawer .cd-empty { padding: var(--space-4, 16px); font-size: var(--fs-xs);
                              color: var(--app-text-secondary, var(--text-muted)); }
    #money-drawer .cd-padgroup + .cd-padgroup { margin-top: var(--space-4, 16px); }
    #money-drawer .cd-padname { font-size: var(--fs-xs); font-weight: 700;
                                color: var(--app-text-secondary, var(--text-muted));
                                margin-bottom: var(--space-2, 8px); }

    /* Dragging a unit onto its pad. The grab cursor and the handle only appear
       at a multi-pad property, because at a one-pad property the gesture has no
       meaning and offering it implies a distinction that does not exist. */
    #money-drawer .cd-drag { cursor: grab; }
    #money-drawer .cd-drag:active { cursor: grabbing; }
    #money-drawer .cd-drag::before { content: '\22EE\22EE'; letter-spacing: -2px;
                                     color: var(--app-text-secondary, var(--text-muted));
                                     font-size: var(--fs-xs); flex: 0 0 auto;
                                     margin-right: calc(var(--space-1, 4px) * -1); }
    #money-drawer .cd-dragging { opacity: .45; }
    #money-drawer .cd-drop { min-height: 2.75rem; transition: background var(--duration-base, .15s); }
    #money-drawer .cd-drop.cd-over { background: var(--status-info-soft);
                                     border-color: var(--status-info-border); }
    /* An EMPTY pad must still be a visible target, or dragging the last unit out
       of one leaves nowhere to drop it back. */
    #money-drawer .cd-drop:empty::after { content: 'Drop a unit here';
                                          display: block; padding: var(--space-4, 16px);
                                          font-size: var(--fs-xs);
                                          color: var(--app-text-secondary, var(--text-muted)); }

    /* ── Photos on an equipment row (23-equipment-photos.js) ──────────────
       The strip is ALWAYS present, even empty, because it is the drop target
       and a target that appears only when occupied cannot receive the first
       photo. Dragging OFF is as prominent as dragging on: the record is the
       claim about now, and a photo of a pump replaced in August is not wrong,
       it is just no longer a picture of what is there. */
    #money-drawer .ep-strip { display: flex; flex-wrap: wrap; gap: var(--space-2, 8px);
                              padding: var(--space-2, 8px) 0 var(--space-2, 8px) var(--space-4, 16px);
                              min-height: 2.5rem; align-items: center;
                              transition: background var(--duration-base, .15s); }
    #money-drawer .ep-strip-empty { font-size: var(--fs-xs);
                                    color: var(--app-text-secondary, var(--text-muted)); }
    #money-drawer .ep-drop.ep-over { background: var(--status-info-soft);
                                     outline: 1px dashed var(--status-info-border); }
    #money-drawer .ep-thumb { position: relative; width: 62px; height: 62px;
                              border-radius: var(--radius-sm); overflow: hidden;
                              cursor: grab; flex: 0 0 auto;
                              background: var(--app-neutral-bg, rgba(127,127,127,.12)); }
    #money-drawer .ep-thumb:active { cursor: grabbing; }
    #money-drawer .ep-thumb img { width: 100%; height: 100%; object-fit: cover; display: block; }
    #money-drawer .ep-thumb-missing { width: 100%; height: 100%; }
    #money-drawer .ep-dragging { opacity: .45; }
    /* A photo the triage could read a nameplate on. Marked, not auto-filed:
       nothing here writes a link from a machine read. */
    #money-drawer .ep-thumb-sug { outline: 2px solid var(--status-warning-border, #b8860b); }
    #money-drawer .ep-thumb-read { position: absolute; left: 0; right: 0; bottom: 0;
                                   padding: 1px 3px; font-size: 9px; line-height: 1.25;
                                   background: rgba(0,0,0,.62); color: #fff;
                                   white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
    /* The photo itself is the open-big control: a 62px thumbnail cannot show a
       nameplate, so opening full size is what lets someone CHECK a suggestion
       rather than trust it. Invisible button over the image so the whole tile
       is the target. */
    #money-drawer .ep-thumb-open { position: absolute; inset: 0; border: 0; padding: 0;
                                   background: transparent; cursor: zoom-in; }
    /* Always visible, not hover-only: this drawer is used on a tablet, where a
       hover-only control is an absent one. */
    #money-drawer .ep-thumb-del { position: absolute; top: 2px; right: 2px;
                                  width: 20px; height: 20px; border: 0; border-radius: 50%;
                                  background: rgba(0,0,0,.62); color: #fff;
                                  display: flex; align-items: center; justify-content: center;
                                  line-height: 1; cursor: pointer; padding: 0; }
    #money-drawer .ep-thumb-del svg { width: 12px; height: 12px; }
    #money-drawer .ep-thumb-new { position: absolute; top: 2px; left: 2px;
                                  width: 20px; height: 20px; border: 0; border-radius: 50%;
                                  background: var(--status-info-border, #2a6ebb); color: #fff;
                                  font-size: 15px; font-weight: 700; line-height: 1;
                                  cursor: pointer; padding: 0; }
    #money-drawer .ep-tray { margin-top: var(--space-3, 12px); padding: var(--space-3, 12px);
                             border-radius: var(--radius-md);
                             background: var(--app-neutral-bg, rgba(127,127,127,.08)); }
    #money-drawer .ep-tray-head { font-size: var(--fs-xs); font-weight: 700;
                                  margin-bottom: var(--space-2, 8px); }
    #money-drawer .ep-tray-sug { display: inline-block; margin-left: var(--space-2, 8px);
                                 padding: 1px 7px; border-radius: 999px;
                                 background: var(--status-warning-soft, rgba(190,140,0,.18));
                                 font-weight: 600; }
    #money-drawer .ep-tray-grid { display: flex; flex-wrap: wrap; gap: var(--space-2, 8px); }
    #money-drawer .ep-tray-note { margin: var(--space-2, 8px) 0 0; font-size: var(--fs-xs);
                                  color: var(--app-text-secondary, var(--text-muted)); }

    /* Opens OVER a drawer someone is mid-task in, so clicking outside and
       Escape both close it. A modal that traps them is worse than no modal. */
    .ep-lightbox { position: fixed; inset: 0; z-index: 9000; display: grid; place-items: center;
                   background: rgba(0,0,0,.72); padding: var(--space-4, 16px); }
    .ep-lightbox-inner { max-width: min(92vw, 900px); max-height: 92vh; overflow: auto;
                         background: var(--app-surface, #fff); border-radius: var(--radius-md);
                         padding: var(--space-3, 12px); text-align: center; }
    .ep-lightbox-inner img { max-width: 100%; max-height: 74vh; border-radius: var(--radius-sm);
                             display: block; margin: 0 auto; }
    .ep-lightbox-read { margin-top: var(--space-2, 8px); font-size: var(--fs-xs);
                        color: var(--app-text-secondary, var(--text-muted)); }
    .ep-lightbox-close { margin-top: var(--space-3, 12px); padding: 8px 16px; min-height: 44px;
                         border: 1px solid currentColor; border-radius: var(--radius-sm);
                         background: none; font: inherit; cursor: pointer; }
    .ep-newequip { text-align: left; max-width: 420px; }
    .ep-newequip h3 { margin: 0 0 var(--space-2, 8px); font-size: var(--fs-sm, .95rem); }
    .ep-newequip img { max-height: 210px; }
    .ep-newequip-guess { margin: var(--space-2, 8px) 0; font-size: var(--fs-xs);
                         color: var(--app-text-secondary, var(--text-muted)); }
    .ep-newequip label { display: block; margin-top: var(--space-2, 8px); font-size: var(--fs-xs); }
    .ep-newequip select, .ep-newequip input { width: 100%; min-height: 44px; margin-top: 4px;
                                              font: inherit; padding: 0 8px;
                                              border: 1px solid var(--app-border, #ccc);
                                              border-radius: var(--radius-sm); }
    .ep-newequip-btns { display: flex; gap: var(--space-2, 8px); margin-top: var(--space-3, 12px); }
    .ep-newequip-btns button { flex: 1 1 0; }
    .ep-newequip-save { min-height: 44px; border: 0; border-radius: var(--radius-sm);
                        background: var(--cta-bg, #1d7a42); color: var(--cta-text, #fff);
                        font: inherit; font-weight: 700; cursor: pointer; }
    .ep-newequip-msg { margin: var(--space-2, 8px) 0 0; font-size: var(--fs-xs);
                       color: var(--status-danger-text, #a33); }

    #money-drawer .cd-row { display: flex; gap: var(--space-3, 12px); align-items: baseline;
                            padding: var(--space-2, 8px) 0; font-size: var(--fs-xs); }
    #money-drawer .cd-row + .cd-row { border-top: 1px solid var(--border); }
    #money-drawer .cd-row-main { flex: 1; min-width: 0; }
    #money-drawer .cd-note { font-size: var(--fs-xs); margin-top: var(--space-2, 8px); }

    /* Service log + visit notes, moved onto the office pool card 2026-09-19.
       Scoped to #money-drawer like everything else in this block: these class
       names are generic enough that an unscoped rule would reach markup that
       has nothing to do with the drawer. */
    #money-drawer .cd-svc-row { display: flex; justify-content: space-between; gap: var(--space-3, 12px);
                                padding: var(--space-2, 8px) 0; font-size: var(--fs-xs);
                                border-bottom: 1px solid var(--border); }
    #money-drawer .cd-svc-row:last-child { border-bottom: 0; }
    #money-drawer .cd-svc-type { font-weight: 650; }
    #money-drawer .cd-svc-date { color: var(--app-text-secondary, var(--text-muted)); white-space: nowrap; }

    #money-drawer .cd-note-row { padding: var(--space-2, 8px) 0; border-bottom: 1px solid var(--border); }
    #money-drawer .cd-note-row:last-child { border-bottom: 0; }
    #money-drawer .cd-note-top { display: flex; gap: var(--space-2, 8px); align-items: baseline;
                                 font-size: var(--fs-xs); }
    #money-drawer .cd-note-date { font-weight: 650; }
    #money-drawer .cd-note-slug { color: var(--app-text-secondary, var(--text-muted));
                                  text-transform: capitalize; }
    /* A note is prose someone wrote in the field; preserve their line breaks
       rather than running it into one paragraph. */
    #money-drawer .cd-note-text { margin: var(--space-1, 4px) 0 0; font-size: var(--fs-xs);
                                  line-height: 1.5; white-space: pre-wrap; }

    #money-drawer .cd-fold > summary { cursor: pointer; font-size: var(--fs-xs); font-weight: 650;
                                       color: var(--app-text-secondary, var(--text-muted));
                                       padding: var(--space-2, 8px) 0; }
    #money-drawer .cd-fold[open] > summary { margin-bottom: var(--space-1, 4px); }
    #money-drawer .cd-note b { color: var(--app-text-secondary, var(--text-muted)); font-weight: 600; }
    #money-drawer .cd-acts { display: flex; gap: var(--space-2, 8px); flex-wrap: wrap;
                             margin-top: var(--space-4, 16px); }
    #money-drawer .cd-msg { font-size: var(--fs-xs); margin-top: var(--space-2, 8px); }
    #money-drawer .cd-alert { font-size: var(--fs-xs); margin: var(--space-2, 8px) 0;
                              background: var(--status-danger-soft); color: var(--status-danger-text);
                              border-left: 3px solid var(--status-danger-border);
                              padding: var(--space-2, 8px) var(--space-3, 12px);
                              border-radius: var(--radius-sm, 6px); }

    /* Each section owns its own menu. One mega-menu beside the address was tidy
       and wrong: "Add equipment" is not a property action, and burying it means
       knowing the menu exists before you can find the thing you are looking at. */
    #money-drawer .cd-menu-wrap { position: relative; display: inline-block; }
    #money-drawer .cd-kebab { background: none; border: 1px solid var(--border);
                              border-radius: var(--radius-sm, 6px);
                              padding: .15rem .45rem; cursor: pointer;
                              font-size: var(--fs-sm); line-height: 1.2;
                              color: var(--app-text-secondary, var(--text-muted)); }
    #money-drawer .cd-kebab:hover { background: var(--app-neutral-bg, var(--border)); }
    #money-drawer .cd-menu { position: absolute; right: 0; top: calc(100% + 4px); z-index: 30;
                             background: var(--app-surface, #fff); border: 1px solid var(--border);
                             border-radius: var(--radius-md, 10px);
                             box-shadow: var(--shadow-md, 0 6px 20px rgba(0,0,0,.12));
                             min-width: 218px; padding: var(--space-1, 4px); display: none; }
    #money-drawer .cd-menu.open { display: block; }
    #money-drawer .cd-menu button { display: block; width: 100%; text-align: left;
                                    background: none; border: 0; font: inherit;
                                    font-size: var(--fs-xs); cursor: pointer;
                                    padding: var(--space-2, 8px) var(--space-3, 12px);
                                    border-radius: var(--radius-sm, 6px); }
    #money-drawer .cd-menu button:hover { background: var(--app-neutral-bg, var(--border)); }
    #money-drawer .cd-menu button.danger { color: var(--status-danger-text); }
    #money-drawer .cd-menu-sep { height: 1px; background: var(--border);
                                 margin: var(--space-1, 4px) var(--space-2, 8px); }


    /* What Geoff did, folded onto the booking it belongs to. */
    #money-drawer .cr-did {
      font-size: var(--fs-xs); color: var(--app-text-secondary, var(--text-muted));
      margin-top: .2rem;
    }
    #money-drawer .cr-more { margin-top: var(--space-3, 12px); }

    /* A contact detail that OPENS the conversation. Styled as the value it is,
       not as a button -- it is still the phone number, it just does something. */
    #money-drawer .cr-contact-open {
      background: none; border: 0; padding: 0; font: inherit; cursor: pointer;
      color: var(--status-info-text); text-decoration: underline;
      text-underline-offset: 2px; text-decoration-thickness: 1px;
    }
    #money-drawer .cr-contact-open:hover { text-decoration-thickness: 2px; }


    /* The ownership forms (22-pool-property.js). */
    #money-drawer .cp-form { margin-top: var(--space-3, 12px); }
    #money-drawer .cd-edit { border: 1px solid var(--border);
                             border-radius: var(--radius-md, 10px);
                             padding: var(--space-4, 16px); }
    #money-drawer .cd-edit h4 { font-size: var(--fs-sm); font-weight: 700;
                                margin-bottom: .2rem; }
    #money-drawer .cd-edit p { font-size: var(--fs-xs);
                               color: var(--app-text-secondary, var(--text-muted));
                               margin-bottom: var(--space-3, 12px); }
    #money-drawer .cd-edit label { display: block; font-size: var(--fs-eyebrow);
                                   text-transform: uppercase; letter-spacing: .04em;
                                   font-weight: 700; margin: var(--space-3, 12px) 0 .2rem;
                                   color: var(--app-text-secondary, var(--text-muted)); }
    #money-drawer .cd-edit input,
    #money-drawer .cd-edit select { width: 100%; padding: .4rem .55rem; font: inherit;
                                    font-size: var(--fs-xs);
                                    border: 1px solid var(--border);
                                    border-radius: var(--radius-sm, 6px); }
    #money-drawer .cp-results { max-height: 9rem; overflow: auto; }
    #money-drawer .cp-hit { padding: .35rem .5rem; cursor: pointer; font-size: var(--fs-xs);
                            border-bottom: 1px solid var(--border); }
    #money-drawer .cp-hit:hover { background: var(--app-neutral-bg, var(--border)); }
    #money-drawer .cp-picked { margin-top: .3rem; }

    /* Ledger rows */
    #money-drawer .cr-row {
      display: flex; align-items: flex-start; gap: .75rem;
      padding: .7rem .1rem; border-bottom: 1px solid var(--border);
    }
    #money-drawer .cr-row.is-open { border-bottom-color: transparent; }
    #money-drawer .cr-row-main { flex: 1; min-width: 0; }
    #money-drawer .cr-row-title {
      font-weight: 700; display: flex; flex-wrap: wrap;
      align-items: center; gap: .35rem;
    }
    #money-drawer .cr-row-meta {
      font-size: var(--fs-xs); color: var(--text-muted); margin-top: .15rem;
    }
    #money-drawer .cr-row-why {
      font-size: var(--fs-xs); color: var(--text-muted);
      margin: .3rem 0 0; max-width: 46ch;
    }
    #money-drawer .cr-row-act {
      display: flex; gap: .4rem; align-items: center; flex-shrink: 0;
    }
    #money-drawer .cr-tag {
      font-size: var(--fs-xs); font-weight: 700; color: var(--text-muted);
      border: 1px solid var(--border); border-radius: 999px; padding: .1rem .5rem;
      white-space: nowrap;
    }

    /* Nested recurring visits, indented and rested on a rule so the plan they
       belong to stays visibly their parent when the list is long. */
    /* ── Customer record: Activity timeline ───────────────────────────────
       The one place a CLOSED task can be read once the deck's 7-second undo
       has gone. Deliberately lighter than the money ledger above it: this is
       history, not a control surface, so no cards and no buttons. */
    #cr-activity .cr-sec-title { margin: 1.4rem 0 .6rem; font-size: var(--fs-sm); font-weight: 650;
      letter-spacing: .05em; text-transform: uppercase; color: var(--text-strong); }
    .cr-tl { position: relative; margin: 0; padding: 0 0 0 1.4rem; list-style: none; }
    .cr-tl:before { content: ""; position: absolute; left: 5px; top: .5rem; bottom: .5rem;
      width: 2px; background: var(--border); }
    .cr-tl li { position: relative; display: flex; gap: .6rem; align-items: baseline; padding: .4rem 0; }
    .cr-tl-dot { position: absolute; left: -1.4rem; top: .75rem; width: 11px; height: 11px;
      border-radius: 50%; background: var(--white); border: 2px solid var(--border); }
    .cr-tl-dot.flag { border-color: var(--status-warning); }
    .cr-tl-dot.done { border-color: var(--status-success); }
    .cr-tl-dot.drop { border-color: var(--border); }
    /* Estimate opened: the one row here that is evidence of the CUSTOMER
       doing something rather than us. Tinted to match the card's Opened. */
    .cr-tl-dot.open { border-color: var(--status-info); }
    .cr-tl-ev { flex: 1; min-width: 0; font-size: var(--fs-sm); line-height: 1.45; }
    .cr-tl-ev b { color: var(--text-strong); font-weight: 600; }
    .cr-tl-why { display: block; color: var(--text-muted); }
    .cr-tl-when { flex: none; font-size: var(--fs-xs); color: var(--text-muted); }

    /* "Opened 28 Aug" on an estimate card. Tinted so it reads as a signal in a
       line of plain facts, but not badged: it is evidence of attention, not a
       status, and the DATE is the actionable half. */
    .quote-meta .q-opened { color: var(--status-info-text); font-weight: 600; }

    #money-drawer .cr-nested {
      margin: 0 0 .5rem 1rem; padding-left: .75rem;
      border-left: 2px solid var(--border);
    }

    /* The refund panel, opened UNDER the row it belongs to rather than as a
       modal over the drawer. A modal on top of a drawer is two dismissable
       layers for one decision, and it hides the row you are refunding at the
       moment you confirm the amount. */
    #money-drawer .cr-refund {
      background: var(--sand); border: 1px solid var(--border);
      border-radius: var(--radius-sm, 8px);
      padding: .7rem .8rem; margin: 0 0 .7rem;
    }
    #money-drawer .rf-lede {
      margin: 0 0 .6rem; font-size: var(--fs-xs); color: var(--text-muted); max-width: 52ch;
    }
    #money-drawer .rf-pay,
    .modal .rf-pay {
      display: flex; align-items: center; gap: .75rem; flex-wrap: wrap;
      padding: .5rem 0; border-bottom: 1px solid var(--border);
    }
    #money-drawer .rf-pay:last-child,
    .modal .rf-pay:last-child { border-bottom: 0; padding-bottom: 0; }
    #money-drawer .rf-pay-main,
    .modal .rf-pay-main { flex: 1; min-width: 11rem; }
    #money-drawer .rf-pay-name,
    .modal .rf-pay-name { font-weight: 700; }
    #money-drawer .rf-pay-sub,
    .modal .rf-pay-sub {
      font-size: var(--fs-xs); color: var(--text-muted); margin-top: .1rem;
    }
    #money-drawer .rf-pay.is-muted,
    #money-drawer .rf-pay.is-done,
    .modal .rf-pay.is-muted,
    .modal .rf-pay.is-done { opacity: .62; }
    #money-drawer .rf-pay-act,
    .modal .rf-pay-act { display: flex; gap: .5rem; align-items: center; }

    /* The amount sits in a field with the dollar sign OUTSIDE the input, so the
       figure a person types is the whole value of the box and there is no
       currency character to delete or duplicate. */
    #money-drawer .rf-amt,
    .modal .rf-amt {
      display: inline-flex; align-items: center; gap: .15rem;
      border: 1px solid var(--border); border-radius: var(--radius-sm, 8px);
      background: var(--white); padding: 0 .1rem 0 .45rem;
    }
    #money-drawer .rf-amt > span,
    .modal .rf-amt > span { color: var(--text-muted); font-size: var(--fs-xs); }
    #money-drawer .rf-amt input,
    .modal .rf-amt input {
      width: 5.5rem; border: 0; background: none; font: inherit;
      padding: .45rem .3rem; outline-offset: -2px;
    }


    /* The clickable customer name (crNameLink in js/21-customer-record.js).
       NOT scoped to the drawer: it renders inside the panels. A real <button>
       rather than an <a> or a span, because it performs an action rather than
       navigating, so it is keyboard-reachable and announced correctly with no
       aria plumbing. It inherits type from whatever it sits in, so the row's
       own heading styling still decides its size and weight. */
    .cr-name-link {
      background: none; border: 0; padding: 0; margin: 0;
      font: inherit; color: inherit; text-align: inherit;
      cursor: pointer; border-bottom: 1px dotted var(--border);
    }
    .cr-name-link:hover,
    .cr-name-link:focus-visible { border-bottom-color: currentColor; }

    /* The visit line in the ledger, as a link to the customer's own report.
       Dotted like .cr-name-link, because it is the same gesture -- this text
       opens a record -- and one affordance should not have two looks. */
    .cr-did-link {
      color: inherit; text-decoration: none;
      border-bottom: 1px dotted var(--border);
    }
    .cr-did-link:hover,
    .cr-did-link:focus-visible { border-bottom-color: currentColor; }

    /* A tray thumb held back behind Show more. display:contents so the
       wrapper adds no box to the grid; the global [hidden] rule above is
       !important and so still wins while it is hidden. */
    .ep-tray-more { display: contents; }
    .ep-tray-showmore { margin-top: 0.5rem; }

    /* ── Chemistry in the customer record ───────────────────────────────
       Chart then table. The office asks whether a pool is DRIFTING, which
       is a visual question; the technician's view is numbers, because he is
       deciding a dose on the spot. Same RPC behind both. */
    .cp-chem-alert { margin: 0 0 10px; padding: 8px 11px; border-radius: 8px;
      background: var(--status-danger-soft, #fbeaea); color: var(--status-danger-text, #9b2c2c);
      font-size: 12.5px; font-weight: 650; line-height: 1.5; }
    .cp-chem-chart { margin: 0 0 10px; }
    /* --text-primary is not a token this project has, and unlike its twelve
       siblings it was written WITHOUT a fallback -- so `color` was invalid and
       the label silently inherited instead of rendering the strong ink. The
       real name is --text-strong (tokens.css); --text-muted on the line below
       is the deliberate contrast to it. */
    .cp-chem-clabel { display: flex; align-items: baseline; gap: 8px; font-size: 12px;
      font-weight: 700; color: var(--text-strong); margin-bottom: 2px; }
    .cp-chem-target { font-size: 11px; font-weight: 600; color: var(--text-muted); }
    .cp-chem-table { width: 100%; border-collapse: collapse; font-size: 12.5px; margin-top: 4px; }
    .cp-chem-table th { font-size: 10px; letter-spacing: .05em; text-transform: uppercase;
      color: var(--text-muted); font-weight: 800; text-align: right; padding: 5px 6px;
      border-bottom: 1px solid var(--border); white-space: nowrap; }
    .cp-chem-table td { padding: 5px 6px; text-align: right; border-bottom: 1px solid var(--border);
      font-variant-numeric: tabular-nums; white-space: nowrap; }
    .cp-chem-table tr:last-child td { border-bottom: 0; }
    /* Transposed: the date is the row label and the one left-aligned column.
       The metric count is fixed, the visit count is not, so the axis that can
       overflow scrolls inside its own box rather than moving the page. */
    .cp-chem-table th.cp-chem-date,
    .cp-chem-table td.cp-chem-date { text-align: left; font-weight: 700; padding-left: 0; white-space: nowrap; }
    .cp-chem-scroll { overflow-x: auto; }
    .cp-chem-v { font-weight: 650; }
    .cp-lo { color: var(--status-danger-text, #9b2c2c); font-weight: 800; }
    .cp-hi { color: var(--status-warning-text, #8a5a12); font-weight: 800; }
    .cp-chem-na { color: #b9c9d2; }
    .cp-chem-foot { margin: 8px 0 0; font-size: 11.5px; line-height: 1.5; color: var(--text-muted); }

    /* Collapsible dashboard sections. Collapsing hides the BODY and keeps the
       head, so the section still announces itself and its count when shut --
       a collapsed section that vanished entirely would read as a missing
       feature. Matches the sys-health pattern: `.open` is the state class and
       the chevron rotates off it. */
    .dash-collapsible > .dash-panel-head,
    .dash-collapsible > .section-header { cursor: pointer; }
    .dash-collapsible > .dash-panel-head .chev,
    .dash-collapsible > .section-header .chev {
      width: var(--icon-sm); height: var(--icon-sm); margin-left: auto;
      flex-shrink: 0; opacity: .55; transition: transform .15s;
      transform: rotate(-90deg);
    }
    .dash-collapsible.open > .dash-panel-head .chev,
    .dash-collapsible.open > .section-header .chev { transform: rotate(0deg); }
    /* Down to one child selector 2026-09-27. .dash-tab-bar went with the
       Schedule panel and .tk-flagged-sub / #nr-list with the flagged-customers
       block; both are now drawers reached from elsewhere. */
    .dash-collapsible > .dash-panel-body { display: none; }
    .dash-collapsible.open > .dash-panel-body { display: block; }

/* A charged pay-link form is a receipt, not an input. Muting the FIELDS only --
   #pl-result is not a .field, so the link, Copy and the charge outcome stay at
   full contrast, which is what the office actually needs to read next. */
#pl-panel-body.pl-locked .field { opacity: .55; }
#pl-panel-body.pl-locked .field,
#pl-panel-body.pl-locked .field * { cursor: not-allowed; }

/* The day offer in the parts rail. Sized off .pf-date so the row reads as one
   control strip rather than a form that grew. [hidden] is set imperatively on
   these, and the project's global [hidden] rule is what makes that stick
   against element-level display (see CLAUDE.md). */
/* The sequential day question (2026-09-29). .pf-step is a wrapping flex row,
   so each of these wrappers takes a full basis to start its own line -- the
   card then reads as one question after another rather than as a strip of
   controls that happens to wrap. They stay inline-flex internally so the label
   and its controls sit together on that line. */
.pf-whoday, .pf-dayopts, .pf-sendrow {
  display: inline-flex; flex-wrap: wrap; gap: .4rem; align-items: center;
  flex: 1 1 100%;
}
/* [hidden] must beat the display above. The project's global
   `[hidden]{display:none!important}` exists for exactly this (CLAUDE.md), but
   these are declared here too so the rule survives a stylesheet that loads
   without it. */
.pf-whoday[hidden], .pf-dayopts[hidden], .pf-sendrow[hidden] { display: none !important; }
.pf-sendnote { flex: 0 1 auto; }

.pf-mode { font-size: var(--fs-xs); padding: .28rem .4rem;
           border: 1px solid var(--border-control); border-radius: 6px;
           background: var(--white); color: var(--text-strong); font-family: inherit; }
.pf-days { display: inline-flex; gap: .3rem; flex-wrap: wrap; }
.pf-days .pf-day { font-size: var(--fs-xs); padding: .28rem .4rem;
                   border: 1px solid var(--border-control); border-radius: 6px;
                   background: var(--white); color: var(--text-strong); font-family: inherit; }

/* ── Shorten a link (sql/794) ──────────────────────────────────────────────
   Almost nothing here, on purpose. The two inputs are `.field`, the button is
   `.btn .btn-primary .btn-block`, and Recent links is `.settings-acc` (the
   Settings panel's own <details> accordion). What remains is the one thing the
   system has no component for: a short link you tap to copy, and the list of
   past ones under it.

   Every colour, size and rule below is a declared token. An earlier pass here
   invented `--text-soft`, `--surface-soft` and `--border-soft` with hard-coded
   fallbacks, which is how a fourth almost-right grey gets into a codebase that
   already has three named ones. */

/* THE RESULT. The only loud thing in the drawer, because it is the one thing
   you came for and the one thing you are about to paste. */
#sl-result { margin-top: 1rem; padding: .85rem; border-radius: var(--radius);
             background: var(--status-info-soft); border: 1px solid var(--status-info-border);
             text-align: center; }
#sl-result .sl-result-cap { font-size: var(--fs-eyebrow); font-weight: 700;
                            color: var(--text-muted); text-transform: uppercase;
                            letter-spacing: .08em; margin-bottom: .35rem; }
/* A button, not a div: it copies on tap, so it has to be focusable and
   announced as something pressable. --status-info-text, not --ocean: this sits
   on --status-info-soft, where the plain ocean fails AA (tokens.css says so in
   as many words). */
#sl-result .sl-short { font-family: var(--font-sans); font-size: var(--fs-base);
                       font-weight: 700; color: var(--status-info-text);
                       background: none; border: 0; padding: 0; cursor: pointer;
                       word-break: break-all; }

/* A hint under a .field control. Matches the field caption's muted colour but
   stays sentence case, because it is a sentence and not a label. */
#sl-modal .sl-share, #sl-modal .sl-retry { font-family: var(--font-sans); font-size: var(--fs-xs);
                     font-weight: 600; color: var(--text-link); background: none; border: 0;
                     padding: 0; cursor: pointer; text-decoration: underline; min-height: 44px; }
#sl-modal .sl-share { display: block; margin: .25rem auto 0; }
#sl-modal .sl-retry { min-height: 0; padding: .5rem 0 .5rem .25rem; }
#sl-modal .sl-manual { margin-top: .75rem; }
#sl-modal .sl-manual input { width: 100%; font-size: 16px; min-height: 44px; padding: 0 .75rem;
                     border: 1px solid var(--app-border-soft); border-radius: var(--radius); }
#sl-modal .sl-hint { font-size: var(--fs-xs); color: var(--text-muted); margin-top: .35rem; }

/* RECENT LINKS. Deliberately quiet: inside a closed accordion, on the app's
   border token, with no fills and no accent beyond the link itself. */
#sl-modal .sl-acc { margin-bottom: 0; }
#sl-modal .sl-empty { font-size: var(--fs-xs); color: var(--text-muted); padding: .2rem 0 .6rem; }
#sl-modal .sl-row { display: flex; gap: .75rem; align-items: flex-start;
                    padding: .6rem 0; border-top: 1px solid var(--app-border-soft); }
#sl-modal .sl-row:first-child { border-top: 0; }
/* min-width:0 is what actually lets the ellipsis fire below: a flex child
   defaults to min-width:auto and refuses to shrink under its content, so a long
   URL would widen the row instead of truncating (the grid-track lesson in
   Paperdash DECISIONS, same cause, different layout mode). */
#sl-modal .sl-row-main { flex: 1 1 auto; min-width: 0; }
#sl-modal .sl-row-side { flex: 0 0 auto; display: flex; flex-direction: column;
                         align-items: flex-end; gap: .2rem; }
#sl-modal .sl-copy { font-family: var(--font-sans); font-size: var(--fs-xs); font-weight: 600;
                     color: var(--text-link); background: none; border: 0; padding: 0;
                     cursor: pointer; display: block; max-width: 100%;
                     overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
#sl-modal .sl-row-meta { font-size: var(--fs-eyebrow); color: var(--text-muted); margin-top: .1rem;
                         overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
#sl-modal .sl-opens { font-size: var(--fs-eyebrow); color: var(--text-muted); white-space: nowrap; }
/* A switched-off link stays listed and readable, just visibly retired: it is
   still the record of something we sent, and hiding it would make "did I turn
   that off?" unanswerable. */
#sl-modal .sl-off .sl-copy { text-decoration: line-through; opacity: .55; }
#sl-modal .sl-off .sl-row-meta { opacity: .55; }

/* ── The supersede step (sql/815) ──────────────────────────────────────────
   "Is this one instead of that, or as well as it?" Rendered into the estimate
   drawer's step container, so it is scoped like everything else that lives
   inside that modal. The status pills reuse the documented --status-*-text on
   --status-*-soft pairs rather than hand-picked colours; tokens.css records
   those as AA at this size. */
#create-quote-modal .cq-sup-sub {
  margin: 0 0 1rem; font-size: var(--fs-sm); color: var(--text-muted); line-height: 1.5;
}
#create-quote-modal .cq-sup-list { margin-bottom: 1rem; }
#create-quote-modal .cq-sup-row {
  display: flex; gap: .7rem; align-items: flex-start;
  border: 1.5px solid var(--border); border-radius: 12px;
  padding: .85rem .95rem; margin-bottom: .6rem; cursor: pointer;
}
#create-quote-modal .cq-sup-row:hover { border-color: var(--ocean); }
#create-quote-modal .cq-sup-row input { margin-top: .2rem; flex: 0 0 auto; }
#create-quote-modal .cq-sup-row-text { display: block; }
#create-quote-modal .cq-sup-row-t { display: block; font-weight: 700; font-size: var(--fs-sm); }
#create-quote-modal .cq-sup-row-m {
  display: block; font-size: var(--fs-xs); color: var(--text-muted);
  line-height: 1.6; margin-top: .2rem;
}
#create-quote-modal .cq-sup-pill {
  display: inline-block; font-size: var(--fs-xs); font-weight: 700;
  border-radius: 999px; padding: 1px 8px;
}
#create-quote-modal .cq-sup-pill-info    { color: var(--status-info-text);    background: var(--status-info-soft); }
#create-quote-modal .cq-sup-pill-danger  { color: var(--status-danger-text);  background: var(--status-danger-soft); }
#create-quote-modal .cq-sup-pill-warning { color: var(--status-warning-text); background: var(--status-warning-soft); }
#create-quote-modal .cq-sup-carry {
  display: block; background: var(--status-success-soft); color: var(--status-success-text);
  border-radius: 10px; padding: .7rem .8rem; margin-bottom: 1rem;
  font-size: var(--fs-sm); font-weight: 650; line-height: 1.5; cursor: pointer;
}

/* ─── Cost review drawer (job_costs) ──────────────────────────────────────
   Every custom property below is checked against brand/tokens.css. A
   var(--name, #fallback) renders fine when --name does not exist, which is
   exactly how a fourth almost-right grey enters a codebase that already has
   three named ones -- so there are no fallbacks here on purpose. */
.mr-cost-row {
  border: 1px solid var(--app-border-soft);
  border-radius: var(--radius-md);
  padding: var(--space-3);
  margin-bottom: var(--space-3);
  background: var(--app-surface);
}
.mr-cost-row.suspect {
  border-color: var(--status-danger-border);
  background: var(--status-danger-soft);
}
.mr-cost-row.done { opacity: .55; }
.mr-cost-head { display: flex; flex-wrap: wrap; gap: var(--space-2); align-items: baseline; }
.mr-cost-head .mr-note { font-size: var(--fs-xs); color: var(--app-text-secondary); }
/* The original prose. Monospace because it is a QUOTATION of what somebody
   typed, not our own copy, and it must not read as if we wrote it. */
.mr-cost-src {
  font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
  font-size: var(--fs-xs);
  color: var(--app-text-secondary);
  background: var(--app-neutral-bg);
  border-radius: var(--radius-sm);
  padding: var(--space-2);
  margin: var(--space-2) 0;
  white-space: pre-wrap;
}
.mr-cost-fields { display: flex; flex-wrap: wrap; gap: var(--space-3); }
.mr-cost-fields label {
  display: flex; align-items: center; gap: var(--space-1);
  font-size: var(--fs-xs); color: var(--app-text-secondary);
}
.mr-cost-fields input {
  width: 8.5rem; padding: var(--space-1) var(--space-2);
  border: 1px solid var(--app-border); border-radius: var(--radius-sm);
  font: inherit; font-size: var(--fs-sm);
}
.mr-cost-actions {
  display: flex; flex-wrap: wrap; align-items: center;
  gap: var(--space-2); margin-top: var(--space-3);
}
.mr-cost-said { font-size: var(--fs-xs); color: var(--app-text-secondary); }
@media (max-width: 560px) {
  .mr-cost-fields { flex-direction: column; align-items: stretch; }
  .mr-cost-fields input { width: 100%; }
}

/* ─── THE CUSTOMER RECORD, PHONE-READY (#money-drawer.cr-mode) ───────────────────────────────────────
   Rules in MOBILE-COMPONENTS-SPEC.md. The drawer shell is shared with the money and KPI drawers, so
   every rule here hangs off .cr-mode. Button SIZES live in the button component block above; this
   block only sizes non-button controls (fields, links, thumbnails). */
#money-drawer.cr-mode .drawer-header { padding: 10px 8px 10px 16px; gap: 8px; }
#money-drawer.cr-mode .drawer-body { padding: 0 16px 16px; }
#money-drawer.cr-mode .cr-summary { margin: 12px 0 0; padding-bottom: 0; border-bottom: 0; font-size: .9375rem; }
#money-drawer.cr-mode .cr-alerts { display: flex; flex-direction: column; gap: 10px; margin-top: 12px; }
#money-drawer.cr-mode .cr-tabpanel { display: flex; flex-direction: column; gap: 14px; padding-top: 6px; }
#money-drawer.cr-mode .cr-tabpanel[hidden] { display: none; }
#money-drawer.cr-mode .cr-sec-title { margin: 6px 0 0; font-size: .75rem; font-weight: 700; letter-spacing: .06em;
                                      text-transform: uppercase; color: var(--text-muted); }
#money-drawer.cr-mode .cr-head-foot { display: flex; align-items: center; justify-content: space-between; gap: 12px; flex-wrap: wrap; }
#money-drawer.cr-mode .cr-ct-list { display: flex; flex-direction: column; gap: 10px; }
#money-drawer.cr-mode .cr-ct-acts { display: flex; flex-wrap: wrap; gap: 8px; align-items: center; margin-top: 6px; }
#money-drawer.cr-mode .cr-ct-acts .m-iconbtn { margin-left: auto; }
#money-drawer.cr-mode .cr-ct-add { display: flex; flex-direction: column; gap: 12px; }
/* Fields: 48px and 16px, so iOS does not zoom the page when one takes focus. */
#money-drawer.cr-mode input, #money-drawer.cr-mode select, #money-drawer.cr-mode textarea { min-height: 48px; font-size: 1rem; }
#money-drawer.cr-mode input[type="checkbox"], #money-drawer.cr-mode input[type="radio"] { min-height: 0; }
/* The ledger: each row's actions drop under it as a full row instead of squeezing the name. */
#money-drawer.cr-mode .cr-row { flex-wrap: wrap; }
#money-drawer.cr-mode .cr-row-act { flex: 1 1 100%; flex-wrap: wrap; }
#money-drawer.cr-mode .cr-row-act .m-btn { flex: 1 1 auto; }
#money-drawer.cr-mode .cr-row-meta, #money-drawer.cr-mode .cr-did { font-size: .875rem; }
#money-drawer.cr-mode .cr-row-meta a, #money-drawer.cr-mode .cr-did-link { display: inline-flex; align-items: center; min-height: 44px; color: var(--tf-ocean-800); }
/* Callouts from MCallout.ask() that open above the pinned bar. */
#money-drawer.cr-mode > .m-callout { margin: 8px 16px 0; flex: none; }
/* The pool section: every tappable thing is 44px. */
#money-drawer.cr-mode .cd-kebab { min-width: 44px; min-height: 44px; font-size: 1.25rem; }
#money-drawer.cr-mode .cd-link, #money-drawer.cr-mode .cd-gapbtn, #money-drawer.cr-mode .cr-name-link {
  display: inline-flex; align-items: center; justify-content: center; min-height: 44px; min-width: 44px; }
#money-drawer.cr-mode .cd-fold > summary { display: flex; align-items: center; min-height: 44px; }
#money-drawer.cr-mode .cp-hit { display: flex; align-items: center; gap: 8px; min-height: 44px; }
#money-drawer.cr-mode .cd-td-main { min-width: 0; }
#money-drawer.cr-mode .ep-thumb { width: 96px; height: 96px; }
/* The delete X is a small quiet dot in the corner. Its tap zone is still 44px, but most of that sits OUTSIDE
   the tile (the tile no longer clips), so it hides almost none of the picture. */
#money-drawer.cr-mode .ep-thumb { overflow: visible; }
#money-drawer.cr-mode .ep-thumb img { border-radius: var(--radius-sm); }
#money-drawer.cr-mode .ep-thumb-del { width: 44px; height: 44px; top: -12px; right: -12px; background: none; border-radius: 0; opacity: .9; }
#money-drawer.cr-mode .ep-thumb-del:hover, #money-drawer.cr-mode .ep-thumb-del:focus-visible { opacity: 1; }
#money-drawer.cr-mode .ep-thumb-del svg { width: 14px; height: 14px; padding: 6px; border-radius: 50%; background: rgba(0,0,0,.6); box-sizing: content-box; }
#money-drawer.cr-mode .ep-thumb-new { width: 44px; height: 44px; border-radius: 0; font-size: 18px; top: 0; left: 0; }
/* The delete question lives on the photo: a check and a cross over the tile. */
#money-drawer .ep-confirm { position: absolute; inset: 0; border-radius: var(--radius-sm); z-index: 3; display: flex; flex-direction: column;
                            align-items: center; justify-content: center; gap: 2px; padding: 2px;
                            background: rgba(0,0,0,.78); color: #fff; text-align: center; }
#money-drawer .ep-confirm-q { font-size: .6875rem; font-weight: 700; line-height: 1.2; }
#money-drawer .ep-confirm-acts { display: flex; gap: 4px; }
#money-drawer .ep-confirm-acts button { width: 44px; height: 44px; border: 0; border-radius: 10px; padding: 0;
                                        display: flex; align-items: center; justify-content: center; cursor: pointer; }
#money-drawer .ep-confirm-acts svg { width: 22px; height: 22px; }
#money-drawer .ep-confirm-yes { background: var(--status-danger); color: var(--white); }
#money-drawer .ep-confirm-no { background: var(--white); color: var(--text-strong); }
/* The photo viewer opens over everything: its filing control is as big as the rest. */
.ep-newequip select, .ep-newequip input { font-size: 1rem; min-height: 48px; }
/* The first section in a panel sits at the top of it, not 40px down under a rule. */
#money-drawer.cr-mode .cr-tabpanel > .cd-sec:first-child { margin-top: 0; border-top: 0; padding-top: 0; }
#money-drawer.cr-mode .cd-link svg, #money-drawer.cr-mode .cd-gapbtn svg, #money-drawer.cr-mode .cd-plus svg { width: 20px; height: 20px; }
#money-drawer.cr-mode .cd-plus { color: var(--tf-ocean-800); border-color: var(--tf-ocean-800); }

/* ── The photo viewer: dark glass, the picture large, three quiet icons, one pill ─────────────────── */
.ep-viewer { display: block; overflow: hidden; padding: 0; background: rgba(8, 12, 16, .88);
             -webkit-backdrop-filter: blur(14px); backdrop-filter: blur(14px); }
.ep-viewer .ep-vw-scroll { position: absolute; inset: 0; overflow: auto; }
/* The bar and foot sit OUTSIDE the scroller: backdrop-filter makes the viewer a containing block, so a fixed child inside a scrolling viewer would scroll away with the picture. */
.ep-viewer .ep-vw-stage { min-height: 100%; width: max-content; min-width: 100%; box-sizing: border-box; display: flex;
                          align-items: center; justify-content: center; padding: 68px 12px 96px; }
.ep-viewer img { display: block; max-width: min(calc(100vw - 24px), 1200px); max-height: calc(100dvh - 164px);
                 border-radius: 14px; box-shadow: 0 24px 64px rgba(0, 0, 0, .5); cursor: zoom-in; }
.ep-viewer.ep-zoomed img { max-width: none; max-height: none; border-radius: 8px; cursor: zoom-out; }
.ep-vw-bar { position: fixed; top: max(12px, env(safe-area-inset-top)); left: 12px; right: 12px; z-index: 2;
             display: flex; align-items: center; gap: 8px; pointer-events: none; }
.ep-vw-bar > * { pointer-events: auto; }
.ep-vw-spacer { flex: 1; }
.ep-vw-tool { width: 44px; height: 44px; padding: 0; border: 1px solid rgba(255, 255, 255, .16); border-radius: 50%;
              background: rgba(255, 255, 255, .12); color: #fff; cursor: pointer; display: flex; align-items: center;
              justify-content: center; -webkit-backdrop-filter: blur(10px); backdrop-filter: blur(10px); }
.ep-vw-tool:hover { background: rgba(255, 255, 255, .22); }
.ep-vw-tool svg { width: 20px; height: 20px; }
.ep-vw-foot { position: fixed; left: 12px; right: 12px; bottom: max(16px, env(safe-area-inset-bottom)); z-index: 2;
              display: flex; flex-direction: column; align-items: center; gap: 10px; pointer-events: none; }
.ep-vw-foot > * { pointer-events: auto; }
.ep-viewer .ep-lightbox-read { margin: 0; color: rgba(255, 255, 255, .78); font-size: .8125rem; text-align: center; }
.ep-file label { display: inline-flex; align-items: center; gap: 6px; height: 44px; padding: 0 6px 0 16px; border-radius: 999px;
                 border: 1px solid rgba(255, 255, 255, .16); background: rgba(255, 255, 255, .12); color: rgba(255, 255, 255, .78);
                 font-size: .875rem; font-weight: 600; -webkit-backdrop-filter: blur(10px); backdrop-filter: blur(10px); }
.ep-file select { min-height: 44px; max-width: 62vw; padding: 0 8px; border: 0; background: transparent; color: #fff;
                  font-size: 1rem; font-weight: 600; }
.ep-file option { color: #111; }
.ep-viewer.ep-zoomed .ep-vw-foot { display: none; }

/* ── Google pin: the panel above the viewer's foot, and the posted dot ───────────────────────────── */
.ep-vw-pin { position: relative; }
.ep-vw-pin.ep-posted { background: var(--success, #2d7a4f); border-color: transparent; }
.ep-gbp { width: min(100%, 440px); }
.ep-gbp-card { background: #fff; color: var(--tf-ocean-800, #0b4f6c); border-radius: 14px; padding: 12px; font-size: .875rem; }
.ep-gbp-card b { display: block; color: var(--navy); font-size: .9375rem; }
.ep-gbp-card p { margin: 0 0 8px; color: var(--muted); }
.ep-gbp-why { background: var(--warning-bg); color: var(--warning); border-radius: 10px; padding: 10px 12px; margin-bottom: 8px; }
.ep-gbp-ok { background: var(--success-bg); color: var(--success); border-radius: 10px; padding: 10px 12px; margin-bottom: 8px; }
.ep-gbp-row { display: flex; align-items: center; gap: 12px; }
.ep-gbp-spin { width: 22px; height: 22px; border-radius: 50%; border: 3px solid var(--border); border-top-color: var(--cerulean); animation: ep-gbp-spin 1s linear infinite; flex: none; }
@keyframes ep-gbp-spin { to { transform: rotate(360deg); } }
.ep-viewer.ep-zoomed .ep-gbp { display: none; }

/* ── Paging through photos in the viewer ─────────────────────────────────────────────────────────── */
.ep-vw-nav { position: fixed; top: 50%; transform: translateY(-50%); z-index: 2; }
.ep-vw-prev { left: 10px; }
.ep-vw-next { right: 10px; }
.ep-vw-count { color: rgba(255, 255, 255, .78); font-size: .8125rem; font-weight: 600; padding: 0 6px; }
.ep-viewer.ep-zoomed .ep-vw-nav { display: none; }

/* ── New Job, Take-card and Take-payment drawers on a phone (2026-10-17) ────────────────────────────
   Same markup, ids and handlers as before; this block raises every target to 44px and every field to
   16px, turns the stepper into a real progress bar and the footer into a pinned action bar. Rules live
   here, keyed on the drawer ids, so the other drawers that share .drawer are untouched. Tokens only. */
#create-modal, #phone-pay-modal, #sub-checkout-modal { height: 100dvh; }
#create-modal .drawer-body, .pay-drawer .drawer-body { padding: 16px; overscroll-behavior: contain; }
#create-modal .drawer-header, .pay-drawer .drawer-header { padding: 10px 8px 10px 16px; }

/* Progress: four 44px segments. Done is green, the current step shows its name, the rest show a number. */
#create-stepper { padding: 8px 12px; gap: 6px; }
#create-stepper .step-pill { min-height: 44px; display: flex; align-items: center; justify-content: center; gap: 6px;
  padding: 0 4px; font-size: .8125rem; white-space: nowrap; }
#create-stepper .step-pill .step-num { width: 28px; height: 28px; line-height: 28px; margin: 0; font-size: .8125rem; text-align: center; flex: none; }
#create-stepper .step-pill.done { cursor: pointer; }
@media (max-width: 560px) {
  #create-stepper .step-pill { flex: 1; }
  #create-stepper .step-pill.active { flex: 3; }
  #create-stepper .step-pill:not(.active) .step-lbl { display: none; }
}

/* Footer: Back and Continue as one pinned bar, safe-area aware. */
#create-footer { padding: 12px 16px calc(12px + env(safe-area-inset-bottom, 0px)); gap: 10px; flex-wrap: wrap; }
#create-footer > .m-btn { flex: 1; min-width: 0; }
#create-footer > #create-nav-next-btn, #create-footer > #create-confirm-btn { flex: 2; }
#create-footer .modal-error:empty { display: none; }

/* Fields: 48px controls at 16px. */
#create-modal .field input:not([type="checkbox"]):not([type="radio"]),
#create-modal .field select { height: 48px; min-height: 48px; font-size: 1rem; }
#create-modal .field textarea { font-size: 1rem; min-height: 96px; }
#create-modal .of-ask-in input, #create-modal .of-edit-row textarea { font-size: 1rem; min-height: 44px; }

/* Whole-card choices for the radios and checkboxes. */
#create-modal .field label.deposit-opt { padding: 12px 14px; border: 1px solid var(--border); border-radius: 12px;
  background: var(--white); margin-bottom: 8px; min-height: 48px; align-items: flex-start; gap: 12px; }
#create-modal .field label.deposit-opt input[type="radio"],
#create-modal .field label.deposit-opt input[type="checkbox"] { width: 24px; height: 24px; min-width: 24px; margin: 0; }
#create-modal .field label.deposit-opt:has(input:checked) { border-color: var(--ocean); background: var(--aqua-soft); }
#create-modal .deposit-opt-hint { font-size: .8125rem; }
#create-silent-note { margin-top: 8px; }
#create-silent-note:empty { display: none; }

/* Tap targets that were text-sized. */
#create-modal .cust-result { min-height: 52px; display: flex; flex-direction: column; justify-content: center; }
#create-modal .cust-selected { min-height: 52px; }
#create-modal .cust-selected button, #create-modal .create-strip-change, #create-modal .of-toggle, #create-modal .of-edit,
#create-modal .of-r, #create-modal .review-edit-link, #create-modal .of-ask-in button {
  min-height: 44px; min-width: 44px; display: inline-flex; align-items: center; justify-content: center; }
#create-modal .of-r { display: flex; width: 100%; justify-content: space-between; }
#create-modal .of-edit-row { display: flex; gap: 8px; align-items: flex-start; margin-top: 6px; }
#create-modal .of-edit-row textarea { flex: 1; min-width: 0; padding: 10px 12px; border: 1px solid var(--border-control); border-radius: 12px; font-family: var(--font-sans); }
#create-modal .review-edit-link svg { width: 22px; height: 22px; }
#create-modal .btn-remove-line, #create-modal .btn-add-line { min-width: 44px; min-height: 44px; }
#create-modal .field label.deposit-opt { position: relative; }
#create-modal .field label.deposit-opt:has(.tpl-peek) { padding-right: 56px; }
#create-modal .tpl-peek { position: absolute; top: 2px; right: 4px; width: 44px; height: 44px; margin: 0; border: 0; border-radius: 0; background: none; font-size: 0; }
#create-modal .tpl-peek::before { content: "i"; width: 24px; height: 24px; border: 1.5px solid var(--ocean); border-radius: 50%; color: var(--ocean);
  display: flex; align-items: center; justify-content: center; font-size: .875rem; font-weight: 700; font-style: italic; line-height: 1; }
#create-modal .jl { min-height: 56px; }
#create-modal .of-ask-in { flex-wrap: wrap; }
#create-modal .of-ask-in input { flex: 1 1 100%; }

/* Take card by phone / Take payment: Stripe owns the iframe, so all we do is give it room. */
.pay-drawer .pay-summary { margin-bottom: 12px; }
.pay-drawer .pay-note { margin: 0 0 12px; font-size: .8125rem; color: var(--text-muted); }
.pay-drawer .drawer-body { padding-bottom: calc(16px + env(safe-area-inset-bottom, 0px)); }
.pay-drawer [id$="-mount"] { min-height: 120px; }

/* Photo removal request card (02-financial.js photoReqRowHTML) */
#panel-tasks .pr-kick { font-size: var(--fs-xs, .75rem); color: var(--text-muted); text-transform: uppercase; letter-spacing: .04em; }
#panel-tasks .pr-img { display: block; width: 100%; max-width: 360px; max-height: 240px; object-fit: cover; border-radius: 12px; margin: .6rem 0; }
#panel-tasks .pr-note { background: var(--surface-sand, #f5ead8); border-radius: 10px; padding: .5rem .7rem; margin: .4rem 0; font-size: .95rem; }
#panel-tasks .pr-fine { font-size: .8rem; color: var(--text-muted); margin: .4rem 0; }
#panel-tasks .pr-acts { display: flex; align-items: center; gap: .5rem; flex-wrap: wrap; }
#panel-tasks .pr-acts[hidden], #panel-tasks .pr-ask[hidden] { display: none; }
#panel-tasks .pr-ask { display: flex; align-items: center; gap: .5rem; background: var(--text-strong); color: #fff; border-radius: 12px; padding: .4rem .7rem; margin-top: .5rem; }
#panel-tasks .pr-ask span { flex: 1; font-size: .9rem; }
#panel-tasks .pr-ibtn { width: 44px; height: 44px; display: inline-flex; align-items: center; justify-content: center; flex: none; }

/* Photo needs review card (02-financial.js photoReviewRowHTML, sql/893) */
#panel-tasks .rv-img { position: relative; display: block; width: 100%; max-width: 360px; aspect-ratio: 4 / 3; margin: .6rem 0; padding: 0; border: 0; border-radius: 12px; overflow: hidden; background: var(--text-strong); cursor: pointer; }
#panel-tasks .rv-img[hidden] { display: none; }
#panel-tasks .rv-img img { position: absolute; inset: 0; width: 100%; height: 100%; object-fit: cover; filter: blur(28px); transform: scale(1.1); }
#panel-tasks .rv-img.shown img { filter: none; transform: none; }
#panel-tasks .rv-veil { position: absolute; inset: 0; display: flex; flex-direction: column; align-items: center; justify-content: center; gap: .4rem; background: rgba(6, 40, 64, .55); color: #fff; font-size: .9rem; }
#panel-tasks .rv-img.shown .rv-veil { display: none; }
#panel-tasks .rv-keep { flex: 1; min-height: 44px; }
#panel-tasks .rv-dest { border: 2px solid var(--tf-ocean); border-radius: 14px; padding: .5rem .7rem; margin-top: .6rem; }
#panel-tasks .rv-dest[hidden] { display: none; }
#panel-tasks .rv-dest-h { font-weight: 600; color: var(--text-strong); margin-bottom: .2rem; }
#panel-tasks .rv-opt { display: flex; align-items: center; gap: .75rem; width: 100%; min-height: 56px; padding: .25rem 0; border: 0; border-top: 1px solid var(--app-border-soft); background: none; text-align: left; font: inherit; color: inherit; cursor: pointer; }
#panel-tasks .rv-opt[hidden] { display: none; }
#panel-tasks .rv-opt b { display: block; color: var(--text-strong); }
#panel-tasks .rv-opt small { color: var(--text-muted); }
#panel-tasks .rv-ic { width: 44px; height: 44px; border-radius: 50%; background: var(--tf-aqua-soft); color: var(--tf-cerulean); display: inline-flex; align-items: center; justify-content: center; flex: none; }

/* ── Estimates, on every width: Charge is the money button ────────────────────────────────
   Navy, like every other button that moves money (Collect, Charge on the visit panel). It was ocean,
   the same as Send, so the one that charges a card read as a peer of the one that texts a link. */
.btn-quote-charge { background: var(--tf-navy); border-color: var(--tf-navy); color: #fff; }
.btn-quote-charge:hover { background: var(--tf-ink-700); border-color: var(--tf-ink-700); color: #fff; }
/* The one-line summary under a card's title. Shown only where a card is a row (phones). */
.q-row-sub { display: none; }

/* An inline question placed before a card's actions row spans the whole card, so on desktop it does not
   land in whatever grid track auto-placement picks. */
.quote-card > .m-callout { grid-column: 1 / -1; }

/* ── Under 861px: a field is 16px, an estimate is a row, the form's Save is pinned ──────────
   16px is for iOS, which zooms the page when a smaller field takes focus and stays zoomed. It is set on
   the shared .field class, so every drawer that uses one inherits it. */
@media (max-width: 860px) {
  .field input, .field select, .field textarea { font-size: 1rem; }
  /* The older modals set their own 15px on .modal-card fields, which outranks .field. Same fix, same reason. */
  .modal-card select, .modal-card textarea, .modal-card input[type="text"], .modal-card input[type="email"],
  .modal-card input[type="tel"], .modal-card input[type="date"], .modal-card input[type="search"],
  .modal-card input[type="number"] { font-size: 1rem; }
  .quote-line-row input { font-size: 1rem; min-height: 44px; }

  /* The + button already offers "Build a quote", so the header button is a second 30px entrance. */
  #btn-new-estimate { display: none; }
  .quote-panel-header { margin-bottom: .6rem; }

  /* Filters: one row that scrolls sideways, never wraps; each chip is a 44px target. */
  .quote-filter-tabs {
    display: flex; flex-wrap: nowrap; overflow-x: auto; gap: 8px; padding: 2px 0 8px;
    background: transparent; border-radius: 0; scrollbar-width: none;
  }
  .quote-filter-tabs::-webkit-scrollbar { display: none; }
  .quote-filter-tab {
    flex: none; min-height: 44px; padding: 0 16px; font-size: .9375rem; white-space: nowrap;
    background: var(--tf-aqua-050); border: 1px solid var(--tf-ink-200);
  }
  .quote-filter-tab.active { background: var(--tf-ocean-800); border-color: var(--tf-ocean-800); color: #fff; }

  /* A card is one tappable row: title and customer on the left, status and total on the right. The
     actions, the dates and the strips all live in the sheet it opens (13-estimates.js, qsOpen). */
  .quote-list { gap: .6rem; margin-bottom: 6rem; }
  .quote-card {
    grid-template-columns: minmax(0, 1fr) auto 14px; gap: .1rem .7rem; align-items: center;
    padding: .75rem .85rem; min-height: 72px; cursor: pointer; border-left-width: 3px;
  }
  .quote-card:focus-visible { outline: 2px solid var(--tf-ocean-800); outline-offset: 2px; }
  .quote-card > div:first-child { display: contents; }
  .quote-card > :not(:first-child) { display: none; }
  .quote-card::after { content: "\203A"; grid-column: 3; grid-row: 1 / span 2; font-size: 1.6rem; line-height: 1; color: var(--tf-ink-400); }
  .quote-card .quote-title {
    grid-column: 1; margin: 0; font-size: .9375rem; line-height: 1.3;
    display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical; overflow: hidden;
  }
  .quote-card .quote-meta { display: none; }
  .q-row-sub { display: block; grid-column: 1; font-size: .8125rem; color: var(--text-muted); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
  .quote-card .quote-money { grid-column: 2; grid-row: 1 / span 2; flex-direction: column; align-items: flex-end; gap: 4px; margin: 0; flex-wrap: nowrap; }
  .quote-card .quote-money > :nth-child(n+3) { display: none; }

  /* New Estimate: the same drawer, sized for a thumb. The actions row stays at the bottom of the screen
     while the form scrolls, so Save is never below the fold. */
  #qs-host .m-sheet-title-wrap > * { align-self: flex-start; }
  #create-quote-modal .modal-card { padding: 1rem 1rem 0; }
  #create-quote-modal .modal-x {
    min-width: 44px; min-height: 44px; display: inline-flex; align-items: center; justify-content: center; font-size: 1.6rem;
  }
  #create-quote-modal .quote-line-row { grid-template-columns: 80px 1fr 44px; }
  #create-quote-modal .quote-line-row > :first-child { grid-column: 1 / -1; }
  #create-quote-modal .btn-remove-line { min-width: 44px; min-height: 44px; font-size: 1.5rem; }
  #create-quote-modal .btn-add-line { min-height: 48px; font-size: .9375rem; }
  #create-quote-modal .field input[type="checkbox"], #create-quote-modal .field input[type="radio"],
  #create-quote-modal .deposit-opt input { width: 24px; height: 24px; }
  #create-quote-modal .move-actions {
    position: sticky; bottom: 0; z-index: 2; margin: 1.25rem -1rem 0; padding: .75rem 1rem;
    padding-bottom: calc(.75rem + env(safe-area-inset-bottom, 0px));
    background: var(--white); border-top: 1px solid var(--tf-ink-100); display: flex; gap: .6rem;
  }
  #create-quote-modal .move-actions .btn { flex: 1; min-height: 48px; }
}

/* ── Schedule on a phone ───────────────────────────────────────────────────────────────────────
   One compact bar (Tech chip, then the Today/view button). Every control is 44px.
   The Assign dropdown is shown as a plain value; a tap opens a list (openAssignSheet). */
@media (max-width: 860px) {
  #panel-schedule .dispatch-topbar { flex-wrap: nowrap; padding: 8px 14px; gap: 8px; }
  #panel-schedule .dispatch-topbar-left { flex: 1; min-width: 0; }
  /* Day view steps by day (the bar under this one), so the week arrows and label would repeat it. */
  #panel-schedule .dispatch-board.is-day .dispatch-topbar-left > .dispatch-nav-btn,
  #panel-schedule .dispatch-board.is-day .dispatch-week-label { display: none; }
  #panel-schedule .dispatch-week-label { min-width: 0; flex: 1; }
  /* Its three controls live in the ⋯ sheet on a phone. Hidden outright, not :empty, because the whitespace
     left in the markup stops :empty matching and an empty full-width group squeezes the week label to nothing. */
  #panel-schedule .dispatch-topbar-right { display: none; }
  .dispatch-nav-btn { width: 44px; height: 44px; }
  .dispatch-today-btn { height: 44px; padding: 0 14px; font-size: .9375rem; flex: none; }
  /* "All assigned" is good news, not a task: on a phone it costs a button's worth of room, so it only shows
     when something needs assigning. */
  .dispatch-unassigned-badge.all-clear { display: none; }
  .dispatch-optimize-btn { min-height: 44px; font-size: .9375rem; }
  .dispatch-unassigned-badge { min-height: 44px; }
  .dispatch-tech-tabs { overflow-x: auto; flex-wrap: nowrap; scrollbar-width: none; }
  .dispatch-tech-tabs::-webkit-scrollbar { display: none; }
  .dispatch-tech-tab { flex: none; min-height: 44px; padding: 0 16px; font-size: .9375rem; }

  /* A flat row, not a filled box: still a 44px target, but a stop is mostly text and a filled bar under
     every one made the list read as a column of buttons. */
  .dispatch-stop-assign {
    min-height: 44px; margin-top: 4px; padding: 0 2px 0 0; justify-content: space-between;
    border: 0; border-top: 1px solid var(--tf-ink-100); border-radius: 0; background: transparent; cursor: pointer;
  }
  .dispatch-stop-assign::after { content: "\203A"; font-size: 1.4rem; line-height: 1; color: var(--tf-ink-400); }
  .dispatch-assign-select {
    pointer-events: none; flex: 1; margin-right: 8px; padding: 0; text-align: right;
    border: 0; background: transparent; background-image: none; font-size: 1rem; color: var(--tf-ocean-800);
  }

}

/* ── The + menu ("New") on every width: markers; on a phone: one line per option ─────────────────
   The full explanation of an option is still in the DOM (.fd-opt-full) and opens from the info button, so
   nothing a person needs to read was removed, only moved out of the way. */
.fd-mark { margin-left: .5rem; vertical-align: middle; }
/* .m-iconbtn (loaded after this file) sets display:inline-flex at the same specificity, so name both classes. */
.fd-info.m-iconbtn { display: none; }
@media (max-width: 860px) {
  .fd-srch { font-size: 1rem; min-height: 48px; }
  .fd-newp { min-height: 48px; font-size: .9375rem; }
  .fd-chip-c { min-height: 44px; min-width: 44px; }
  .fd-close { min-width: 44px; min-height: 44px; display: inline-flex; align-items: center; justify-content: center; }
  .fd-res button { min-height: 48px; }
  .fd-opt-wrap { position: relative; }
  .fd-opt { min-height: 56px; padding-right: 64px; }
  .fd-info.m-iconbtn { display: inline-flex; position: absolute; right: 8px; top: 6px; }
  .fd-opt-short { display: block; }
  .fd-opt-full { display: none; }
  .fd-opt-wrap.open .fd-opt-short { display: none; }
  .fd-opt-wrap.open .fd-opt-full { display: block; }
}
@media (min-width: 861px) { .fd-opt-short { display: none; } }


/* ── Schedule on a phone, second pass: stops, the drag handle, Re-optimize ─────────────────────── */
.dispatch-grip { display: none; }
@media (max-width: 860px) {
  /* Room on the right for the drag handle. The "$ due" pill floats top-right instead of sitting BESIDE the
     stop's content, where it squeezed that one stop's body (and its Assign row) narrower than the rest. */
  .dispatch-stop { position: relative; padding-right: 56px; }
  .dispatch-stop-due { position: absolute; top: 8px; right: 56px; margin: 0; }
  .dispatch-stop:has(.dispatch-stop-due) .dispatch-stop-svc { padding-right: 5.5rem; }
  .dispatch-grip {
    display: flex; position: absolute; right: 4px; top: 50%; transform: translateY(-50%);
    width: 44px; height: 44px; align-items: center; justify-content: center;
    border: 0; border-radius: 12px; background: transparent; color: var(--tf-ink-400);
    touch-action: none; cursor: grab;
  }
  .dispatch-grip:active { background: var(--tf-aqua-050); color: var(--tf-ocean-800); }
  .dispatch-stop.dragging { background: var(--tf-aqua-050); box-shadow: var(--shadow-md); }
  /* Re-optimize rewrites a day's order, so it should not be the biggest thing on the screen: a quiet
     44px outlined button at the end of the technician's row, not a full-width bar. */
  .dispatch-optimize-btn {
    flex: 0 0 auto; margin-left: auto; padding: 0 14px; font-size: .875rem; font-weight: 600;
    background: transparent; border: 1px solid var(--tf-ink-200); color: var(--tf-ocean-800);
  }
}

/* The visit panel's body is a column with one gap, so a block added to it never touches its neighbour. The
   customer chip sat directly on the money card because each child owned its own (missing or different)
   margin. Margins on the children are reset so the gap is the only spacing. */
.ap-body { display: flex; flex-direction: column; gap: 14px; }
.ap-body > * { margin-top: 0; margin-bottom: 0; }

/* Vertical rhythm fixes found by the 2026-10-18 spacing sweep. */
.ap-field + .ap-field { margin-top: 14px; }
.ap-exp input + .ap-hint, .ap-exp textarea + .ap-hint, .ap-exp select + .ap-hint { margin-top: 6px; }
#create-extra-services { margin-bottom: 14px; }
#create-quote-modal .modal-title-row { margin-bottom: 14px; }

/* ── Google photo review (js/29-gbp-photos.js) ─────────────────────────────────────────────────────────
   Uses the app's own tokens and the shared .btn component; nothing here defines a button. */
.gbp-review { display: flex; flex-direction: column; gap: .9rem; }
.gbp-count { font-size: var(--fs-xs); color: var(--muted); }
.gbp-bar { height: 6px; border-radius: 3px; background: var(--border); overflow: hidden; }
.gbp-bar i { display: block; height: 100%; width: 0; background: var(--success); transition: width .2s; }
.gbp-card { background: var(--surface); border: 1px solid var(--border); border-radius: var(--radius); padding: .85rem; box-shadow: var(--shadow-sm); }
.gbp-chips { display: flex; flex-wrap: wrap; gap: .35rem; margin-bottom: .6rem; }
.gbp-chip { font-size: var(--fs-xs); font-weight: 600; border-radius: 999px; padding: .1rem .6rem; background: var(--info-bg); color: var(--cerulean); }
.gbp-chip.after { background: var(--success-bg); color: var(--success); }
.gbp-chip.work { background: var(--warning-bg); color: var(--warning); }
.gbp-chip.when { background: transparent; border: 1px solid var(--border); color: var(--muted); font-weight: 500; }
.gbp-grid { display: grid; grid-template-columns: 1fr; gap: .9rem; }
@media (min-width: 720px) { .gbp-grid.pair { grid-template-columns: 1fr 1fr; } }
.gbp-ph { container-type: inline-size; position: relative; aspect-ratio: 4 / 3; border-radius: var(--radius); overflow: hidden; background: var(--aqua-soft); }
.gbp-ph img { width: 100%; height: 100%; object-fit: cover; display: block; }
.gbp-tag { position: absolute; top: .4rem; left: .4rem; background: rgba(6,40,64,.72); color: #fff; font-size: var(--fs-xs); border-radius: 8px; padding: .05rem .45rem; }
/* the same share of the photo's width the finished photo uses (GBP_FONT, GBP_PADFR in 29-gbp-photos.js), so the preview shows exactly what gets drawn */
.gbp-band { position: absolute; left: 0; right: 0; bottom: 0; padding: 1.1cqw 2.2cqw; background: rgba(6,40,64,.84); color: #fff; font-size: 3.4cqw; line-height: 1.28; font-weight: 600; }
.gbp-lab { display: block; font-size: var(--fs-xs); font-weight: 600; color: var(--navy); margin: .6rem 0 .2rem; }
.gbp-label { width: 100%; min-height: 3.6rem; border: 1px solid var(--border-control); border-radius: var(--radius); padding: .55rem .65rem; font: inherit; font-size: var(--fs-sm); color: var(--text); background: var(--surface); resize: vertical; }
.gbp-label:focus-visible { outline: none; box-shadow: var(--tf-focus-ring); border-color: var(--ocean); }
.gbp-count-line { font-size: var(--fs-xs); color: var(--muted); text-align: right; margin-top: .15rem; }
.gbp-count-line.over { color: var(--danger); font-weight: 700; }
.gbp-err { background: var(--danger-bg); color: var(--danger); border-radius: var(--radius); padding: .45rem .65rem; font-size: var(--fs-sm); margin-top: .4rem; }
.gbp-warn { background: var(--warning-bg); color: var(--warning); border-radius: var(--radius); padding: .45rem .65rem; font-size: var(--fs-sm); margin-top: .5rem; }
.gbp-acts { display: flex; flex-wrap: wrap; align-items: center; gap: .45rem; margin-top: .65rem; }
.gbp-after { display: inline-flex; flex-wrap: wrap; gap: .45rem; }
.gbp-confirm { background: var(--warning-bg); color: var(--warning); border-radius: var(--radius); padding: .6rem .75rem; margin-top: .6rem; font-size: var(--fs-sm); }
.gbp-done { font-size: var(--fs-sm); font-weight: 700; color: var(--success); margin-top: .5rem; }
.gbp-item.gbp-settled .gbp-ph { opacity: .5; }
.gbp-hint { font-size: var(--fs-xs); color: var(--muted); margin: .6rem 0 0; line-height: 1.4; }

/* ── The Photos library (31-gbp-library.js) ───────────────────────────────────────────────────── */
#ct-photos { display: flex; flex-direction: column; gap: .75rem; }
.gl-head { display: flex; align-items: flex-start; justify-content: space-between; gap: .75rem; flex-wrap: wrap; }
.gl-h { margin: 0; color: var(--navy); font-size: var(--fs-lg); }
.gl-sub { font-size: var(--fs-sm); color: var(--muted); }
.gl-head-acts { display: flex; align-items: center; gap: .5rem; }
.gl-head-acts svg, .gl-bar svg { width: 18px; height: 18px; flex: none; }
.gl-head-acts .btn { min-height: 44px; display: inline-flex; align-items: center; gap: .4rem; }
.gl-n:not(:empty) { background: var(--cerulean); color: #fff; border-radius: 999px; padding: 0 .45rem; font-size: var(--fs-xs); margin-left: .25rem; }
.gl-find-note { background: var(--info-bg); color: var(--cerulean); border-radius: var(--radius); padding: .5rem .75rem; font-size: var(--fs-sm); }
.gl-f { min-height: 44px; border: 1px solid var(--border-control); border-radius: var(--radius); background: var(--surface); color: var(--text); padding: 0 .65rem; font: inherit; font-size: var(--fs-sm); min-width: 0; flex: 0 1 11rem; }
.gl-f[type="search"] { flex: 1 1 14rem; }
.gl-count { margin: 0; font-size: var(--fs-xs); color: var(--muted); }
.gl-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(190px, 1fr)); gap: .75rem; }
.gl-card { position: relative; background: var(--surface); border: 1px solid var(--border); border-radius: var(--radius); overflow: hidden; cursor: pointer; }
.gl-card.off { cursor: default; }
.gl-card.off .gl-img { opacity: .55; }
.gl-card.sel { outline: 3px solid var(--ocean); outline-offset: -3px; }
.gl-img { position: relative; aspect-ratio: 4 / 3; background: var(--aqua-soft); }
.gl-img img { width: 100%; height: 100%; object-fit: cover; display: block; }
.gl-chk { position: absolute; top: .45rem; left: .45rem; width: 30px; height: 30px; border-radius: 50%; border: 2px solid #fff; background: rgba(6,40,64,.65); display: flex; align-items: center; justify-content: center; color: #fff; }
.gl-chk svg { width: 16px; height: 16px; opacity: 0; }
.gl-card.sel .gl-chk { background: var(--ocean); border-color: var(--ocean); }
.gl-card.sel .gl-chk svg { opacity: 1; }
.gl-tag { position: absolute; top: .45rem; right: .45rem; background: rgba(6,40,64,.72); color: #fff; font-size: var(--fs-xs); border-radius: 8px; padding: .05rem .45rem; }
.gl-full { position: absolute; right: .45rem; bottom: .45rem; width: 32px; height: 32px; border-radius: 50%; background: rgba(6,40,64,.72); color: #fff; display: flex; align-items: center; justify-content: center; }
.gl-full svg { width: 16px; height: 16px; }
.gl-meta { padding: .5rem .65rem .65rem; font-size: var(--fs-xs); color: var(--muted); display: grid; gap: .25rem; }
.gl-who { color: var(--navy); font-weight: 600; display: flex; gap: .35rem; align-items: center; min-width: 0; }
.gl-who span { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.gl-who svg { width: 13px; height: 13px; flex: none; color: var(--muted); }
.gl-badge { font-size: var(--fs-xs); font-weight: 700; border-radius: 999px; padding: .05rem .55rem; width: max-content; }
.gl-badge.ok { background: var(--success-bg); color: var(--success); }
.gl-badge.no { background: var(--danger-bg); color: var(--danger); }
.gl-badge.warn { background: var(--warning-bg); color: var(--warning); }
.gl-badge.info { background: var(--info-bg); color: var(--cerulean); }
.gl-badge.mute { background: transparent; border: 1px solid var(--border); color: var(--muted); }
.gl-sub2, .gl-why { font-size: var(--fs-xs); color: var(--muted); }
.gl-act { background: none; border: 0; padding: 0; min-height: 44px; text-align: left; font: inherit; font-size: var(--fs-xs); font-weight: 700; color: var(--cerulean); cursor: pointer; }
.gl-ask { background: var(--warning-bg); color: var(--warning); border-radius: var(--radius); padding: .5rem .6rem; font-size: var(--fs-xs); }
.gl-ask-acts { display: flex; gap: .4rem; margin-top: .4rem; flex-wrap: wrap; }
.gl-more { display: flex; justify-content: center; margin: .75rem 0 4rem; }
.gl-bar { position: fixed; left: 0; right: 0; bottom: 0; z-index: 40; background: var(--surface); border-top: 1px solid var(--border); padding: .6rem 1rem calc(.6rem + env(safe-area-inset-bottom, 0px)); display: flex; align-items: center; justify-content: center; gap: .5rem; flex-wrap: wrap; }
.gl-bar[hidden] { display: none; }
.gl-tray { display: grid; gap: .6rem; padding-bottom: .5rem; }
.gl-tray .gl-f { flex: none; width: 100%; }
.gl-tray-acts { display: flex; gap: .5rem; justify-content: flex-end; margin-top: .4rem; }
.gl-prep { display: grid; gap: .9rem; }
.gl-prep-item { display: grid; grid-template-columns: minmax(0, 1fr); gap: .6rem; background: var(--surface); border: 1px solid var(--border); border-radius: var(--radius); padding: .75rem; }
.gl-prep-ph { container-type: inline-size; position: relative; aspect-ratio: 4 / 3; border-radius: var(--radius); overflow: hidden; background: var(--aqua-soft); }
.gl-prep-ph img { width: 100%; height: 100%; object-fit: cover; display: block; }
.gl-row { display: flex; align-items: center; gap: .6rem; color: var(--muted); font-size: var(--fs-sm); }
.gl-spin { width: 16px; height: 16px; border-radius: 50%; border: 2px solid var(--border); border-top-color: var(--ocean); animation: gl-spin .8s linear infinite; }
@keyframes gl-spin { to { transform: rotate(360deg); } }
@media (prefers-reduced-motion: reduce) { .gl-spin { animation: none; } }
@media (max-width: 860px) { .gl-grid { grid-template-columns: 1fr 1fr; gap: .5rem; } .gl-find-t { display: none; } .gl-head-acts .btn { padding: 0 .75rem; } }

.gl-up { display: grid; gap: .75rem; }
.gl-drop { display: grid; gap: .25rem; justify-items: center; text-align: center; border: 2px dashed var(--border); border-radius: var(--radius); padding: 1.1rem; cursor: pointer; color: var(--muted); font-size: var(--fs-xs); }
.gl-drop b { color: var(--navy); font-size: var(--fs-sm); }
.gl-up-fields { display: grid; gap: .5rem; }
.gl-up-fields .gl-f { flex: none; width: 100%; }
.gl-up-row { display: flex; justify-content: space-between; align-items: center; gap: .5rem; font-size: var(--fs-sm); padding: .25rem 0; border-bottom: 1px solid var(--border); }
.gl-up-row small { color: var(--muted); }
.gl-up-status { background: var(--info-bg); color: var(--cerulean); border-radius: var(--radius); padding: .5rem .75rem; font-size: var(--fs-sm); }
.gl-attach { background: var(--info-bg); color: var(--navy); }
.gl-attach .gl-f { flex: none; width: 100%; }

/* ── AI cost drill-down and prices (js/32-ai-cost.js) ── */
.ai-tabs { display: flex; gap: .4rem; margin-bottom: .5rem; }
.ai-tabs button { font: inherit; font-size: var(--fs-xs); font-weight: 700; min-height: 44px; padding: 0 .9rem; border-radius: 999px; border: 1px solid var(--border); background: var(--surface); color: var(--text-strong, inherit); cursor: pointer; }
.ai-tabs button[aria-pressed="true"] { background: var(--navy); color: #fff; border-color: var(--navy); }
.ai-fr { display: block; width: 100%; text-align: left; font: inherit; color: inherit; background: none; border: 0; border-top: 1px solid var(--border); padding: .6rem .1rem; min-height: 48px; cursor: pointer; }
.ai-fr-top { display: flex; gap: .5rem; align-items: baseline; }
.ai-fr-top b { flex: 1; min-width: 0; }
.ai-fr-top span { font-variant-numeric: tabular-nums; font-weight: 700; }
.ai-fr-sub { display: block; font-size: var(--fs-xs); color: var(--text-muted); margin-top: .1rem; }
.ai-meter { display: block; height: 6px; border-radius: 3px; background: var(--border); margin-top: .4rem; }
.ai-meter i { display: block; height: 100%; border-radius: 3px; background: var(--navy); }
.ai-more { padding: .1rem .1rem .6rem; font-size: var(--fs-xs); }
.ai-more[hidden] { display: none; }
.ai-more div { display: flex; justify-content: space-between; gap: .5rem; border-top: 1px dashed var(--border); padding: .3rem 0; font-variant-numeric: tabular-nums; }
.ai-price-row { display: grid; grid-template-columns: repeat(auto-fit, minmax(150px, 1fr)); gap: .5rem; align-items: end; }
.ai-price-row > b { grid-column: 1 / -1; }
.ai-price-cell { display: flex; flex-direction: column; gap: .2rem; font-size: var(--fs-xs); color: var(--text-muted); }
.ai-price-cell input { min-height: 48px; }
.ai-price-status { grid-column: 1 / -1; font-size: var(--fs-xs); color: var(--text-muted); }
.ai-price-status.bad { color: var(--status-danger-text); }

/* ── Schedule Week and Month on a phone (14-dispatch.js: renderWeekRows, renderMonthPhone, wireTechChip) ──
   Week is one m-row per day; Month is a seven-column grid that fits the screen; Tech is one chip and a picker
   sheet. Desktop keeps the columns, the month cells and the tab row, so the chip is hidden there. */
@media (min-width: 861px) { .dispatch-tech-chip.m-chip-btn { display: none; } }
@media (max-width: 860px) {
  #panel-schedule .dispatch-tech-bar-label, #panel-schedule .dispatch-tech-tabs { display: none; }
  #panel-schedule .dispatch-tech-bar { padding: 8px 14px; }
  #panel-schedule .dispatch-grid-wrap { overflow-x: visible; }
  .dispatch-grid.sp-week, .dispatch-grid.sp-month { display: block; min-width: 0; width: auto; padding: 0 14px; }
  /* The service-colour legend explains colours the phone rows do not use, and it scrolled sideways. */
  #dispatch-legend { display: none; }
  .sp-techopts { display: flex; flex-direction: column; gap: 10px; }
  .sp-mgrid { display: grid; grid-template-columns: repeat(7, 1fr); gap: 4px; }
  .sp-mhead { text-align: center; font-size: .6875rem; font-weight: 700; letter-spacing: .06em; color: var(--text-muted); padding: 4px 0; }
  .sp-mcell { min-height: 52px; min-width: 0; border-radius: 10px; border: 1px solid var(--tf-ink-100); background: var(--tf-white); display: flex; flex-direction: column; align-items: center; justify-content: center; gap: 4px; padding: 0; cursor: pointer; font-family: var(--font-sans); }
  .sp-mcell b { font-size: .9375rem; color: var(--text-strong); font-variant-numeric: tabular-nums; }
  .sp-mcell.out b { color: var(--tf-ink-400); }
  .sp-mcell.today b { color: var(--tf-ocean-800); text-decoration: underline; text-underline-offset: 3px; }
  .sp-mcell[aria-pressed="true"] { border-color: var(--tf-ocean-800); background: var(--tf-aqua-050); }
  .sp-dots { display: flex; gap: 3px; min-height: 6px; }
  .sp-dots i { width: 6px; height: 6px; border-radius: 50%; background: var(--tf-ocean-800); }
  .sp-dots i.full { background: var(--tf-coral-text); }
  .sp-legend { display: flex; gap: 14px; font-size: .75rem; color: var(--text-muted); margin: 10px 0 12px; }
  .sp-legend span::before { content: ""; display: inline-block; width: 8px; height: 8px; border-radius: 50%; background: var(--tf-ocean-800); margin-right: 6px; }
  .sp-legend span.f::before { background: var(--tf-coral-text); }
}

/* ── Dashboard on a phone (28-kpi-board.js: kpiRenderStrip; 08-goals-photos.js: starts shut) ──────────────────
   Order is money strip, then the work queue, then the goals block as one row that opens the six stages. The
   strip is hidden from 861px, where the page is exactly what it was. */
@media (min-width: 861px) { .kpi-strip { display: none; } }
@media (max-width: 860px) {
  #panel-dashboard .dash-wrap { display: flex; flex-direction: column; }
  #panel-dashboard .kpi-strip { order: 0; }
  #panel-dashboard .dash-grid { order: 1; }
  #panel-dashboard .goals-block { order: 2; margin-top: 12px; }
  .kpi-period-opts { display: flex; flex-direction: column; gap: 10px; }
  /* The period moved into the strip's chip, so the head toggle would say it twice. */
  #goals-block .goals-block-head .gb-toggle { display: none; }
  #goals-block .goals-block-head { min-height: 56px; }
  #goals-block .gb-title { font-size: 1rem; }
  #panel-dashboard .kpi-bar-top button { min-height: 44px; }
  #panel-dashboard .tf-deck-more { min-height: 48px; min-width: 44px; }
  #dash-quo-btn { min-height: 44px; }
}

/* ── Home-screen app on an iPhone: keep clear of the clock, camera and home bar ──────────────────────────────
   The app runs standalone with a translucent status bar and viewport-fit=cover (index.html), so on a phone the
   page draws UNDER the status bar and the notch or Dynamic Island. Only the bottom bar and the photo viewer had
   allowed for that. Every full-height surface (top bar, drawers, the + menu, the new-estimate panel) now starts
   its first row below the inset, and the drawers' last row clears the home bar. env() is 0 everywhere else, so
   desktop and any browser tab are unchanged. */
/* Content scrolls UNDER a translucent status bar, so a sticky top bar at 92% alpha lets it show through behind the
   clock and battery. This fixed, opaque band is exactly the safe-area height (0 on desktop) and sits above the
   top bar and below drawers, which pad themselves. */
body::before {
  content: '';
  position: fixed; top: 0; left: 0; right: 0;
  height: env(safe-area-inset-top, 0px);
  background: var(--bg);
  z-index: 50;
  pointer-events: none;
}
@media (max-width: 860px) {
  .topbar { padding-top: calc(12px + env(safe-area-inset-top, 0px)); }
  .drawer-header { padding-top: calc(1.25rem + env(safe-area-inset-top, 0px)); }
  .dash-drawer-head { padding-top: calc(1.25rem + env(safe-area-inset-top, 0px)); }
  .fd-head { padding-top: calc(1.1rem + env(safe-area-inset-top, 0px)); }
  #create-quote-modal .modal-card { padding-top: calc(1.75rem + env(safe-area-inset-top, 0px)); padding-bottom: calc(2.5rem + env(safe-area-inset-bottom, 0px)); }
  .drawer-footer { padding-bottom: calc(1rem + env(safe-area-inset-bottom, 0px)); }
  .fd-body { padding-bottom: calc(1.35rem + env(safe-area-inset-bottom, 0px)); }
}

/* ── Estimates on a phone: filter chip and 44px field-quote controls (13-estimates.js: wireQuoteFilterChip) ── */
@media (min-width: 861px) { .qf-chip.m-chip-btn { display: none; } }
@media (max-width: 860px) {
  #panel-estimates .qf-main { display: none; }
  #panel-estimates .qf-chip { margin-bottom: 12px; }
  .qf-opts { display: flex; flex-direction: column; gap: 10px; }
  #panel-estimates .fq-actions .btn { min-height: 44px; }
  #panel-estimates .cr-name-link { min-height: 44px; display: inline-flex; align-items: center; }
}


/* ── Settings on a phone: every field 16px (iOS zooms the page on anything smaller) and every control 44px ── */
@media (max-width: 860px) {
  #panel-settings input:not([type=checkbox]):not([type=radio]), #panel-settings select, #panel-settings textarea { font-size: 1rem; }
  #panel-settings .settings-num-input, #panel-settings .send-link-url, #panel-settings .dash-d-input { min-height: 44px; }
  #panel-settings .canned-save, #panel-settings .send-btn, #panel-settings .dash-btn-sm { min-height: 44px; }
  #panel-settings .settings-info { min-width: 44px; min-height: 44px; }
}

/* Its font size lived inline (so a phone rule could not raise it); it is here now, 16px on a phone via the Settings block above. */
.canned-variant { font-size: var(--fs-sm); }
@media (max-width: 860px) { #panel-settings .send-link-row { flex-wrap: wrap; } }

/* The Pipeline's phone layout now lives with its shared components (m-components.css: Board and DataTable) and
   reputation.css (.pl-* page layout). The 16px field rule below still applies to the lead drawer's fields via .m-field. */

/* Partners: one pane at a time behind the count tabs (15-partners-leads.js). */
#panel-partners .pt-pane { display: none; }
#panel-partners .pt-pane.pt-on { display: block; }

/* Partners: door tiles that read at a glance (15-partners-leads.js renderPartnerDoors). The conversion is the headline
   and carries the colour; the three counts sit in the shared stat strip; one bar shows paid over created. */
.pd-door { border-top: 3px solid var(--tf-ocean-800); box-shadow: var(--shadow-sm); }
.pd-head { align-items: flex-start; gap: 12px; padding-top: 12px; }
.pd-head b { display: block; font-family: var(--font-display); font-size: 1.05rem; color: var(--text-strong); }
.pd-head small { display: block; font-size: .8125rem; color: var(--text-muted); margin-top: 2px; font-family: var(--font-sans); }
.pd-conv { flex: none; font-family: var(--font-display); font-size: 1.6rem; line-height: 1; text-align: right; color: var(--text-muted); }
.pd-conv em { display: block; font: 700 .6875rem var(--font-sans); letter-spacing: .06em; text-transform: uppercase; margin-top: 3px; }
.pd-conv.good { color: var(--status-success-text); }
.pd-conv.mid { color: var(--status-warning-text); }
.pd-conv.low { color: var(--tf-coral-text); }
.pd-cell { cursor: default; }
.pd-foot { display: flex; align-items: center; gap: 10px; padding: 0 14px 12px; font-size: .75rem; color: var(--text-muted); }
.pd-bar { flex: 1; height: 6px; border-radius: 99px; background: var(--tf-ink-100); overflow: hidden; }
.pd-bar s { display: block; height: 100%; background: var(--tf-ocean-800); }

/* ── Schedule day view on a phone: first stop near the top, Re-optimize behind the ⋯ sheet ─────────────────
   (14-dispatch.js wireSchedulePhone). The dark "today" banner repeated what the day bar already says; the tech row is
   one compact line; Re-optimize is a sheet row, so the button above the stops is gone. Approved prototype:
   prototypes/mobile-wave3.html?view=dayview. */
@media (max-width: 860px) {
  #panel-schedule .dispatch-board.is-day .dispatch-today-strip { display: none; }
  #panel-schedule .dispatch-tech-bar { padding: 4px 14px; min-height: 0; }
}


/* ── Small controls found by the 2026-10-20 audit: top-bar icons, the bell's list, sign-in, Shorten a link ── */
@media (max-width: 860px) {
  .topbar .tb-alertbtn { width: 44px; height: 44px; }
}
#login-view .btn-signin { min-height: 48px; }
#login-view .pw-eye { min-width: 44px; min-height: 44px; }
#login-view #forgot-btn, #login-view .btn-link { min-height: 44px; display: inline-flex; align-items: center; }
#sl-modal #sl-make { min-height: 48px; }

/* ── The bell (07-shell-nav.js renderNotifications) ───────────────────────────────────────────────────────────
   One control for Unread / Read (a two-way toggle), a quiet "Mark all read" link, and rows that are the tap target:
   they open what they are about, or clear themselves when there is nothing to open. No per-row buttons. A dropdown
   from 861px; a bottom sheet on a phone. */
.tb-bellpop .tbn-head { display: none; }
.tb-bellpop .tbn-tools { display: flex; align-items: center; justify-content: space-between; gap: 8px; padding: 2px 4px 8px; }
.tb-bellpop .tbn-seg { display: inline-flex; padding: 3px; border-radius: 10px; background: var(--tf-ink-100); gap: 2px; }
.tb-bellpop .tbn-seg button { min-height: 32px; padding: 0 12px; border: 0; border-radius: 8px; background: transparent; color: var(--text-muted); font: 600 var(--fs-xs) var(--font-sans); cursor: pointer; }
.tb-bellpop .tbn-seg button[aria-pressed="true"] { background: var(--tf-white); color: var(--text-strong); box-shadow: var(--shadow-sm); }
.tb-bellpop .tbn-all { border: 0; background: transparent; color: var(--tf-ocean-800); font: 600 var(--fs-xs) var(--font-sans); cursor: pointer; padding: 6px 4px; }
.tb-bellpop .tbn-all:hover { text-decoration: underline; }
.tb-bellpop .tbn-row:hover { background: var(--tf-aqua-050); }
.tb-bellpop .tbn-row:focus-visible { outline: 2px solid var(--tf-ocean-800); outline-offset: -2px; }
.tb-bellpop .tbn-dot { flex: none; width: 9px; height: 9px; margin-top: 5px; border-radius: 50%; background: var(--tf-ocean-800); }
.tb-bellpop .tbn-row:not(.unread) .tbn-dot { background: transparent; border: 1.5px solid var(--tf-ink-300); }
.tb-bellpop .tbn-go-ic { flex: none; align-self: center; width: 18px; height: 18px; color: var(--tf-ink-400); }
.tb-bellpop .tbn-undo { flex: none; align-self: center; border: 0; background: transparent; color: var(--text-muted); font: 600 var(--fs-xs) var(--font-sans); text-decoration: underline; cursor: pointer; padding: 6px 4px; }
.tb-bellpop .tbn-foot { padding: 10px 10px 6px; font-size: var(--fs-xs); color: var(--text-muted); }
.tb-bellpop .tbn-ask { display: block; }
#tbn-scrim { display: none; }
@media (max-width: 860px) {
  .tb-bellpop { position: fixed; left: 0; right: 0; bottom: 0; top: auto; width: auto; max-height: 86dvh; z-index: 262; border-radius: 20px 20px 0 0; padding: 8px 12px calc(12px + env(safe-area-inset-bottom, 0px)); }
  #tbn-scrim { position: fixed; inset: 0; z-index: 261; background: rgba(6, 40, 64, .45); }
  #tbn-scrim:not([hidden]) { display: block; }
  .tb-bellpop .tbn-head { display: flex; align-items: center; justify-content: space-between; padding: 6px 4px 8px; }
  .tb-bellpop .tbn-title-lg { font-family: var(--font-display); font-size: 1.15rem; color: var(--text-strong); }
  .tb-bellpop .tbn-seg button { min-height: 40px; padding: 0 14px; font-size: .875rem; }
  .tb-bellpop .tbn-all { min-height: 44px; font-size: .875rem; }
  .tb-bellpop .tbn-row { min-height: 56px; padding: 12px 8px; }
  .tb-bellpop .tbn-undo { min-height: 44px; }
  .tb-bellpop .tbn-title { font-size: .9375rem; }
  .tb-bellpop .tbn-text { font-size: .875rem; }
}


    /* Phone search: magnifier opens the same box as a top bar. */
    .tb-searchbtn { display: none; }
    .tb-search-close { display: none; }
    @media (max-width: 860px) {
      .tb-searchbtn { display: inline-flex; }
      body.tb-search-on .tb-search {
        display: flex; position: fixed; top: 0; left: 0; right: 0; width: auto; z-index: 270;
        border-radius: 0; border: none; border-bottom: 1px solid var(--border);
        padding: calc(env(safe-area-inset-top, 0px) + 8px) 12px 8px; min-height: 56px;
      }
      body.tb-search-on .tb-search input { font-size: 16px; min-height: 40px; }
      body.tb-search-on .tb-search-close {
        display: inline-flex; align-items: center; min-height: 44px; padding: 0 6px;
        background: none; border: none; color: var(--cerulean); font-weight: 600; font-size: var(--fs-sm);
      }
      body.tb-search-on .tb-search-results {
        position: fixed; top: calc(env(safe-area-inset-top, 0px) + 56px); left: 0; right: 0;
        border-radius: 0; max-height: calc(100dvh - 56px - env(safe-area-inset-top, 0px)); z-index: 270;
      }
      body.tb-search-on .tb-search-results .tsr-row { padding: 14px 16px; font-size: var(--fs-sm); }
    }

    /* Settings: group headings + find */
    .settings-find { margin-bottom: 14px; }
    .settings-find input {
      width: 100%; box-sizing: border-box; min-height: 44px; padding: 0 14px; font-size: 16px;
      border: 1px solid var(--border); border-radius: 10px; background: #fff; color: var(--text);
      font-family: var(--font-sans);
    }
    .settings-find-none { padding: 12px 4px; color: var(--text-muted); font-size: var(--fs-sm); }
    .settings-group-h {
      margin: 22px 0 8px; font-size: var(--fs-eyebrow); font-weight: 700; letter-spacing: .1em;
      text-transform: uppercase; color: var(--text-muted);
    }
    .settings-group-h[hidden], .settings-acc[hidden] { display: none; }

    /* ─── Campaigns: phone layout (2026-10-20) ───────────────────────────────
       Desktop keeps the screen as it was: the explanatory blocks are opened by script and their
       summaries hidden, and the new bars and rows below are phone-only until the desktop version is
       approved. */
    .camp-plist { display: none; }
    details.camp-more > summary { display: none; }
    .cctl-dot { display: inline-block; flex: none; width: 10px; height: 10px; border-radius: 50%; background: var(--tf-ink-300); }
    .cctl-dot.running { background: var(--status-success); }
    .cctl-dot.paused { background: var(--status-warning); }
    @media (max-width: 860px) {
      #panel-campaign > main { padding: 14px 16px 24px !important; }
      #camp-select { width: 100%; min-height: 44px; font-size: 16px; }
      .camp-head { margin-bottom: .6rem; }
      details.camp-more > summary {
        display: flex; align-items: center; min-height: 44px; cursor: pointer; list-style: none;
        color: var(--tf-ocean-800); font: 600 var(--fs-sm) var(--font-sans);
      }
      details.camp-more > summary::-webkit-details-marker { display: none; }
      .camp-verdict { padding: .9rem; margin-bottom: .75rem; }
      .camp-verdict .cv-title { font-size: 1.05rem; flex-wrap: wrap; row-gap: 6px; }
      .camp-ctl {
        display: flex; align-items: center; gap: 10px; position: sticky; top: var(--tb-h, 64px); z-index: 14;
        background: var(--bg, #f7fbfc); padding: 8px 0; margin-bottom: 4px;
      }
      .camp-ctl .cctl-state { display: flex; align-items: center; gap: 8px; flex: 1; font-weight: 700; }
      .camp-ctl .camp-btn { min-height: 48px; }
      .camp-need { display: block; margin: 0 0 .75rem; border-radius: 12px; background: var(--status-warning-soft); padding: 4px 14px 10px; }
      .camp-need > summary { min-height: 44px; display: flex; align-items: center; cursor: pointer; font-weight: 700; color: var(--status-warning-text); list-style: none; }
      .camp-need > summary::-webkit-details-marker { display: none; }
      .camp-need ul { margin: 0; padding-left: 18px; font-size: var(--fs-sm); color: var(--text-strong); }
      .camp-need li { margin: 3px 0; }
      .camp-run-bar {
        display: flex; align-items: center; justify-content: space-between; gap: 10px; padding: 0 16px;
        min-height: 44px; background: var(--status-success-soft); border-bottom: 1px solid var(--status-success-border);
        font-size: var(--fs-sm); font-weight: 600; color: var(--status-success-text);
      }
      .camp-run-bar[hidden] { display: none; }
      .camp-run-bar .crb-text { display: flex; align-items: center; gap: 8px; min-width: 0; }
      .camp-rates, .camp-cards { grid-template-columns: 1fr 1fr; gap: 10px; }
      .camp-rate-card, .camp-card { padding: 12px; }
      .camp-rate-card .crc-value { font-size: 1.5rem; }
      .camp-rate-card .crc-label, .camp-card .cc-h { font-size: .68rem; letter-spacing: .06em; margin-bottom: .4rem; }
      .camp-card-wide { grid-column: 1 / -1; }
      .camp-table-wrap { display: none; }
      .camp-plist { display: block; }
      .camp-prow { display: flex; align-items: center; gap: 10px; min-height: 56px; padding: 8px 0; border-top: 1px solid var(--border); cursor: pointer; }
      .camp-prow:first-child { border-top: 0; }
      .camp-prow:focus-visible { outline: 2px solid var(--tf-ocean-800); outline-offset: -2px; }
      .camp-prow .cpr-main { flex: 1; min-width: 0; }
      .camp-prow .cpr-main strong, .camp-prow .cpr-main span { display: block; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
      .camp-prow .cpr-main span { font-size: var(--fs-xs); color: var(--text-muted); }
      .camp-prow .cpr-go { flex: none; width: 18px; height: 18px; color: var(--tf-ink-400); }
      .camp-people-bar { flex-direction: column; align-items: stretch; gap: .5rem; }
      .camp-filters { flex-wrap: nowrap; overflow-x: auto; -webkit-overflow-scrolling: touch; padding-bottom: 4px; }
      #camp-people-search { width: 100%; min-height: 48px; font-size: 16px; box-sizing: border-box; }
      .camp-grid { grid-template-columns: 1fr; }
      .camp-grid input, .camp-grid textarea, .camp-place-form select, .camp-place-form input { font-size: 16px; min-height: 44px; }
      .camp-status-row { flex-direction: column; align-items: stretch; gap: .6rem; }
    }
    #camp-person-body .cps-pills, .camp-pdrawer .cps-pills { margin-bottom: 8px; }
    #camp-person-body .cps-row, .camp-pdrawer .cps-row { display: flex; justify-content: space-between; gap: 12px; padding: 10px 0; border-top: 1px solid var(--border); font-size: var(--fs-sm); }
    #camp-person-body .cps-row span, .camp-pdrawer .cps-row span { color: var(--text-muted); }
    #camp-person-body .cps-act, .camp-pdrawer .cps-act { padding-top: 12px; }

    /* Campaigns, desktop (2026-10-20): the pinned control sits in the page header, the four numbers share
       one row, the sending bar and the needs-you line show at every width, and a person opens in a panel
       beside the table. Phone rules above still win under 861px. */
    .camp-ctl { display: flex; align-items: center; gap: 12px; }
    .camp-ctl .cctl-state { display: flex; align-items: center; gap: 8px; font-weight: 700; }
    .camp-need { display: block; margin: 0 0 .75rem; border-radius: 12px; background: var(--status-warning-soft); padding: 4px 16px 10px; }
    .camp-need > summary { min-height: 40px; display: flex; align-items: center; cursor: pointer; font-weight: 700; color: var(--status-warning-text); list-style: none; }
    .camp-need > summary::-webkit-details-marker { display: none; }
    .camp-need ul { margin: 0; padding-left: 18px; font-size: var(--fs-sm); color: var(--text-strong); }
    .camp-run-bar {
      display: flex; align-items: center; justify-content: space-between; gap: 10px; padding: 0 28px; min-height: 40px;
      background: var(--status-success-soft); border-bottom: 1px solid var(--status-success-border);
      font-size: var(--fs-sm); font-weight: 600; color: var(--status-success-text);
    }
    .camp-run-bar[hidden] { display: none; }
    .camp-run-bar .crb-text { display: flex; align-items: center; gap: 8px; min-width: 0; }
    .camp-psplit { display: block; }
    .camp-pdrawer { display: none; }
    @media (min-width: 861px) {
      .camp-head { align-items: center; gap: 14px; }
      #panel-campaign > main { display: grid; grid-template-columns: repeat(auto-fit, minmax(210px, 1fr)); gap: 12px; align-items: start; }
      #panel-campaign > main > * { grid-column: 1 / -1; margin-top: 0; }
      #camp-verdict, #camp-progress, .camp-rates, .camp-cards { display: contents; }
      #camp-verdict > *, #camp-progress > .camp-card-wide { grid-column: 1 / -1; }
      #camp-verdict .camp-rate-card, #camp-progress .camp-cards > .camp-card { grid-column: span 1; height: 100%; }
      .camp-card-wide { grid-column: 1 / -1; margin-bottom: 0; }
      .camp-psplit.has { display: grid; grid-template-columns: minmax(0, 1fr) 270px; gap: 16px; }
      .camp-psplit.has .camp-pdrawer { display: block; align-self: start; position: sticky; top: 12px; background: var(--tf-aqua-050); border-radius: 12px; padding: 14px; }
      .camp-pdrawer h4 { margin: 0 0 2px; font-size: 1rem; }
      .camp-pdrawer .cp-sub { color: var(--text-muted); margin-bottom: 8px; word-break: break-word; }
      .camp-table tr[data-prow] { cursor: pointer; }
      .camp-table tr[data-prow]:hover, .camp-table tr[data-prow].sel { background: var(--tf-aqua-050); }
    }

/* ── Shell pass 2026-10-20: alerts on this device, phone top bar ─────────── */
.alerts-dev { display: flex; flex-direction: column; }
.alerts-row { display: flex; align-items: center; justify-content: space-between; gap: 14px; padding: 12px 0; border-bottom: 1px solid var(--border); }
.alerts-row:last-child { border-bottom: 0; }
.alerts-txt { display: flex; flex-direction: column; gap: 2px; min-width: 0; }
.alerts-txt strong { color: var(--text-strong); font-size: var(--fs-sm); }
.alerts-txt span { color: var(--text-muted); font-size: var(--fs-sm); }
.alerts-status { flex: none; font-size: var(--fs-sm); font-weight: 600; color: var(--text-muted); }
.alerts-status--blocked { color: var(--tf-coral-text); }
@media (max-width: 860px) {
  /* Sound lives in Settings on a phone; the bar keeps search and the bell. */
  .tb-alertctl { display: none; }
}

/* ── Work queue deck, 2026-10-20 task-flow pass: rank line, swipe pager, compact actions ── */
.tf-deck-rank { margin: 0 0 .5rem; font-size: var(--fs-xs); color: var(--text-muted); }
.tf-deck-pager .m-wp-page { padding-bottom: 28px; }
.tf-deck-peek { min-height: 120px; border: 1px dashed var(--border); border-radius: 14px; padding: 1rem;
  display: flex; flex-direction: column; gap: .2rem; color: var(--text-muted); margin: 0 .25rem; }
.tf-deck-peek b { color: var(--text-strong); font-weight: 600; }
.tf-deck-peek:empty { border-color: transparent; }
.tf-deck-skipline { margin-top: .25rem; text-align: center; }
@media (max-width: 767px) {
  .tf-deck-nav { flex-wrap: nowrap; }
  .tf-deck-nav .bar { display: block; }
  /* First action full width, the rest two across: the whole row fits under the card without scrolling. */
  .tf-deck-acts { display: grid; grid-template-columns: 1fr 1fr; gap: .5rem; }
  .tf-deck-acts .btn:first-child { grid-column: 1 / -1; }
  .tf-deck-acts .tf-deck-more-acts { display: contents; }
}

/* Inbox: lanes and tech chips share ONE row that scrolls sideways instead of wrapping or stacking. */
#panel-inbox .inbox-tabrow.inbox-chiprow { flex-wrap: nowrap; overflow-x: auto; margin-bottom: 1.1rem; scrollbar-width: none; -webkit-overflow-scrolling: touch; }
#panel-inbox .inbox-tabrow.inbox-chiprow::-webkit-scrollbar { display: none; }
#panel-inbox .inbox-tabrow.inbox-chiprow .inbox-tab { flex: none; white-space: nowrap; }
@media (max-width: 860px) {
  #panel-inbox .inbox-tabrow.inbox-chiprow .inbox-status-tabs .inbox-tab { flex: none; padding: 0 14px; }
}

/* Estimates: heading over the five newest, shown when Needs action is empty. */
.quote-recent-head { margin: 14px 0 6px; font-size: var(--fs-eyebrow); font-weight: 700; letter-spacing: .09em; text-transform: uppercase; color: var(--text-muted); }
@media (max-width: 560px) {
  .alerts-row { flex-direction: column; align-items: flex-start; }
}

/* ── Who owes us drawer, 2026-10-20: plain rows, one Collect per row ── */
.mr-owed-sum { margin: 0 0 .5rem; font-size: var(--fs-sm); color: var(--text-muted); }
.mr-owed-sum b { color: var(--text-strong); font-size: var(--fs-md); }
.mr-owed-row { align-items: center; }
.mr-owed-row.is-static { cursor: default; }
.mr-owed-row.is-static:hover { background: none; }
.mr-owed-main { flex: 1; min-width: 0; display: flex; flex-direction: column; gap: 2px; }
.mr-owed-main b { color: var(--text-strong); }
.mr-owed-main span { color: var(--text-muted); font-size: var(--fs-sm); }
.mr-owed-main .mr-owed-note { color: var(--status-warning-text); }
.mr-owed-amt { flex: none; font-weight: 700; color: var(--text-strong); font-variant-numeric: tabular-nums; }
.mr-owed-act { flex: none; font-weight: 700; font-size: var(--fs-sm); color: var(--tf-ocean-800); padding-left: 4px; }

/* ── Customer drawer, 2026-10-20: owing chip, quiet Refund link ── */
#money-drawer .cr-owing-chip { margin: 0 2px; border-color: var(--status-warning-text); color: var(--status-warning-text); }
#money-drawer .cr-owing-chip b { color: inherit; }
#money-drawer .cr-refund-link { color: var(--text-muted); font-weight: 500; }

/* ── Schedule stops on a phone, 2026-10-20: one-line rows, route menu, Next row ── */
#panel-schedule .dispatch-stop-city, #panel-schedule .dispatch-colmenu, #panel-schedule .dispatch-nextrow { display: none; }
@media (max-width: 860px) {
  .dispatch-stop-body { display: flex; align-items: center; flex-wrap: wrap; gap: 4px 8px; min-width: 0; }
  .dispatch-stop-assign { flex: 1 1 100%; order: 9; }
  .dispatch-stop-customer { order: 1; flex: 0 1 auto; max-width: 36%; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
    font-weight: 600; color: var(--text-strong); font-size: var(--fs-sm); }
  #panel-schedule .dispatch-stop-city { display: block; order: 2; flex: 0 1 auto; max-width: 24%; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
    color: var(--text-muted); font-size: var(--fs-xs); }
  .dispatch-stop-live { order: 3; flex: none; font-size: 0; line-height: 0; }
  .dispatch-stop-svc { order: 4; flex: 1 1 0; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; font-weight: 400;
    color: var(--text-muted); padding-right: 0 !important; }
  .dispatch-stop-reason, .dispatch-stop-addr, .dispatch-stop-email, .dispatch-stop-status { display: none; }
  .dispatch-stop { min-height: 56px; align-items: center; }
  .dispatch-stop-due { position: static; order: 5; }
  .dispatch-stop:has(.dispatch-stop-due) .dispatch-stop-svc { padding-right: 0; }
  #panel-schedule .sched-onetech .dispatch-stop-assign { display: none; }
  /* Re-optimize hides behind the route menu; the real button stays in the DOM and is pressed by the menu row. */
  #panel-schedule .dispatch-tech-col-name .dispatch-optimize-btn { display: none; }
  #panel-schedule .dispatch-colmenu { display: inline-flex; margin-left: auto; }
  #panel-schedule .dispatch-nextrow { display: flex; align-items: center; justify-content: space-between; margin: 0 8px; }
}

    /* Phone: keep the last row clear of the floating plus button (56px button + 16px gap = 72px). Approved
       2026-10-20: the Dashboard queue gets half of that (36px), Reputation and Schedule the full 72px. */
    @media (max-width: 860px) {
      #panel-dashboard { padding-bottom: 36px; }
      #panel-reputation, #panel-schedule { padding-bottom: 72px; }
    }

/* ── Work queue table view, 2026-10-20: status, days waiting, amount due, detail behind More ── */
.q-rows-head, .q-row { display: grid; grid-template-columns: minmax(0, 1fr) 7.5rem 8rem 6.5rem 4rem; gap: 2px 12px; align-items: center; padding: .65rem 1rem; }
.q-rows-head { font-size: .6875rem; font-weight: 700; letter-spacing: .07em; text-transform: uppercase; color: var(--text-muted); border-bottom: 1px solid var(--border); }
.q-rows-head .num, .q-amt { text-align: right; }
.q-row { border-bottom: 1px solid var(--border); cursor: pointer; }
.q-row:hover { background: var(--tf-aqua-050); }
.q-row:focus-visible { outline: 2px solid var(--tf-ocean-800); outline-offset: -2px; }
.q-who { min-width: 0; display: flex; flex-direction: column; }
.q-name { font-weight: 700; color: var(--text-strong); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.q-kind { font-size: var(--fs-xs); color: var(--text-muted); }
.q-wait { font-size: var(--fs-sm); color: var(--text-muted); font-variant-numeric: tabular-nums; }
.q-wait.old { color: var(--status-danger-text); font-weight: 600; }
.q-amt { font-weight: 700; color: var(--text-strong); font-variant-numeric: tabular-nums; }
.q-amt small { display: block; font-weight: 500; font-size: .6875rem; color: var(--text-muted); }
.q-more2 { border: 0; background: none; color: var(--tf-ocean-800); font: 600 var(--fs-sm) var(--font-sans); min-height: 44px; padding: 0 6px; cursor: pointer; justify-self: end; }
.q-detail { grid-column: 1 / -1; font-size: var(--fs-sm); line-height: 1.5; color: var(--text); background: var(--tf-ink-100); border-radius: 8px; padding: 10px 12px; }
.q-detail[hidden] { display: none; }
#queue-table { container-type: inline-size; }
/* The queue panel is half the screen on a desktop, so the stacked row is chosen by the panel's own width, not the window's. */
@container (max-width: 640px) {
  .q-rows-head { display: none; }
  .q-row { grid-template-columns: minmax(0, 1fr) auto; padding: .7rem 1rem; }
  .q-who { grid-column: 1; }
  .q-amt { grid-column: 2; grid-row: 1 / 3; }
  .q-status { grid-column: 1; grid-row: 3; display: inline-flex; }
  .q-wait { grid-column: 1; grid-row: 4; }
  .q-more2 { grid-column: 2; grid-row: 3 / 5; align-self: center; }
}

/* Photos reshape (2026-10-20): task banner, one search box + Filters sheet, status chips, selection tray. */
.gl-task { display: flex; align-items: center; justify-content: space-between; gap: .75rem; background: var(--info-bg); border-radius: var(--radius); padding: .6rem .85rem; }
.gl-task b { display: block; color: var(--navy); }
.gl-task span { font-size: var(--fs-sm); color: var(--muted); }
.gl-task .gl-act { flex: none; font-size: var(--fs-sm); padding: 0 .4rem; }
.gl-bar-top { display: flex; gap: .5rem; align-items: center; }
.gl-bar-top .gl-f[type="search"] { flex: 1 1 auto; min-height: 48px; font-size: 16px; }
.gl-fbtn { position: relative; min-height: 48px; min-width: 48px; display: inline-flex; align-items: center; justify-content: center; gap: .4rem; flex: none; }
.gl-fbtn svg { width: 18px; height: 18px; flex: none; }
.gl-fbtn .gl-n { position: absolute; top: -6px; right: -6px; min-width: 20px; height: 20px; line-height: 20px; padding: 0 .3rem; text-align: center; margin: 0; }
.gl-chips { display: flex; gap: .5rem; overflow-x: auto; padding: .1rem 0 .25rem; scrollbar-width: none; }
.gl-chips::-webkit-scrollbar { display: none; }
.gl-chip { flex: none; min-height: 44px; padding: 0 .9rem; border: 1px solid var(--border); border-radius: 999px; background: var(--surface); color: var(--text); font: inherit; font-size: var(--fs-sm); font-weight: 600; cursor: pointer; }
.gl-chip[aria-pressed="true"] { background: var(--ocean); border-color: var(--ocean); color: #fff; }
.gl-lbl { font-size: var(--fs-sm); font-weight: 600; color: var(--navy); margin-top: .25rem; }
.gl-bar-n { margin-right: auto; max-width: 1008px; }
.gl-bar .btn { min-height: 44px; }
@media (max-width: 860px) {
  .gl-bar { padding-left: .75rem; padding-right: .75rem; bottom: calc(var(--mb-tabbar-h) + var(--mb-safe-b)); padding-bottom: .6rem; z-index: 62; }
  .gl-bar-n { flex: 1 0 100%; text-align: center; }
  .gl-bar .btn-primary { flex: 1 1 auto; }
  .gl-grid { padding-bottom: 5rem; }
}

/* Bell rows: title and text each stop at two lines, so a phone sheet shows far more than five alerts. The full
   text is one tap away (the row opens its target). Approved 2026-10-20. */
.tb-bellpop .tbn-title, .tb-bellpop .tbn-text {
  display: -webkit-box; -webkit-box-orient: vertical; -webkit-line-clamp: 2; overflow: hidden; overflow-wrap: anywhere;
}
