/* OnSight — the merchant portal.

   Navy, Carolina blue and glass (Greg, 2026-08-13). Numbers are tabular-lined
   everywhere they are money, because a column of figures that does not align is
   a column nobody trusts.

   TWO RULES THIS PALETTE EXISTS TO KEEP
   -------------------------------------
   1. `--accent` ACTS and `--red` WARNS. They used to be the same crimson, so
      "New invoice" and "Delete" wore one color. Anything you can press is
      Carolina blue; red is warnings and destruction, and nothing else.

   2. `--accent` IS OVERWRITTEN AT RUNTIME on the customer surfaces. The pay
      page and the portal call OSP.applyAccent() with the BUSINESS's own color,
      because a customer has a relationship with their landscaper, not with us.
      That is why the token is named for its job and not for a hue — it was
      called --crimson, which stopped being true the moment a business set green.

   WHERE THE GLASS STOPS
   ---------------------
   Frosted panels over a blurred ground are hard to read in daylight, which is
   exactly where a tablet gets used. So `.glass` is for CHROME — the rail, the
   summary strips, the project tiles. Anywhere dense text lives (tables, forms,
   documents) uses `.solid`: 94% white with a plain hairline. Every glass rule
   also sets a real background color, so a browser without backdrop-filter gets
   an opaque panel rather than text on nothing. */

:root {
  /* Ink is navy now — the darkest text color, not a hue of its own. */
  --ink:#13294B; --ink2:#1E3A63; --steel:#5A6B80; --mute:#8A99AC;
  /* ⚠ NAVY AT LOW OPACITY, NOT A FLAT GREY. rgba keeps the line in the navy
     family on any surface it lands on — over white it reads #D0D5DE, over the
     band fill it stays navy rather than turning muddy, and it needs no second
     token for dark mode. One number to tune if the borders read too heavy or
     too faint: raise or lower the alpha here and all 292 uses follow. */
  --hair:rgba(19,41,75,.20); --paper:#FFFFFF; --band:#F2F7FC; --white:#fff;

  /* ⚠ THE AURA LIVES HERE, IN ONE PLACE. Greg asked for "a little more" on
     2026-09-03 and will ask again; hunting a box-shadow through four hundred
     kilobytes of stylesheet is not a thing anybody should do twice.

     Two layers, and they do different jobs. The first is CONTACT — a tight,
     nearly opaque line that says the card is resting on something. The second
     is AMBIENT — wide, soft and offset downward, which is the part that reads
     as a glow. Raising only the ambient makes the aura bigger; raising the
     contact just makes everything look dirty. */
  --aura:
    0 1px 2px rgba(19,41,75,.06),
    0 10px 26px -12px rgba(19,41,75,.42),
    0 2px 8px -4px rgba(42,110,158,.18);
  /* A step darker than --hair, for an edge that has to hold its own against
     the page rather than merely separate two things touching each other.
     Same hue, more of it: a grey that had drifted off the blue would read as
     a different family sitting next to every other line on the screen. */
  --hair-strong:rgba(19,41,75,.34);
  /* THE QUIET AURA. Greg, 2026-09-05: "a little less aura on follow ups that
     aren't past due / in the red". Contact line kept, ambient halved — so a
     follow-up section still sits on the page but no longer competes with the
     one row that is actually late, which keeps the full --aura. */
  --aura-soft:
    0 1px 2px rgba(19,41,75,.05),
    0 6px 16px -12px rgba(19,41,75,.22);

  /* UNC's own two, and the deep ground under them.
     TWO CAROLINA BLUES, AND THE SPLIT IS NOT COSMETIC. White on #4B9CD3 is
     3.0:1, which fails WCAG AA for button-sized text — so the bright one is
     for FILLS AND TINTS ONLY (bars, the active nav wash, focus rings, the tile
     hairline) and never carries white text or sits on white as a label.
     `--accent` is the deeper one at 5.5:1, and it is what buttons and links
     use. It is also the token applyAccent() overwrites with the business's own
     color, which is why the acting color has to be the one named plainly. */
  --navy:#13294B; --navy-deep:#0A1730;
  --accent:#2A6E9E; --accent-dark:#22597F;
  --accent-bright:#4B9CD3; --accent-lt:#7BAFD4;

  --green:#2FA37A; --green-bg:#E4F5EE;
  --amber:#8A6100; --amber-bg:#FFF6E5;
  --red:#C93B40; --red-bg:#FCECEC;
  --danger:#C93B40; --danger-dark:#AE2F34;

  /* Glass, in one place so every panel agrees. */
  /* ⚠ GLASS WAS WHITE-ON-WHITE AND WOULD HAVE VANISHED. The old values were
     62% white over a blue field — the tint behind them was the whole effect.
     On a white ground a white panel is invisible, so the material inverts: a
     whisper of navy over white, which still reads as a pane and still blurs
     whatever passes behind it. */
  --glass-bg:rgba(19,41,75,.045);
  --glass-brd:rgba(19,41,75,.13);
  --glass-blur:blur(18px) saturate(150%);
  --solid-bg:rgba(255,255,255,.94);
  --shadow-sm:0 1px 2px rgba(19,41,75,.05);
  --shadow:0 1px 2px rgba(19,41,75,.05), 0 12px 34px -14px rgba(19,41,75,.28);
  --shadow-lift:0 2px 4px rgba(19,41,75,.07), 0 20px 40px -18px rgba(19,41,75,.5);

  --r-sm:10px; --r:14px; --r-lg:20px;
  /* ⚠ WHAT A STICKY PANEL HAS TO CLEAR. `header.topbar` is itself sticky at
     top:0 and 48px tall, so anything that sticks at top:14px slides UNDERNEATH
     it and loses its own first 34px — which is how the client list lost its
     search box and the identity panels lost their names. 48 + 14 of air. */
  --stick:62px;
  --rail:224px;
}

/* ---- NAVY NIGHT (Greg, 2026-09-05, from three palettes) --------------------
   The brand at night: the navy becomes the ground instead of the text, panels
   are one step lighter navy, and the bright Carolina blue — kept off white
   because white text fails on it — becomes the accent here, where it passes
   (7.3:1 on #0E1729). Only tokens change; every component that paints through
   a token follows. Three states: data-theme="dark" (chosen), data-theme=
   "light" (chosen), and nothing stamped, which follows the device. */
:root[data-theme="dark"], :root:not([data-theme="light"])[data-theme="system-dark"] {
  --ink:#E8EEF6; --ink2:#C9D5E6; --steel:#B7C3D3; --mute:#8593A6;
  --hair:rgba(232,238,246,.16); --hair-strong:rgba(232,238,246,.30);
  --paper:#16213A; --band:#1B2A47; --white:#16213A;
  --navy:#E8EEF6; --navy-deep:#0A1730;
  --accent:#4B9CD3; --accent-dark:#7FB8E3; --accent-bright:#4B9CD3; --accent-lt:#2A6E9E;
  --green:#7ED2A2; --green-bg:#163427;
  --amber:#F1C46B; --amber-bg:#3A2E10;
  --red:#F29A8F; --red-bg:#42201C;
  --danger:#F29A8F; --danger-dark:#F7B5AD;
  --glass-bg:rgba(232,238,246,.05); --glass-brd:rgba(232,238,246,.14);
  --solid-bg:rgba(22,33,58,.96);
  --shadow-sm:0 1px 2px rgba(0,0,0,.35);
  --shadow:0 1px 2px rgba(0,0,0,.35), 0 12px 34px -14px rgba(0,0,0,.7);
  --shadow-lift:0 2px 4px rgba(0,0,0,.4), 0 20px 40px -18px rgba(0,0,0,.85);
  --aura:0 1px 2px rgba(0,0,0,.3), 0 10px 26px -12px rgba(0,0,0,.75), 0 2px 8px -4px rgba(75,156,211,.25);
  --aura-soft:0 1px 2px rgba(0,0,0,.25), 0 6px 16px -12px rgba(0,0,0,.5);
  --tint:rgba(75,156,211,.18);
  color-scheme:dark;
}
:root[data-theme="dark"] body, :root[data-theme="system-dark"] body { background:#0E1729; }
:root[data-theme="dark"] .rail, :root[data-theme="system-dark"] .rail { background:#0A1730; }
/* Buttons: white text on --accent stays legible because --accent is the bright
   blue here and its text is navy. */
:root[data-theme="dark"] a.button, :root[data-theme="dark"] button,
:root[data-theme="system-dark"] a.button, :root[data-theme="system-dark"] button { color:#0E1729; }
:root[data-theme="dark"] .ghost, :root[data-theme="dark"] .link, :root[data-theme="dark"] button.ghost, :root[data-theme="dark"] button.link,
:root[data-theme="system-dark"] .ghost, :root[data-theme="system-dark"] .link, :root[data-theme="system-dark"] button.ghost, :root[data-theme="system-dark"] button.link { color:var(--accent); }
:root[data-theme="dark"] input, :root[data-theme="dark"] select, :root[data-theme="dark"] textarea,
:root[data-theme="system-dark"] input, :root[data-theme="system-dark"] select, :root[data-theme="system-dark"] textarea { background:#111B31; color:var(--ink); border-color:var(--hair); }
:root[data-theme="dark"] img.pay__logo, :root[data-theme="system-dark"] img.pay__logo { background:#fff; border-radius:8px; padding:4px; }

* { box-sizing:border-box; }
html, body { height:100%; }
body {
  margin:0; color:var(--ink);
  /* WHITE. Greg, 2026-09-03: "make the entire thing with a white background
     and the navy blue boarders, and a small aura".

     This replaced the fixed blue field the glass used to blur against. Panels
     no longer float on a tint — they are separated by a navy hairline and a
     small glow instead, which is why --hair below stopped being a pale blue
     line and became navy at low opacity. */
  background:#fff;
  font:14px/1.55 -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
  -webkit-font-smoothing:antialiased;
}

/* The two surface treatments, named so a view can ask for the right one. */
.glass {
  background:var(--glass-bg);
  backdrop-filter:var(--glass-blur); -webkit-backdrop-filter:var(--glass-blur);
  border:1px solid var(--glass-brd); border-radius:var(--r); box-shadow:var(--shadow);
}
.solid {
  background:var(--solid-bg); border:1px solid var(--hair);
  border-radius:var(--r); box-shadow:var(--shadow);
}
a { color:inherit; }
h1 { font-size:22px; margin:0; letter-spacing:-.01em; }
h2 { font-size:11px; letter-spacing:.1em; text-transform:uppercase; color:var(--steel);
     margin:0 0 10px; font-weight:700; }
h3 { font-size:16px; margin:0; }
p { margin:0 0 10px; }
hr { border:0; border-top:1px solid var(--hair); margin:8px 0; }

.mono { font-family:ui-monospace, SFMono-Regular, Menlo, monospace; font-size:12.5px; }
.num, td.num, th.num { text-align:right; font-variant-numeric:tabular-nums; }
.muted { color:var(--mute); }
.sub { font-size:11.5px; color:var(--mute); font-weight:400; margin-top:2px; }
.mid { text-align:center; }
.loading { padding:40px; color:var(--mute); }

/* ---- Buttons ------------------------------------------------------------- */
/* An <a> that looks like a button. Some destinations HAVE to be real hrefs —
   middle-click, copy link, and a phone handing a maps URL to the maps app —
   so `a.button` has been written in the views for a while. It just never had
   a rule, which is why "Open the vehicle" on a finished sheet has been
   rendering as underlined body text. The declarations below are the ones an
   anchor does not inherit; everything else comes from the shared block. */
a.button {
  display:inline-flex; align-items:center; justify-content:center;
  text-decoration:none; box-sizing:border-box;
}
a.button, button {
  font:13px inherit; font-weight:600; padding:9px 17px; border:0; border-radius:11px;
  min-height:40px; cursor:pointer; white-space:nowrap;
  background:var(--accent); color:var(--btn-fg, #fff);
  box-shadow:var(--shadow-sm), 0 6px 16px -8px rgba(42,110,158,.5);
  transition:filter .14s ease, background .14s ease;
}
a.button:hover, button:hover { background:var(--accent-dark); }
/* A ghost button is glass too, so it belongs on the field rather than fighting
   it — but with a real background behind the blur, for browsers without one. */
a.button.ghost, button.ghost {
  background:rgba(255,255,255,.72); color:var(--ink2);
  border:1px solid rgba(19,41,75,.14); box-shadow:none;
  backdrop-filter:blur(8px); -webkit-backdrop-filter:blur(8px);
}
a.button.ghost:hover, button.ghost:hover { background:rgba(255,255,255,.95); }
button.link { background:none; color:var(--accent-dark); padding:0; font-size:12px;
              min-height:0; box-shadow:none; font-weight:600; }
button.link:hover { background:none; text-decoration:underline; }

/* A LINK THAT IS ALSO DANGEROUS IS STILL A LINK. `button.danger` is defined
   below this and fills its background, so `button.link.danger` — Retire on a
   pinpad, Delete on a file, Remove on a saved card, twelve places in all —
   came out as a solid red slab wearing .link's `padding:0` and the base
   button's 11px corner radius. On a 14px-tall box that radius eats into the
   letters at both ends, which is what "does not fit in its pill" looks like.

   It was never meant to be a pill. It sits inline in a list row beside plain
   text, so it is red TYPE, and the fill is left to the real danger button —
   the confirm inside the dialog these all open. */
button.link.danger { background:none; color:var(--red); box-shadow:none; }
button.link.danger:hover { background:none; color:var(--danger-dark, #8a1f24);
                           text-decoration:underline; }

/* Voiding an invoice or reversing a payment cannot be undone, so those
   controls should not look like the ones that can. Ghost + danger reads as a
   quiet warning; solid danger is the confirm inside the dialog. */
button.danger { background:var(--danger, #a4262c); color:#fff; }
button.danger:hover { background:var(--danger-dark, #8a1f24); }
button.ghost.danger { background:var(--white); color:var(--danger, #a4262c);
                      border:1px solid var(--danger, #a4262c); }
button.ghost.danger:hover { background:var(--red-bg); }
button[disabled].danger:hover { background:var(--danger, #a4262c); }
button.block { width:100%; }
button.icon {
  background:none; color:var(--mute); font-size:18px; line-height:1; padding:2px 8px;
  font-weight:400; min-height:0; box-shadow:none; border-radius:var(--r-sm);
}
button.icon:hover { background:var(--band); color:var(--ink); }
button.chipbtn { background:var(--band); color:var(--ink2); font-size:11.5px; padding:5px 11px;
                 font-weight:500; min-height:0; box-shadow:none; border-radius:99px; }

/* ---- Sign in -------------------------------------------------------------- */
.signin { min-height:100vh; min-height:100dvh; display:grid; place-items:center; padding:24px; }
.signin__card { background:var(--solid-bg); border:1px solid var(--glass-brd);
                border-radius:var(--r-lg); box-shadow:var(--shadow);
                padding:28px; width:100%; max-width:380px; }
.signin__brand { display:flex; align-items:center; gap:9px; margin-bottom:22px; }
.signin__brand img { height:38px; display:block; }
/* ⚠ .signin__form HAD NO RULE AT ALL. Both sign-in screens — the merchant/ISO
   one in app.js and the customer one in portal.js — wrote this class and the
   stylesheet never defined it, so the two fields and the button were adjacent
   block elements with nothing between them and Sign in sat welded to the
   bottom of the password box. */
.signin__form { display:flex; flex-direction:column; gap:14px; }
/* The button is a decision, not the next field: it gets more air than the gap
   between two boxes somebody is tabbing through. */
.signin__form > button[type=submit] { margin-top:6px; }
.signin__forgot { margin-top:14px; text-align:center; }
.signin__back { margin-top:16px; text-align:center; }
.signin__lede { margin:-8px 0 16px; font-size:13.5px; color:var(--steel); }

.signin__demo { margin-top:20px; padding-top:16px; border-top:1px solid var(--hair); }
.signin__chips { display:flex; flex-wrap:wrap; gap:6px; margin-top:8px; }
.formerr { background:var(--red-bg); color:var(--red); padding:9px 12px; border-radius:var(--r-sm);
           font-size:12.5px; margin-bottom:12px; }
.formerr:empty { display:none; }
.pill { background:var(--accent); color:#fff; padding:3px 8px; border-radius:99px;
        font-size:9px; letter-spacing:.1em; font-weight:700; }

/* ---- Frame ---------------------------------------------------------------- */
#root { min-height:100vh; min-height:100dvh; }
/* THE STATUS BAR SITS ON TOP OF THIS IN THE iOS APP. In a browser there is
   nothing above the page and the inset is zero, so this costs nothing there —
   but inside the app shell the clock and the battery were printed straight
   over the logo. The navy deliberately keeps running underneath them, which is
   what an iOS app is supposed to look like; it is the CONTENT that moves down.
   The bottom bar already did this — see .tabbar — and the top never had to
   until there was an app. */
.rail { position:fixed; inset:0 auto 0 0; width:var(--rail); color:#fff;
        background:linear-gradient(172deg, var(--navy) 0%, var(--navy-deep) 100%);
        display:flex; flex-direction:column;
        padding:calc(18px + env(safe-area-inset-top, 0px)) 0 18px; }
.rail__brand { display:flex; align-items:center; gap:8px; padding:0 20px 20px;
               text-decoration:none; }
.rail__brand img { height:28px; display:block; }
/* THE NAV SCROLLS, THE BRAND AND THE FOOT DO NOT. The rail is a fixed
   full-height flex column, so before this the list simply ran off the bottom
   of the screen and behind the user block — invisibly, with no scrollbar to
   suggest anything was missing. It got worse every time a tab was added, and
   worse again on a laptop with the browser chrome taking 120px.

   `min-height:0` is the load-bearing line. A flex child's default minimum is
   its content, so `overflow-y:auto` alone does nothing here: the item refuses
   to shrink below its natural height and scrolls the page instead of itself.

   overscroll-behavior stops a flick at the end of the nav from scrolling the
   page behind it, which on a trackpad is how you lose your place twice. */
.rail__nav { display:flex; flex-direction:column; gap:2px; padding:0 12px;
             flex:1 1 auto; min-height:0; overflow-y:auto; overscroll-behavior:contain;
             scrollbar-width:thin; scrollbar-color:rgba(255,255,255,.22) transparent; }
/* A default scrollbar is a light-gray slab on a navy rail. */
.rail__nav::-webkit-scrollbar { width:6px; }
.rail__nav::-webkit-scrollbar-track { background:transparent; }
.rail__nav::-webkit-scrollbar-thumb { background:rgba(255,255,255,.18); border-radius:99px; }
.rail__nav:hover::-webkit-scrollbar-thumb { background:rgba(255,255,255,.3); }
.navitem { display:flex; align-items:center; gap:10px; padding:10px 12px; border-radius:10px;
           color:rgba(255,255,255,.72); text-decoration:none; font-size:13.5px; font-weight:500; }
.navitem:hover { background:rgba(255,255,255,.08); color:#fff; }
/* The active item is a tint of the accent rather than a solid slab: on a navy
   ground a full-strength fill reads as a button someone forgot to remove. */
.navitem.is-on { background:rgba(75,156,211,.22); color:#fff; font-weight:600;
                 box-shadow:inset 0 0 0 1px rgba(123,175,212,.34); }
.navitem__icon { width:16px; text-align:center; opacity:.85; }

/* Sidebar dropdown groups: Work and Payments. The header is a button and
   must not inherit the crimson button skin. */
.navgroup { display:flex; flex-direction:column; }
button.navgroup__head { width:100%; background:none; text-align:left; font-weight:500;
                        font-size:13.5px; box-shadow:none; min-height:0; }
button.navgroup__head:hover { background:rgba(255,255,255,.08); color:#fff; }
.navgroup__chev { margin-left:auto; font-size:10px; opacity:.7;
                  transition:transform .15s ease; }
.navgroup:not(.is-open) .navgroup__chev { transform:rotate(-90deg); }
.navgroup:not(.is-open) .navgroup__kids { display:none; }
.navgroup__kids { display:flex; flex-direction:column; gap:2px; margin-top:2px; }
.navgroup__kids .navitem { padding-left:34px; }
.navgroup__head .navbadge { margin-left:auto; }
.navgroup__head .navbadge + .navgroup__chev { margin-left:8px; }

/* The row-level "this is what the sidebar number meant" chip. Red dot +
   label; sits beside the status badge and disappears once the item is no
   longer newer than the viewer's watermark. */
.newchip { display:inline-flex; align-items:center; gap:5px; margin-left:7px;
           background:var(--red-bg); color:var(--red); font-size:10.5px;
           font-weight:700; padding:2px 8px; border-radius:999px; white-space:nowrap;
           vertical-align:middle; }
.newchip::before { content:''; width:6px; height:6px; border-radius:50%;
                   background:var(--red); flex:none; }

/* Batches: the daily-close summary and footer */
.batchsum { display:flex; gap:26px; align-items:stretch; margin:14px 0; flex-wrap:wrap; }
.batchsum__net { min-width:220px; }
.batchsum__amt { display:block; font-size:30px; letter-spacing:-.02em; margin:2px 0 8px;
                 font-variant-numeric:tabular-nums; }
.batchsum__split { display:flex; gap:14px; align-items:center; margin-left:auto; }
.batchsum__cell { border:1px solid var(--hair); border-radius:var(--r); padding:14px 22px;
                  text-align:center; min-width:130px; }
.batchsum__cell b { display:block; font-size:19px; font-variant-numeric:tabular-nums; }
.batchsum__cell--bad b { color:var(--red); }
/* The total-minus-fees box (#28): a three-line sum, read top to bottom. The
   answer is the loud line; the two above it are the working. */
.feebox { border:1px solid var(--hair); border-radius:var(--r); padding:12px 18px 11px;
          min-width:260px; background:var(--paper); }
.feebox__k { display:block; font-size:9.5px; font-weight:700; letter-spacing:.12em;
             text-transform:uppercase; color:var(--mute); margin-bottom:6px; }
.feebox__line { display:flex; justify-content:space-between; gap:18px; align-items:baseline;
                font-size:12.5px; color:var(--steel); padding:3px 0; }
.feebox__line b { font-variant-numeric:tabular-nums; color:var(--ink); font-weight:600;
                  text-align:right; white-space:nowrap; }
.feebox__line--less b { color:var(--green); }
.feebox__line--sum { border-top:1px solid var(--hair); margin-top:4px; padding-top:8px;
                     color:var(--ink); font-weight:600; }
.feebox__line--sum b { font-size:17px; font-weight:800; letter-spacing:-.02em; }
.feebox__s { display:block; font-size:11px; color:var(--mute); margin-top:6px; }
.batchfoot { display:flex; align-items:baseline; gap:14px; flex-wrap:wrap;
             margin-top:12px; font-size:12.5px; }
.batchfoot__tots { margin-left:auto; display:flex; gap:18px;
                   font-variant-numeric:tabular-nums; }
tr.rowmuted td { color:var(--mute); }
tr.rowmuted .badge { opacity:.8; }

/* Terminal: collapsible sections, the way the gateway console groups the
   optional half of the form. */
.termsec { border:1px solid var(--hair); border-radius:var(--r); margin:14px 0 0; overflow:hidden; }
button.termsec__head { width:100%; background:var(--paper); color:var(--ink);
                       text-align:left; display:flex; align-items:center;
                       padding:11px 14px; font-weight:600; font-size:13px; border-radius:0; }
button.termsec__head:hover { background:var(--band); }
.termsec__chev { margin-left:auto; transition:transform .15s ease; color:var(--mute); }
.termsec.is-open .termsec__chev { transform:rotate(90deg); }
.termsec__body { display:none; padding:14px; }
.termsec.is-open .termsec__body { display:block; }
.termsec__body .field:first-child { margin-top:0; }
textarea { font:14px inherit; width:100%; padding:10px 12px;
           border:1px solid rgba(19,41,75,.16); border-radius:11px; resize:vertical; }

/* Import wizard instruction lists */
.hintlist { margin:10px 0 14px 20px; font-size:13px; color:var(--steel); }
.hintlist li { margin:5px 0; }

/* The unread count. Red even when the row is selected — it sits on a darker
   pill so it stays legible on the crimson active row. */
.navbadge { margin-left:auto; min-width:19px; height:19px; padding:0 5px;
            border-radius:999px; background:var(--red); color:#fff;
            font-size:11px; font-weight:700; line-height:19px; text-align:center; }
.navitem.is-on .navbadge { background:var(--danger-dark); }
/* The nav takes the slack now, so the foot only needs to stay put and keep
   its own space when the list above is long. */
.rail__foot { margin-top:auto; padding:16px 20px 0; flex:0 0 auto; }
.rail__who { margin-bottom:10px; }
.rail__who b { display:block; font-size:13px; }
.rail__who span { font-size:11px; color:rgba(255,255,255,.5); text-transform:capitalize; }

/* Quiet by default and only underlined on approach: this sits above Sign out,
   and the two must not read as a pair of equally likely things to press. */
.rail__sec { display:block; font-size:11.5px; margin:0 0 8px;
             color:rgba(255,255,255,.62); text-decoration:none; }
.rail__sec:hover, .rail__sec:focus-visible { color:#fff; text-decoration:underline; }

/* Enrolment: the QR and the typed-out secret beneath it. White plate behind
   the code because a scanner needs the quiet zone whatever the page theme. */
.mfaqr { display:flex; justify-content:center; padding:14px;
         background:#fff; border:1px solid var(--hair); border-radius:8px; }
.mfaqr img { display:block; width:200px; height:200px; image-rendering:pixelated; }
.mfasecret { display:inline-block; font-family:var(--mono, ui-monospace, monospace);
             font-size:13px; letter-spacing:.08em; word-break:break-all;
             background:var(--paper); border:1px solid var(--hair);
             border-radius:6px; padding:8px 10px; }

/* The earliest-opening button, above the two date fields it fills. Sits on
   its own row so it reads as an alternative to typing a date rather than a
   third field. */
.whenpick { display:flex; align-items:center; gap:12px; flex-wrap:wrap;
            margin:0 0 4px; }
.whenpick .sub { font-size:12px; color:var(--mute); }
/* Once a time is set the row stops being a button with a caption and becomes
   the answer, with the two ways to change it beside it. */
.whenpick__set { display:flex; align-items:baseline; gap:10px; padding:9px 14px;
                 border-radius:999px; background:var(--paper);
                 border:1px solid var(--hair); }
.whenpick__set b { font-size:14px; }
.whenpick__t { font-size:14px; font-variant-numeric:tabular-nums;
               color:var(--steel); }
/* Promised, or any time that day. Sits on its own line under the answer so
   it reads as a property of the booking rather than a third way to change
   it — the two buttons beside it already are that. */
.whenpick__mode { flex-basis:100%; display:flex; align-items:center; gap:8px; }

/* Findings that carry a price. A row is: tick, what was found, what fixes it —
   read left to right in the order the conversation happens. */
.offers { display:flex; flex-direction:column; }
.offerrow { display:grid; grid-template-columns:auto 1fr minmax(0, 15rem);
            gap:12px; align-items:start; padding:12px 0;
            border-top:1px solid var(--hair); }
.offerrow:first-child { border-top:0; }
.offerrow__pick { margin:2px 0 0; }
.offerrow__main { min-width:0; }
.offerrow__title { display:flex; align-items:center; gap:8px; flex-wrap:wrap; }
.offerrow__pkg { min-width:0; }
.offerrow__pkg select { width:100%; }
.offers__foot { display:flex; justify-content:flex-end; margin-top:14px;
                padding-top:14px; border-top:1px solid var(--hair); }
@media (max-width:720px) {
  .offerrow { grid-template-columns:auto 1fr; }
  /* The package drops under what it fixes rather than squeezing beside it. */
  .offerrow__pkg { grid-column:2; }
}

/* A second row of tabs under the first: same shape, quieter, so the two read
   as a hierarchy rather than as two equal choices on one screen. */
.tabs--sub { margin-top:-4px; }
.tabs--sub .tab { font-size:12.5px; padding:5px 11px; }

/* The mileage a car has driven past a recommendation. Overdue is the only
   state that earns colour here — everything else on this row is already quiet,
   and a screen where each line shouts says nothing. */
.offeredrow__odo { font-variant-numeric: tabular-nums; }
.offeredrow__odo.is-due b { color: var(--danger); }
.offeredrow__odo.is-due span { color: var(--mute); }

/* The way out of the challenge screen, kept visibly less loud than the form. */
.signin__recover { font-size:12px; margin-top:14px; }

.main { margin-left:var(--rail); min-height:100vh; min-height:100dvh; display:flex; flex-direction:column; }

/* The acting banner: an ISO or platform user working inside a downstream
   org. Amber, full-width, impossible to miss — whose books you are in must
   never be ambiguous. */
.actbar { background:var(--amber); color:#fff; padding:9px 26px; font-size:13px;
          display:flex; align-items:center; gap:14px; }
.actbar b { color:#fff; }
.actbar__exit { margin-left:auto; background:transparent; color:#fff;
                border:1px solid rgba(255,255,255,.55); border-radius:7px;
                padding:5px 12px; font-size:12.5px; cursor:pointer; white-space:nowrap; }
.actbar__exit:hover { background:rgba(255,255,255,.14); }

/* ⚠ IT SCROLLS AWAY. Greg, 2026-09-05: "the business name doesn't need to sit
   idle. let it scroll up if the user scrolls up... let the text flow up through
   the entire screen when scrolling."

   It was sticky, which spent a permanent band of every screen on a name you
   already know, and on a phone that band is a tenth of the display. In flow it
   costs that height once, at the top of each page, and then gives it back.

   The page still OPENS with it — the router scrolls to the top on every
   navigation, which is what makes this safe rather than disorienting. See the
   note beside window.scrollTo in app.js.

   position:relative, not static: the glass and its blur still have to composite
   above .view when a page's first element carries its own background. */
.topbar { padding:12px 26px; display:flex; align-items:center; gap:14px;
          background:rgba(255,255,255,.72); border-bottom:1px solid rgba(255,255,255,.8);
          backdrop-filter:var(--glass-blur); -webkit-backdrop-filter:var(--glass-blur);
          position:relative; z-index:5; }
/* An anchor since 2026-09-05 (it goes home) — but it must not read as a link:
   it is still the title of the screen you are on. */
.topbar__org { display:flex; align-items:center; gap:10px;
               color:inherit; text-decoration:none; }
.topbar__org:active { opacity:.6; }
.topbar__org b { font-size:15px; }
.orgswitch { margin-left:auto; max-width:280px; }
/* ⚠ A QUERY CONTAINER, AND THE REASON MATTERS. Every responsive grid on a
   page lives INSIDE this, to the right of a 224px rail. A @media query asks
   the viewport, so a rule written for "below 880px" fired on a 1100px window —
   the content was only 876px wide — and collapsed the dashboard, invoices,
   estimates and reports grids to a single column with the right half blank.
   Container queries ask this box instead, which is the box the layout is
   actually in. */
.view { padding:26px; flex:1; container-type:inline-size; container-name:view; }

.page__head { display:flex; align-items:center; gap:14px; margin-bottom:18px; flex-wrap:wrap; }
/* align-items:center, not the default stretch: a plain link next to a button
   otherwise stretches to the button's height and renders its text against the
   top of that box, sitting a few pixels above the buttons' center line. It
   reads as a mistake because it is one. flex-wrap so a long action row breaks
   instead of squeezing the title. */
.page__actions { margin-left:auto; display:flex; gap:8px;
                 align-items:center; flex-wrap:wrap; }
.back { display:inline-block; font-size:12px; color:var(--mute); text-decoration:none; margin-bottom:14px; }
.back:hover { color:var(--ink); }
section { margin-top:26px; }

/* ---- Cards, tiles --------------------------------------------------------- */
/* Cards hold forms and paragraphs, so they are solid, not glass. */
.card { background:var(--solid-bg); border:1px solid var(--hair); border-radius:var(--r);
        padding:18px; box-shadow:var(--shadow); }
.card__head { display:flex; align-items:center; gap:10px; margin-bottom:12px; }
.card__head h2 { margin:0; }
/* AN ANCHOR COUNTS AS A BUTTON HERE. Several card actions are real links now
   (Add someone goes to a page, not a dialog), and a rule that named only
   <button> left them stuck against the heading. */
.card__head button,
.card__head a.button { margin-left:auto; }
.card__head button ~ button,
.card__head button ~ a.button,
.card__head a.button ~ button,
.card__head a.button ~ a.button { margin-left:0; }
.cols { display:grid; grid-template-columns:repeat(auto-fit, minmax(280px, 1fr)); gap:14px; }
.cols2 { display:grid; grid-template-columns:repeat(auto-fit, minmax(340px, 1fr)); gap:14px; }

.tiles { display:grid; grid-template-columns:repeat(auto-fit, minmax(170px, 1fr)); gap:12px; }
/* ⚠ WHAT FOLLOWS THE TILES CARRIES THE GAP, AND MOST THINGS FORGOT TO.
   `.view` is a plain block with no row gap and `.tiles` has no bottom margin,
   so the spacing after a tile strip comes entirely from the next block's own
   margin-top. `.tablewrap` sets 14px, which is why every screen ending in a
   table looked right — and why the two-pane screens and a bare paragraph sat
   flush against the tiles' bottom edge, reading as tucked underneath them.
   Same 14px, stated for the blocks that never claimed it. */
.tiles + .timepick, .tiles + .pickpane, .tiles + p { margin-top:14px; }
/* auto-fit puts four tiles into three columns and leaves the fourth sitting
   alone beside a hole, which reads as a row that failed rather than a row that
   wrapped. Below the width where that starts happening, wrap them evenly. */
@media (max-width:1040px) { .tiles { grid-template-columns:repeat(2, minmax(0, 1fr)); } }
@media (max-width:560px)  { .tiles { grid-template-columns:1fr; } }
.tile { background:var(--glass-bg); border:1px solid var(--glass-brd);
        border-radius:var(--r); padding:14px 16px; box-shadow:var(--shadow);
        backdrop-filter:var(--glass-blur); -webkit-backdrop-filter:var(--glass-blur); }
.tile__label { display:block; font-size:9.5px; letter-spacing:.09em; text-transform:uppercase;
               color:var(--mute); font-weight:700; }
.tile__value { display:block; font-size:22px; margin-top:3px; font-variant-numeric:tabular-nums; }
.tile__sub { font-size:11.5px; color:var(--mute); }
.tile--bad .tile__value { color:var(--red); }
.tile--good .tile__value { color:var(--green); }
/* `warn` has been passed by the properties, vault and jobs screens since each
   was written and never had a rule, so it drew as an ordinary tile — the tone
   said nothing. Amber, between "fine" and "wrong". */
.tile--warn .tile__value { color:var(--amber); }

/* ---- Dashboard: the cards below the tiles ----------------------------------
   A figure you cannot open is a figure you have to go and find by hand, so the
   work and aging tiles are links. They must still read as tiles — the cue is
   the cursor and a lift on hover, not a different box. */
.tile--link { cursor:pointer; transition:transform .12s ease, box-shadow .12s ease; }
.tile--link:hover { transform:translateY(-1px); box-shadow:0 6px 18px rgba(19,41,75,.10); }
.tile--link:focus-visible { outline:none; border-color:var(--accent);
                            box-shadow:0 0 0 3.5px rgba(75,156,211,.22); }

.dashcard { margin-top:22px; }
.dashcard__note { margin-top:-4px; font-size:12px; }
.dashcard .tiles { margin-top:10px; }
.dashcard__foot { margin-top:12px; padding-top:10px; border-top:1px solid var(--hair);
                  display:flex; flex-direction:column; gap:3px;
                  font-size:11.5px; color:var(--mute); }

/* Two cards side by side where there is room, stacked where there is not.
   The GRID owns its top margin; its children do not. Letting a child's margin
   push the row down leaves the container itself flush against whatever is
   above, so the spacing vanishes the moment the first card is the one that
   gets hidden for having no data. */
.dashgrid { display:grid; grid-template-columns:1fr 1fr; gap:22px 26px;
            align-items:start; margin-top:22px; }
.dashgrid > .dashcard { margin-top:0; }

/* Two stacked tile grids read as one grid with a broken row — the columns of
   a five-tile row and a four-tile row do not line up. Kept as a fallback for
   any screen that does stack them; the dashboard gives the second row a card
   and a heading instead. */
.tiles + .tiles { margin-top:22px; }

/* The invoice and estimate lists lead with a row of tiles and then the filter
   tabs. `.tabs` only ever had margin below it, so the tabs sat flush against
   the bottom edge of the tiles and read as a caption on them. */
.tiles + .tabs { margin-top:20px; }

/* Recurring has no filter tabs, so its search box follows the tiles directly
   and hit the same fault the rule above fixes — with nothing between them the
   box sat against the bottom edge of the pills and read as attached to them.
   The tabs rule was written for one adjacency and there are two. */
.tiles + .toolbar { margin-top:20px; }

/* An empty grid still occupies its own gap once it has children; before the
   summary arrives it must take no room at all, or the page jumps. */
.tiles:empty { display:none; }

/* A table dropped straight after a card or a row of tiles had nothing between
   them — on Batches the summary box's bottom edge met the table's dark header
   exactly, so the two read as one broken object. Tables inside a `section`
   already get their air from the heading above; these are the cases with no
   heading to provide it. */
.tiles + .tablewrap, .card + .tablewrap, .tablewrap + .tablewrap { margin-top:14px; }

/* ---- Air after a tile row, as a RULE rather than a list ------------------

   Greg, 2026-09-03: "on the dashboard for the iso side, the most outstanding
   and needs attention boxes are sitting right under the pills up top, these
   need space between them. And again look for that across the system and note
   that there needs to be space going forward whenever creating a pill."

   ⚠ THIS WAS AN ENUMERATION AND THAT IS EXACTLY WHY IT KEPT BREAKING. The
   space after a tile row was spelled out per neighbour — .tiles + .tiles,
   + .tabs, + .toolbar, + .tablewrap, + .timepick, + .pickpane, + p, + .reach,
   + .reccols — so anything NOT on the list got zero. Measuring every route on
   all three hosts found four of them sitting flush:

     .tiles + .cols       ISO dashboard — "Most volume" / "Needs attention"
     .tiles + .card       ISO updates
     .tiles + .txbar      Obligations — the search bar
     .tiles + .uploadbar  Files

   The universal selector makes the DEFAULT correct, so the next block type
   dropped under a tile row is spaced without anybody remembering to add a
   rule. Every existing pair above still wins on specificity — one class plus
   one class beats one class plus a star — so the deliberate tightenings are
   untouched. Do not turn this back into a list. */
/* ⚠ 14px, NOT 20px, from 2026-09-07. Greg: "make it all 14px." Measured, the
   page had two rhythms — 20px after a tile strip and 14px after everything
   else — and a gap that is 6px wider than every other gap on the same screen
   is exactly the thing that reads as sloppy without anybody being able to say
   why. One number now. The universality of the rule is unchanged and must
   stay: it was an enumeration once, which is why it kept breaking. */
.tiles + * { margin-top:14px; }

/* Same fault, smaller: a callout has a top margin and no bottom one, so
   whatever follows it in the page flow sits flush.

   ⚠ SCOPED TO THE PAGE FLOW ON PURPOSE. `.form` is a flex column with a gap,
   and margins do not collapse in a flex container — they ADD to it. An
   unscoped rule here would quietly inflate the spacing under every notice in
   every dialog on the product. */
.view > .notice + * { margin-top:14px; }

/* ---- A pill always gets air ---------------------------------------------

   Greg, 2026-09-03: "note that there needs to be space going forward whenever
   creating a pill."

   A .chip is INLINE, so a block dropped after one sits on its baseline with
   nothing between them — 2px on the ISO Updates table, where the audience line
   ran into the category pill. The rule is here rather than on each screen so
   the next pill followed by a caption is spaced without anybody noticing they
   had to think about it. */
.chip + .sub, .chip + .hint, .chip + p { margin-top:6px; }

/* And two pills on one caption line must not touch. `.sub` is a block
   everywhere in this stylesheet, so a margin here cannot add itself to a flex
   gap — which is why the rule is scoped to it rather than written as a bare
   `.chip + .chip`. Files had "Company policy" and "Shared" flush against each
   other at 0px. */
.sub > .chip + * { margin-left:6px; }
/* "+ Add line" sat against the bottom edge of the lines table it adds to. */
.tablewrap + .btnrow { margin-top:12px; }

/* ---- A pill row under a box -----------------------------------------------

   The other half of the tile rule, found on the rent roll 2026-09-07. Air
   ABOVE a pill row was only ever guaranteed when a tile strip came first,
   because `.tiles + *` makes that case universal. `.tabs` carries a bottom
   margin and no top one, so a card followed by filter pills met it at 0px —
   on a page whose other gaps were 20px and 14px, which is what "the spacing
   is not uniform" looks like.

   ⚠ NOT written as `* + .tabs`, deliberately. A pill row is often the FIRST
   thing under a page heading, where the heading's own bottom margin already
   does the work and a top margin would double it. The blocks named here are
   the ones a pill row can legitimately follow. */
.card + .tabs, .tablewrap + .tabs, .toolbar + .tabs { margin-top:14px; }

/* ---- The maintenance board (R5, design A1) ---------------------------------

   Four lanes across, cards down. Deliberately NOT the pipeline board's aurora
   field: that board is a sales pipeline where a card is dragged to move a deal
   forward, and this one must not invite dragging at all — the lane is derived
   from the clock, so moving a card by hand would have to change the agreement.
   Plain columns say "read me" where a drag surface says "rearrange me".

   The gaps are the house 14px, and `.mlane__head` carries the label's own air
   so a lane with nothing in it still lines up with the ones that do. */
.mboard { display:grid; grid-template-columns:repeat(auto-fit, minmax(210px, 1fr)); gap:14px; }
.mlane { min-width:0; }
.mlane__head { font-size:11px; font-weight:700; letter-spacing:.06em; text-transform:uppercase;
               color:var(--mute); margin-bottom:10px; }
.mlane__head.is-bad { color:var(--red); }
.mlane > div > .mcard + .mcard { margin-top:10px; }

.mcard { background:var(--solid-bg); border:1px solid var(--hair); border-radius:var(--r-sm);
         padding:11px 13px; box-shadow:var(--shadow); cursor:pointer; }
.mcard:hover { border-color:var(--hair2); }
/* Past its reply target. The red edge rather than a red fill, because a lane
   of solid red blocks stops meaning anything by the third one. */
.mcard.is-late { border-color:var(--red); }
.mcard__top { display:flex; align-items:center; gap:8px; flex-wrap:wrap; }
.mcard__top b { font-size:12.5px; min-width:0; overflow:hidden; text-overflow:ellipsis; white-space:nowrap; }
.mcard__what { font-size:12.5px; color:var(--ink2); margin-top:5px; }
.mcard__foot { display:flex; align-items:center; gap:8px; flex-wrap:wrap; margin-top:8px; }
.mcard__foot .sub { margin:0; }

@media (max-width:760px) { .mboard { grid-template-columns:1fr; } }

/* ---- The row of buttons at the foot of a card -----------------------------

   `.card__head` is the strip at the TOP of a card, with its actions pushed
   right. There was no counterpart for the bottom, where the buttons lead
   rather than follow — so the Leases tab drew three of them with no gap at
   all and no air above. Same 10px as the head, so the two read as one family. */
.acts { display:flex; align-items:center; gap:10px; flex-wrap:wrap; margin-top:14px; }
.acts button, .acts a.button { margin-left:0; }

/* Two cards stacked in a plain container met edge to edge and read as ONE card
   with a stray hairline through it — Settings › Receipts was the clearest
   case. Excluded from the grid containers that already space their cards with
   `gap`, where a margin would stack on top of the gap rather than replace it.
   `.editorwrap` and `.cols*` are those; add any new card grid here too. */
:not(.cols):not(.cols2):not(.dashgrid):not(.editorwrap):not(.tiles):not(.termgrid) > .card + .card {
  margin-top:14px;
}

/* A total divided up: label, figure, and the share as a bar. */
.shares { display:flex; flex-direction:column; gap:11px; margin-top:10px; }
.share__head { display:flex; justify-content:space-between; align-items:baseline; gap:10px; }
.share__name { font-size:13px; color:var(--ink); }
.share__amt { font-size:13px; font-variant-numeric:tabular-nums; }
.share__track { height:6px; border-radius:99px; background:var(--paper); margin:5px 0 3px; overflow:hidden; }
.share__fill { height:100%; border-radius:99px; background:var(--accent); }
.share__sub { font-size:11px; color:var(--mute); font-variant-numeric:tabular-nums; }
.share--link { cursor:pointer; }
.share--link:hover .share__name { color:var(--accent); }
.share--link:hover .share__fill { filter:brightness(1.08); }

@media (max-width: 900px) {
  .dashgrid { grid-template-columns:1fr; gap:0; }
}

/* ---- Tables ---------------------------------------------------------------- */
/* Dense text: solid panel, no blur. */
.tablewrap { overflow-x:auto; border:1px solid var(--hair); border-radius:var(--r);
             background:var(--solid-bg); box-shadow:var(--shadow); }
table { width:100%; border-collapse:collapse; font-size:13px; }
/* The header row carries the whole table's meaning and was set two points
   smaller than the smallest thing under it — legible, but reading as a caption
   rather than as the row that names the columns. Bigger, with room around it;
   the tracking comes down because wider letters no longer need as much. */
th { text-align:left; font-size:11.5px; letter-spacing:.055em; text-transform:uppercase;
     color:#fff; background:var(--navy); padding:13px 12px; font-weight:700; white-space:nowrap; }
td { padding:10px 12px; border-top:1px solid var(--hair); vertical-align:top; }
tbody tr:nth-child(even) { background:var(--paper); }
tr.clickable { cursor:pointer; }
tr.clickable:hover { background:rgba(75,156,211,.08); }
tfoot td { border-top:1px solid var(--hair); }
tfoot tr.is-strong td { border-top:2px solid var(--ink); font-size:14px; }
table.lines td { vertical-align:middle; }
table.lines input[type=text], table.lines input[type=number] { padding:6px 8px; font-size:13px; }
table.lines td:first-child input + input { margin-top:4px; font-size:12px; color:var(--steel); }

.toolbar { margin-bottom:12px; max-width:420px; }

/* The lists that carry a search box AND a date range need the room, and the
   two wrap onto separate lines rather than squeezing on a laptop. */
.toolbar--wide { max-width:none; display:flex; gap:10px 16px; flex-wrap:wrap;
                 align-items:center; }
.toolbar--wide input[type=search] { width:320px; max-width:100%; }

.daterange { display:flex; align-items:center; gap:7px; flex-wrap:wrap; }
.daterange select { width:auto; }
.daterange input[type=date] { width:auto; }
.daterange__sep { font-size:12px; color:var(--mute); }
.daterange__clear { padding:6px 12px; font-size:12.5px; }
.daterange__clear[disabled] { opacity:.45; cursor:default; }

/* ---- Sortable column headers ------------------------------------------------
   The arrow is drawn HERE, off `data-dir`, and is deliberately not a character
   in the DOM: labelTables() reads `th.textContent` to label cells for the
   phone's card layout, so a glyph inside the header would put "Total ▼" on
   every amount on a phone.

   A stable gutter is reserved on every sortable header so the row does not
   shift sideways when the arrow appears — the columns would jog every time you
   changed the order, which reads as the table redrawing wrongly. */
th.th--sort { cursor:pointer; user-select:none; }
/* The glyph is always present and always the same width — only its opacity
   and its direction change. Reserving the space with a blank and swapping in
   an arrow on hover would jog every column to the left of it sideways, which
   reads as the table redrawing wrongly rather than as a hover. */
th.th--sort::after { content:'\25BC'; margin-left:5px; font-size:8px; opacity:0; }
th.th--sort:hover::after { opacity:.5; }
th.th--sort[data-dir]::after { opacity:1; }
th.th--sort[data-dir=asc]::after { content:'\25B2'; }
th.th--sort:hover { background:var(--ink2); }
th.th--sort:focus-visible { outline:2px solid var(--accent); outline-offset:-2px; }

.tabs { display:flex; gap:4px; margin-bottom:14px; flex-wrap:wrap; }
.tab { padding:6px 13px; border-radius:99px; font-size:12.5px; text-decoration:none;
       color:var(--steel); background:#fff; border:1px solid var(--hair); font-weight:500; }
.tab.is-on { background:var(--ink); color:#fff; border-color:var(--ink); }

/* ---- Badges, chips --------------------------------------------------------- */
.badge { display:inline-block; font-size:10px; font-weight:700; letter-spacing:.04em;
         text-transform:uppercase; padding:3px 9px; border-radius:99px;
         background:var(--band); color:var(--ink2); white-space:nowrap; }
.badge--paid, .badge--accepted { background:var(--green-bg); color:var(--green); }
.badge--partial, .badge--viewed { background:var(--amber-bg); color:var(--amber); }
.badge--draft { background:var(--band); color:var(--mute); }
.badge--overdue, .badge--void, .badge--declined, .badge--expired { background:var(--red-bg); color:var(--red); }
.chip { display:inline-block; font-size:11px; padding:2px 9px; border-radius:99px;
        background:var(--band); color:var(--ink2); font-weight:600; white-space:nowrap; }
.chip--surcharge { background:#FBE6E9; color:var(--accent); }
/* The margin's three moods (job costing, 2026-09-06): below cost, on the
   goal, under it. Red is the app's warning colour and nothing else. */
.chip--bad  { background:#FBE6E9; color:var(--red); }
.chip--ok   { background:#E1F3EB; color:#1E7A5B; }
.chip--warn { background:#FBEEDB; color:#9A5B12; }
td.is-bad, .is-bad { color:var(--red); }
.tile--ok .tile__value { color:#1E7A5B; }
/* A member (plans, 2026-09-06): gold, because it is the one chip that means
   "this customer pays us every month" and it should read at a glance. */
.chip--member { background:#FFF4DA; color:#8A6100; }
.calchip__member { font-size:9px; margin-left:4px; opacity:.9; }
.who__member { display:inline-block; margin-top:8px; background:rgba(255,201,138,.18); color:#FFC98A;
               border-radius:99px; padding:2px 9px; font-size:11px; font-weight:700; letter-spacing:.04em; }
.chip--dual_pricing { background:rgba(75,156,211,.16); color:#2A6E9E; }
/* Tone on a chip. `span.bad` and `.warn` already exist but only set a text
   color, which on a filled pill reads as a mistake rather than a signal. */
.chip.bad  { background:var(--red-bg); color:var(--red); }
.chip.warn { background:var(--amber-bg); color:var(--amber); }
.chip.ok   { background:var(--green-bg); color:var(--green); }

/* ---- The recommendation link ---------------------------------------------
   One piece of deferred work, read on a phone by somebody who half-remembers
   the conversation. The photograph carries it, so it goes first and goes wide. */
.rec__shots { display:flex; gap:8px; overflow-x:auto; margin:0 0 14px;
              scroll-snap-type:x mandatory; -webkit-overflow-scrolling:touch; }
/* CONTAIN, NOT COVER. These are photographs of a defect, and cover crops
   whatever is nearest the edge — which on a picture taken to show a corroded
   fitting is quite often the corroded fitting. */
.rec__shot { width:100%; max-width:100%; flex:0 0 100%; scroll-snap-align:center;
             border-radius:10px; object-fit:contain; max-height:320px; background:var(--band); }
.rec__title { font-size:21px; line-height:1.25; margin:0 0 8px; }
.rec__scope { margin:10px 0 4px; color:var(--ink2); }
.rec__scope p { margin:0 0 6px; }

/* ---- Detail --------------------------------------------------------------- */
.summary { display:flex; gap:18px; flex-wrap:wrap; background:#fff; border:1px solid var(--hair);
           border-radius:var(--r); padding:18px; margin-bottom:18px; }
.summary__main { flex:1; min-width:260px; }
.summary__row { display:flex; align-items:center; gap:10px; flex-wrap:wrap; margin-bottom:10px; }
.summary__row h2 { font-size:18px; text-transform:none; letter-spacing:0; color:var(--ink); margin:0; }
.summary__due { text-align:right; min-width:200px; padding-left:18px; border-left:1px solid var(--hair); }
.summary__due span { display:block; font-size:9.5px; letter-spacing:.09em; text-transform:uppercase;
                     color:var(--mute); font-weight:700; }
.summary__due b { font-size:24px; font-variant-numeric:tabular-nums; }
/* A deposit still owed is the one thing on this panel that asks for an action,
   so it carries weight; once received it is a fact and steps back. */
.summary__due .is-owed { color:var(--accent); font-weight:600; }
.summary__due .is-good { color:var(--green); }

dl.kv { display:grid; grid-template-columns:auto 1fr; gap:6px 16px; margin:0; font-size:13px; }
dl.kv dt { color:var(--mute); font-size:12px; }
dl.kv dd { margin:0; }
dl.kv--inline { grid-template-columns:repeat(auto-fit, minmax(90px, max-content) 1fr); }

ul.list { list-style:none; margin:0; padding:0; }
ul.list li { padding:9px 0; border-top:1px solid var(--hair); }
ul.list li:first-child { border-top:0; }
ul.list li.clickable:hover { background:var(--paper); }
ul.timeline { list-style:none; margin:0; padding:0; font-size:12.5px; }
ul.timeline li { padding:6px 0; border-top:1px solid var(--hair); text-transform:capitalize; }
/* A sentence on a history line — "$350.00 refunded, returned work" — must not
   have every word capitalised like the one-word events around it. */
ul.timeline .timeline__detail { text-transform:none; color:var(--ink2); }

.note { background:var(--paper); border-left:3px solid var(--hair); padding:10px 14px;
        border-radius:0 6px 6px 0; font-size:13px; margin-top:16px; }
.note--private { border-left-color:var(--amber); background:var(--amber-bg); }
/* THE BAND COLOR IS ALMOST THE PAGE COLOR. On the pale blue gradient these
   screens sit on, a notice with a --band fill and no edge is invisible: the
   credit-card surcharge line under an invoice read as loose text spilled below
   the document rather than a note belonging to it. A hairline is what makes it
   a box, whatever it is sitting on. */
.notice { background:var(--paper); border:1px solid var(--hair); border-radius:7px;
          padding:11px 14px; font-size:12.5px; color:var(--ink2); margin-top:16px; }
.notice--warn { background:var(--amber-bg); border-color:rgba(138,97,0,.24); color:var(--amber); }
/* A notice that is reporting a failure, not just informing. `.bad` was being
   used on notices and on inline spans before it existed anywhere — both
   rendered as ordinary text, so a failed load looked like a caption. */
.notice.bad { background:var(--red-bg); border-color:rgba(201,59,64,.24); color:var(--red); }
span.bad { color:var(--red); font-weight:600; }
.linkbox { font-family:ui-monospace, Menlo, monospace; font-size:12px; background:var(--band);
           padding:10px 12px; border-radius:var(--r-sm); word-break:break-all; margin:10px 0; }

/* ---- Forms ----------------------------------------------------------------- */
.form { display:flex; flex-direction:column; gap:12px; }
/* A card whose body IS a form. `.card` is display:block, so fields dropped
   straight into one sit flush against each other and a hint runs into the next
   field's label with nothing between them — which is what the terminal looked
   like. The gap belongs to the container, not to a margin on every field. */
.card.form > h2 { margin-bottom:0; }

/* The same collision wherever a card was NOT given `.form` — Settings stacks
   fields directly into twenty-six of them. Rather than converting each by
   hand and missing one, space any two form blocks that end up adjacent inside
   a plain card. Scoped to `:not(.form)` so it never doubles up with the gap on
   a card that does use the flex container. */
.card:not(.form) > :is(.field, .row2, .row3, .btnrow, .hint, .check)
                 + :is(.field, .row2, .row3, .btnrow, .hint, .check) {
  margin-top:12px;
}
.field { display:flex; flex-direction:column; gap:4px; }
/* THE hidden ATTRIBUTE HAS TO WIN. The browser's own [hidden] rule is
   display:none at the lowest possible specificity, so any class that sets
   display — .field above, and every flex row in here — silently beats it, and
   an element the code believes is hidden goes on rendering. Found when the
   "Worked by" field on a flat-rate shop kept showing its hint. Stated once,
   globally, rather than per component. */
[hidden] { display:none !important; }
label { font-size:10px; letter-spacing:.07em; text-transform:uppercase; color:var(--mute); font-weight:700; }
input, select, textarea {
  font:14px inherit; padding:10px 12px; border:1px solid rgba(19,41,75,.16);
  border-radius:11px; background:rgba(255,255,255,.88); color:var(--ink); width:100%;
  min-height:40px;
}

/* A NATIVE DATE OR TIME FIELD CLIPS ITSELF SILENTLY. Its internals — hh, mm,
   the AM/PM box and the picker button — are drawn by the browser, and when
   they do not fit it simply cuts the last one off WITHOUT reporting any
   overflow: scrollWidth still equals clientWidth, so nothing in a stylesheet
   or a test can tell. Opening hours read "07:00 AI" for months.

   The floor is what the widest form needs — a two-digit hour, minutes, AM/PM
   and the button — with the horizontal padding trimmed to buy it room. */
input[type=time], input[type=date], input[type=datetime-local], input[type=month] {
  padding-left:9px; padding-right:9px; min-width:142px;
}
input[type=datetime-local] { min-width:210px; }

/* A RADIO IS A GLYPH, NOT A FIELD, and the rule above was giving every one of
   them width:100%, min-height:40px, 12px of padding and an 11px radius. Inside
   a flex row that made the control take whatever space the label left over —
   a different amount on every row — so a column of three options fanned out
   into a ragged staircase with 300px-wide circles. Checkboxes were mostly
   spared only because several screens had already patched them one at a time.

   Fixed once, here, rather than in each of the fifteen places that draw one.
   Screens wanting a bigger hit area still override it: .optrow, .featrow, the
   checklist and the line-item table all set their own size after this. */
input[type=radio], input[type=checkbox] {
  width:16px; height:16px; min-height:0; padding:0; flex:none;
  border-radius:0; accent-color:var(--accent); vertical-align:middle;
}
input[type=radio] { border-radius:50%; }
/* A ring rather than an outline: it reads as focus on a soft ground, where a
   hard 2px outline against glass looks like an error state. */
input:focus, select:focus, textarea:focus {
  outline:none; border-color:var(--accent);
  box-shadow:0 0 0 3.5px rgba(75,156,211,.22);
}
input[disabled], select[disabled] { background:var(--paper); color:var(--mute); }
textarea { resize:vertical; }

/* ---- Dropdowns ---------------------------------------------------------------

   Greg, 2026-08-31: "make all dropdowns in the system match the design scheme
   of the portal. not a grey box with white writing."

   THE CAUSE WAS A MISSING LINE, not a missing rule. `input, select, textarea`
   above already paints a select white with navy text — but a <select> keeps
   the operating system's own widget chrome unless you take it away, and with
   no `color-scheme` declared anywhere the OS hands it the SYSTEM's scheme.
   Anybody running their Mac or Windows in dark mode therefore got a dark grey
   control with white text sitting in the middle of a white form, and the
   native option list opened dark too. Nothing in the stylesheet said light,
   so the browser was right to guess.

   Two lines fix the whole app, all 120 of them, including the four built with
   a bare el('select') that no component wraps:

     color-scheme:light   the app IS a light design — this also makes the
                          native option popup render light, which is the half
                          that CSS cannot reach on macOS
     appearance:none      hand the control's paint job to the rules above

   THE CHEVRON IS THE CUSTOMER PICKER'S CHEVRON. .acwrap__caret draws a 9px
   box rotated 45° in --accent; a replaced element cannot carry a ::after, so
   the same shape is an inline SVG here. Same angle, same weight, same colour —
   "just like the customer selections" means the closed control reads as the
   same kind of thing whether the list behind it is ours or the browser's. */
:root { color-scheme:light; }

select {
  appearance:none; -webkit-appearance:none; -moz-appearance:none;
  cursor:pointer;
  /* Room for the chevron, matching the picker's own `padding-right:40px` at
     this control's smaller scale. */
  padding-right:34px;
  background-image:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='12' height='8' viewBox='0 0 12 8'%3E%3Cpath d='M1.5 1.5 6 6l4.5-4.5' fill='none' stroke='%232A6E9E' stroke-width='2.2' stroke-linecap='round' stroke-linejoin='round'/%3E%3C/svg%3E");
  background-repeat:no-repeat;
  background-position:right 12px center;
  background-size:12px 8px;
}
select:hover { border-color:var(--hair-strong); }
/* Honoured on Windows and Linux, ignored on macOS where the popup is drawn by
   the OS — which is what color-scheme above is for. */
select option { background:var(--white); color:var(--ink); }
select option:disabled { color:var(--mute); }
/* The disabled rule above wins the background; the chevron would otherwise sit
   on it looking live. */
select[disabled] { background-image:none; cursor:not-allowed; }

/* The list itself. `appearance:none` above only reaches the closed control —
   the list a <select> opens is drawn by the OS, which is why restyling the box
   was not enough and a Mac in dark mode kept showing a grey menu with white
   writing under a white field. lib.js suppresses that list and draws this one.

   THE MEASUREMENTS ARE THE CUSTOMER PICKER'S, not new ones: .acbox and .acitem
   are what "just like the customer selections" means, and two dropdown popups
   a few pixels apart would be worse than one that was merely wrong. Same
   surface, same hairline, same lift, same row height, same hover wash. It sits
   in <body> so a card with its own overflow cannot clip it. */
.selpop {
  position:absolute; z-index:60; max-height:320px; overflow-y:auto;
  border-radius:var(--r); padding:0;
  background:var(--solid-bg); border:1px solid var(--hair);
  box-shadow:var(--shadow-lift);
  backdrop-filter:blur(10px); -webkit-backdrop-filter:blur(10px);
}
.selpop__item {
  padding:10px 13px; font-size:13.5px; line-height:1.35; cursor:pointer;
  color:var(--ink); border-bottom:1px solid var(--hair); white-space:nowrap;
}
.selpop__item:last-child { border-bottom:0; }
.selpop__item.is-on { background:rgba(75,156,211,.12); }
/* The one currently chosen, so an opened list says where you are as well as
   where you could go. Weight rather than colour: the hover wash is the thing
   that should move as you arrow through it. */
.selpop__item.is-sel { font-weight:600; }
.selpop__item.is-off { color:var(--mute); cursor:default; }
.selpop__item.is-off.is-on { background:none; }
/* An <optgroup> label. Not selectable, and set apart from the entries under it
   so a long list says which section you are reading. */
.selpop__group { padding:9px 13px 4px; font-size:10px; font-weight:700;
                 letter-spacing:.07em; text-transform:uppercase; color:var(--mute);
                 background:var(--paper); border-bottom:1px solid var(--hair); }
.selpop__group + .selpop__item { border-top:0; }

/* A card nested inside a form card — the email block and the text block of a
   notice being written. Flatter than a real card so the page does not read as
   boxes inside boxes. */
.subcard { border:1px solid var(--hair); border-radius:var(--r-sm); padding:14px;
           box-shadow:none; background:var(--paper); gap:10px; }
.subcard h3 { margin:0; font-size:12px; letter-spacing:.05em; text-transform:uppercase;
              color:var(--steel); }

/* ---- The customer book, as a CRM --------------------------------------------
   Two questions get answered above the table — who wants something from me,
   and how did they find me — so the tiles are the first thing on the page and
   the ones that lead somewhere are links. */
.crmtiles { display:grid; gap:12px; margin-bottom:14px;
            grid-template-columns:repeat(auto-fit, minmax(150px, 1fr)); }
/* ── Segment tiles (Customers, option 13) ────────────────────────────────────

   Greg approved these on 2026-09-03: the dashboard's aging tile, reused as the
   thing you press to filter the book.

   ⚠ EACH TILE TAKES A WASH OF ITS OWN HUE BECAUSE THE PAGE IS WHITE NOW. On
   the old blue field a white tile was lighter than the ground and that alone
   separated it. On white there is no contrast left to spend, so the tile
   carries 5% of its own colour and a firmer edge — it stops being a white box
   on white and becomes an object.

   ⚠ AND THE GLOW IS A THIRD OF THE DASHBOARD'S. Those three tiles carry money
   getting late; these carry how many customers you have. Browsing does not
   need heat. */
.crmtile { --h:var(--accent-bright); position:relative; isolation:isolate;
           display:block; padding:15px 17px; text-decoration:none;
           border-radius:var(--r);
           background:linear-gradient(180deg,
             color-mix(in srgb, var(--h) 5%, #fff),
             color-mix(in srgb, var(--h) 2%, #fff));
           border:1px solid color-mix(in srgb, var(--h) 40%, var(--hair));
           box-shadow:var(--aura);
           transition:border-color .12s ease, transform .12s ease, box-shadow .12s ease; }
.crmtile::after { content:""; position:absolute; z-index:-1;
           left:16%; right:16%; bottom:-8px; height:16px; border-radius:50%;
           background:var(--h); opacity:.12; filter:blur(11px); }
a.crmtile:hover { transform:translateY(-2px);
           border-color:color-mix(in srgb, var(--h) 60%, var(--hair)); }
/* The tile whose segment is showing. Two pixels, not a fill — a solid tile
   would hide the count, which is the reason to look at it. */
.crmtile.is-on { border-width:2px; padding:14px 16px; }

.crmtile--lead { --h:#C08A1E; }
.crmtile--work { --h:#4B9CD3; }
.crmtile--cust { --h:#2FA37A; }
.crmtile--info { --h:#8A99AC; }
.crmtile--lead b { color:var(--amber); }
.crmtile--work b { color:var(--accent); }
.crmtile--cust b { color:#1E7A5A; }

@supports not (color: color-mix(in srgb, red 50%, blue)){
  .crmtile { background:var(--white); border-color:var(--hair); }
}
.crmtile b { display:block; font-size:24px; color:var(--ink);
             font-variant-numeric:tabular-nums; line-height:1.1; }
.crmtile > span { display:block; font-size:12px; font-weight:600; color:var(--ink2);
                  margin-top:4px; }
.crmtile .sub { font-size:11px; color:var(--mute); margin-top:2px; }
.crmtile--flat { cursor:default; }

.crmbar { display:flex; align-items:center; justify-content:space-between;
          gap:14px; flex-wrap:wrap; margin-bottom:12px; }
.crmbar__filters { display:flex; gap:10px; align-items:center; flex:1;
                   min-width:260px; max-width:520px; }
.crmbar__filters select { width:auto; min-width:140px; flex:none; }

/* The segment is this page's main control, so it is a pill row rather than another
   dropdown competing with the filters beside it. */
.segpills { display:flex; gap:6px; flex-wrap:wrap; }
.segpill { padding:7px 14px; border-radius:999px; border:1px solid var(--hair);
           background:var(--white); color:var(--ink2); font-size:12.5px;
           font-weight:600; text-decoration:none; white-space:nowrap; }
.segpill:hover { border-color:var(--accent); color:var(--accent); }
.segpill.is-on { background:var(--accent); border-color:var(--accent); color:#fff; }

/* Stage reads at a glance down a column, so it is colour as well as a word —
   new is the one that needs answering and is the only one that shouts. */
.stagechip { display:inline-block; padding:3px 9px; border-radius:999px;
             font-size:11px; font-weight:700; letter-spacing:.02em;
             background:var(--band); color:var(--ink2); white-space:nowrap; }
.stagechip--new { background:var(--amber-bg); color:var(--amber); }
.stagechip--contacted { background:rgba(75,156,211,.15); color:var(--accent); }
.stagechip--quoted { background:rgba(75,156,211,.15); color:var(--accent); }
.stagechip--customer { background:var(--green-bg); color:var(--green); }
.stagechip--lost { background:var(--paper); color:var(--mute); }

/* ---- One person's history ----------------------------------------------- */
.tline { display:flex; flex-direction:column; }
.tline__row { display:flex; gap:12px; padding:11px 2px;
              border-bottom:1px solid var(--hair); }
.tline__row:last-child { border-bottom:0; }
.tline__dot { flex:none; width:26px; height:26px; border-radius:50%;
              background:var(--band); color:var(--ink2); display:flex;
              align-items:center; justify-content:center; font-size:12px; }
.tline__dot--enquiry { background:var(--amber-bg); color:var(--amber); }
.tline__dot--visit { background:rgba(75,156,211,.14); color:var(--accent); }
.tline__dot--payment { background:var(--green-bg); color:var(--green); }
.tline__main { flex:1; min-width:0; }
.tline__main b { display:block; font-size:13px; color:var(--ink); }
.tline__main .body { font-size:12.5px; color:var(--ink2); margin-top:3px;
                     white-space:pre-wrap; }
.tline__when { flex:none; font-size:11.5px; color:var(--mute); text-align:right;
               white-space:nowrap; }

/* Something to be copied rather than read: monospace, scrollable sideways so a
   long URL does not force the page to, and selectable in one gesture. */
/* ---- National holidays ------------------------------------------------------
   Two columns of switches, because eleven rows in one column is a scroll for
   something you set once. The date sits under the name: "the third Monday in
   January" is not a thing anybody can picture. */
.holidaygrid { display:grid; gap:8px;
               grid-template-columns:repeat(auto-fit, minmax(260px, 1fr)); }
.holidayrow { display:flex; align-items:center; gap:11px; cursor:pointer;
              border:1px solid var(--hair); border-radius:var(--r-sm);
              padding:10px 13px; background:var(--white); }
.holidayrow input { width:16px; height:16px; min-height:0; margin:0; flex:none; }
.holidayrow__main { flex:1; min-width:0; }
.holidayrow__main b { display:block; font-size:13px; color:var(--ink);
                      text-transform:none; letter-spacing:normal; font-weight:600; }
.holidayrow__main .sub { font-size:11.5px; color:var(--mute); margin-top:1px; }
.holidayrow__state { flex:none; font-size:11px; font-weight:700; color:var(--mute);
                     text-transform:uppercase; letter-spacing:.05em; }
.holidayrow.is-open { border-color:var(--accent); background:rgba(75,156,211,.06); }
.holidayrow.is-open .holidayrow__state { color:var(--accent); }

.codebox { font:12px/1.55 ui-monospace,SFMono-Regular,Menlo,monospace;
           background:var(--paper); border:1px solid var(--hair);
           border-radius:var(--r-sm); padding:12px 14px; margin:0;
           overflow-x:auto; white-space:pre; color:var(--ink2);
           user-select:all; }

/* The compact ones already reserve their own right padding further down the
   sheet; only the chevron needs to come in to match. */
.pager__act select, .daterange select, .txsearch select, .sortpick select {
  background-position:right 9px center; background-size:10px 7px;
}

/* ---- The money box ----------------------------------------------------------
   The dollar sign sits INSIDE the border, so the control reads as one field
   rather than a label stuck to a box. The input keeps the real border and the
   real focus ring; the wrapper only positions the sign. Figures are
   tabular-nums and right-aligned, which is what makes a column of prices
   scannable — the decimal points line up. */
.moneybox { position:relative; display:block; width:100%; }
.moneybox__sign {
  position:absolute; left:12px; top:50%; transform:translateY(-50%);
  font-size:13px; font-weight:600; color:var(--mute); pointer-events:none;
}
.moneybox__in {
  padding-left:25px; text-align:right;
  font-variant-numeric:tabular-nums; font-feature-settings:'tnum';
}
/* Empty reads as prompt, not as a figure: a right-aligned "0.00" placeholder
   hard against the edge looks like a value already entered. */
.moneybox__in:placeholder-shown { text-align:left; }
.moneybox.is-off .moneybox__sign { color:var(--hair); }
/* In a lines table the cells are tight and already right-aligned. */
table.lines .moneybox__in { padding:6px 8px 6px 20px; font-size:13px; }
table.lines .moneybox__sign { left:8px; font-size:12px; }
.hint { font-size:11.5px; color:var(--mute); }
.row2 { display:grid; grid-template-columns:1fr 1fr; gap:12px; }
.row3 { display:grid; grid-template-columns:1fr 1fr 1fr; gap:12px; }

/* ---- The vertical's preset, on the boarding form ----------------------------
   What the trade starts with, and every switch it starts with, before the
   business exists. Ticked from the template and free to change — the drift
   line says so when it has been. */
.preset { margin:14px 0 0; }
.preset > label { display:block; font-size:12px; font-weight:700; color:var(--steel);
                  margin-bottom:6px; }
.preset__body { border:1px solid var(--hair); border-radius:var(--r-sm); background:var(--paper);
                padding:12px 14px; }
.preset__what { font-size:13px; line-height:1.5; margin-bottom:10px; }
.preset__grid { display:grid; grid-template-columns:repeat(auto-fit, minmax(130px, 1fr));
                gap:6px 14px; }
/* These are options, not field labels — the global uppercase `label` rule reads
   as a heading for something below it, which is the wrong job here. */
.preset__f { display:flex; align-items:center; gap:7px; font-size:13px; cursor:pointer;
             padding:3px 0; text-transform:none; letter-spacing:0;
             font-weight:400; color:var(--ink); }
.preset__f input { width:15px; height:15px; margin:0; flex:none; accent-color:var(--accent); }
.preset__drift:not(:empty) { margin-top:10px; padding-top:9px; border-top:1px solid var(--hair);
                             font-size:12px; color:var(--steel); }

@media (max-width:640px) {
  .preset__grid { grid-template-columns:1fr 1fr; }
}
.checks { display:flex; gap:16px; flex-wrap:wrap; }
.check { display:flex; align-items:center; gap:7px; text-transform:none; letter-spacing:0;
         font-size:13px; color:var(--ink); font-weight:400; }
.check input { width:auto; }

/* ---- Editor ----------------------------------------------------------------- */
.editorwrap { display:grid; grid-template-columns:minmax(0,1fr) 300px; gap:14px; align-items:start; }
.editor { display:flex; flex-direction:column; gap:14px; min-width:0; }
.side { position:sticky; top:20px; }
@media (max-width:1100px) { .editorwrap { grid-template-columns:1fr; } }
.progline { margin-top:12px; font-size:12.5px; color:var(--steel);
            display:flex; align-items:center; gap:7px; flex-wrap:wrap; }
.totals { font-size:13px; }
.totals__head { display:grid; grid-template-columns:1fr auto auto; gap:14px;
                font-size:9.5px; letter-spacing:.08em; text-transform:uppercase;
                color:var(--mute); font-weight:700; margin-bottom:6px; }
.totals__head span:not(:first-child) { min-width:84px; text-align:right; }
.totals__row { display:grid; grid-template-columns:1fr auto auto; gap:14px; padding:4px 0; }
.totals__row span:not(:first-child) { min-width:84px; }
.totals__row.is-strong { font-weight:700; font-size:15px; }
.totals__note { margin-top:10px; font-size:11.5px; color:var(--accent);
                background:var(--red-bg); padding:8px 10px; border-radius:var(--r-sm); }
.progbox { display:flex; align-items:center; gap:10px; margin-bottom:10px; }
.preview { background:var(--paper); border-radius:7px; padding:12px 14px; font-size:13px; }
.preview__row { display:flex; justify-content:space-between; gap:14px; padding:3px 0; }
.preview__row.is-strong { font-weight:700; border-top:1px solid var(--hair); margin-top:5px;
                          padding-top:8px; font-size:15px; }
.preview__note { font-size:11.5px; color:var(--mute); margin-top:8px; }

.empty { background:#fff; border:1px solid var(--hair); border-radius:var(--r); padding:40px;
         text-align:center; color:var(--mute); }
.empty p { margin:0 0 6px; }
.empty p:last-of-type { margin-bottom:0; }
.empty button { margin-top:16px; }

/* A load that failed reads differently from a list that is simply empty. The
   first line is plain, the second is the gateway's or Postgres's own words —
   small, because it is for whoever is debugging, not for the person waiting. */
.empty--err { border-color:rgba(201,59,64,.3); background:var(--red-bg); }
.empty--err > p:first-child { color:var(--ink); font-weight:600; }
.empty--err .muted { font-size:12px; word-break:break-word; }

/* ---- Modal, toast ------------------------------------------------------------ */
.modal { position:fixed; inset:0; background:rgba(16,24,32,.5); display:grid;
         place-items:center; padding:22px; z-index:60; }
.modal__panel { background:#fff; border-radius:var(--r-lg); width:100%; max-width:480px;
                max-height:90vh; display:flex; flex-direction:column; overflow:hidden; }
.modal__panel--wide { max-width:760px; }
.modal__head { display:flex; align-items:center; padding:16px 18px; border-bottom:1px solid var(--hair); }
.modal__head button { margin-left:auto; }
.modal__body { padding:18px; overflow-y:auto; }
.modal__foot { display:flex; justify-content:flex-end; gap:8px; padding:14px 18px;
               border-top:1px solid var(--hair); background:var(--paper); }

.toasts { position:fixed; right:18px; bottom:18px; display:flex; flex-direction:column;
          gap:8px; z-index:80; }
.toast { background:var(--ink); color:#fff; padding:11px 16px; border-radius:7px;
         font-size:13px; box-shadow:0 6px 20px rgba(16,24,32,.22); transition:opacity .3s, transform .3s; }
.toast--err { background:var(--accent); }
.toast.is-out { opacity:0; transform:translateY(6px); }

/* ---- Narrow ------------------------------------------------------------------- */

/* The line-item editor stacks into cards. Crews invoice on site, and a
   six-column table below ~700px is a horizontal scroll with the amount —
   the number they are checking — off the edge of the screen. */
@media (max-width:700px) {
  .tablewrap--lines { overflow:visible; border:0; background:none; border-radius:0; }
  table.lines, table.lines tbody { display:block; }
  table.lines thead { display:none; }
  table.lines tr {
    display:grid; gap:10px; padding:14px; margin-bottom:10px;
    grid-template-columns:1fr 1fr 64px;
    grid-template-areas:"desc desc del" "qty rate tax" "amount amount amount";
    background:#fff; border:1px solid var(--hair); border-radius:var(--r-sm);
  }
  table.lines tbody tr:nth-child(even) { background:#fff; }
  table.lines td { border:0; padding:0; }
  td.c-desc { grid-area:desc; }
  td.c-qty { grid-area:qty; }
  td.c-rate { grid-area:rate; }
  td.c-tax { grid-area:tax; text-align:left; align-self:end; }
  td.c-del { grid-area:del; justify-self:end; }
  td.c-amount { grid-area:amount; display:grid; grid-template-columns:auto 1fr;
                align-items:baseline; border-top:1px solid var(--hair); padding-top:9px; }
  td.c-amount > * { grid-column:2; }
  td.c-qty::before, td.c-rate::before, td.c-tax::before, td.c-amount::before {
    display:block; content:''; font-size:9.5px; letter-spacing:.08em;
    text-transform:uppercase; color:var(--mute); font-weight:700; margin-bottom:3px;
  }
  td.c-qty::before { content:'Qty'; }
  td.c-rate::before { content:'Rate'; }
  td.c-tax::before { content:'Taxable'; }
  td.c-amount::before { content:'Amount'; margin-bottom:0; }
  td.c-tax input[type=checkbox] { width:22px; height:22px; }
}

/* Fingers, not cursors. 44px is the floor for anything tapped, and inputs
   hold 16px type because below that iOS Safari zooms the page on focus and
   leaves the crew stranded mid-form. */
@media (pointer:coarse) {
  button { min-height:44px; padding:10px 18px; }
  button.icon { min-height:44px; min-width:44px; padding:2px 10px; }
  button.link { min-height:44px; padding:10px 8px; font-size:13px;
                display:inline-flex; align-items:center; }
  button.chipbtn { min-height:36px; }
  input, select, textarea { min-height:44px; font-size:16px; }
  table.lines input[type=text], table.lines input[type=number] { font-size:16px; }
  .navitem { padding:12px 14px; }
  .tab { padding:10px 15px; }
  .checklist__item input[type=checkbox] { width:20px; height:20px; }
  /* Same reasoning as the checkbox above: a 16px target is not a finger. */
  input[type=radio], input[type=checkbox] { width:20px; height:20px; }
  .rowline__act { padding-top:0; }
}

/* Off by default; the tablet block below turns it on. Stated here rather than
   after that block, where it would simply win and the bar would never show. */
.tabbar { display:none; }

/* TABLET — THE STRIP ACROSS THE TOP IS GONE (Greg, 2026-08-27, for the iPad
   mini). The rail used to lie down horizontally here, which read fine on paper
   and badly on the device: thirteen destinations in a 744pt strip is a
   sideways scroller, so the brand scrolled off the left, most of the app was
   off the right, and the one thing a navigation bar has to do — show you where
   you can go — it could not do. A mini is held in two hands at a bay door, and
   the thumb bar the phone already had is the right answer.

   Narrow-layout rules only. The NAVIGATION query below is a different
   question and deliberately a different block. */
@media (max-width:860px) {
  .view { padding:16px; }
  .row2, .row3 { grid-template-columns:1fr; }
  .summary__due { border-left:0; padding-left:0; text-align:left; }
}

/* WHERE THE THUMB BAR LIVES, and why it is not just a width.

   Greg, 2026-08-27: "when i put in horizontal mode it changes back to what the
   web looks like — it needs to keep the menu bar at the bottom." An iPad mini
   in landscape is 1133pt, which is wider than a lot of laptops, so a width
   alone cannot tell the two apart: raise the number and every small browser
   window loses its side rail; leave it and the mini loses its bar the moment
   somebody turns the slab.

   What actually separates them is the hand. A tablet has a coarse pointer and
   cannot hover; a laptop has a fine one and can.

   ⚠ THAT IS NO LONGER THE RULE. Greg, 2026-09-08: an iPad gets the portal
   exactly as a computer does — rail on the left — so it does not get this bar
   in either orientation.

   The condition is NARROW OR SHORT, not narrow alone, and the reason is
   counter-intuitive: an iPad in portrait (744pt) is narrower than an iPhone in
   landscape (852pt). Width alone would give the phone-on-its-side a rail it
   has no height for. See the long note above isHandheld() in
   views/mobile-home.js.

   ⚠ KEEP IN STEP WITH isHandheld(). That function picks the home screen and
   this block draws the bar; if the two disagree the screen is half phone and
   half desk. */
@media (max-width:700px), (max-height:500px) {
  .rail { display:none; }
  .main { margin-left:0; }

  .tabbar {
    display:flex; position:fixed; left:0; right:0; bottom:0; z-index:60;
    background:var(--navy); border-top:1px solid rgba(255,255,255,.08);
    /* The inset is the iOS home indicator and Android's gesture pill. Without
       it the last row of buttons sits under the bar the OS draws and the
       bottom third of every tap lands on the system instead of on us. */
    padding-bottom:env(safe-area-inset-bottom, 0px);
  }
  .tabitem {
    flex:1 1 0; min-width:0; display:flex; flex-direction:column;
    align-items:center; justify-content:center; gap:3px;
    padding:8px 2px 7px; min-height:54px;
    background:none; border:0; border-radius:0;
    color:rgba(255,255,255,.62); text-decoration:none;
    font:500 10px/1.1 inherit; letter-spacing:.01em;
    -webkit-tap-highlight-color:transparent;
  }
  .tabitem__icon { font-size:17px; line-height:1; }
  .tabitem__label { max-width:100%; overflow:hidden; text-overflow:ellipsis;
                    white-space:nowrap; }
  .tabitem.is-on { color:#fff; }
  .tabitem.is-on .tabitem__icon { transform:translateY(-1px); }

  /* THE + IN THE MIDDLE. Raised out of the bar rather than sitting in it,
     because it is not a place you go — it is the thing you do, and a shop
     raises far more work than it navigates. Deliberately not marked
     is-on-able: it opens a sheet and the bar stays where it was. */
  .tabitem--new { flex:0 0 auto; justify-content:flex-start; padding-top:0; }
  .tabnew {
    width:52px; height:52px; margin-top:-14px; border-radius:50%;
    display:flex; align-items:center; justify-content:center;
    background:var(--accent); color:#fff; font-size:26px; line-height:1;
    border:3px solid var(--navy);
    box-shadow:0 4px 14px rgba(0,0,0,.28);
  }
  .tabitem--new:active .tabnew { transform:scale(.94); }
  .tabitem--new .tabitem__label { margin-top:2px; }

  /* The bar floats over the page, so the page has to end above it — otherwise
     the last row of any list is unreachable, which is the classic way a
     bottom bar eats a Save button. */
  .main { padding-bottom:calc(56px + env(safe-area-inset-bottom, 0px)); }

  /* The org name is the one bit of the top bar worth its height here.

     AND IT NOW CARRIES THE STATUS BAR (Greg, 2026-08-27: "the ipad mini app is
     sitting too high to where it says apex auto is sitting behind the ipad
     clock in the top left").

     The top inset used to live on .rail, which was the first thing on the
     page. Hiding the rail to give the mini a thumb bar took the inset with it
     and left the org name under the clock — a fault introduced by that change
     and only visible inside the app, where there is a status bar at all.

     The padding goes on the bar itself rather than on .main because it is the
     bar that has to clear the clock, and it still does now that the bar scrolls
     (2026-09-05): the page opens at the top, so the name is under the padding
     the moment anybody can read it, and after that both leave together. */
  /* ⚠ --statuspad IS MEASURED, NOT DECLARED. See measureStatusPad() in app.js:
     inside the shell env() reports 0 on some devices while the web view still
     fills the screen, so the shell is asked instead and the answer lands in
     this variable. env() is the fallback for a plain browser, where it is
     correct, and 0 is the fallback for both — a browser has no status bar. */
  .topbar { padding:calc(10px + var(--statuspad, env(safe-area-inset-top, 0px))) 14px 10px; }
  .orgswitch { max-width:150px; }
}

/* A phone on its side is wide: five items across 852pt would each be 170px and
   read as a row of buttons rather than a bar. Held to a comfortable width and
   centred, so the thumbs still reach and the bar still looks like one.

   This is now the LANDSCAPE PHONE case specifically — wide but short. Tablets
   no longer reach here at all; they have the rail. */
@media (min-width:701px) and (max-height:500px) {
  .tabbar { justify-content:center; }
  .tabitem { flex:0 1 132px; min-height:60px; font-size:11px; }
  .tabitem__icon { font-size:19px; }
}

/* ============================================================================
   PHONE
   ============================================================================
   Below 700px the laid-down rail stopped working: eight items and two open
   groups made a 230px-tall sideways scroller, a third of the screen given to
   navigation you had to swipe to read. The rail goes away and a thumb bar
   takes over. */

@media (max-width: 700px) {
  /* The rail, the thumb bar and the page's bottom padding are handled in the
     tablet block above, which now covers everything below 860. What is left
     here is what is true of a PHONE and not of a mini. */
  .main { min-height:auto; }
  .view { padding:14px; }

  /* A modal is a full sheet at this size; a centered dialog with margins
     wastes the only screen there is. */
  .modal { padding:0; align-items:flex-end; }
  .modal__panel, .modal__panel--wide {
    width:100%; max-width:none; max-height:92vh; max-height:92dvh;
    border-radius:16px 16px 0 0;
  }
  .modal__foot { padding-bottom:calc(14px + env(safe-area-inset-bottom, 0px)); }

  .page__head { gap:10px; margin-bottom:14px; }
  .page__head h1 { font-size:22px; }
  .page__actions { width:100%; }
  .page__actions button { flex:1 1 auto; }

  /* Tabs and filter rows scroll sideways rather than wrapping into three
     stacked rows of pills, which is what the calendar's view switcher did.
     Bled to the screen edges so the strip reads as continuing off-screen
     rather than as a row that happens to be clipped. */
  .tabs, .reporttabs {
    flex-wrap:nowrap; overflow-x:auto; scrollbar-width:none;
    -webkit-overflow-scrolling:touch; margin-left:-14px; margin-right:-14px;
    padding-left:14px; padding-right:14px;
  }
  .tabs::-webkit-scrollbar, .reporttabs::-webkit-scrollbar { display:none; }
  .tab { flex:0 0 auto; }
  /* Except inside a toolbar, where the strip is one control among several and
     must stay within the row rather than bleeding past its neighbors. */
  .calbar > .tabs, .txbar > .tabs { margin-left:0; margin-right:0;
                                    padding-left:0; padding-right:0; }

  /* The calendar's toolbar on a phone: the month and the way through it get a
     row of their own, above the view switcher. */
  .calbar { gap:8px; }
  .calbar__title { width:100%; order:-1; font-size:18px; }
  .calwrap { grid-template-columns:1fr; }

  /* A month grid is 7 columns of 48px here, which renders a job as "S…". The
     view defaults to Day on a phone (calendar.js) — this only keeps the grid
     legible for anyone who deliberately chooses Month anyway. */
  .calcell { min-height:66px; padding:3px; }
  .calcell__num { font-size:11px; width:20px; height:20px; }
  .calmonth__head span { padding:6px 2px; font-size:8.5px; letter-spacing:.04em; }

  /* ---- A list becomes a stack of cards ------------------------------------
     Only tables from ui.table(), which stamped every cell with its column
     name. The header row goes; each cell says what it is beside what it says.
     The first column is the row's title — it is the invoice number or the
     customer's name at every call site — so it reads as a heading rather than
     as another label/value pair. */
  .tablewrap--auto {
    border:0; background:none; box-shadow:none; overflow:visible; border-radius:0;
  }
  table.table--auto, table.table--auto tbody { display:block; }
  table.table--auto thead { display:none; }
  table.table--auto tr {
    display:block; background:var(--solid-bg); border:1px solid var(--hair);
    border-radius:var(--r-sm); padding:11px 13px; margin-bottom:9px;
    box-shadow:var(--shadow);
  }
  table.table--auto tbody tr:nth-child(even) { background:var(--solid-bg); }
  table.table--auto td {
    display:flex; align-items:baseline; justify-content:space-between; gap:14px;
    border:0; padding:3px 0; text-align:right;
  }
  table.table--auto td::before {
    content:attr(data-label); flex:none; text-align:left;
    font-size:9.5px; letter-spacing:.08em; text-transform:uppercase;
    color:var(--mute); font-weight:700;
  }
  table.table--auto td:not([data-label])::before { content:none; }
  /* The title line: no label, bigger, and it owns the top of the card. */
  table.table--auto td:first-child {
    display:block; text-align:left; font-size:15px; font-weight:600;
    padding:0 0 6px; margin-bottom:5px; border-bottom:1px solid var(--hair);
  }
  table.table--auto td:first-child::before { content:none; }
  /* A cell holding only a control or a badge should not be pushed to a corner
     by a label it does not need. */
  table.table--auto td:empty { display:none; }
}

/* A stacked card is a screen layout, never a printed one: paper is not 390px
   wide and a printed invoice must stay a table. */
@media print {
  .tablewrap--auto { border:1px solid var(--hair); background:#fff; }
  table.table--auto, table.table--auto tbody { display:table; }
  table.table--auto tbody { display:table-row-group; }
  table.table--auto thead { display:table-header-group; }
  table.table--auto tr { display:table-row; padding:0; margin:0; border:0; box-shadow:none; }
  table.table--auto td { display:table-cell; padding:9px 12px; }
  table.table--auto td::before { content:none; }
}

/* ---- The sheet a group opens ---------------------------------------------
   From the bottom, where the thumb already is, rather than from the middle. */
.sheet {
  position:fixed; inset:0; z-index:70; display:flex; align-items:flex-end;
  background:rgba(11,26,48,.45); opacity:0; transition:opacity .18s ease;
}
.sheet.is-open { opacity:1; }
.sheet__panel {
  width:100%; background:var(--solid-bg); border-radius:16px 16px 0 0;
  padding:8px 14px calc(16px + env(safe-area-inset-bottom, 0px));
  transform:translateY(14px); transition:transform .18s ease;
  max-height:82vh; max-height:82dvh; overflow-y:auto;
  /* The sheet can be pulled down to close it (armSheetDrag in app.js). Without
     this, reaching the end of the list hands the scroll to the page behind and
     the whole app slides around under the sheet. */
  overscroll-behavior:contain;
}
.sheet.is-open .sheet__panel { transform:translateY(0); }
/* ⚠ THIS IS A PROMISE, AND IT IS NOW KEPT. It sat here doing nothing until
   2026-09-05: it is the one mark that tells somebody a sheet can be pulled
   down, and it taught the gesture without answering it. No hit area of its own
   is needed: the drag is armed on the WHOLE panel, so the handle only has to
   say so. */
.sheet__grab { width:40px; height:5px; border-radius:99px; background:var(--steel);
               opacity:.35; margin:0 auto 10px; }
.sheet__title { font-size:12px; letter-spacing:.09em; text-transform:uppercase;
                color:var(--mute); margin:0 0 6px; font-weight:700; }
.sheet__kids { display:flex; flex-direction:column; }
.sheetlink {
  display:flex; align-items:center; gap:12px; padding:14px 6px;
  color:var(--ink); text-decoration:none; font-size:15px;
  border-bottom:1px solid var(--hair);
}
.sheet__kids .sheetlink:last-child { border-bottom:0; }
.sheetlink__icon { width:20px; text-align:center; color:var(--steel); }
.sheetlink.is-on { color:var(--accent); font-weight:600; }
.sheet__who { padding:2px 6px 10px; border-bottom:1px solid var(--hair); margin-bottom:4px; }
.sheet__who b { display:block; font-size:15px; }
.sheet__who span { font-size:12px; color:var(--mute); text-transform:capitalize; }

/* ---- Merchant feature menu ------------------------------------------------
   The merchant no longer needs a scrolling rail to find a feature. The Menu
   button opens one flat, permission-filtered grid from the live navigation
   list. Platform navigation keeps its separate rail. */
.has-menuboard .main { margin-left:0; }
.menurail {
  position:fixed; inset:0 auto 0 0; z-index:58; width:86px;
  display:flex; flex-direction:column; align-items:center; gap:14px; overflow:hidden;
  padding:16px 10px; background:var(--solid-bg); border-right:1px solid var(--hair);
}
.has-menuboard .main { margin-left:86px; }
.topmenu {
  display:inline-flex; align-items:center; justify-content:center; gap:7px;
  min-height:38px; padding:8px 11px; border:1px solid var(--hair-strong);
  border-radius:9px; background:var(--white); color:var(--ink); font:600 13px/1 inherit;
  cursor:pointer; white-space:nowrap;
}
.topmenu:hover { border-color:var(--accent); background:var(--tint); }
.topmenu__icon { font-size:16px; line-height:1; color:var(--accent-dark); }
.menurail .topmenu { width:66px; flex-direction:column; gap:3px; font-size:10px; }
.menurail__favorites {
  display:flex; flex:1 1 auto; width:100%; min-height:0; flex-direction:column;
  align-items:center; gap:6px; overflow-y:auto; padding:0 0 8px;
}
.menufavorite {
  position:relative; display:flex; flex:0 0 auto; width:66px; min-height:44px;
  flex-direction:column; align-items:center; justify-content:center; gap:2px;
  padding:5px 3px; border:1px solid transparent; border-radius:9px;
  color:var(--mute); text-align:center; text-decoration:none;
}
.menufavorite:hover, .menufavorite:focus-visible, .menufavorite.is-on {
  border-color:var(--hair-strong); background:var(--tint); color:var(--accent-dark);
}
.menufavorite__icon { font-size:16px; line-height:1; }
.menufavorite__label { max-width:58px; overflow:hidden; font-size:10px; line-height:12px; text-overflow:ellipsis; white-space:nowrap; }
.pricingsetup { margin-top:14px; }
.pricingsetup__rows { display:grid; gap:10px; margin-top:14px; }
.pricingsetup__row { display:flex; align-items:center; justify-content:space-between; gap:14px; padding:12px; border:1px solid var(--hair); border-radius:var(--r-sm); }
.pricingsetup__row b { display:block; }
.matrixbands { display:grid; gap:10px; margin-top:14px; }
.matrixbands__head, .matrixbands__row { display:grid; grid-template-columns:34px 1fr 18px 1fr 120px 1fr; align-items:center; gap:8px; }
.matrixbands__head { color:var(--muted); font-size:12px; }
.matrixbands__n { font-weight:700; text-align:center; }
@media (max-width:600px) { .matrixbands__head { display:none; } .matrixbands__row { grid-template-columns:28px 1fr 16px 1fr; } .matrixbands__row select, .matrixbands__row input:last-child { grid-column:2 / span 3; } }
.topmenu--mobile { display:none; }
.topbar__brand { display:block; flex:0 0 auto; line-height:0; }
.topbar__brand img { display:block; height:30px; width:auto; }
.has-menuboard .orgswitch { margin-left:0; }
.has-menuboard .topbar__org { margin-left:auto; }

.menuboard { min-width:min(640px, calc(100vw - 64px)); }
.menuboard__intro { display:flex; align-items:baseline; justify-content:space-between; gap:14px; }
.menuboard__intro p { margin:0 0 12px; }
.menuboard__search { display:block; width:100%; min-height:42px; margin-bottom:14px; }
.menuboard__grid {
  display:grid; grid-template-columns:repeat(4, minmax(0, 1fr)); gap:14px;
  max-height:min(56vh, 520px); overflow:auto; padding:1px;
}
.menugrid__cell { position:relative; min-width:0; }
.menugrid__tile {
  display:flex; min-height:104px; flex-direction:column; justify-content:flex-end; gap:9px;
  padding:14px; border:1px solid var(--hair); border-radius:var(--r-sm);
  background:var(--glass-bg); color:var(--ink); text-decoration:none;
  transition:transform .14s ease, border-color .14s ease, box-shadow .14s ease;
}
.menugrid__tile:hover, .menugrid__tile:focus-visible {
  border-color:var(--accent); box-shadow:var(--shadow-sm); transform:translateY(-1px);
}
.menugrid__icon { color:var(--accent-dark); font-size:21px; line-height:1; }
.menugrid__label { font-weight:650; font-size:13px; line-height:1.2; }
.menugrid__favorite {
  position:absolute; top:7px; right:7px; z-index:1; width:28px; height:28px; padding:0;
  border:0; border-radius:50%; background:transparent; color:var(--steel); cursor:pointer; font-size:18px;
}
.menugrid__favorite:hover, .menugrid__favorite.is-on { color:var(--amber); background:var(--amber-bg); }
.menuboard__empty { margin:20px 0 4px; color:var(--mute); text-align:center; }
.menubadge {
  display:inline-flex; align-items:center; justify-content:center; min-width:17px; height:17px;
  padding:0 4px; border-radius:99px; background:var(--danger); color:#fff;
  font-size:9px; font-weight:700; line-height:1;
}
.menugrid__tile .menubadge { position:absolute; left:10px; top:10px; }
.topmenu .menubadge { margin-left:1px; }

@media (max-width:700px) {
  .has-menuboard .main { margin-left:0; }
  .menurail { display:none; }
  .topmenu--mobile { display:inline-flex; }
  .topbar__brand img { height:25px; }
  .menuboard { min-width:0; }
  .menuboard__intro { display:block; }
  .menuboard__intro .muted { margin-bottom:10px; }
  .menuboard__grid { grid-template-columns:repeat(2, minmax(0, 1fr)); gap:14px; max-height:54vh; }
  .menugrid__tile { min-height:96px; }
  .tabitem .topmenu__icon { font-size:17px; line-height:1; }
}

@media (prefers-reduced-motion: reduce) {
  .menugrid__tile { transition:none; }
}

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

/* ============================================================================
   Customer-facing surfaces — the portal and the pay link.

   Branded as the BUSINESS, not as OnSight: the customer has a relationship
   with their landscaper, not with us. Centered, roomy, and readable on a phone,
   because that is where a pay link gets opened.
   ========================================================================= */

body.portal { background:var(--band); }

.ptop { background:#fff; border-bottom:1px solid var(--hair); padding:16px 22px;
        display:flex; align-items:center; gap:14px; flex-wrap:wrap; }
.ptabs { overflow-x:auto; -webkit-overflow-scrolling:touch; flex-wrap:nowrap; }
.ptabs .tab { flex:none; }

/* iOS-flavored portal chrome: pill tabs, grouped inset cards, hairline
   separators, a chevron that promises a detail screen. Scoped to the portal —
   the merchant console keeps its own look. */
body.portal { background:#F2F3F6; }
/* 8px between the pills and 16px under them: a pill needs air from its
   neighbours and from the first card (Greg, 2026-09-05, #2). */
body.portal .ptabs { gap:8px; padding:12px 22px 16px; border-bottom:0; }
body.portal .ptabs .tab { border:0; background:#E7E9EC; color:#4A5560; font-size:13.5px;
                          font-weight:600; padding:9px 18px; border-radius:99px; }
body.portal .ptabs .tab.is-on { background:var(--ink); color:#fff; }
body.portal .badge { border-radius:99px; padding:3px 10px; }
body.portal .pcard, body.portal .phero { border-radius:18px; border-color:rgba(16,24,32,.06); }

.psection { margin-bottom:24px; }
.psection__title { font-size:11.5px; letter-spacing:.07em; text-transform:uppercase;
                   color:var(--mute); font-weight:700; margin:0 6px 8px; }
.pcardlist { background:#fff; border:1px solid rgba(16,24,32,.06); border-radius:16px;
             box-shadow:0 1px 3px rgba(16,24,32,.05); overflow:hidden; }
.pcardlist + .pcardlist { margin-top:10px; }
.irows { list-style:none; margin:0; padding:0; }
.irow { display:flex; align-items:center; gap:14px; padding:14px 16px 14px 18px;
        cursor:pointer; }
.irow + .irow { border-top:1px solid #F1F2F4; }
.irow:hover { background:#F8F9FA; }
.irow--static { cursor:default; }
.irow--static:hover { background:transparent; }
.irow__main { flex:1; min-width:0; }
.irow__main b { font-size:15px; display:block; }
.irow__sub { display:block; font-size:12.5px; color:var(--mute); margin-top:1px;
             overflow:hidden; text-overflow:ellipsis; white-space:nowrap; }
/* 3px put the status pill against the underside of the figure. A pill needs
   air on every side, including from the number it belongs to. */
.irow__end { display:flex; flex-direction:column; align-items:flex-end; gap:6px; flex:none; }
.irow__amt { font-size:15px; font-variant-numeric:tabular-nums; }
.irow__chev { color:#C6CAD0; font-size:22px; line-height:1; font-weight:400; flex:none; }
.pempty { padding:20px; color:var(--mute); font-size:13.5px; }
.pcols3 { display:grid; grid-template-columns:repeat(3, minmax(0, 1fr)); gap:14px;
          margin-bottom:24px; align-items:start; }
@media (max-width:920px) { .pcols3 { grid-template-columns:1fr; } }
.pcols3 .psection { margin-bottom:0; }
.pcolscroll { max-height:540px; overflow-y:auto; }
.irow--divider { list-style:none; padding:10px 18px 6px; font-size:10.5px; letter-spacing:.07em;
                 text-transform:uppercase; color:var(--mute); font-weight:700;
                 border-top:1px solid #F1F2F4; background:#FAFBFC; }
body.portal .pview { max-width:1120px; }
.pmore { margin-top:8px; }
.pmore summary { list-style:none; cursor:pointer; display:inline-block; background:#E7E9EC;
                 color:#4A5560; font-size:12.5px; font-weight:600; padding:8px 16px;
                 border-radius:99px; user-select:none; }
.pmore summary::-webkit-details-marker { display:none; }
.pmore summary:hover { background:#DFE2E6; }
.pmore[open] summary { margin-bottom:8px; }
.pwallet { padding:18px 20px; }
.ptop__brand b { display:block; font-size:17px; }
.ptop__logo { display:block; max-height:34px; max-width:180px; margin-bottom:4px; }
.pay__logo { display:block; max-height:44px; max-width:200px; margin-bottom:8px; }
.ptop__brand span { font-size:11px; letter-spacing:.08em; text-transform:uppercase; }
.ptop__who { margin-left:auto; display:flex; align-items:center; gap:12px; font-size:13px; }

.ptabs { background:#fff; border-bottom:1px solid var(--hair); padding:0 22px 12px;
         display:flex; gap:6px; }
.pview { max-width:880px; margin:0 auto; padding:22px; }

.psummary { background:#fff; border:1px solid var(--hair); border-radius:10px;
            padding:20px; margin-bottom:18px; display:flex; gap:20px; flex-wrap:wrap; }
.psummary__main { flex:1; min-width:220px; }
.psummary__main > span { display:block; font-size:10px; letter-spacing:.09em;
                         text-transform:uppercase; color:var(--mute); font-weight:700; }
.psummary__main > b { display:block; font-size:30px; font-variant-numeric:tabular-nums;
                      line-height:1.2; margin-top:2px; }
.psummary__note { flex-basis:100%; font-size:12px; color:var(--steel);
                  border-top:1px solid var(--hair); padding-top:12px; }
.psummary__pay { min-width:270px; }

.payopt { display:flex; justify-content:space-between; gap:14px; padding:7px 0;
          font-size:13px; border-bottom:1px solid var(--hair); }
.payopt:last-of-type { border-bottom:0; }
.payopt b { font-variant-numeric:tabular-nums; }
.payopt.is-strong { font-weight:700; }
.psummary__pay button { margin-top:12px; }

/* Portal v2 — the dashboard home and its cards. Roomy, readable on a phone,
   accented in the business's own color via the vars applyAccent() sets. */
.phero { background:#fff; border:1px solid var(--hair); border-radius:14px;
         padding:26px 28px; margin-bottom:14px; display:flex; align-items:center;
         gap:20px; flex-wrap:wrap; box-shadow:0 1px 3px rgba(16,24,32,.05); }
.phero__main { flex:1; min-width:200px; }
.phero__label { display:block; font-size:10.5px; letter-spacing:.09em; text-transform:uppercase;
                color:var(--mute); font-weight:700; }
.phero__value { display:block; font-size:38px; line-height:1.15; margin-top:4px;
                font-variant-numeric:tabular-nums; letter-spacing:-.01em; }
.phero__sub { display:block; font-size:13px; color:var(--steel); margin-top:4px; }
.phero--good .phero__value { color:var(--green); font-size:30px; }
.phero__cta { font-size:15px; padding:13px 26px; border-radius:var(--r-sm); }
.pnudge { cursor:pointer; border-left:3px solid var(--accent); }
.pnudge:hover { background:var(--paper); }
.pnudge--quiet { cursor:default; border-left:3px solid var(--accent); margin-top:14px; }
.pnudge--quiet:hover { background:#fff; }
.pgrid { display:grid; grid-template-columns:repeat(auto-fit, minmax(240px, 1fr));
         gap:14px; }
.pgrid--two { grid-template-columns:repeat(auto-fit, minmax(300px, 1fr)); }
.pcard { background:#fff; border:1px solid var(--hair); border-radius:12px;
         padding:18px 20px; box-shadow:0 1px 3px rgba(16,24,32,.04); }
.pnext b { display:block; font-size:17px; }
.pnext span { font-size:13px; color:var(--steel); }
.pmini { list-style:none; margin:0; padding:0; }
.pmini li { display:flex; justify-content:space-between; gap:12px; padding:6px 0;
            font-size:13px; border-top:1px solid var(--paper); }
.pmini li:first-child { border-top:0; }
.pmini b { font-variant-numeric:tabular-nums; }
.plink { display:inline-block; margin-top:10px; font-size:12.5px; color:var(--accent);
         text-decoration:none; font-weight:600; }
.plink:hover { text-decoration:underline; }
.pwallet { display:flex; gap:16px; align-items:flex-start; flex-wrap:wrap; }
.pwallet__icon { font-size:26px; line-height:1; padding-top:2px; }
.pwallet__main { flex:1; min-width:220px; }
.pwallet__main p { margin:4px 0 0; font-size:12.5px; }
.pwallet__actions { display:flex; gap:8px; flex-wrap:wrap; }

.pnext__who { margin-top:8px; }
.est_by { display:flex; align-items:center; justify-content:space-between; gap:12px; }

/* ---- A face on a person ----------------------------------------------------

   One avatar treatment, used on the staff list, the assignment pickers and the
   estimate. Initials when there is no photograph — a grey silhouette reads as
   a broken image, and initials read as a person nobody has photographed yet. */

.stafffoto__cell { flex:0 0 auto; display:flex; flex-direction:column;
                   align-items:center; gap:5px; margin-right:12px; }
.stafffoto, .stafffoto__img, .stafffoto__ini {
  width:44px; height:44px; border-radius:50%; display:flex;
  align-items:center; justify-content:center; overflow:hidden; }
.stafffoto__img { object-fit:cover; display:block; }
.stafffoto__ini { background:var(--band); color:var(--accent-dark);
                  font:700 14px/1 inherit; letter-spacing:.02em; }
/* The whole avatar is the picker, with a small pen over the corner. */
button.stafffoto__btn { position:relative; padding:0; border:0; background:none;
                        box-shadow:none; min-height:0; border-radius:50%;
                        cursor:pointer; line-height:0; }
.stafffoto__pen { position:absolute; right:-2px; bottom:-2px; width:18px; height:18px;
                  border-radius:50%; background:var(--accent); color:#fff;
                  font-size:10px; line-height:18px; text-align:center;
                  border:2px solid var(--white); }
.stafffoto__share { font-size:11px; gap:5px; }
.stafffoto__share span { color:var(--steel); }
.stafffoto__rm { font-size:11px; }

/* The same face, smaller, in an assignment list or beside a name. */
.facechip { display:inline-flex; align-items:center; gap:7px; min-width:0; }
.facechip__img, .facechip__ini {
  flex:0 0 auto; width:26px; height:26px; border-radius:50%;
  display:flex; align-items:center; justify-content:center; overflow:hidden; }
.facechip__img { object-fit:cover; display:block; }
.facechip__ini { background:var(--band); color:var(--accent-dark);
                 font:700 10px/1 inherit; }

/* Who did the work, on the customer's inspection sheet. */
.insp_by { display:flex; align-items:center; gap:12px; margin-top:14px;
           padding-top:14px; border-top:1px solid var(--hair); }
.insp_by__face { flex:0 0 auto; width:44px; height:44px; border-radius:50%;
                 object-fit:cover; background:var(--band); display:flex;
                 align-items:center; justify-content:center;
                 font:700 14px/1 inherit; color:var(--accent-dark);
                 letter-spacing:.02em; }
.insp_by__who { min-width:0; }
.insp_by__who b { display:block; font-size:15px; color:var(--ink); }
.insp_by__who span { font-size:12.5px; color:var(--steel); }

/* ---- Recording a clip on the car ------------------------------------------

   The stage is a fixed 16:9 box so the modal does not jump when the camera
   reports its real dimensions a beat after the stream opens — a dialog that
   resizes under a technician's thumb is a dialog they mis-tap. */

.vidrec { display:flex; flex-direction:column; gap:10px; }
.vidrec__stage { position:relative; width:100%; aspect-ratio:16/9;
                 background:#000; border-radius:var(--r); overflow:hidden; }
.vidrec__stage video { width:100%; height:100%; object-fit:cover; display:block; }
.vidrec__time { position:absolute; top:10px; right:12px; z-index:2;
                font:600 13px/1 ui-monospace,SFMono-Regular,Menlo,monospace;
                color:#fff; background:rgba(0,0,0,.55);
                padding:5px 9px; border-radius:999px;
                font-variant-numeric:tabular-nums; }
/* The one unmistakable signal that it is running. */
.vidrec.is-live .vidrec__time { background:var(--red); }
.vidrec.is-live .vidrec__time::before { content:'● '; }
.vidrec__hint { font-size:12.5px; color:var(--steel); }
.vidrec__acts { display:flex; gap:8px; flex-wrap:wrap; justify-content:flex-end;
                width:100%; }

/* A clip in the technician's item list, beside the photographs. */
.inspthumb__wrap { position:relative; display:inline-block; }
.inspthumb--video { background:#000; }
.inspthumb__len { position:absolute; bottom:6px; right:6px;
                  font:600 10px/1 ui-monospace,SFMono-Regular,Menlo,monospace;
                  color:#fff; background:rgba(0,0,0,.6);
                  padding:3px 6px; border-radius:999px; pointer-events:none; }

/* And on the customer's sheet: a clip that has outlived its bytes. */
.insp_gone { width:calc(50% - 4px); max-width:280px; border-radius:8px;
             background:var(--paper); border:1px dashed var(--hair-strong);
             padding:14px; display:flex; flex-direction:column; gap:4px; }
.insp_gone b { font-size:13px; color:var(--ink2); }
.insp_gone span { font-size:12px; color:var(--steel); line-height:1.45; }
.insp_video { background:#000; }

/* ---- Fraud controls --------------------------------------------------------

   One card per rule, and the card is visibly dead until the rule is switched
   on: a limit nobody enabled must not read like a limit that is in force. That
   is `.is-off` — dimmed and, in the markup, genuinely disabled, because a
   greyed field somebody can still type into is a lie. */

.fraudpane { display:flex; flex-direction:column; gap:14px; }
.fraudrule__head { display:flex; align-items:center; justify-content:space-between;
                   gap:12px; margin-bottom:2px; }
.fraudrule__sw { align-items:center; }
.fraudrule__sw b { font-size:15px; }
.fraudrule__blurb { margin:0 0 12px; max-width:70ch; font-size:13px; }
.fraudrule__body { display:flex; flex-direction:column; gap:12px; }
.fraudrule__body textarea { width:100%; font-family:ui-monospace,SFMono-Regular,Menlo,monospace;
                            font-size:12.5px; }
.fraudrule__by { display:flex; flex-direction:column; gap:8px; margin-top:2px; }
.fraudrule__by .check { align-items:flex-start; }
.fraudrule__chanwrap { margin-top:14px; padding-top:12px; border-top:1px solid var(--hair); }
.fraudrule__chanlbl { display:block; font-size:11px; letter-spacing:.09em;
                      text-transform:uppercase; color:var(--mute); font-weight:700;
                      margin-bottom:7px; }
.fraudrule__chans { display:flex; flex-wrap:wrap; gap:8px 18px; }
.fraudrule__foot { display:flex; justify-content:flex-end; margin-top:14px; }
.is-off { opacity:.45; }
.fraudrule__not { margin:8px 0 0; padding-left:18px; font-size:13px;
                  color:var(--steel); line-height:1.55; }
.fraudrule__not li { margin-bottom:6px; }
.fraudrule__not b { color:var(--ink2); }

/* ---- Telling Properties from Contacts --------------------------------------

   Greg, 2026-09-02: "help the user see the difference between contacts and
   properties". The two sat side by side on the customer record wearing the
   same small-caps grey heading, the same Add link, and rows of the same shape
   — a bold line over a grey line. Nothing distinguished a place you drive to
   from a person you ring except the two words at the top.

   Three cues, cheapest first: a line saying what the section is for, a marker
   on every row so the difference reads before the words do, and a rule down
   the middle so they stop looking like one continuous band. */

.sechint { display:block; font-size:12.5px; line-height:1.45; color:var(--steel);
           margin:-2px 0 10px; max-width:44ch; }
.sechint__ico { display:inline-block; margin-right:7px; color:var(--accent-lt);
                font-size:11px; vertical-align:1px; }

/* A person. Initials in a circle read as somebody before a word is read. */
.pavatar { flex:0 0 auto; width:30px; height:30px; border-radius:50%;
           display:flex; align-items:center; justify-content:center;
           background:var(--band); color:var(--accent-dark);
           font-size:11.5px; font-weight:700; letter-spacing:.02em;
           font-variant-numeric:tabular-nums; }

/* A place. Deliberately square against the contact's circle — the shapes
   differ even in peripheral vision, which is the whole point. */
.ppin { flex:0 0 auto; width:26px; height:26px; border-radius:8px;
        display:flex; align-items:center; justify-content:center;
        background:var(--paper); border:1px solid var(--hair);
        color:var(--accent); font-size:11px; }

/* ⚠ THE ACTIONS WRAP, THE ADDRESS DOES NOT. Passport and Directions sit on
   this row as siblings, so adding a 26px marker took the width out of the
   address instead — "3711 San Felipe / Street" over two lines, which is the
   opposite of easier to read. Wrapping lets the buttons drop to their own line
   and hands the full width back to the thing somebody is scanning for. */
.proprow { display:flex; align-items:flex-start; gap:10px; flex-wrap:wrap; }
.proprow > .proprow__main { flex:1 1 220px; min-width:0; }
.proprow__acts { display:flex; align-items:center; gap:8px; flex-wrap:wrap;
                 margin-left:auto; }

/* ⚠ A VERTICAL RULE, AND ONLY WHILE THEY ARE SIDE BY SIDE. Below the stacking
   breakpoint the second card sits UNDER the first, where a left border is a
   stripe down the side of nothing — so it becomes a top rule instead. */
@media (min-width:761px) {
  .recbody .cols--2 > .card + .card { border-left:1px solid var(--hair);
                                      padding-left:22px; }
}
@media (max-width:760px) {
  .recbody .cols--2 > .card + .card { border-top:1px solid var(--hair);
                                      padding-top:16px; margin-top:4px; }
}

/* A file a client sent, on the firm's chase list. Small and quiet: the row is
   still about the ask, and the file is evidence hanging off it. */
.recfiles { display:flex; flex-wrap:wrap; gap:6px; margin-top:6px; }
button.recfiles__f { display:inline-flex; align-items:center; gap:6px; cursor:pointer;
               border:1px solid var(--hair); background:var(--paper);
               border-radius:999px; padding:3px 10px 3px 4px; min-height:0;
               box-shadow:none; font:inherit; font-size:12px; font-weight:400;
               color:var(--ink2); max-width:260px; }
button.recfiles__f:hover { border-color:var(--accent-lt); background:var(--paper); }
.recfiles__f > span:last-child { overflow:hidden; text-overflow:ellipsis; white-space:nowrap; }
.recfiles__k { flex:none; font-size:9.5px; letter-spacing:.06em; font-weight:600;
               text-transform:uppercase; color:var(--green);
               background:var(--green-bg); border-radius:999px; padding:3px 7px; }

/* ---- The client's documents panel ------------------------------------------

   Lifted from the drawing on onsightpayments.com/business-services, built for
   real: one panel, a navy bar, and three sections in the order a client asks
   about them — what they owe, what they sent, what they were given.

   ⚠ THE BAR IS --navy AND THE BUTTON IS --accent, and they must not be
   swapped. `--accent` is overwritten at runtime with the BUSINESS's own colour
   (see applyAccent), so the thing you press wears the firm's brand and the
   chrome stays OnSight navy. A navy button would be ours; an accent bar would
   put a firm's brand colour behind small white text at whatever contrast they
   happened to choose. */

.cportal { background:var(--white); border:1px solid var(--hair);
           border-radius:var(--r-lg); overflow:hidden; box-shadow:var(--shadow);
           max-width:100%; }
.cportal__rows { min-width:0; }
.cportal__biz, .cportal__who { min-width:0; overflow-wrap:anywhere; }
.cportal__bar { display:flex; align-items:baseline; justify-content:space-between;
                gap:14px; padding:15px 20px; background:var(--navy); color:#fff; }
.cportal__biz { font-weight:700; letter-spacing:-.016em; }
.cportal__who { font-size:12.5px; color:rgba(255,255,255,.72); }

.cportal__sec { padding:16px 20px 8px; border-bottom:1px solid var(--hair); }
.cportal__sec:last-child { border-bottom:0; }
.cportal__k { display:block; font-size:11px; letter-spacing:.13em;
              text-transform:uppercase; color:var(--mute); font-weight:600; }
.cportal__note { display:block; font-size:13px; color:var(--steel); margin-top:4px; }
.cportal__rows { margin-top:8px; }

/* Two columns: everything, and the one control. The button spans both rows so
   a two-line ask does not push it off centre. */
.creq { display:grid; grid-template-columns:minmax(0,1fr) auto; gap:4px 14px;
        align-items:center; padding:12px 0; border-bottom:1px solid var(--hair); }
.creq:last-child { border-bottom:0; }
.creq__t { font-size:15px; font-weight:600; color:var(--navy);
            min-width:0; overflow-wrap:anywhere; }
.creq__d { grid-column:1; font-size:13px; color:var(--steel);
            min-width:0; overflow-wrap:anywhere; }
.creq--done .creq__t { color:var(--steel); }
/* Late is a left edge, not a tinted row: it reads at a glance without turning
   the whole list into a warning. */
.creq--late { border-left:3px solid var(--red); padding-left:12px; }
button.creq__b { grid-row:1 / 3; grid-column:2; border:0; cursor:pointer;
           font-size:13px; font-weight:600; color:#fff; background:var(--accent);
           padding:8px 16px; min-height:0; border-radius:999px; white-space:nowrap;
           box-shadow:none; font-family:inherit; }
.creq__b:hover { background:var(--accent-dark); }
.creq__b:disabled { opacity:.6; cursor:default; }
.creq__s { grid-row:1 / 3; grid-column:2; font-size:12px; font-weight:600;
           color:var(--green); background:var(--green-bg);
           padding:6px 13px; border-radius:999px; white-space:nowrap; }

/* What has already been sent against an ask, under the ask. */
.creq__sent { grid-column:1 / -1; display:flex; flex-wrap:wrap; gap:6px; margin-top:4px; }
button.creq__file { display:inline-flex; align-items:center; gap:7px; cursor:pointer;
              border:1px solid var(--hair); background:var(--paper);
              border-radius:999px; padding:4px 11px 4px 5px; min-height:0;
              box-shadow:none; font:inherit; font-size:12.5px; font-weight:400;
              color:var(--ink2); max-width:100%; }
button.creq__file:hover { border-color:var(--accent-lt); background:var(--paper); }
.creq__file > span:nth-child(2) { overflow:hidden; text-overflow:ellipsis; white-space:nowrap; }
.creq__file em { font-style:normal; color:var(--mute); font-size:11.5px; }
.creq__fk { flex:none; font-size:9.5px; letter-spacing:.06em; font-weight:600;
            color:var(--accent); background:var(--band);
            border-radius:999px; padding:3px 7px; }

/* ⚠ THESE ROWS ARE <button>, so they inherit the shipped button: a filled
   accent pill with a radius and a lift shadow. Every one of those has to be
   undone by name or the list comes out as a stack of blue slabs. The row is a
   row that happens to be pressable, not a button that happens to be wide. */
button.cfile { display:flex; align-items:center; gap:12px; width:100%;
         padding:11px 0; min-height:0; border-radius:0; box-shadow:none;
         border:0; border-bottom:1px solid var(--hair);
         background:none; color:inherit;
         /* ⚠ AND white-space. The base button sets nowrap so a label never
            breaks in half; a file row is a paragraph, and inheriting nowrap
            is what clipped every title mid-word instead of wrapping it. */
         white-space:normal;
         text-align:left; cursor:pointer; font:inherit; }
button.cfile:hover { background:none; }
.cfile:last-child { border-bottom:0; }
button.cfile:hover .cfile__n { color:var(--accent); }
.cfile__i { flex:none; width:46px; text-align:center; font-size:10px;
            letter-spacing:.06em; font-weight:600; color:var(--accent);
            background:var(--band); border-radius:6px; padding:6px 0; }
.cfile__n { flex:1; min-width:0; font-size:14.5px; font-weight:600; color:var(--navy);
             overflow-wrap:anywhere; }
.cfile__n span { display:block; font-size:12.5px; font-weight:400; color:var(--steel);
                 overflow:hidden; text-overflow:ellipsis; white-space:nowrap; }
.cfile__z { flex:none; font-size:12px; color:var(--mute);
            font-variant-numeric:tabular-nums; }

.cportal__foot { display:flex; align-items:center; justify-content:space-between;
                 gap:12px; padding:15px 20px 17px; background:var(--paper); }
.cportal__ask { font-size:14px; color:var(--steel); }
button.btn--mini { border:0; cursor:pointer; font:inherit; font-size:13px; font-weight:600;
             color:#fff; background:var(--accent); padding:9px 18px; min-height:0;
             border-radius:999px; white-space:nowrap; box-shadow:none; }
.btn--mini:hover { background:var(--accent-dark); }

/* ⚠ THE PHONE RULES HAVE TO OUT-SPECIFY THE BUTTON RESET. The desktop
   `button.creq__b` above is a type-plus-class selector; a bare `.creq__b`
   here loses to it, the button stays pinned to column 2, and the panel grows
   wider than the screen — which is what the whole card scrolling sideways
   looked like. Same shape of trap as any reset written by name: whatever
   undoes the default has to be undone at the same weight. */
@media (max-width:560px) {
  .cportal__bar { flex-direction:column; align-items:flex-start; gap:3px; }
  .cportal__sec { padding:14px 15px 6px; }
  .creq { grid-template-columns:minmax(0,1fr); }
  button.creq__b, .creq__s { grid-row:auto; grid-column:1; justify-self:start;
                             margin-top:8px; text-align:center; }
  .creq__sent { margin-top:8px; }
  button.creq__file { max-width:100%; }
  .cfile__z { display:none; }
  .cportal__foot { flex-direction:column; align-items:stretch; gap:10px; padding:14px 15px 16px; }
  .cportal__foot button.btn--mini { width:100%; }
}

.pdocs { list-style:none; margin:0; padding:0; }
.pdoc { background:#fff; border:1px solid var(--hair); border-radius:var(--r); padding:15px 18px;
        margin-bottom:9px; display:flex; align-items:center; gap:16px; cursor:pointer; }
.pdoc:hover { border-color:var(--steel); }
.pdoc__main { flex:1; min-width:0; }
.pdoc__main b { font-size:15px; }
.pdoc__money { text-align:right; font-variant-numeric:tabular-nums; }
.pdoc__money b { font-size:16px; }
.pdoc__status { min-width:84px; text-align:right; }
/* An inspection on the portal's list (Greg, 2026-09-05, task #20). The tally
   is the sheet's own chips, so the row reads the way the page it opens does;
   the summary is the advisor's sentence and gets the room a sentence needs. */
.pinsp .pinsp__tally { display:flex; gap:6px; flex-wrap:wrap; margin-top:8px; }
.pinsp .pinsp__summary { color:var(--ink2); margin-top:4px; }
.pinsp__open { font-size:13px; font-weight:600; color:var(--accent); white-space:nowrap; }
.plist { list-style:none; margin:0; padding:0; }
.plist li { background:#fff; border:1px solid var(--hair); border-radius:var(--r); padding:12px 16px;
            margin-bottom:8px; display:flex; align-items:center; gap:14px; }
.pactions { margin-top:18px; }

/* ---- Pay link ------------------------------------------------------------- */
.paywrap { min-height:100vh; min-height:100dvh; display:flex; justify-content:center;
           align-items:flex-start; padding:26px 16px 60px; }
.paycard { background:#fff; border:1px solid var(--hair); border-radius:12px;
           padding:26px; width:100%; max-width:660px; }
.paycard h1 { font-size:20px; margin-bottom:8px; }
.pay__head { display:flex; gap:16px; align-items:flex-start;
             padding-bottom:18px; border-bottom:1px solid var(--hair); }
.pay__biz b { font-size:18px; }
.pay__no { margin-left:auto; text-align:right; }
.pay__no span { display:block; font-size:10px; letter-spacing:.09em;
                text-transform:uppercase; color:var(--mute); font-weight:700; }
.pay__no b { font-size:20px; }
.pay__due { padding:20px 0; }
.pay__due > span { display:block; font-size:10px; letter-spacing:.09em;
                   text-transform:uppercase; color:var(--mute); font-weight:700; }
.pay__due > b { display:block; font-size:34px; font-variant-numeric:tabular-nums; line-height:1.15; }
.pay__opts { background:var(--paper); border-radius:var(--r-sm); padding:12px 16px; margin-bottom:18px; }
/* A condition on the amount above it, not a competing figure — so it sits
   tight under the total and carries the crimson the document uses for
   anything the customer still has to act on. */
.pay__deposit { margin-top:6px; font-size:12px; font-weight:600; color:var(--accent); }

.pay__to { padding:14px 0; }
.pay__to > span { display:block; font-size:10px; letter-spacing:.09em;
                  text-transform:uppercase; color:var(--mute); font-weight:700; }
/* The business's own fields, three across, the way the PDF prints them under
   BILL TO. Fewer than three do not stretch to fill the row — two fields
   spread edge to edge read as a table with a missing column. */
.docfields { display:grid; grid-template-columns:repeat(3, minmax(0, 1fr));
             gap:10px 22px; justify-content:start;
             padding:12px 0 16px; border-top:1px solid var(--hair); }
.docfields__f > span { display:block; font-size:10px; letter-spacing:.09em;
                       text-transform:uppercase; color:var(--mute); font-weight:700; }
.docfields__f > b { display:block; font-size:13px; word-break:break-word; }

@media (max-width:640px) {
  .docfields { grid-template-columns:repeat(2, minmax(0, 1fr)); }
}

.pay__foot { margin-top:20px; padding-top:16px; border-top:1px solid var(--hair);
             font-size:11.5px; color:var(--mute); text-align:center; }
/* Two stacked full-width buttons always need air between them. This lived
   under .paycard alone, so the same pair inside a .pcard — the public
   booking and manage pages — sat flush with a zero gap. */
.paycard button.block + button.block,
.pcard button.block + button.block { margin-top:8px; }
/* A text box inside a portal card needs air on both sides. Measured at
   0px against the notice above it and the button below on the manage
   page — 12px is the house gap for a column of controls. */
.pcard > .field { margin-top:12px; }
.pcard > .field + * { margin-top:12px; }

@media (max-width:640px) {
  .pdoc { flex-wrap:wrap; }
  .pdoc__status { min-width:0; }
  .pview { padding:14px; }
  .paycard { padding:18px; }
  .pay__due > b { font-size:28px; }
}

/* A link that behaves like a button. Used for the maps deep link, which has to
   be a real <a href> so a phone hands it to the maps app rather than to a
   click handler. */
.btnlink { display:inline-flex; align-items:center; justify-content:center;
           font-size:12px; font-weight:600; text-decoration:none; white-space:nowrap;
           padding:6px 13px; border-radius:99px; color:var(--accent);
           background:rgba(75,156,211,.14); }
.btnlink:hover { background:rgba(75,156,211,.24); }

/* A property row: what it is on the left, what to do about it on the right. */
.proprow { display:flex; align-items:center; gap:12px; }
.proprow__main { flex:1; min-width:0; }
.sub.is-warn { color:var(--amber); }

/* ---- Address autocomplete ---------------------------------------------------
   A suggestion list under a plain text input. It stays a plain input on
   purpose: an address Google has never heard of still types straight through. */
.acwrap { position:relative; }
.acbox { position:absolute; left:0; right:0; top:calc(100% + 4px); z-index:40;
         border-radius:var(--r); overflow:hidden; }
.acbox:not(:empty) {
  background:var(--solid-bg); border:1px solid var(--hair);
  box-shadow:var(--shadow-lift);
  backdrop-filter:blur(10px); -webkit-backdrop-filter:blur(10px);
}
.acitem { padding:10px 13px; font-size:13.5px; cursor:pointer; line-height:1.35;
          border-bottom:1px solid var(--hair); color:var(--ink); }
.acitem:last-child { border-bottom:0; }
.acitem:hover, .acitem.is-on { background:rgba(75,156,211,.12); }
.acitem--dead { color:var(--mute); cursor:default; font-size:12.5px; }
.acitem--dead:hover { background:none; }
/* Inside a dialog the list takes its own room instead of floating over the
   form. .modal__body scrolls, so a floating list was cut off at the dialog's
   foot — eight customers under Send a document ran 217px past it
   (2026-09-14). In the flow the dialog grows to fit, up to its 90vh; the cap
   keeps a long list from pushing Send off the screen. Fixed here rather than
   on that screen, because every picker in a dialog had the same fault. */
.modal .acbox { position:static; }
.modal .acbox:not(:empty) { margin-top:4px; max-height:280px; overflow-y:auto; }

/* ---- A service that repeats -------------------------------------------------
   The cadence is a default for a future schedule, not a schedule, so it reads
   as a quiet property of the item rather than something running right now. */
.recur { border:1px solid var(--hair); border-radius:var(--r); padding:12px 14px;
         background:var(--paper); margin-top:4px; }
.recur__body:not(:empty) { margin-top:10px; }
.recur__body .hint { margin-top:6px; }
.recur__every { display:flex; gap:8px; align-items:center; }
.recur__every input { width:88px; flex:none; }
.sub.is-recur { color:var(--accent); font-weight:600; }

/* ---- Project tiles ----------------------------------------------------------
   A project answers four questions — what it is, whose it is, what is owed, how
   far along — so the tile carries exactly those and nothing else. The bar is
   progress; the hairline along the top repeats it in color so a wall of tiles
   can be read without being read.

   `.ptile`, not `.tile`: the dashboard already has stat tiles and they are a
   different thing. */
.ptiles { display:grid; grid-template-columns:repeat(auto-fill, minmax(252px, 1fr)); gap:14px; }
.ptile {
  position:relative; overflow:hidden; cursor:pointer; padding:16px 17px 15px;
  border-radius:var(--r-lg); border:1px solid var(--glass-brd);
  background:var(--glass-bg); box-shadow:var(--shadow);
  backdrop-filter:var(--glass-blur); -webkit-backdrop-filter:var(--glass-blur);
  transition:transform .16s ease, box-shadow .16s ease;
}
.ptile:hover { transform:translateY(-2px); box-shadow:var(--shadow-lift); }
.ptile:focus-visible { outline:2px solid var(--accent); outline-offset:2px; }
.ptile::before {
  content:''; position:absolute; inset:0 0 auto 0; height:3px;
  background:linear-gradient(90deg, var(--accent-bright), var(--accent-lt));
}
.ptile.is-complete::before { background:linear-gradient(90deg, var(--green), #6FCBA8); }
.ptile.is-hold::before     { background:linear-gradient(90deg, var(--amber), #E8BE58); }
.ptile.is-cancelled::before{ background:linear-gradient(90deg, var(--mute), var(--hair)); }

.ptile__no { font-family:ui-monospace, SFMono-Regular, Menlo, monospace;
             font-size:11px; color:var(--mute); letter-spacing:.04em; }
.ptile__name { font-size:16px; font-weight:650; color:var(--navy); line-height:1.3;
               margin:3px 0 2px; letter-spacing:-.01em; }
.ptile__cust { font-size:12.5px; color:var(--steel); margin-bottom:13px; }
/* Crew and target date. Drawn only when there is something to say — a row of
   dashes on every unassigned project would be a column reporting nothing. */
.ptile__crew { display:flex; flex-wrap:wrap; gap:6px; margin:-8px 0 12px; }
.crewchip, .duechip { display:inline-flex; align-items:center; gap:5px;
                      font-size:11px; font-weight:600; padding:2px 8px;
                      border-radius:999px; border:1px solid var(--hair);
                      color:var(--steel); background:var(--paper); }
.crewchip__dot { width:7px; height:7px; border-radius:50%; flex:none;
                 background:var(--steel); }
/* Late only while the project is still open — see dueChip() for why a project
   that finished after its date is history rather than a warning. */
.duechip.is-late { color:var(--red); border-color:color-mix(in srgb, var(--red) 35%, transparent);
                   background:color-mix(in srgb, var(--red) 8%, transparent); }
.ptile__figs { display:flex; gap:18px; margin-bottom:12px; }
.ptile__fig span { display:block; font-size:9.5px; letter-spacing:.09em;
                   text-transform:uppercase; color:var(--mute); font-weight:700; }
.ptile__fig b { font-size:16px; color:var(--navy); font-variant-numeric:tabular-nums; }
.ptile__foot { display:flex; align-items:center; justify-content:space-between;
               gap:10px; margin-top:9px; font-size:11.5px; color:var(--steel); }

.bar { height:6px; border-radius:99px; background:rgba(19,41,75,.09); overflow:hidden; }
.bar > i { display:block; height:100%; border-radius:99px;
           background:linear-gradient(90deg, var(--accent-bright), var(--accent-lt)); }
.bar.is-complete > i { background:linear-gradient(90deg, var(--green), #6FCBA8); }

@media (max-width:600px) {
  .ptiles { grid-template-columns:1fr; }
}

/* ---- Projects ---------------------------------------------------------------
   A phase is a section and its work is a list of rows — the shape that survives
   being moved to a phone, which is where a project manager actually stands. */
/* TWO PHASES ACROSS, EACH ITS OWN PILL (Greg, 2026-08-25). minmax(0,1fr)
   rather than 1fr: a grid column's default min-width is auto, so one long task
   title would push the column wider than its half and shunt the other out of
   line. align-items:start so a phase with two tasks does not stretch to match
   a neighbour with nine — the whole point is that they read as separate cards,
   not as two columns of one table. */
.phases { display:grid; grid-template-columns:repeat(2, minmax(0, 1fr));
          gap:14px; align-items:start; }
@media (max-width:900px) { .phases { grid-template-columns:1fr; } }

/* THE OUTER EDGE IS THE STRONG LINE, the ones inside are not. Two phases
   side by side need to read as two objects before anything inside them reads
   at all, and darkening every rule equally would just make a busier card.
   So: a defined border and a little lift outside, hairlines within. */
.phaseblk { border:1px solid var(--hair-strong); border-radius:10px;
            overflow:hidden; box-shadow:var(--shadow-sm);
            background:var(--solid-bg); }
.phaseblk--loose { margin-top:14px; }

.phaseblk__head { display:flex; align-items:center; gap:10px; padding:11px 14px;
                  background:var(--paper); border-bottom:1px solid var(--hair);
                  font-size:14px; }
.phaseblk__head .muted { font-size:12px; }
/* The name takes the room and truncates rather than wrapping the state chip
   onto a second line. */
.phaseblk__name { flex:1; min-width:0; overflow:hidden;
                  text-overflow:ellipsis; white-space:nowrap; }

/* Dates and the count, under the name they describe. */
.phaseblk__meta { padding:8px 14px 0; font-size:12px; color:var(--mute);
                  font-variant-numeric:tabular-nums; }

/* How far through, without arithmetic. Hairline on purpose — it informs the
   glance and must not become the loudest thing in the card. */
.phasebar { height:3px; margin:8px 14px 0; border-radius:99px;
            background:var(--hair); overflow:hidden; }
.phasebar__fill { height:100%; background:var(--accent); border-radius:99px; }
.phasebar__fill.is-done { background:var(--green); }

/* The three actions, quiet and at the bottom. In the header they competed with
   the phase name for the same row and, at half width, won. */
.phaseblk__act { display:flex; gap:14px; padding:10px 14px;
                 border-top:1px solid var(--hair); background:var(--paper); }
.phaseblk__act button.link { font-size:12.5px; }

/* Tighter rows inside a pill than in a full-width list: less horizontal room
   to fill, so the vertical rhythm has to close up to match. */
.phaseblk--pill .joblist__row { padding:9px 14px; min-height:0; }
.phaseblk--pill .joblist__main b { font-size:13.5px; }
.phaseblk--pill .joblist__main .sub { font-size:11.5px; }
/* The description, under the name it explains. Small but 13px and full
   contrast — a description nobody can read is decoration. */
/* ---- The map box ------------------------------------------------------------
   Small on purpose. It answers "roughly where is this" at a glance and hands
   the real navigation to the phone, which is why the only control on it is a
   Directions link. The Embed API is free however many times this renders, so
   there is no reason to lazy-gate it beyond loading="lazy".

   `pointer-events` stays ON, unlike the document thumbnails: a map you can
   nudge to see the next street over is worth the interaction, and nothing is
   underneath it to click by mistake. */
.mapbox { position:relative; width:230px; height:150px; flex:none;
          /* --lg is the project page: a project spans a site, so it gets enough
             map to show which part of town, not just which doorstep. */
          border-radius:var(--r); overflow:hidden; background:var(--band);
          border:1px solid var(--hair); box-shadow:var(--shadow-sm); }
.mapbox iframe { width:100%; height:100%; border:0; display:block; }
/* TOP-left, not bottom. Google's Embed puts its logo bottom-left and its data
   attribution bottom-right, and covering either is both ugly and against their
   terms. The top-left corner is the one that stays clear. */
.mapbox__go {
  position:absolute; left:8px; top:8px; z-index:2;
  font-size:11.5px; font-weight:650; text-decoration:none; white-space:nowrap;
  padding:6px 12px; border-radius:99px; color:#fff;
  background:rgba(10,23,48,.78); backdrop-filter:blur(6px);
}
.mapbox__go:hover { background:var(--navy); }
.mapbox--none { display:flex; flex-direction:column; align-items:center;
                justify-content:center; gap:8px; font-size:12px; text-align:center;
                padding:12px; }

/* 215, not 250: the summary text beside it runs out at about this height, and
   a map taller than its neighbor just leaves a hole in the panel. Still two
   and a half times the area of the small one. */
.mapbox--lg { width:380px; height:215px; }

/* The map is the second column, and it keeps its size — the text reflows
   instead, because a map squeezed to a strip stops being a map. */
.summary--map { align-items:stretch; }
.summary--map .summary__main { min-width:300px; }

@media (max-width:900px) {
  .mapbox, .mapbox--lg { width:100%; height:190px; }
}

.summary__desc { font-size:13px; color:var(--steel); line-height:1.5;
                 margin:4px 0 10px; max-width:70ch; }

.phaseblk__when { font-size:11px; color:var(--accent); font-weight:600;
                  background:rgba(75,156,211,.14); padding:2px 8px; border-radius:99px;
                  white-space:nowrap; }
.phaseblk__state { flex:none; font-size:10px; font-weight:700;
                   letter-spacing:.07em; text-transform:uppercase; color:var(--mute); }
.phaseblk__head button.link { flex:none; }
/* The header used to wrap on a phone because it carried six things. It now
   carries three — dot, name, state — and the name truncates, so wrapping it
   would only ever produce a ragged card. */
.phaseblk__dot { width:9px; height:9px; border-radius:50%; flex:none;
                 background:var(--hair); border:2px solid var(--mute); }
.phaseblk__dot.is-in_progress { background:var(--accent); border-color:var(--accent); }
.phaseblk__dot.is-complete { background:var(--green); border-color:var(--green); }
.phaseblk__empty { padding:14px; font-size:13px; color:var(--mute); }

.joblist { list-style:none; margin:0; padding:0; }
.joblist__row { display:flex; align-items:center; gap:12px; padding:12px 14px;
                border-bottom:1px solid var(--hair); cursor:pointer; min-height:44px; }
.joblist__row:last-child { border-bottom:0; }
.joblist__row:hover { background:var(--paper); }
.joblist__main { flex:1; min-width:0; }
.joblist__main b { display:block; font-size:14px; }
.joblist__chev { color:var(--mute); font-size:18px; line-height:1; }

/* ---- Project photos and files -----------------------------------------------
   Photographs are scanned, not read: a grid of square tiles, big enough to
   recognize a plant room at a glance and small enough that twenty fit. */
.gallery { display:grid; grid-template-columns:repeat(auto-fill, minmax(148px, 1fr));
           gap:12px; margin-bottom:14px; }
.gallery__tile {
  position:relative; aspect-ratio:1; border-radius:var(--r); overflow:hidden;
  cursor:pointer; background:var(--solid-bg); border:1px solid var(--glass-brd);
  box-shadow:var(--shadow); display:flex; align-items:center; justify-content:center;
  font-size:12px; transition:transform .16s ease, box-shadow .16s ease;
}
.gallery__tile:hover { transform:translateY(-2px); box-shadow:var(--shadow-lift); }
.gallery__tile:focus-visible { outline:2px solid var(--accent); outline-offset:2px; }
.gallery__load { color:var(--mute); }
.gallery__tile img { width:100%; height:100%; object-fit:cover; display:block; }

/* A document preview is the real first page, scaled into the tile. The embed
   never takes the pointer — the tile is one target, and an embedded PDF would
   otherwise capture the click and try to scroll inside a thumbnail. */
.gallery__pdf { position:absolute; inset:0; overflow:hidden; background:#fff;
                pointer-events:none; }
.gallery__pdf embed { width:100%; height:100%; border:0; display:block;
                      pointer-events:none; }
/* Paper needs an edge to read as paper on a white tile. */
.gallery__tile.is-doc { background:#fff; }
.gallery__tile.is-doc::after {
  content:''; position:absolute; inset:0; pointer-events:none;
  box-shadow:inset 0 0 0 1px rgba(19,41,75,.10); border-radius:inherit;
}

/* The name, over the foot of the tile. Always legible on a document, and only
   on hover over a photograph, where it is usually just noise. */
.gallery__cap {
  position:absolute; left:0; right:0; bottom:0; padding:14px 9px 7px;
  font-size:10.5px; line-height:1.3; color:#fff; pointer-events:none;
  background:linear-gradient(180deg, rgba(10,23,48,0) 0%, rgba(10,23,48,.82) 60%);
  white-space:nowrap; overflow:hidden; text-overflow:ellipsis;
  opacity:0; transition:opacity .15s ease;
}
.gallery__tile.is-doc .gallery__cap, .gallery__tile:hover .gallery__cap,
.gallery__tile:focus-visible .gallery__cap { opacity:1; }

.gallery__x { position:absolute; top:6px; right:6px; width:26px; height:26px;
              border-radius:50%; border:0; cursor:pointer; line-height:1; z-index:2;
              background:rgba(10,23,48,.6); color:#fff; font-size:15px; min-height:0;
              box-shadow:none; padding:0;
              display:flex; align-items:center; justify-content:center; }
.gallery__x:hover { background:var(--red); }

@media (max-width:520px) {
  .gallery { grid-template-columns:repeat(auto-fill, minmax(118px, 1fr)); }
}

/* ---- Signature --------------------------------------------------------------
   `touch-action:none` is what makes this work on a tablet: without it the
   browser treats the first drag as a scroll and the pad never sees the stroke. */
/* No aspect-ratio: the pad is shared now and is asked for at two sizes (560x200
   at a project sign-off, 520x170 on a customer's phone). A <canvas> carries its
   own intrinsic ratio from its width/height attributes, so height:auto already
   scales it correctly — pinning 56/20 here squashed every other size. */
.sigpad { width:100%; max-width:560px; height:auto;
          border:1px dashed var(--hair); border-radius:var(--r-sm); background:#fff;
          touch-action:none; cursor:crosshair; display:block; }
.sigpad__row { display:flex; align-items:center; justify-content:space-between;
               gap:12px; margin-top:6px; }
.sigpad__row .hint { margin:0; }

.signoff { display:flex; align-items:center; gap:18px; flex-wrap:wrap;
           border:1px solid var(--hair); border-radius:10px; padding:14px; }
.signoff__img { width:220px; min-height:74px; display:flex; align-items:center;
                justify-content:center; background:#fff; border:1px solid var(--hair);
                border-radius:var(--r-sm); overflow:hidden; }
.signoff__img img { max-width:100%; max-height:110px; display:block; }

/* ---- Crews, in settings ---------------------------------------------------- */
.crews { display:grid; gap:1px; background:var(--hair); border:1px solid var(--hair);
         border-radius:var(--r-sm); overflow:hidden; margin-top:12px; }
.crewrow { display:flex; align-items:center; gap:12px; background:var(--white);
           padding:12px 14px; }
.crewrow__swatch { width:12px; height:12px; border-radius:3px; background:var(--mute); flex:none; }
.crewrow__main { flex:1; min-width:0; }
.crewrow__main .sub { white-space:nowrap; overflow:hidden; text-overflow:ellipsis; }

/* ---- Who is on a job --------------------------------------------------------
   Rows, not a paragraph of names: a crew is read down, and the lead has to be
   findable without reading the others. */
.people { display:flex; flex-wrap:wrap; gap:8px 10px; }
.person { display:flex; align-items:center; gap:8px; font-size:13.5px;
          background:var(--paper); border:1px solid var(--hair);
          border-radius:999px; padding:5px 13px 5px 9px; }
.person.is-lead { border-color:var(--ink); }
.person__dot { width:9px; height:9px; border-radius:50%; background:var(--mute); flex:none; }
.person__lead { font-size:10px; font-weight:700; letter-spacing:.07em;
                text-transform:uppercase; color:var(--mute); }

/* ---- Time on a job ----------------------------------------------------------
   The figures first and the timer beside them: on a tablet in a van, starting
   and stopping is the whole interaction, so it gets a real target. */
.timebar { display:flex; flex-wrap:wrap; align-items:center; justify-content:space-between;
           gap:14px; border:1px solid var(--hair); border-radius:10px;
           background:var(--paper); padding:14px 16px; margin-bottom:14px; }
.timebar__totals { display:flex; gap:26px; flex-wrap:wrap; }
.timefig > span { display:block; font-size:10px; letter-spacing:.09em;
                  text-transform:uppercase; color:var(--mute); font-weight:700; }
.timefig > b { font-size:19px; font-variant-numeric:tabular-nums; }
.timebar__act button { min-height:44px; padding:0 18px; }

@media (max-width:640px) {
  .timebar { flex-direction:column; align-items:stretch; }
  .timebar__act button { width:100%; }
}

/* ---- Job checklist ----------------------------------------------------------
   A required step reads differently from a suggested one, because one of them
   stops the job being closed and the other does not. */
.checklist { list-style:none; margin:0; padding:0; border:1px solid var(--hair);
             border-radius:var(--r); background:#fff; overflow:hidden; }
.checklist__item { display:flex; align-items:flex-start; gap:11px;
                   padding:11px 14px; border-bottom:1px solid var(--hair); }
.checklist__item:last-child { border-bottom:0; }
.checklist__item input[type=checkbox] { margin-top:2px; width:16px; height:16px; flex:none; }
.checklist__label { flex:1; font-size:13px; }
.checklist__item.is-done .checklist__label { color:var(--mute); text-decoration:line-through; }
.checklist__item.is-done .req { text-decoration:none; }
.req { font-size:10.5px; font-weight:700; text-transform:uppercase; letter-spacing:.5px;
       color:var(--danger, #a4262c); margin-left:6px; }
.fieldset { display:flex; flex-direction:column; gap:10px; }
.fieldset h3 { font-size:13px; margin:4px 0 0; }

/* ---- Contact rows with a portal control ------------------------------------ */
.rowline { display:flex; align-items:flex-start; gap:12px; }
.rowline__main { flex:1; min-width:0; cursor:pointer; }
/* A rowline's own actions sit level with its text. .acts carries a 14px top
   margin because it is normally the row at the FOOT of a card; inside a
   rowline that margin pushed the Copy button below the line it belongs to
   (Calendars › Link, 2026-09-14). */
.rowline > .acts { margin-top:0; flex-shrink:0; }
/* A list row that holds a small form — a name, a row of fields, its Save —
   stacked with the same 10px as .acts rather than laid out sideways like a
   rowline (Calendars › Hours and › People caps, 2026-09-14). */
ul.list li.calrow { display:flex; flex-direction:column; gap:10px; }
ul.list li.calrow > .acts { margin-top:0; }
/* align-items so a pill-shaped action (Remove) lines up with the text links
   beside it instead of sitting proud of them. */
.rowline__act { display:flex; gap:10px; flex-shrink:0; padding-top:2px;
                align-items:center; }
.invite__out { margin-top:4px; }
.invite__out h2 { margin-bottom:6px; }
.invite__out .linkbox { margin:0 0 8px; }

/* ---- Reports -----------------------------------------------------------------
   The bars are drawn with CSS heights from figures the database aggregated —
   the client never adds money. Ink, not accent: this is the merchant's own
   console, and a chart is reading matter, not a call to action. */
.chart { display:flex; align-items:flex-end; gap:10px; height:150px; margin-top:10px; }
.chart__col { flex:1; min-width:0; height:100%; display:flex; flex-direction:column;
              justify-content:flex-end; align-items:center; gap:4px; }
.chart__bar { width:100%; max-width:64px; background:var(--ink); border-radius:4px 4px 0 0; }
.chart__val { font-size:10.5px; color:var(--steel); font-variant-numeric:tabular-nums;
              white-space:nowrap; }
.chart__lab { font-size:10px; color:var(--mute); }
table.report td { padding:8px 6px; }
table.report td:first-child { padding-left:0; }
table.report td:last-child { padding-right:0; }
table.report tbody tr:nth-child(even) { background:transparent; }
.reportrange { display:flex; align-items:center; gap:10px; margin-bottom:12px; flex-wrap:wrap; }
.reportrange input { max-width:170px; }
/* The -6px pulled this caption up under the report pills, leaving 8px — the
   same complaint Greg raised about the ISO dashboard. It belongs near the
   pills it describes, not against them. */
.reportspan { margin:0 0 12px; font-size:12px; }

/* ---- Transactions (Reports) --------------------------------------------------
   The sub-tab bar sits under the page title; the filter bar reads left to
   right the way the gateway portal's does. Dropdowns are <details> — no
   focus-trap machinery, and Escape/blur behavior comes free. */
.subtabs { margin-bottom:16px; border-bottom:1px solid var(--hair); padding-bottom:12px; }
.txbar { display:flex; align-items:center; gap:8px; flex-wrap:wrap; margin-bottom:10px; }
.txbar__spacer { flex:1; }
.txsearch { display:flex; gap:8px; align-items:center; flex-wrap:wrap; }
.txsearch input[type=search] { max-width:260px; }
.txsearch select { width:auto; }
.txgroup { width:auto; }
.txtotals { display:flex; gap:18px; flex-wrap:wrap; font-size:12.5px; color:var(--steel);
            padding:9px 12px; background:#fff; border:1px solid var(--hair);
            border-radius:var(--r-sm); margin-bottom:10px; }
.txtotals b { color:var(--ink); font-variant-numeric:tabular-nums; }
.txtotals--bad b { color:var(--accent); }
.dd { position:relative; }
.dd summary { list-style:none; cursor:pointer; display:inline-block;
              background:#fff; color:var(--ink2); border:1px solid var(--hair);
              border-radius:var(--r-sm); padding:7px 14px; font-size:12.5px; font-weight:600;
              white-space:nowrap; user-select:none; }
.dd summary::-webkit-details-marker { display:none; }
.dd[open] summary { background:var(--band); }
.dd__pop { position:absolute; top:calc(100% + 4px); left:0; z-index:40;
           background:#fff; border:1px solid var(--hair); border-radius:var(--r-sm);
           box-shadow:0 8px 26px rgba(16,24,32,.14); padding:6px;
           min-width:190px; max-height:300px; overflow-y:auto; }
.dd--right .dd__pop { left:auto; right:0; }
.dd__item { display:flex; align-items:center; gap:9px; padding:7px 10px; font-size:13px;
            border-radius:var(--r-sm); cursor:pointer; text-transform:none; letter-spacing:0;
            color:var(--ink); font-weight:400; }
.dd__item:hover { background:var(--paper); }
.dd__item input { width:auto; }
.dd__act { display:block; width:100%; text-align:left; background:none; color:var(--ink);
           font-weight:500; font-size:13px; padding:8px 10px; border-radius:var(--r-sm); }
.dd__act:hover { background:var(--paper); }
tr.is-open td { background:var(--band); }
tr.txdetail > td { padding:0; background:var(--paper); }
tr.txdetail:nth-child(even) { background:transparent; }
.txdetail__wrap { padding:14px; }
.txdetail__wrap .card { box-shadow:none; }
.txactions { display:flex; gap:8px; align-items:center; flex-wrap:wrap; margin-top:12px; }
/* Void / Refund / Recharge on an invoice's payment rows. Link-weight, not
   ghost buttons: three buttons in a table cell at button size shouted over
   the figures they sit beside. Refund carries .danger; a recharge is a sale
   and stays in the accent. */
.payacts { display:flex; gap:12px; align-items:center; flex-wrap:wrap; white-space:nowrap; }
.payacts .muted { font-size:12px; }
.txpager { display:flex; gap:8px; align-items:center; justify-content:flex-end;
           margin-top:12px; }
.txpager .muted { margin-right:auto; font-size:12.5px; }
th.sortable { cursor:pointer; user-select:none; }
th.sortable:hover { background:#1B2530; }
.termmethods { display:flex; gap:8px; flex-wrap:wrap; margin:4px 0 10px; }
.schedline { display:grid; grid-template-columns:1fr 76px 100px auto 36px; gap:8px;
             align-items:center; }
.schedline + .schedline { margin-top:8px; }
.schedline__tax { white-space:nowrap; }
@media (max-width:700px) {
  .schedline { grid-template-columns:1fr 64px 84px; }
  .schedline__tax, .schedline .icon { grid-column:auto; }
}

/* ---- Calendar ----------------------------------------------------------------
   Jobs and events share the grid; the colored left edge is the job type (or
   the event's own color), because a route sheet is read by color first. */
.calbar { display:flex; align-items:center; gap:12px; flex-wrap:wrap; margin-bottom:12px; }
.calbar__title { font-size:16px; margin:0; }
.calwrap { display:grid; grid-template-columns:minmax(0,1fr) 260px; gap:14px; align-items:start; }
/* The team boards want every pixel of width for their columns; the tray drops
   below them rather than beside. */
.calwrap--wide { grid-template-columns:minmax(0,1fr); }
@media (max-width:1000px) { .calwrap { grid-template-columns:1fr; } }
.caltray__item { display:flex; align-items:center; gap:10px; }
.caltray__main { flex:1; min-width:0; }
.caltray__item.is-placing { background:var(--amber-bg); margin:0 -8px; padding:9px 8px; border-radius:var(--r-sm); }
.calplacing { margin-bottom:12px; }

.calchip { display:flex; align-items:baseline; gap:5px; font-size:11.5px; padding:2px 6px 2px 7px;
           background:#fff; border:1px solid var(--hair); border-left:3px solid var(--steel);
           border-radius:4px; cursor:pointer; overflow:hidden; white-space:nowrap; }
.calchip:hover { background:var(--band); }
.calchip__time { color:var(--mute); font-size:10px; flex:none; }
.calchip__label { overflow:hidden; text-overflow:ellipsis; }
.calchip--event { background:var(--paper); }
.calchip--done .calchip__label { text-decoration:line-through; color:var(--mute); }

/* ---- The month ---------------------------------------------------------------
   Brought into line with the canvas views. It had a dark navy heading and
   outlined white chips while the week beside it had a pale heading and solid
   team color — two screens that are meant to be one screen.

   The compact chip stays: a month cell has room for a line, not a block. What
   changes is that the line is now the crew's color, so scanning a month for
   "where is Crew A this fortnight" works the same way scanning a week does. */
.calmonth { background:#fff; border:1px solid var(--hair); border-radius:var(--r);
            overflow:hidden; }
.calmonth__head { display:grid; grid-template-columns:repeat(7,1fr);
                  background:var(--band); border-bottom:1px solid var(--hair); }
.calmonth__head span { padding:8px; font-size:10.5px; letter-spacing:.06em;
                       text-transform:uppercase; color:var(--steel);
                       font-weight:600; text-align:center; }
.calmonth__grid { display:grid; grid-template-columns:repeat(7,1fr); }

.calcell { min-height:118px; border-top:1px solid var(--hair);
           border-left:1px solid var(--hair); padding:5px 6px 6px;
           display:flex; flex-direction:column; gap:4px; cursor:pointer;
           min-width:0; }
.calcell:nth-child(7n+1) { border-left:0; }
.calcell:hover { background:var(--paper); }
/* Days from the neighboring months are context, not content. */
.calcell--out { background:var(--paper); }
.calcell--out .calcell__num,
.calcell--out .calcell__count { color:var(--mute); }
.calcell--out .calchip { opacity:.6; }

.calcell__head { display:flex; align-items:center; justify-content:space-between;
                 gap:6px; min-height:22px; }
/* inline-flex, not flex: a block-level flex child fills the row it is in, so
   today's pill stretched the whole width of the cell instead of hugging its
   two digits. */
.calcell__num { font-size:12.5px; font-weight:600; min-width:22px; height:22px;
                display:inline-flex; align-items:center; justify-content:center;
                flex:0 0 auto; align-self:flex-start;
                font-variant-numeric:tabular-nums; }
/* The same accent pill the week's heading uses for today. */
.calcell--today .calcell__num { background:var(--accent); color:#fff;
                                border-radius:999px; padding:0 6px; }
.calcell__count { font-size:10px; color:var(--mute);
                  font-variant-numeric:tabular-nums; }
.calcell__items { display:flex; flex-direction:column; gap:3px; min-width:0; }
.calcell__more { align-self:flex-start; font-size:10.5px; padding:0; }

/* Solid, like the canvas chips, and for the same reason: a column of outlined
   white bars is one color from three feet away. currentColor carries the
   crew's color in, exactly as it does on the canvas. */
.calmonth .calchip {
  background:currentColor; border:0; border-left:3px solid rgba(0,0,0,.22);
  border-radius:4px; padding:2px 6px; gap:5px; min-height:17px;
}
.calmonth .calchip .calchip__label { color:#fff; font-size:11px; }
.calmonth .calchip .calchip__time { color:rgba(255,255,255,.85); }
.calmonth .calchip .calchip__icon { filter:none; }
.calmonth .calchip:hover { filter:brightness(1.08);
                           box-shadow:0 1px 5px rgba(19,41,75,.22); }
/* Finished work goes back to an OUTLINE rather than a dimmed solid. Fading a
   solid chip left white text on a pale wash — struck through and barely
   legible, which is the wrong way to retire something you still want to be
   able to read. An outline says "not active" without hiding the words, and it
   keeps the crew's color where the eye expects it. */
.calmonth .calchip--done { background:#fff; border:1px solid var(--hair);
                           border-left:3px solid currentColor; opacity:1; }
.calmonth .calchip--done .calchip__label { color:var(--mute);
                                           text-decoration:line-through; }
.calmonth .calchip--done .calchip__time { color:var(--mute); }
.calmonth .calchip--done:hover { filter:none; background:var(--paper); }

/* The list-shaped week that lived here is gone — weekGrid() emits a canvas
   (.calweekc) now, so every .calweek* rule below it was styling markup that is
   never produced. Deleted rather than left: dead CSS reads as live CSS to the
   next person, and the fortnight wrap it contained had already stopped working
   without anything saying so. See .calweekc for what replaced it. */
@media (max-width:860px) {
  .calweek { grid-template-columns:1fr; }
}

.calday { background:#fff; border:1px solid var(--hair); border-radius:var(--r); overflow:hidden; }
.calday__allday { padding:8px; border-bottom:1px solid var(--hair); display:flex; gap:6px;
                  flex-wrap:wrap; background:var(--paper); }
.calday__row { display:flex; border-top:1px solid var(--hair); min-height:44px; cursor:pointer; }
.calday__row:first-child { border-top:0; }
.calday__row:hover { background:var(--paper); }
.calday__hour { width:64px; flex:none; padding:6px 10px; font-size:10.5px; color:var(--mute);
                text-align:right; border-right:1px solid var(--hair); }
.calday__items { flex:1; padding:5px 8px; display:flex; flex-direction:column; gap:4px; min-width:0; }

.calcustom { min-width:230px; padding:10px; }
.dd__label { display:block; font-size:10px; letter-spacing:.07em; text-transform:uppercase;
             color:var(--mute); font-weight:700; margin:10px 2px 6px; }
.dd__label:first-child { margin-top:2px; }
.calcustom .btnrow button { font-size:11.5px; padding:6px 12px; }
.calcustom input[type=color] { width:34px; height:26px; padding:1px; }

/* ---- Branding (Settings) -----------------------------------------------------
   The logo preview sits on the band, not on white: most logos are dark marks
   on transparency, and white-on-white is how a broken upload hides. */
.logobox { background:var(--band); border:1px solid var(--hair); border-radius:var(--r-sm);
           padding:18px; display:flex; align-items:center; justify-content:center;
           min-height:96px; margin-bottom:12px; }
.logobox img { max-height:64px; max-width:100%; display:block; }
.btnrow { display:flex; gap:8px; align-items:center; flex-wrap:wrap; }
.swatches { display:flex; gap:8px; flex-wrap:wrap; }
.swatch { width:34px; height:34px; border-radius:var(--r-sm); padding:0; border:2px solid var(--hair); }
.swatch.is-on { border-color:var(--ink); outline:2px solid var(--ink); outline-offset:1px; }
.swatch:hover { border-color:var(--steel); background:inherit; }
input[type=color] { padding:2px; height:38px; width:64px; cursor:pointer; }
.accentprev { display:flex; align-items:center; gap:12px; flex-wrap:wrap; margin-bottom:14px; }
.accentprev__btn { display:inline-block; padding:10px 18px; border-radius:var(--r-sm);
                   font-size:13px; font-weight:600; }
/* ---- The ISO's grouped feature switches ---------------------------------
   Thirty-eight checkboxes in one column is a list nobody reads to the bottom
   of, so they are grouped the same way the status page groups components. */
.featgroup + .featgroup { margin-top:18px; padding-top:16px; border-top:1px solid var(--hair); }
.featgroup__name { font-size:11.5px; text-transform:uppercase; letter-spacing:.07em;
                   color:var(--mute); font-weight:600; margin-bottom:10px; }
.featlist { list-style:none; margin:0; padding:0; display:flex;
            flex-direction:column; gap:12px; }

.featrow { display:flex; align-items:flex-start; gap:12px; }
/* A feature row is sometimes an <li> (settings) and sometimes a <label> so the
   whole row is clickable (boarding). As a label it inherits the global caption
   rule — 10px, bold, gray, uppercase — and every feature name and hint on the
   boarding screen came out shouting. Reset first, style second: the same
   lesson the option rows and the upload bar both taught. */
label.featrow, label.featrow * { text-transform:none; letter-spacing:normal; }
label.featrow { font-size:13.5px; font-weight:400; color:var(--ink); cursor:pointer; }
label.featrow .sub { font-size:12.5px; color:var(--mute); font-weight:400; }
.featrow input[type=checkbox],
.featrow input[type=radio] { width:18px; height:18px; margin-top:3px; flex:none; }
.featrow__main b { font-size:14px; }

/* ---- Boarding warnings -------------------------------------------------------
   validateProgram() speaks in three registers and the screen should too: a
   refusal blocks the button, a warning does not, and an obligation is neither. */
.warnbox { display:flex; flex-direction:column; gap:6px; }
.warnbox:empty { display:none; }
.warnbox__err  { background:var(--red-bg);   color:var(--red);    padding:8px 11px; border-radius:var(--r-sm); font-size:12.5px; }
.warnbox__warn { background:var(--amber-bg); color:var(--amber);  padding:8px 11px; border-radius:var(--r-sm); font-size:12.5px; }
.warnbox__info { background:var(--band);     color:var(--ink2);   padding:8px 11px; border-radius:var(--r-sm); font-size:12.5px; }
button[disabled] { opacity:.5; cursor:not-allowed; }
button[disabled]:hover { background:var(--accent); }

/* ---- The card face -----------------------------------------------------------
   A live picture of what is being typed, which is the one bit of feedback that
   says the machine is reading the number before anything is submitted. Ours is
   built from our own navy rather than the reference's purple: the brand tints
   the sheen, it does not repaint the card, so four brands still look like four
   cards from the same company.

   Everything here is decorative. The number shown is whatever was typed; it is
   not stored, and for a card on file only the last four exist to show. */
.vcard { position:relative; width:100%; max-width:340px; aspect-ratio:1.586;
         border-radius:var(--r-lg); padding:20px 22px; color:#fff; overflow:hidden;
         display:flex; flex-direction:column; justify-content:space-between;
         background:
           radial-gradient(120% 140% at 85% -10%, rgba(123,175,212,.55), transparent 55%),
           radial-gradient(90% 120% at -10% 110%, rgba(75,156,211,.35), transparent 60%),
           linear-gradient(158deg, var(--navy) 0%, var(--navy-deep) 100%);
         box-shadow:var(--shadow-lift);
         transition:background .35s ease; }
/* A faint weave, so the face has a surface instead of being a flat fill. */
.vcard::after { content:''; position:absolute; inset:0; pointer-events:none; opacity:.06;
  background-image:linear-gradient(rgba(255,255,255,.9) 1px, transparent 1px),
                   linear-gradient(90deg, rgba(255,255,255,.9) 1px, transparent 1px);
  background-size:7px 7px; }
.vcard > * { position:relative; z-index:1; }

.vcard__chip { width:42px; height:31px; border-radius:6px; flex:none;
               background:linear-gradient(140deg, #E4C97E, #B9975B 55%, #E8D79A);
               box-shadow:inset 0 0 0 1px rgba(255,255,255,.35); }
.vcard__chip::after { content:''; display:block; margin:9px auto 0; width:24px; height:12px;
                      border:1px solid rgba(0,0,0,.22); border-left:0; border-right:0; }

.vcard__brand { position:absolute; top:18px; right:22px; z-index:2;
                font-size:15px; font-weight:800; letter-spacing:.1em;
                text-transform:uppercase; color:#fff; text-align:right;
                text-shadow:0 1px 3px rgba(0,0,0,.35);
                max-width:52%; line-height:1.2; }

/* ⚠ SIXTEEN DIGITS HAVE TO FIT. At 23px with .055em tracking a full
   "4111 1111 1111 1111" is 19 characters and ~296px wide inside a 256px slot —
   the last four digits were simply clipped, on every surface that draws this
   card: the terminal, the pay page and the vault.

   Sized against the CARD rather than the viewport, so it is right at whatever
   width the card is given. 5.2cqi is ~17.7px on a 340px card and ~18.7px on a
   360px one; the clamp keeps it sane at the extremes. */
.vcard { container-type:inline-size; }
.vcard__num { font-size:clamp(14px, 5.2cqi, 20px); font-weight:700; letter-spacing:.04em;
              font-variant-numeric:tabular-nums;
              font-family:ui-monospace, SFMono-Regular, Menlo, monospace;
              text-shadow:0 1px 2px rgba(0,0,0,.28); word-break:break-all; }

.vcard__foot { display:flex; gap:20px; align-items:flex-end; }
.vcard__slot { min-width:0; }
.vcard__cap { font-size:8.5px; letter-spacing:.13em; text-transform:uppercase;
              color:rgba(255,255,255,.62); }
.vcard__val { font-size:12.5px; font-weight:600; margin-top:2px;
              font-variant-numeric:tabular-nums;
              white-space:nowrap; overflow:hidden; text-overflow:ellipsis; }

/* The brand shifts the sheen. Restraint is the point — no scheme logos, no
   wholesale repaint, just enough that a switch registers. */
.vcard--visa       { background:
  radial-gradient(120% 140% at 85% -10%, rgba(146,182,235,.55), transparent 55%),
  radial-gradient(90% 120% at -10% 110%, rgba(75,156,211,.4), transparent 60%),
  linear-gradient(158deg, #16305C 0%, #0A1730 100%); }
.vcard--mastercard { background:
  radial-gradient(120% 140% at 85% -10%, rgba(232,160,120,.42), transparent 55%),
  radial-gradient(90% 120% at -10% 110%, rgba(201,90,72,.3), transparent 60%),
  linear-gradient(158deg, #1B2C4A 0%, #0A1730 100%); }
.vcard--amex       { background:
  radial-gradient(120% 140% at 85% -10%, rgba(123,207,212,.5), transparent 55%),
  radial-gradient(90% 120% at -10% 110%, rgba(47,163,122,.28), transparent 60%),
  linear-gradient(158deg, #103F5A 0%, #08202F 100%); }
.vcard--discover   { background:
  radial-gradient(120% 140% at 85% -10%, rgba(240,186,120,.5), transparent 55%),
  radial-gradient(90% 120% at -10% 110%, rgba(211,140,60,.28), transparent 60%),
  linear-gradient(158deg, #22314B 0%, #0A1730 100%); }

/* ---- Picking a customer, or making one ---------------------------------------
   Reuses the address list's furniture. The create row is set apart because it
   is the only entry that does something other than choose. */
.custpick .acitem { display:flex; align-items:baseline; justify-content:space-between; gap:12px; }
.custpick .acitem small { flex:none; }
.acitem--make { color:var(--accent); font-weight:600;
                background:var(--band); border-top:1px solid var(--hair); }
.acitem--make:hover, .acitem--make.is-on { background:rgba(75,156,211,.2); }
.acitem--make small { font-weight:400; }

/* ---- Spreading one payment over several invoices -----------------------------
   The preview is the safeguard: nobody should press a button that moves money
   across four invoices without first seeing which four and how much each. */
.alloc { border:1px solid var(--hair); border-radius:var(--r); overflow:hidden;
         background:var(--paper); }
.allocrow { display:flex; align-items:center; justify-content:space-between; gap:12px;
            padding:9px 13px; border-bottom:1px solid var(--hair); font-size:13px; }
.allocrow:last-child { border-bottom:0; }
.allocrow__inv { font-weight:600; color:var(--ink); }
.allocrow__of { color:var(--mute); font-size:12px; margin-left:6px; }
.allocrow__amt { font-variant-numeric:tabular-nums; font-weight:600; }
.allocrow.is-clearing .allocrow__amt { color:var(--green); }
.alloc__foot { padding:9px 13px; background:var(--band); font-size:12.5px; color:var(--ink2);
               display:flex; justify-content:space-between; gap:12px; }
.alloc__foot b { font-variant-numeric:tabular-nums; }

/* ---- A card the gateway is holding -------------------------------------------
   Shown as a choice, not a field: the number is not ours to display, so the
   row is brand, last four and expiry, which is what somebody recognizes. */
.pmrow { display:flex; align-items:center; gap:11px; padding:10px 12px;
         border:1px solid var(--hair); border-radius:var(--r-sm); background:#fff;
         cursor:pointer; font-size:13px; }
.pmrow + .pmrow { margin-top:7px; }
.pmrow.is-on { border-color:var(--accent); box-shadow:0 0 0 2px rgba(42,110,158,.16); }
.pmrow__dots { font-variant-numeric:tabular-nums; letter-spacing:.06em; color:var(--ink); }
.pmrow__brand { font-weight:600; color:var(--ink); }
.pmrow__exp { margin-left:auto; color:var(--mute); font-size:12px;
              font-variant-numeric:tabular-nums; }

/* ---- The card block, and the arithmetic beneath it ---------------------------
   The reference keeps Amount / Surcharge / TOTAL together in one line so they
   are read as a sum rather than three separate facts. Ours does the same, and
   sits directly above the button, so the last thing seen before committing is
   the figure being committed to. */
.cardblock:not(:empty) { border:1px solid var(--hair); border-radius:var(--r);
                         padding:14px; background:var(--paper); margin:4px 0 12px; }
.cardblock .field:first-child { margin-top:0; }
.cardblock .hint { margin-top:6px; }

.termtotals { margin-top:16px; }
.termtotals .alloc + .hint { margin-top:8px; }
.termsum { display:flex; align-items:stretch; gap:1px; margin-top:12px;
           border:1px solid var(--hair); border-radius:var(--r); overflow:hidden;
           background:var(--hair); }
.termsum__cell { flex:1; background:#fff; padding:10px 13px; min-width:0; }
.termsum__cell span { display:block; font-size:10.5px; letter-spacing:.09em;
                      text-transform:uppercase; color:var(--mute); }
.termsum__cell b { display:block; font-size:17px; margin-top:3px; color:var(--ink);
                   font-variant-numeric:tabular-nums; }
.termsum__cell.is-total { background:var(--band); }
.termsum__cell.is-total b { font-size:21px; color:var(--navy); }

@media (max-width:560px) {
  .termsum { flex-direction:column; }
}

/* ---- The receipt -------------------------------------------------------------
   Built on the pay page's furniture, because a customer who has an invoice from
   this business should recognize the receipt as coming from the same place. The
   difference is what leads: an invoice leads with what is owed, a receipt with
   what was paid, and the receipt says so in a word before it says so in a
   figure. */
.rcpt__paid { padding:22px 0 18px; }
.rcpt__stamp { display:inline-block; font-size:10.5px; letter-spacing:.13em;
               text-transform:uppercase; font-weight:700; color:var(--green);
               border:1.5px solid var(--green); border-radius:5px;
               padding:3px 9px; margin-bottom:10px; }
.rcpt__paid > b { display:block; font-size:38px; line-height:1.1;
                  font-variant-numeric:tabular-nums; color:var(--ink); }
.rcpt__paid .sub { margin-top:4px; }

.rcpt__grid { border-top:1px solid var(--hair); padding-top:14px; }
.rcpt__row { display:flex; justify-content:space-between; gap:20px;
             padding:6px 0; font-size:13.5px; }
.rcpt__row > span { color:var(--steel); flex:none; }
.rcpt__row > b { text-align:right; word-break:break-word; }

.rcpt h2 { font-size:11px; letter-spacing:.09em; text-transform:uppercase;
           color:var(--mute); margin:22px 0 8px; }
.rcpt__to { padding:16px 0 4px; border-top:1px solid var(--hair); margin-top:18px; }
.rcpt__to > span { display:block; font-size:10px; letter-spacing:.09em;
                   text-transform:uppercase; color:var(--mute); font-weight:700; }
.rcpt__msg { margin-top:16px; font-size:13.5px; color:var(--ink2); }
.rcpt__acts { display:flex; gap:8px; margin-top:22px; }
.rcpt__acts button { flex:1; }
.ok { color:var(--green); font-weight:600; }

/* ---- Cards on file -----------------------------------------------------------
   TWO LAYOUTS FOR THE SAME FACT, and that is deliberate rather than a
   duplication:

   .cardrow  is a row inside the customer record's Cards on file CARD, and is
             shaped like .proprow next to it so Details, Properties, Contacts
             and Cards all read as one grid of standing facts. A customer
             ringing up about "the card ending 4242" should be looking at the
             same row the person answering is.
   .pmlist   is the customer's own portal wallet, which is a phone-settings
             list rather than a desk card.  */
.cardrow { display:flex; align-items:center; gap:12px; }
.cardrow__main { flex:1; min-width:0; }
.cardrow__main > b { display:flex; align-items:center; gap:7px; }

.pmlist { list-style:none; margin:0 0 10px; padding:0; }
.pmlist__row {
  display:flex; align-items:center; gap:10px;
  padding:10px 0; border-bottom:1px solid var(--hair);
}
.pmlist__row:last-child { border-bottom:0; }
.pmlist__main { flex:1; min-width:0; }
.pmlist__main > b { display:block; }
.pmlist__main .sub { margin-top:2px; }
.pmlist__row .badge, .pmlist__row button { flex:none; }
.pmhost { min-height:24px; }

@media (max-width:560px) {
  /* The buttons wrap under the card rather than squeezing the name to nothing. */
  .pmlist__row { flex-wrap:wrap; }
  .pmlist__main { flex:1 0 100%; }
}

/* ---- The approval popup -------------------------------------------------------
   Shown the instant a card is approved, over whatever screen took it. It has
   to answer one question from across a counter, so the word and the amount are
   sized to be read standing up, and everything underneath is the proof a
   dispute would ask for.

   It reuses .rcpt__row from the receipt page deliberately: the popup, the
   printed receipt and the PDF are the same facts in the same order, and a
   customer comparing them should find nothing that moved. */
.rcptpop__hero { text-align:center; padding:6px 0 18px; }
.rcptpop__stamp {
  display:inline-block; font-size:10.5px; letter-spacing:.13em; text-transform:uppercase;
  font-weight:800; color:var(--green); background:var(--green-bg);
  border-radius:99px; padding:4px 12px;
}
.rcptpop__amt {
  display:block; margin-top:10px; font-size:34px; line-height:1.1;
  font-variant-numeric:tabular-nums; letter-spacing:-.02em;
}
.rcptpop__hero .sub { margin-top:6px; }
.rcptpop__grid { margin-top:4px; }
.rcptpop__inv { border-top:1px solid var(--hair); margin-top:12px; padding-top:10px; }

.rcptpop__send { border-top:1px solid var(--hair); margin-top:14px; padding-top:14px; }
.rcptpop__send > label {
  display:block; font-size:10.5px; letter-spacing:.09em; text-transform:uppercase;
  color:var(--steel); font-weight:700; margin-bottom:6px;
}
.rcptpop__send-row { display:flex; gap:8px; align-items:stretch; }
.rcptpop__send-row input { flex:1; min-width:0; }
.rcptpop__send-row button { flex:none; white-space:nowrap; }
.rcptpop__note { font-size:12.5px; color:var(--mute); margin-top:8px; }

/* Four across on a desk, two-by-two on a phone — never a single scrolling
   column of buttons, which is what flex-wrap gives you if the widths are left
   to the labels. */
.rcptpop__acts {
  display:grid; grid-template-columns:repeat(4, 1fr); gap:8px;
  margin-top:18px;
}
.rcptpop__acts button { width:100%; padding-left:6px; padding-right:6px; }
@media (max-width:560px) {
  .rcptpop__acts { grid-template-columns:repeat(2, 1fr); }
  .rcptpop__amt { font-size:30px; }
}

/* ---- Printing it -------------------------------------------------------------
   The browser's own Print is one of the two things Greg asked for, so what
   comes out of it has to be a receipt rather than a screenshot of a web page:
   no buttons somebody cannot press on paper, no card chrome, no shadow, and
   black text on white however the screen was themed. */
@media print {
  body, body.portal { background:#fff; }
  .paywrap { display:block; padding:0; min-height:0; }
  .paycard { border:0; border-radius:0; padding:0; max-width:none; box-shadow:none; }
  .rcpt__acts, .toasts, .modal, button { display:none !important; }
  .rcpt__paid > b { font-size:32px; }
  /* A table that breaks across pages must not lose its heading. */
  thead { display:table-header-group; }
  tr { break-inside:avoid; }
  a[href]:after { content:''; }
}

/* ---- Settings: a choice of three, and a preview ------------------------------
   Radios that are rows rather than dots in a line: each option carries a
   sentence explaining what it does to the business, and a sentence needs a
   line of its own. The whole row is the label, so the hit area is the option
   rather than a 13px circle. */
.optlist { display:flex; flex-direction:column; gap:8px; margin:4px 0 6px; }
/* A `label`, so it inherits the global form-label rule — 10px, uppercase,
   letter-spaced, gray. That is right for the word above a text box and quite
   wrong for a sentence somebody has to read, so it is undone here. */
.optrow { display:flex; gap:12px; align-items:flex-start; cursor:pointer;
          border:1px solid var(--hair); border-radius:var(--r-sm);
          padding:12px 14px; background:#fff;
          font-size:14px; letter-spacing:normal; text-transform:none;
          color:var(--ink); font-weight:400; }
.optrow:hover { border-color:var(--accent-lt); }
.optrow.is-on { border-color:var(--accent); background:var(--paper);
                box-shadow:0 0 0 2px rgba(42,110,158,.14); }
.optrow input[type=radio] { width:17px; height:17px; margin-top:2px; flex:none; }
.optrow__main b { display:block; font-size:14px; font-weight:600; color:var(--ink); }
.optrow__main .sub { text-transform:none; letter-spacing:normal;
                     font-weight:400; font-size:12.5px; }

/* Sub-switches that only exist because the one above them is on. Indented and
   railed so the dependency reads without a sentence explaining it. */
.list.is-nested { margin-left:18px; padding-left:16px;
                  border-left:2px solid var(--band); }

/* The receipt preview. A sketch of the customer's copy that redraws as the
   switches move — reading a column of checkboxes and imagining the result is
   exactly the work a preview should be doing instead. */
.rcptprev__card { border:1px solid var(--hair); border-radius:var(--r);
                  background:#fff; padding:18px; }
.rcptprev__head { display:flex; justify-content:space-between; align-items:baseline;
                  gap:12px; padding-bottom:12px; border-bottom:1px solid var(--hair); }
.rcptprev__head b { font-size:14px; }
.rcptprev__head span { font-size:12px; color:var(--mute);
                       font-variant-numeric:tabular-nums; }
.rcptprev__paid { padding:14px 0 12px; }
.rcptprev__paid > b { display:block; font-size:26px; line-height:1.15;
                      font-variant-numeric:tabular-nums; color:var(--ink); }
.rcptprev__rows, .rcptprev__inv { border-top:1px solid var(--hair); padding-top:8px; }
.rcptprev__inv { margin-top:8px; }
.rcptprev__to { border-top:1px solid var(--hair); margin-top:10px; padding-top:12px; }
.rcptprev__to > span { display:block; font-size:9.5px; letter-spacing:.09em;
                       text-transform:uppercase; color:var(--mute); font-weight:700; }
.rcptprev__msg { margin-top:12px; font-size:12.5px; color:var(--ink2); }

/* ---- Internal files ----------------------------------------------------------
   Marked on the tile itself rather than in a list elsewhere: the entire value
   of the flag is knowing at a glance which of these the customer can see. The
   badge reads as a warning, the toggle sits beside Remove and stays quiet
   until the tile is hovered — the same weight as the action next to it. */
.gallery__lock { position:absolute; top:6px; left:6px; z-index:2;
                 background:var(--amber); color:#fff;
                 font-size:9.5px; font-weight:700; letter-spacing:.07em;
                 text-transform:uppercase; padding:3px 7px; border-radius:5px;
                 box-shadow:0 1px 3px rgba(10,23,48,.35); }
.gallery__eye { position:absolute; top:6px; right:36px; width:26px; height:26px;
                border-radius:50%; border:0; cursor:pointer; line-height:1; z-index:2;
                background:rgba(10,23,48,.6); color:#fff; font-size:14px; min-height:0;
                box-shadow:none; padding:0;
                display:flex; align-items:center; justify-content:center; }
.gallery__eye:hover { background:var(--navy); }

/* ---- Finding a receipt ------------------------------------------------------- */
.rcptbox__head { display:flex; justify-content:space-between; align-items:baseline;
                 gap:12px; padding-bottom:12px; margin-bottom:4px;
                 border-bottom:1px solid var(--hair); }
.rcptbox__head b { font-size:16px; font-variant-numeric:tabular-nums; }
.rcptbox .row2 { align-items:end; }

/* ---- A project, as the customer sees it --------------------------------------
   A vertical run of stages with a rail down the side, because what a customer
   wants from this screen is "where are we up to" — a sequence, read top to
   bottom, not a table. The completed ones go quiet rather than disappearing:
   progress is only legible against what came before it. */
.pcard__head { display:flex; justify-content:space-between; align-items:flex-start;
               gap:16px; }
.pcard__head h1 { font-size:20px; margin:0 0 4px; }
.pcard__head p { margin:0; font-size:13.5px; }

.pphases { list-style:none; margin:0; padding:0 0 0 22px; position:relative; }
.pphases:before { content:''; position:absolute; left:5px; top:6px; bottom:6px;
                  width:2px; background:var(--band); border-radius:2px; }
.pphase { position:relative; background:#fff; border:1px solid var(--hair);
          border-radius:var(--r); padding:14px 16px; margin-bottom:10px; }
.pphase:before { content:''; position:absolute; left:-21px; top:18px;
                 width:12px; height:12px; border-radius:50%;
                 background:#fff; border:2px solid var(--accent-lt); }
.pphase.is-done:before { background:var(--green); border-color:var(--green); }
.pphase.is-done { background:var(--paper); }
.pphase__head { display:flex; justify-content:space-between; align-items:baseline;
                gap:12px; }
.pphase__head b { font-size:14.5px; }
.ptasks { list-style:none; margin:10px 0 0; padding:10px 0 0;
          border-top:1px solid var(--hair); }
.ptasks li { display:flex; justify-content:space-between; gap:12px;
             font-size:13px; padding:4px 0; }

@media (max-width:520px) {
  .pcard__head { flex-direction:column; gap:8px; }
}

/* ---- The team ---------------------------------------------------------------
   A suspended member stays on the list and goes quiet: removing them from view
   is how you forget somebody still has an account. */
.rowline.is-off { opacity:.55; }
.rowline.is-off:hover { opacity:.8; }
.chip--role { background:var(--band); color:var(--ink2); font-weight:700;
              margin-right:8px; }
/* A destructive action sitting in a row of link-buttons.
   This was red text on a red fill — unreadable, and reported as such. Two rules
   collided: `button.link` sets background:none and padding:0, `button.danger`
   sets a red background and white text, and .danger wins the background on
   source order. Then this rule's own `color:var(--red)` — three classes deep,
   so more specific than `button.danger` — put the text back to red ON that red.
   Now it is deliberate: a small red pill with white text and room to breathe. */
.rowline__act button.link.danger {
  background:var(--danger); color:#fff;
  padding:5px 12px; min-height:0; border-radius:99px;
  font-size:12px; font-weight:600; line-height:1.35;
  box-shadow:none;
}
.rowline__act button.link.danger:hover {
  background:var(--danger-dark); color:#fff; text-decoration:none;
}


/* ---- Offering the portal from a customer -------------------------------------
   Shown where the dead end used to be: a customer with no contacts had the
   words "No contacts." and nothing to press, while a portal login needs a
   contact to bind to. This makes the contact on the way through. */
.portalcta { display:flex; align-items:center; gap:16px; flex-wrap:wrap;
             background:var(--paper); border:1px solid var(--hair);
             border-radius:var(--r); padding:14px 16px; margin-bottom:12px; }
.portalcta__main { flex:1; min-width:200px; }
.portalcta__main b { font-size:14px; }
.portalcta button { flex:none; }

/* ---- Fees in the batch -------------------------------------------------------
   The figure the daily close is actually read for by whoever is reconciling.
   Green because it is money kept, not money owed — the same green the receipt
   uses for PAID, so the two documents agree about what good news looks like.
   The summary cell that used to carry it became the .feebox line (#28). */
.feeamt { color:var(--green); font-weight:600; }

/* ---- A customer's project history --------------------------------------------
   Rows rather than the tiles the Projects screen uses: this is a list read for
   "what have we done for them and when", scanned top to bottom, not a board of
   work in progress. */
/* The row of three cards above it has no bottom margin, so this sat flush
   against them. 26px is what the page already puts between major blocks —
   `section` uses it — so this joins the existing rhythm rather than inventing
   a gap of its own. */
.projhistcard { margin-top:26px; }
.projhist .card__head { margin-bottom:10px; }
.projhist__list { margin-top:12px; }
.projhist__money { text-align:right; min-width:120px; flex:none; }
.projhist__money b { font-variant-numeric:tabular-nums; }
.projhist__status { min-width:96px; text-align:right; flex:none; }
.projhist__custom { display:flex; gap:12px; align-items:flex-end; flex-wrap:wrap;
                    margin-top:12px; }
/* `hidden` is only display:none in the UA sheet, so any display declaration on
   the element beats it and the panel stays visible. Stated here or the custom
   range shows under every preset. */
.projhist__custom[hidden] { display:none; }
.projhist__custom .field { margin:0; }
.projhist__custom button { flex:none; }
/* A compact tab strip for a filter that lives inside a card, rather than the
   page-level one. */
.tabs--sm .tab { font-size:12.5px; padding:6px 12px; }

@media (max-width:640px) {
  .projhist__money, .projhist__status { text-align:left; min-width:0; }
}

/* ---- Terminal: the pinned face, the docs, the commit bar ---------------------

   THE FACE IS PINNED, and this is the whole reason the picture exists: you
   watch the number land on it while you key it. It sticks to the top of its
   own column, so the fields underneath scroll past it rather than it scrolling
   away from them. 62px clears the sticky topbar (48) plus the grid gap (14).

   The band is opaque and bled out to the card's padding, so what passes behind
   it goes UNDER a clean edge instead of showing around the card's corners — a
   transparent sticky element reads as a rendering fault. --solid-bg is 94%
   opaque, which left the fields just legible through it, so it is blurred too;
   fully opaque white would sit a shade brighter than the panel and show a
   seam. */
.termface {
  position:sticky; top:62px; z-index:1;
  background:var(--white);
  margin:0 -18px 14px; padding:0 18px 14px;
}
/* THE BAND AND ITS PANEL HAVE TO BE THE SAME COLOUR, and that is why these two
   cards are opaque white while every other .card in the app is --solid-bg
   (94% white). A translucent band is not an option — the fields scrolling
   underneath ghost straight through it and read as a rendering fault, which
   is what a frosted blur here actually looked like. But painting only the band
   opaque puts a brighter rectangle inside a dimmer panel and shows a seam.
   Both, or neither. Over this page's near-white gradient the 6% is not a
   colour anybody can see; a seam between two of them would have been. */
/* A GRID OF ITS OWN, not the shared .cols2, which is
   `repeat(auto-fit, minmax(340px, 1fr))` and resolves to THREE tracks on a wide
   screen — so this page's two cards sat in two of them and left the last third
   of the monitor empty.

   Capped at two tracks by giving every track a floor of half the container, so
   the count follows THIS BOX rather than the viewport: the nav rail is 224px,
   and a viewport media query is wrong by a rail's width. Below the floor 340px
   takes over, two no longer fit, and auto-fit drops to one on its own. */
.termgrid {
  display:grid; gap:14px;
  grid-template-columns:repeat(auto-fit, minmax(max(340px, (100% - 14px) / 2), 1fr));
}

/* BOTH COLUMNS END TOGETHER. Grid items stretch by default, but only if
   nothing sets them otherwise — the right card was ending short wherever its
   contents were shorter than the left, which on a page made of two white
   panels reads as one of them being cut off. */
.termgrid > .card { background:var(--white); align-self:stretch; }
/* Smaller here than on the pay page. A 340px face is 214px tall, and pinned
   that is a third of a laptop viewport spent on a picture — enough of the
   screen that the fields it accompanies get pushed off it. */
.termface .vcard { max-width:296px; }

/* Reference material, folded away. It is the tenth payment of the day that
   sets the layout, not the first. */
.termdocs { margin-top:18px; border-top:1px solid var(--hair); padding-top:12px; }
.termdocs > summary { cursor:pointer; font-weight:600; font-size:13px;
                      color:var(--mute); list-style:none; padding:4px 0; }
.termdocs > summary::-webkit-details-marker { display:none; }
.termdocs > summary::before { content:'▸ '; display:inline-block; width:1em; }
.termdocs[open] > summary::before { content:'▾ '; }
.termdocs > summary:hover { color:var(--ink); }
.termdocs h3 { font-size:12.5px; letter-spacing:.04em; margin:14px 0 4px; }
.termdocs p { margin:0; font-size:12.5px; line-height:1.5; }

/* Tighter than the app's default form rhythm, and only here. This column
   carries nine stacked fields and then the money; at the standard 12px gap it
   ran past the card column and left the total below the fold on a laptop. The
   CONTROLS are untouched — same height, same padding as the card column
   beside them, because two columns of inputs at different sizes is the kind of
   difference you feel without being able to name it. Only the air between them
   is smaller. */
.termpay { gap:9px; }
.termpay .field { gap:3px; }
/* A little more room before the money than between two fields: it is the
   answer, not another question. */
.termpay .termtotals { margin-top:5px; }

/* One column: nothing is beside anything, so a pinned face would just hover
   over the fields it belongs to. */
@media (max-width:700px) {
  .termface { position:static; margin:0 0 14px; padding:0; }
}

/* ---- Filtering the columns ---------------------------------------------------
   The summary carries the state. A board showing three of eight crews looks
   identical to a shop that only has three, so the control says which it is
   rather than leaving the office manager to wonder where Sheet metal went. */
.calfilter summary.is-on { background:var(--accent); border-color:var(--accent);
                           color:#fff; }
.calfilter[open] summary.is-on { background:var(--accent); }
.calfilter__pop { min-width:230px; max-height:360px; }
/* Twenty people scroll inside the menu rather than running off the screen.
   The rows are tightened first: a checkbox wearing the app's form padding made
   each one 64px tall, so an eight-crew shop had to scroll to see a list that
   easily fits. */
.calfilter__list { max-height:250px; overflow-y:auto; margin:4px 0; }
.calfilter__list .dd__item { padding:5px 10px; gap:8px; }
.calfilter__list input[type="checkbox"] { flex:none; width:15px; height:15px;
                                          margin:0; padding:0;
                                          /* The app gives inputs a 40px tap
                                             target; here the whole label row is
                                             the target, so the box itself does
                                             not need to be one. */
                                          min-height:0; }
.calfilter__name { flex:1; min-width:0; overflow:hidden; text-overflow:ellipsis;
                   white-space:nowrap; }
.calfilter__pend { font-size:10.5px; color:var(--mute); text-transform:uppercase;
                   letter-spacing:.05em; flex:none; }
.calfilter__pop .hint { padding:4px 10px 2px; }
.calfilter__pop .btnrow { padding:2px 4px 0; }

/* Somebody who has been added but has not signed in yet. They are schedulable,
   so the name is not dimmed — only the sub-line says so. */
.calteam__person.is-pend .sub,
.calteamw__who.is-pend .sub { color:var(--amber); }
.calteamw__who .sub { font-size:10.5px; }

/* ---- The team boards ---------------------------------------------------------
   Two shapes for two questions. TEAM DAY is the dispatch board — a column per
   person, time down the page — and answers "who is free at two". TEAM WEEK is
   the planning board — a row per person, the week across — and answers "who is
   light on Thursday".

   Both are built so the header cannot drift out of line with its cells: a
   column board that quietly lies about whose work is whose is worse than no
   board. See .calteam__scroll below for how. */
.calteam { border:1px solid var(--hair); border-radius:var(--r); overflow:hidden;
           background:#fff; }

/* ONE scroller holding the head AND the body.

   They used to be siblings, with only the body scrolling. Two columns hid it;
   nine did not — scrolling right slid the columns under a set of names that
   stayed put, so the board confidently labeled Maintenance's work as Service
   North's. A calendar that lies about whose job it is, is worse than no
   calendar. The head is now inside the scroller and sticky to the top, and the
   hour gutter is sticky to the left, so the two things that say what a box
   MEANS — whose column, what time — are the two things that never leave. */
.calteam__scroll { max-height:calc(100vh - 300px); max-height:calc(100dvh - 300px); overflow:auto; }

/* Wide enough for its columns, never narrower than the scroller: below the
   width where 150px columns fit, this grows and the board scrolls sideways;
   above it, the columns share the space instead of huddling at the left. */
.calteam__track { min-width:max-content;
                   /* The floor a column may shrink to before the board scrolls
                      sideways instead. 120px is where a crew name, a count and
                      a wrapped job title still read; eight crews and the
                      unassigned column clear a laptop at that width, which is
                      the case this board is for. Columns grow past it to share
                      whatever the screen actually has. */
                   --calcolmin:120px; }

/* The head and the body are the same flex layout, deliberately — a grid for
   one and a flex row for the other is how two rows of boxes that must line up
   stop lining up. */
.calteam__head,
.calteam__body { display:flex; align-items:stretch; }
.calteam__head { background:var(--band); position:sticky; top:0; z-index:6; }
.calteam__head > .calgutter__spacer { flex:none; width:64px; position:sticky;
                                      left:0; z-index:7; background:var(--band);
                                      border-right:1px solid var(--hair); }
.calteam__person { flex:1 1 0; min-width:var(--calcolmin); padding:10px 9px;
                   border-left:1px solid var(--hair); }
/* Sized so a two-word crew name — "Service North" is the long one in practice —
   still fits a column at the floor width rather than ellipsing. A dispatch
   board where half the columns read "Service..." is no board at all. */
.calteam__person b { display:block; font-size:12.5px; white-space:nowrap;
                     overflow:hidden; text-overflow:ellipsis; }
.calteam__person .sub { font-size:11px; }
.calteam__person.is-none b { color:var(--steel); }

/* Team week */
/* ---- The team week -----------------------------------------------------------
   A matrix: rows are crews, columns are days. It cannot be a time canvas — both
   axes are already spoken for — so it matches the rest in language instead: the
   week canvas's day headings, the month's solid chips, the same today marker.

   The crew names are pinned to the left edge so a fortnight can scroll sideways
   without losing track of whose row you are reading. */
.calteamw__scroll { overflow-x:auto; border:1px solid var(--hair);
                    border-radius:var(--r); background:#fff; }
.calteamw { min-width:100%; }
.calteamw--long { min-width:max-content; }
.calteamw__row { display:flex; border-top:1px solid var(--hair); }
.calteamw__row:first-child { border-top:0; }
.calteamw__row--head { background:var(--band); position:sticky; top:0; z-index:3; }

.calteamw__who { width:170px; flex:none; padding:9px 12px;
                 border-right:1px solid var(--hair); min-width:0;
                 position:sticky; left:0; z-index:2; background:#fff; }
.calteamw__row--head .calteamw__who { background:var(--band); z-index:4; }
.calteamw__who b { font-size:13px; white-space:nowrap; overflow:hidden;
                   text-overflow:ellipsis; display:flex; align-items:center;
                   gap:7px; }
.calteamw__who .sub { display:block; font-size:10.5px; color:var(--mute);
                      margin-top:2px; }
.calteamw__who.is-none b { color:var(--steel); }
.calteamw__wholbl { font-size:10.5px; letter-spacing:.06em;
                    text-transform:uppercase; color:var(--steel);
                    font-weight:600; }

.calteamw__side { flex:1; min-width:0; display:flex; flex-direction:column; }
.calteamw__days { display:grid;
                  grid-template-columns:repeat(var(--calcols,7), minmax(0, 1fr));
                  flex:1; }
/* The week canvas's heading, in a row: small uppercase weekday over a large
   date, with today in an accent pill. */
.calteamw__day { padding:7px 8px; text-align:center;
                 border-left:1px solid var(--hair); }
.calteamw__day b { display:block; font-size:10.5px; letter-spacing:.06em;
                   text-transform:uppercase; color:var(--steel); }
.calteamw__num { font-size:15px; font-weight:600; line-height:1.25;
                 font-variant-numeric:tabular-nums; display:inline-block; }
.calteamw__day.is-today .calteamw__num { background:var(--accent); color:#fff;
                                         border-radius:999px; padding:0 7px; }

.calteamw__cell { min-height:74px; padding:5px; border-left:1px solid var(--hair);
                  min-width:0; cursor:pointer; display:flex;
                  flex-direction:column; gap:3px; }
.calteamw__cell:hover { background:var(--paper); }
.calteamw__cell.is-today { background:rgba(75,156,211,.05); }
/* A fortnight keeps each day readable rather than sharing the width fourteen
   ways; the scroller takes the overflow. */
.calteamw--long .calteamw__day,
.calteamw--long .calteamw__cell { min-width:132px; }

/* Solid, exactly as in the month and on the canvases. The row already says
   which crew it is, so within a row the color is confirmation rather than
   information — but switching Color by to Status or Service type turns it
   back into information without the board changing shape. */
.calteamw .calchip {
  background:currentColor; border:0; border-left:3px solid rgba(0,0,0,.22);
  border-radius:4px; padding:2px 6px; min-height:17px;
}
.calteamw .calchip .calchip__label { color:#fff; font-size:11px; }
.calteamw .calchip .calchip__time { color:rgba(255,255,255,.85); }
.calteamw .calchip:hover { filter:brightness(1.08);
                           box-shadow:0 1px 5px rgba(19,41,75,.22); }
.calteamw .calchip--done { background:#fff; border:1px solid var(--hair);
                           border-left:3px solid currentColor; }
.calteamw .calchip--done .calchip__label { color:var(--mute);
                                           text-decoration:line-through; }
.calteamw .calchip--done .calchip__time { color:var(--mute); }
.calteamw .calchip--done:hover { filter:none; background:var(--paper); }

/* ---- Crew commitments --------------------------------------------------------
   A project that names a crew and a target date holds that crew for a stretch,
   drawn as one band across the days rather than a chip per day.

   IT SITS ABOVE THE CELLS, NOT OVER THEM. An overlay would have to sit on top
   of the day cells to span them, and would then swallow the clicks that
   schedule work — the band's whole purpose is to inform a scheduler, so
   stealing the scheduling gesture would be self-defeating. The days grid is
   wrapped in a column instead and the band gets its own row of the same seven
   tracks, so it lines up exactly and blocks nothing.

   TINTED, NOT SOLID. This is a claim on capacity, not a booking; it must stay
   quieter than the actual stops sitting underneath it. --cband carries the
   crew's own color so the band and the crew's swatch read as the same thing,
   with a neutral fallback for a crew that has none. */
.calteamw__side { flex:1; min-width:0; display:flex; flex-direction:column; }
.calteamw__bands { display:grid;
                    grid-template-columns:repeat(var(--calcols,7), minmax(0, 1fr));
                   gap:2px; padding:3px 0 0; }
.cband { --cband:var(--steel);
         background:color-mix(in srgb, var(--cband) 14%, transparent);
         border:1px solid color-mix(in srgb, var(--cband) 38%, transparent);
         border-left-width:3px; border-radius:4px;
         padding:2px 6px; min-width:0; cursor:pointer;
         display:flex; align-items:center; }
.cband span { font-size:10.5px; font-weight:600; letter-spacing:.01em;
              color:color-mix(in srgb, var(--cband) 72%, var(--ink));
              white-space:nowrap; overflow:hidden; text-overflow:ellipsis; }
.cband:hover { background:color-mix(in srgb, var(--cband) 22%, transparent); }
/* Running in from before this week, or out past the end of it: the cap on that
   side goes, so the band reads as continuing rather than as starting here. */
.cband.is-open { border-left-width:1px; border-top-left-radius:0;
                 border-bottom-left-radius:0; }
.cband.is-tail { border-right:0; border-top-right-radius:0;
                 border-bottom-right-radius:0; }
/* Day view: one day has nothing to span, so it is a tag under the crew name. */
.cbandrow { display:flex; flex-direction:column; gap:2px; margin-top:4px; }
.cband.is-inline { padding:1px 5px; }

/* ---- Dragging work about -----------------------------------------------------
   A drop moves real work, so the target has to be unmistakable while the drag
   is in flight and invisible the rest of the time. */
.calchip[draggable="true"] { cursor:grab; }
.calchip.is-dragging { opacity:.4; cursor:grabbing; }
.is-dragging-job .calteamw__cell,
.is-dragging-job .calday__row,
.is-dragging-job .calcell,
.is-dragging-job .calweekc__body .calcol { transition:background .1s ease; }
.is-dropping { background:rgba(42,110,158,.14) !important;
               outline:2px solid var(--accent); outline-offset:-2px; }

/* An undoable toast lives longer and carries its way out on the right. */
.toast--action { display:flex; align-items:center; gap:14px; }
.toast__do { background:rgba(255,255,255,.16); color:#fff; border:0;
             font-size:12px; font-weight:700; padding:5px 12px; border-radius:99px;
             min-height:0; box-shadow:none; flex:none; cursor:pointer; }
.toast__do:hover { background:rgba(255,255,255,.28); }

@media (max-width:820px) {
  .calteamw__who { width:110px; }
  .calteam__scroll { max-height:none; }
}

/* ---- A day as a canvas -------------------------------------------------------
   Hour rows were a lie about duration — a four-hour install and a fifteen
   minute call drew the same box — and two jobs at the same time stacked so the
   second read as "later" rather than "at once". A job's top is now where it
   starts and its height is how long it runs; overlapping work shares the width.
   Positions are computed in JS (see layout()); this only has to get out of the
   way. */
.calcol { position:relative; border-left:1px solid var(--hair); min-width:0;
          cursor:pointer; }
.callines { position:absolute; inset:0; pointer-events:none; }
.calline { border-bottom:1px solid var(--hair); }
.calline:last-child { border-bottom:0; }

.calgutter { flex:none; width:64px; position:sticky; left:0; z-index:5;
             background:#fff; }
.calgutter__spacer { display:block; }
.calgutter__h { font-size:10.5px; color:var(--mute); text-align:right;
                padding:2px 8px 0 0; border-right:1px solid var(--hair);
                border-bottom:1px solid var(--hair); box-sizing:border-box; }

/* A chip that knows its own length. Absolutely placed, so it needs its own
   padding and a minimum height that keeps one line readable. */
/* align-items:stretch overrides the baseline alignment the base .calchip sets
   for its single-line form. In a COLUMN the cross axis is horizontal, so
   baseline sized every stacked row to its own text and the lines overflowed
   the chip instead of ellipsing — hard-clipped mid-word by overflow:hidden,
   which is exactly the case (a chip narrowed by an overlap) where the
   customer's name most needs to end in a legible "…". */
.calchip--box { position:absolute; margin:0; overflow:hidden;
                border-left-width:3px; border-left-style:solid;
                display:flex; flex-direction:column; align-items:stretch; gap:1px;
                padding:3px 6px; box-sizing:border-box; z-index:1;
                /* A tint of the job's own color rather than plain white. Four
                   overlapping boxes on a white canvas read as one pale mass;
                   a filled block reads as four. The tint is faint enough that
                   the label stays black-on-near-white rather than needing a
                   second color decision per job type. */
                background:
                  linear-gradient(0deg, rgba(255,255,255,.86), rgba(255,255,255,.86)),
                  linear-gradient(0deg, currentColor, currentColor);
                box-shadow:0 1px 2px rgba(19,41,75,.10); }
/* currentColor drives the tint, so it has to BE the job's color — the label
   sets its own so it does not inherit a wash. */
.calchip--box { color:var(--steel); }
.calchip--box .calchip__label,
.calchip--box .calchip__time { color:var(--ink); }
.calchip--box .calchip__time { color:var(--steel); }
.calchip--box:hover { box-shadow:0 2px 8px rgba(19,41,75,.18); z-index:2; }
.calchip--box .calchip__time { font-size:10px; }
.calchip--box .calchip__label { white-space:normal; line-height:1.25; }
/* Side by side, with a hairline so two touching boxes do not read as one. */
.calchip--box + .calchip--box { border-left-color:inherit; }
.calcol .calchip--box { outline:1px solid #fff; outline-offset:-1px; }

/* Where we are in the day. Only ever drawn on today. */
.calnow { position:absolute; left:0; right:0; height:0;
          border-top:2px solid var(--red); z-index:3; pointer-events:none; }
.calnow__dot { position:absolute; left:-4px; top:-4px; width:8px; height:8px;
               border-radius:50%; background:var(--red); }

/* Team day: gutter plus one canvas per column, scrolled together. The column
   rule matches .calteam__person exactly; see the note on .calteam__head. */
.calteam__body > .calcol { flex:1 1 0; min-width:var(--calcolmin); }
.calteam__swatch { display:inline-block; width:8px; height:8px; border-radius:2px;
                   margin-right:6px; vertical-align:baseline; }

/* Day view uses the same canvas, with one column. */
.calday__scroll { max-height:calc(100vh - 300px); max-height:calc(100dvh - 300px); overflow:auto;
                  border:1px solid var(--hair); border-radius:var(--r);
                  background:#fff; }
.calday__body { display:flex; align-items:stretch; }
.calday__body > .calcol { flex:1; }

/* ---- Activity: who changed what -----------------------------------------------
   A log is read by scanning down one column until a name or a time catches the
   eye, then stopping. So the row is a fixed grid — time, person, sentence — and
   the columns line up all the way down rather than reflowing per row. The diff
   only appears when a row is opened; a screen that shows every field of every
   change is one nobody reads twice. */

.actv__filters { display:grid; grid-template-columns:repeat(4, minmax(0, 1fr));
                 gap:12px; margin:14px 0 6px; }
.actv__custom { grid-column:1 / -1; display:grid;
                grid-template-columns:repeat(2, minmax(0, 220px)); gap:12px; }
@media (max-width:820px) { .actv__filters { grid-template-columns:repeat(2, minmax(0, 1fr)); } }

.actv__list { border-top:1px solid var(--hair); margin-top:10px; }

.actv__day { position:sticky; top:0; z-index:1; background:var(--band);
             color:var(--ink2); font-size:10.5px; font-weight:700;
             letter-spacing:.07em; text-transform:uppercase;
             padding:6px 10px; border-bottom:1px solid var(--hair); }

/* ══ THE ACTIVITY ROW ═════════════════════════════════════════════════════
   Greg, 2026-09-04: "pick 2 with 1s english."

   Three columns — when, who and what, then the change itself. The values used
   to live in a panel that only appeared after a click and are now part of the
   row, so the grid lost its fourth "1 field" column and gained a wide one. */
.actv__row { display:grid; grid-template-columns:62px 210px minmax(0, 1fr);
             gap:12px; align-items:start; padding:10px;
             border-bottom:1px solid var(--hair); font-size:13px; }
.actv__row:hover { background:var(--paper); }

.actv__time { color:var(--mute); font-size:11.5px; padding-top:2px;
              font-variant-numeric:tabular-nums; }

.actv__who { min-width:0; }
.actv__name { display:block; font-weight:700; color:var(--ink);
              overflow:hidden; text-overflow:ellipsis; white-space:nowrap; }
/* Not a name. See the note in row() — an unattributed row must not read like a
   colleague on a page that promises to list only colleagues. */
.actv__nobody { font-weight:500; color:var(--mute); font-style:italic; }
.actv__did { color:var(--steel); font-size:11.5px; margin-top:2px;
             overflow:hidden; text-overflow:ellipsis; white-space:nowrap; }

/* The verb carries the weight: 'deleted' is the one a manager is scanning for,
   so it is the one that is red. */
.actv__verb { font-size:10.5px; text-transform:uppercase; letter-spacing:.05em;
              font-weight:700; }
.actv__verb--created { color:var(--green, #2FA37A); }
.actv__verb--edited  { color:var(--accent); }
.actv__verb--deleted { color:var(--red); }
.actv__row.is-gone .actv__did { text-decoration:line-through;
                                text-decoration-color:var(--mute); }

.actv__delta { display:grid; gap:4px; min-width:0; }
.actv__note { font-size:11.5px; color:var(--steel); }
.actv__none { font-size:12.5px; color:var(--mute); }

/* The field name is a fixed column so the labels line up down the list; the two
   values then sit NEXT to their arrow rather than in tracks of their own. On a
   wide screen equal tracks threw "Net 30" and "→" half the card apart, which
   reads as two unrelated facts instead of one change. */
.actv__line { display:flex; gap:8px; align-items:baseline; font-size:12.5px; }
.actv__field { width:190px; flex:none; color:var(--mute);
               overflow:hidden; text-overflow:ellipsis; white-space:nowrap; }

/* The old value struck through and the new one plain, both as chips so the
   pair reads as one change rather than as two words that happen to be near
   each other. Struck through VISIBLY — at --hair the line was in the
   stylesheet and absent on the screen. */
.actv__from { flex:0 1 auto; color:var(--red); background:var(--red-bg);
              padding:2px 6px; border-radius:4px; font-size:12px;
              text-decoration:line-through; text-decoration-thickness:1px;
              text-decoration-color:color-mix(in srgb, var(--red) 60%, transparent);
              overflow:hidden; text-overflow:ellipsis; white-space:nowrap; }
.actv__to { flex:0 1 auto; color:var(--green); background:var(--green-bg);
            padding:2px 6px; border-radius:4px; font-size:12px; font-weight:600;
            overflow:hidden; text-overflow:ellipsis; white-space:nowrap; }
.actv__arrow { color:var(--mute); flex:none; }
.actv__more { font-size:11.5px; justify-self:start; padding:0; }

@media (max-width:820px) {
  .actv__row { grid-template-columns:52px minmax(0, 1fr); }
  .actv__delta { grid-column:2; }
  .actv__field { width:100%; }
  .actv__line { flex-wrap:wrap; gap:2px 8px; }
  .actv__arrow { display:none; }
}
}

.actv__empty { padding:28px 10px; display:grid; gap:4px; justify-items:center;
               text-align:center; }
.actv__pager { display:flex; align-items:center; justify-content:space-between;
               gap:12px; margin-top:12px; flex-wrap:wrap; }

/* ---- Product categories ------------------------------------------------------
   The list a business files its price book under. Managed from the Products
   page, offered as a dropdown when an item is created. */

.catmgr__add { display:flex; gap:8px; align-items:center; margin-bottom:14px; }
.catmgr__add input { flex:1; min-width:0; }
/* The inline "add a category" row inside the item form. Tucked under the
   dropdown it belongs to rather than floating beside it. */
.prodcat__new { margin-top:-4px; }
.prodcat__new .btnrow { margin-top:8px; }

/* ---- Terminal: which invoices are being paid ---------------------------------
   A row per open invoice, ticked. Wide targets and a visible ticked state,
   because this is operated at a counter with somebody waiting — reading it
   wrong takes money off the wrong invoice. */

.picklist { border:1px solid var(--hair); border-radius:var(--r-sm);
            overflow:hidden; margin-top:8px; background:#fff; }
.picklist__row { display:flex; align-items:center; gap:10px; padding:9px 12px;
                 border-bottom:1px solid var(--hair); cursor:pointer;
                 margin:0; font-weight:400; }
.picklist__row:last-child { border-bottom:0; }
.picklist__row:hover { background:var(--paper); }
.picklist__row.is-on { background:var(--band); }
.picklist__row input[type="checkbox"] { flex:none; width:16px; height:16px;
                                        min-height:0; margin:0; padding:0; }
.picklist__main { flex:1; min-width:0; }
.picklist__main b { font-size:13px; }
.picklist__main .sub { font-size:11px; }
.picklist__amt { font-weight:600; font-variant-numeric:tabular-nums;
                 white-space:nowrap; }
.picklist__foot { display:flex; align-items:center; justify-content:space-between;
                  gap:12px; flex-wrap:wrap; margin-top:8px; font-size:12.5px;
                  color:var(--steel); }
.picklist__foot .btnrow { margin:0; gap:12px; }
/* A ticked invoice the money does not reach. Stated, not omitted. */
.allocrow.is-none { opacity:.62; }
.allocrow.is-none .allocrow__amt { color:var(--mute); }

/* ---- Charges on a service call ----------------------------------------------
   What the call is billing for, itemized. Quantities carry the business's own
   unit, so "1.34 lb" reads the way they say it. */

.joblines { border:1px solid var(--hair); border-radius:var(--r-sm);
            overflow:hidden; background:#fff; }
.jobline { display:flex; align-items:center; gap:12px; padding:9px 12px;
           border-bottom:1px solid var(--hair); }
/* A charge the job TYPE brings, drawn before it exists. Not clickable, and set
   apart so it never reads as a line somebody added by hand — it lands when the
   stop is saved and is edited on the stop afterwards. */
.jobline--auto { background:var(--paper); border-style:dashed; cursor:default; }
.jobline--auto .jobline__main b { color:var(--ink2); }
.jobline:last-child { border-bottom:0; }
.jobline__main { flex:1; min-width:0; }
.jobline__main b { font-size:13px; }
.jobline__main .sub { font-size:11.5px; }
.jobline__amt { font-weight:600; font-variant-numeric:tabular-nums; white-space:nowrap; }
.jobline__act { display:flex; gap:10px; flex-shrink:0; }
.jobline__total { display:flex; align-items:baseline; justify-content:space-between;
                  gap:12px; margin-top:10px; padding-top:10px;
                  border-top:1px solid var(--hair); font-size:13px; }
.jobline__total b { font-size:16px; font-variant-numeric:tabular-nums; }

/* ---- Reports: the last fortnight ---------------------------------------------
   Fourteen narrow columns rather than a handful of wide ones, so the labels go
   short and the quiet days stay visible instead of collapsing to nothing. */
.chart--days .chart__col { min-width:0; }
.chart--days .chart__val { font-size:10px; }
.chart--days .chart__lab { font-size:10px; white-space:nowrap; }
.chart--days .chart__col.is-quiet .chart__bar { background:var(--hair); }
.daytot { display:flex; align-items:baseline; gap:8px; }
.daytot b { font-size:19px; font-variant-numeric:tabular-nums; }

/* ---- Tab strips used INLINE in a toolbar ------------------------------------
   .tabs carries a 14px bottom margin, which is right when a strip sits above
   the content it switches. In a centered flex row that margin is part of the
   box being centered, so the strip rides about 7px high and every control
   beside it looks misaligned — which is exactly how it looked on the calendar,
   where the tabs sat at y=132 and Customize and the crew filter at y=138. */
.calbar > .tabs,
.txbar > .tabs { margin-bottom:0; }

/* And the dropdowns beside them wear the tab's pill rather than the form
   control's rounded rectangle: 6px/13px and weight 500 make them the same
   33px-high shape, so the row reads as one set of controls instead of two
   kinds that nearly line up. */
.calbar > .dd > summary,
.txbar > .dd > summary { padding:6px 13px; font-weight:500; border-radius:99px; }

/* A file whose bytes were archived. The tile stays, so the project still shows
   what was taken and when — an archived record reads as a record; a missing
   tile reads as data loss. */
.gallery__gone { display:flex; flex-direction:column; align-items:center;
                 justify-content:center; gap:4px; height:100%;
                 background:var(--band); color:var(--steel); font-size:12px; }
.gallery__gone-mark { font-size:22px; color:var(--mute); line-height:1; }
.gallery__gone small { color:var(--mute); font-size:11px; }

/* ---- The pager --------------------------------------------------------------
   One control under every list. Hidden entirely when everything fits, because
   a row that only ever says "1–7 of 7" is furniture. */
.pager { display:flex; align-items:center; justify-content:space-between;
         gap:12px; flex-wrap:wrap; margin-top:12px; }
.pager__count { font-size:12.5px; color:var(--steel);
                font-variant-numeric:tabular-nums; }
.pager__act { display:flex; align-items:center; gap:8px; }
.pager__act select { width:auto; min-height:0; padding:6px 26px 6px 10px;
                     font-size:12.5px; }
/* No count and no buttons — the size on its own belongs at the right, where
   the size control sits on every paged list. */
.pager--size { justify-content:flex-end; }

/* ---- The card form (Freedom to Design) --------------------------------------
   Our markup, Dejavoo's field ids. The number sits in a relative box so the
   detected brand can sit inside the field's right edge — reading back the
   scheme as it is typed is the cheapest confirmation that the digits went in
   correctly. */
.ftdform { display:flex; flex-direction:column; gap:12px; margin:14px 0; }
.ftdnum { position:relative; }
.ftdnum input { padding-right:96px; font-variant-numeric:tabular-nums; letter-spacing:.02em; }
.ftdbrand {
  position:absolute; right:12px; top:50%; transform:translateY(-50%);
  font-size:11px; font-weight:600; color:var(--steel); pointer-events:none;
  max-width:84px; overflow:hidden; text-overflow:ellipsis; white-space:nowrap;
}
#ccexpiry, #cccvv { font-variant-numeric:tabular-nums; }
.ftdnote { margin-top:10px; font-size:11.5px; }

/* ── The text conversation on a customer record ─────────────────────────────
   Two-sided, so direction is readable at a glance without labels: what the
   customer sent sits left on paper, what the business sent sits right in the
   accent tint. Deliberately NOT the rounded speech bubbles of a chat app —
   this is a record somebody reads in a business screen, not a messenger. */
.msgsection { margin-top:22px; }

/* A BOX WITH ITS OWN SCROLL, not a list that grows the page. A customer two
   years in has hundreds of messages, and letting them push the invoices and
   schedules below the fold would make the record worse the longer somebody
   has been a customer.

   Bordered and inset so it reads as a container rather than as a gap — the
   scrollbar alone is not enough of a cue when the conversation happens to be
   short. */
.msgbox-scroll { max-height:360px; overflow-y:auto; overscroll-behavior:contain;
                 border:1px solid var(--hair); border-radius:10px;
                 background:var(--white); padding:12px; }
.msgs { list-style:none; margin:0; padding:0; display:flex;
        flex-direction:column; gap:10px; }

.msg { max-width:min(520px, 86%); padding:9px 12px; border-radius:10px;
       border:1px solid var(--hair); background:var(--paper); }
.msg__body { white-space:pre-wrap; word-break:break-word; line-height:1.45; }
.msg__meta { display:flex; flex-wrap:wrap; align-items:center; gap:8px;
             margin-top:5px; font-size:11.5px; color:var(--mute); }

/* Outbound pulls right and takes the accent tint. --accent-bright is a FILL
   here and carries no text of its own, which is the rule the rebrand set:
   white on #4B9CD3 is 3.0:1 and fails, so the body text stays --ink. */
.msg--out { align-self:flex-end; background:rgba(75,156,211,.12);
            border-color:rgba(75,156,211,.34); }
.msg--in  { align-self:flex-start; }

/* A send that failed is the one thing on this panel somebody must not scroll
   past: the merchant believes it arrived. */
.chip--warn { background:#FBE6E9; color:var(--accent); }
.msg__err { color:var(--accent); }

@media (max-width:700px) {
  .msg { max-width:92%; }
}

/* Replying from the customer record. One box, one button — the merchant is
   answering a question, not running a campaign. */
.msgcompose { margin-top:14px; }
.msgrow { display:flex; gap:8px; align-items:flex-end; }
.msgbox { flex:1; resize:vertical; min-height:44px; font:inherit;
          padding:9px 11px; border:1px solid var(--hair); border-radius:8px;
          background:var(--white); color:var(--ink); }
.msgbox:focus { outline:2px solid var(--accent-bright); outline-offset:1px;
                border-color:var(--accent); }
.msgrow button { white-space:nowrap; }
.msgnote { margin-top:7px; }

/* Consent state above the conversation. One line: the verdict, the wording it
   rests on, and the way to change it. */
.msgconsent { margin-bottom:12px; }
.msgstate { display:flex; flex-wrap:wrap; align-items:center; gap:10px;
            padding:9px 11px; border:1px solid var(--hair); border-radius:8px;
            background:var(--paper); }
.msgstate .sub { flex:1; min-width:200px; margin-top:0; }

/* Charges chosen while raising a stop. Set apart from the plain fields around
   it — this is a list somebody builds, not a value they type. */
.billable { margin:4px 0 2px; padding:14px; border:1px solid var(--hair);
            border-radius:10px; background:var(--paper); }
.billable .card__head { margin-bottom:10px; }
.billable .jobline__total { margin-top:8px; }
.billhost > button.ghost { margin-top:10px; }

/* The customer picker's chevron. This control is an input, so without a mark
   it reads as a text box and nobody clicks it — the native select it replaced
   at least had an arrow. Drawn rather than a glyph so its weight and color
   are ours, and sized to be seen: the complaint about the old one was that it
   was too faint to notice. */
.acwrap__caret {
  position:absolute; right:6px; top:50%; transform:translateY(-50%);
  width:30px; height:30px; padding:0; min-height:0; border:0; border-radius:8px;
  background:transparent; cursor:pointer; display:flex;
  align-items:center; justify-content:center;
}
.acwrap__caret::after {
  content:''; width:9px; height:9px; margin-top:-3px;
  border-right:2.5px solid var(--accent); border-bottom:2.5px solid var(--accent);
  transform:rotate(45deg);
}
.acwrap__caret:hover { background:rgba(75,156,211,.14); }
.acwrap__caret:hover::after { border-color:var(--ink); }

/* Room for it, so a long customer name does not run under the chevron. */
.acwrap.custpick input { padding-right:40px; }

/* Charging the card on file from the send dialog. Boxed and tinted so it reads
   as a different KIND of action from the send it sits beside — one emails a
   bill, the other takes money, and they should not look like two checkboxes. */
.chargecard { margin:12px 0 4px; padding:11px 12px; border-radius:9px;
              border:1px solid rgba(75,156,211,.34); background:rgba(75,156,211,.10); }
.chargecard .check { align-items:flex-start; }
.chargecard .hint { margin-top:6px; }

/* Sort control above the projects grid. Projects stayed cards rather than
   becoming a table, so the ordering lives here instead of in column headers. */
.sortpick { display:flex; align-items:center; gap:8px; flex:none; }
.sortpick label { font-size:12px; color:var(--mute); white-space:nowrap; }
.sortpick select { width:auto; min-width:150px; }

/* THREE UP, THEN TWO UP. The customer profile's rows answer different
   questions — who they are, then how they pay — and one auto-fitting grid put
   all five pills in a row on a wide screen and reflowed unpredictably in
   between. Fixed counts rather than minmax(), collapsing to one column on a
   phone where everything is full width anyway.

   Slightly taller than the old five-across row: with three across there is
   room for the map and the lists to breathe, which was the point of the
   change. */
.cols--3 { grid-template-columns:repeat(3, 1fr); }
.cols--2 { grid-template-columns:repeat(2, 1fr); }
.cols--3 > .card, .cols--2 > .card { min-height:200px; }
.cols--2 { margin-top:14px; }

@media (max-width:1000px) { .cols--3 { grid-template-columns:repeat(2, 1fr); } }
@media (max-width:700px) {
  .cols--3, .cols--2 { grid-template-columns:1fr; }
  .cols--3 > .card, .cols--2 > .card { min-height:0; }
}

/* The map inside the Properties pill: at the TOP, addresses under it. Full
   width of the card rather than .mapbox's default 230px, which is sized to sit
   alongside something rather than above it. */
.card--props .mapbox { width:auto; height:150px; }
.propmap { margin-bottom:12px; }
.propmap__where { display:flex; align-items:baseline; gap:8px; flex-wrap:wrap;
                  margin-top:7px; }
.propmap__where .sub { margin-top:0; }

/* The customer profile's own tabs. Same pills the list screens use, with room
   under them before the first card. */
.tabs + .cols, .tabs + section, .tabs + .txhost { margin-top:18px; }
.txhost .tiles { margin-bottom:18px; }

/* Charge-or-record, at the top of the payment dialog. Boxed so the choice
   reads as a fork rather than as two more fields — one of these presents a
   card and the other writes down something that already happened. */
.moderow { display:flex; flex-direction:column; gap:8px; margin-bottom:14px;
           padding:11px 12px; border:1px solid var(--hair); border-radius:9px;
           background:var(--paper); }
.moderow .check { align-items:flex-start; gap:9px; }
.moderow .sub { margin-top:2px; }
.keyedhost:not(:empty) { margin-top:10px; }

/* ---- Opening hours -----------------------------------------------------------
   Seven rows that must read as a single object, so the day names form a column
   you can run an eye down rather than seven independent fields. A closed day
   keeps its times on screen, dimmed — removing them makes the grid jump as you
   tick down it and hides the hours you are about to want back. */
.hoursgrid { display:flex; flex-direction:column; gap:2px; margin:14px 0 12px; }
.hoursgrid__row { display:flex; align-items:center; gap:12px; flex-wrap:wrap;
                  padding:7px 10px; border-radius:6px; }
.hoursgrid__row:nth-child(odd) { background:var(--paper); }
.hoursgrid__day { display:flex; align-items:center; gap:8px; width:132px;
                  flex:none; font-size:13px; font-weight:600; cursor:pointer; }
.hoursgrid__times { display:flex; align-items:center; gap:8px; }
/* Wide enough for "07:00 AM" and the picker beside it. Below this the browser
   silently drops the meridiem and the row reads as a typo. */
.hoursgrid__times input { width:142px; }
.hoursgrid__dash { font-size:12px; color:var(--steel); }
.hoursgrid__warn { font-size:11.5px; }
.hoursgrid__row.is-closed .hoursgrid__day,
.hoursgrid__row.is-closed .hoursgrid__times { opacity:.45; }

/* ---- Earliest opening --------------------------------------------------------
   The answer to "when could this happen". One headline time, then who could do
   it — because on a quiet week the honest answer is "tomorrow at 8, by anyone",
   and a flat list of thirty identical times says that very badly. */
/* Soonest, or next in the area — the same panel answering two questions.
   A quiet segmented control rather than two buttons: switching between them
   is browsing, not an action. */
.schedtabs { display:flex; gap:2px; padding:3px; margin:0 0 12px;
             background:var(--band); border-radius:999px; }
.schedtabs__t { flex:1; cursor:pointer; border:0; background:none; padding:6px 10px;
                border-radius:999px; font:inherit; font-size:12.5px; font-weight:600;
                color:var(--steel); }
.schedtabs__t:hover { color:var(--ink); }
.schedtabs__t.is-on { background:var(--paper); color:var(--ink);
                      box-shadow:0 1px 2px rgba(19,41,75,.10); }
.openings { display:flex; flex-direction:column; gap:10px; }
.opengroup { border:1px solid var(--hair); border-radius:var(--r); overflow:hidden; }
.opengroup__when { display:flex; align-items:baseline; gap:10px; flex-wrap:wrap;
                   padding:9px 12px; background:var(--band);
                   border-bottom:1px solid var(--hair); }
.opengroup__when b { font-size:14px; }
.opengroup__when .sub { font-size:11.5px; color:var(--steel); }
.opengroup__who { display:flex; flex-wrap:wrap; gap:6px; padding:10px 12px; }
.openpick { display:inline-flex; align-items:center; gap:6px; cursor:pointer;
            font-size:12px; font-weight:600; padding:4px 10px; border-radius:999px;
            border:1px solid var(--hair); background:#fff; color:var(--ink); }
.openpick:hover { border-color:var(--accent); color:var(--accent); }
.openpick__dot { width:8px; height:8px; border-radius:50%; flex:none;
                 background:var(--steel); }
.openpick.is-person .openpick__dot { background:transparent;
                                     border:1.5px solid var(--steel); }

/* A remark, not a refusal — used where a chosen time falls outside opening
   hours, which is allowed and merely worth saying. Amber rather than red: red
   is for things that failed, and this has not failed. */
.warn { color:var(--amber); font-weight:600; }

/* ---- Stops grouped by crew ---------------------------------------------------
   A dispatch board: one block per crew, Unassigned first because it is a to-do
   pile and a to-do pile belongs where the eye starts. Each block keeps a real
   table head — the mobile stylesheet reads field labels out of th.textContent,
   so a group without one loses every label below 700px. */
.crewgrps { display:flex; flex-direction:column; gap:14px; }
.crewgrp { border:1px solid var(--hair); border-radius:var(--r); overflow:hidden;
           background:#fff; }
.crewgrp__head { display:flex; align-items:center; gap:10px; flex-wrap:wrap;
                 padding:9px 13px; background:var(--band);
                 border-bottom:1px solid var(--hair); }
.crewgrp__who { display:flex; align-items:center; gap:8px; min-width:0; }
.crewgrp__who b { font-size:13.5px; }
.crewgrp__dot { width:10px; height:10px; border-radius:50%; flex:none;
                background:var(--steel); }
.crewgrp__count { font-size:11.5px; color:var(--steel); }
.crewgrp__more { font-size:11px; color:var(--amber); margin-left:auto; }
.crewgrp__none { padding:13px; font-size:12.5px; color:var(--steel); }
/* Unassigned is the pile to clear, so its heading is the one that carries
   weight — dashed, not colored, because it is an absence rather than a crew. */
.crewgrp.is-none .crewgrp__dot { background:transparent;
                                 border:1.5px dashed var(--steel); }
.crewgrp .tablewrap { border:0; border-radius:0; }

/* ---- Two readings of one list ------------------------------------------------
   A segmented switch rather than a second nav entry: these are the same stops
   answering the same question two ways, not two screens. */
.segbtn { display:inline-flex; border:1px solid var(--hair); border-radius:var(--r-sm);
          overflow:hidden; flex:none; }
.segbtn__b { padding:7px 13px; font-size:12.5px; font-weight:600;
             color:var(--steel); text-decoration:none; background:#fff;
             border-left:1px solid var(--hair); white-space:nowrap; }
.segbtn__b:first-child { border-left:0; }
.segbtn__b:hover { color:var(--ink); background:var(--paper); }
.segbtn__b.is-on { background:var(--accent); color:#fff; }

/* ---- Dragging a stop between crews -------------------------------------------
   The same gesture the calendar uses, so the two boards feel like one product.
   The section is the target rather than its table, because a crew with nothing
   in it has no rows to aim at and is exactly where work gets dragged to. */
.crewgrp.can-drop tbody tr { cursor:grab; }
.crewgrp.can-drop tbody tr:active { cursor:grabbing; }
tr.is-dragging { opacity:.4; }
tr.is-dragging td { background:var(--paper); }

/* While a drag is in flight every crew shows it can take the stop; the one
   under the pointer commits harder. Outline rather than background: the inner
   table is opaque white, so a background tint would only ever show as a thin
   frame nobody notices. */
.is-dragging-job .crewgrp.can-drop { outline:1px dashed var(--hair);
                                     outline-offset:2px; }
.crewgrp.is-dropping { outline:2px solid var(--accent); outline-offset:2px;
                       background:#fff !important; }
.crewgrp.is-dropping .crewgrp__head { background:rgba(42,110,158,.14); }
/* An empty crew is the most likely destination and the least visible one, so
   its placeholder is what lights up. */
.crewgrp.is-dropping .crewgrp__none { color:var(--accent); font-weight:600; }

/* ---- The dispatch map --------------------------------------------------------
   Who is working where. Given real height rather than an aspect ratio: a map
   the size of a postage stamp answers nothing, and the question here spans a
   town. dvh so a phone's collapsing address bar does not clip it. */
.calmap { display:flex; flex-direction:column; gap:10px; }
.calmap__canvas { height:min(68vh, 720px); min-height:360px;
                  border:1px solid var(--hair); border-radius:var(--r);
                  overflow:hidden; background:var(--paper); }
@supports (height:1dvh) { .calmap__canvas { height:min(68dvh, 720px); } }
.calmap__fail { display:flex; align-items:center; justify-content:center;
                height:100%; padding:24px; text-align:center;
                font-size:13px; color:var(--steel); }
.calmap__notice { display:flex; align-items:center; gap:8px; flex-wrap:wrap; }
.calmap__notice button { flex:none; }
/* Only the crews actually on the map today — a key naming eight crews for a
   day worked by two is noise. */
.calmap__legend { display:flex; flex-wrap:wrap; gap:6px 14px; }
.calmap__key { display:inline-flex; align-items:center; gap:6px;
               font-size:11.5px; color:var(--steel); }
.calmap__key i { width:10px; height:10px; border-radius:50%; flex:none; }

/* Google owns the DOM inside an info window, so these are plain descendant
   selectors on its markup rather than our usual block classes. */
.mappop { font-size:12.5px; line-height:1.45; max-width:230px; }
.mappop b { display:block; font-size:13px; margin-bottom:2px; }
.mappop__addr { color:var(--steel); }
.mappop__meta { color:var(--steel); margin:3px 0 5px; }
.mappop a { font-weight:600; }

/* ---- Depots ------------------------------------------------------------------
   A location that has no coordinates cannot anchor a route, so the row says so
   here — while somebody is editing it — rather than at the moment a crew is
   waiting for their day and Optimize refuses. */
.depotrow { display:flex; align-items:center; gap:10px; flex-wrap:wrap;
            padding:10px 0; border-top:1px solid var(--hair); }
.depotrow:first-child { border-top:0; }
.depotrow__main { flex:1 1 220px; min-width:0; }
.depotrow__main b { display:flex; align-items:center; gap:8px; font-size:13px; }
.depotrow__main .sub { font-size:11.5px; color:var(--steel); }
.depotrow button { flex:none; }

/* ---- Optimizing a day --------------------------------------------------------
   The preview is the point of this dialog, so the table gets the room and the
   summary sits above it. Times that MOVE are marked, because "5 stops move" is
   the sentence somebody needs before deciding whether to ring anybody. */
.calmap__bar { display:flex; justify-content:flex-end; }
.optbody { min-width:0; }
.optrun { display:flex; justify-content:flex-end; margin-top:12px; }
.optresult { margin-top:14px; display:flex; flex-direction:column; gap:10px; }
.optsum { display:flex; align-items:baseline; gap:10px; flex-wrap:wrap; }
.optsum b { font-size:14px; }
.optsum .sub { font-size:11.5px; color:var(--steel); }
.optwarn { display:block; }
/* The stops the optimizer could not fit. A plain list under the warning:
   short, and never a table competing with the plan table above it. */
.optleft { margin:6px 0 0; padding-left:18px; font-size:12px; line-height:1.7; }
.optleft .mono { color:var(--steel); }
.optresult table { font-size:12.5px; }
.optresult .warn { font-weight:700; }

/* ---- A fortnight -------------------------------------------------------------
   Fourteen columns squeezed into a laptop is 90px a day, which renders a
   customer name as an ellipsis.

   The old list-shaped week wrapped to two rows of seven. A canvas cannot: the
   rows are hours, and a second row of days below the first would put Tuesday
   8am beside Monday 3pm. So a fortnight SCROLLS SIDEWAYS instead, each day
   keeping a readable minimum width — the same answer the team boards already
   give to the same problem of too many columns. */
/* ONLY when there are more than seven columns. A normal week must keep filling
   the width exactly, and a blanket min-width would hand it a horizontal
   scrollbar it never needs. */
.calweekc--long .calweekc__track { min-width:max-content; }
.calweekc--long .calweekc__body > .calcol,
.calweekc--long .calweekc__day { min-width:150px; }

/* ---- The job-type icon on a chip ---------------------------------------------
   Sized in em so it tracks the chip's own text rather than fighting it, and
   given no color of its own — an emoji brings its own. */
.calchip__icon { font-size:1.05em; line-height:1; margin-right:3px; flex:none; }
.calchip--box .calchip__icon { display:block; margin-bottom:1px; }
/* A wider box for a field that holds one glyph: a full-width input for two
   characters reads as though a sentence is wanted. */
.iconfield { max-width:110px; font-size:18px; text-align:center; }

/* ---- Calendar and map together -----------------------------------------------
   A board answers "when", a map answers "where", and the question a dispatcher
   asks — "can I fit this near where Andre already is" — needs both at once.

   The left half is a LIST, not the hour canvas. The canvas draws duration to
   scale, which is right at full width and unreadable at 40% of it. */
.calsplit { display:grid; grid-template-columns:minmax(280px, 34%) 1fr;
            gap:12px; align-items:stretch;
            height:min(72vh, 760px); min-height:420px; }
@supports (height:1dvh) { .calsplit { height:min(72dvh, 760px); } }

.calsplit__left { display:flex; flex-direction:column; min-width:0;
                  border:1px solid var(--hair); border-radius:var(--r);
                  background:#fff; overflow:hidden; }
.calsplit__head { display:flex; align-items:baseline; gap:8px; flex-wrap:wrap;
                  padding:9px 12px; background:var(--band);
                  border-bottom:1px solid var(--hair); }
.calsplit__head b { font-size:13px; }
.calsplit__head .sub { font-size:11px; color:var(--steel); }
/* The list scrolls, the map does not move — the whole point of pinning them
   side by side is that the map stays put while you read down the day. */
.calsplit__list { flex:1; overflow-y:auto; min-height:0; }
.calsplit__none { padding:20px 14px; font-size:12.5px; color:var(--steel);
                  text-align:center; }

.calsplit__right { min-width:0; display:flex; }
.calsplit__right .calmap { flex:1; min-width:0; }
/* Fills the pane instead of its own viewport-based height — inside the split
   the frame decides the size, not the map. */
.calsplit__right .calmap__canvas { height:auto; flex:1; min-height:0; }

.splitrow { display:flex; align-items:center; gap:10px; padding:8px 12px;
            border-top:1px solid var(--hair); cursor:pointer; }
.splitrow:first-child { border-top:0; }
.splitrow:hover { background:var(--paper); }
.splitrow.is-on { background:rgba(42,110,158,.10);
                  box-shadow:inset 3px 0 0 var(--accent); }
.splitrow:focus-visible { outline:2px solid var(--accent); outline-offset:-2px; }
/* The same number as the pin. When a day has been optimized these are the
   route order, which is the point of having optimized it. */
.splitrow__no { flex:none; width:22px; height:22px; border-radius:50%;
                display:flex; align-items:center; justify-content:center;
                background:var(--band); color:var(--steel);
                font-size:11px; font-weight:700;
                font-variant-numeric:tabular-nums; }
.splitrow.is-on .splitrow__no { background:var(--accent); color:#fff; }
.splitrow__body { flex:1; min-width:0; }
.splitrow__top { display:flex; align-items:baseline; gap:8px; }
.splitrow__top b { font-size:12.5px; white-space:nowrap; overflow:hidden;
                   text-overflow:ellipsis; }
.splitrow__time { margin-left:auto; flex:none; font-size:11.5px;
                  color:var(--steel); font-variant-numeric:tabular-nums; }
.splitrow__sub { display:flex; align-items:center; gap:5px; font-size:11.5px;
                 color:var(--steel); white-space:nowrap; overflow:hidden;
                 text-overflow:ellipsis; }
.splitrow__icon { flex:none; }
.splitrow__dot { flex:none; width:9px; height:9px; border-radius:50%; }
/* Not silently missing: a list of six against a map of six must still be able
   to say a seventh exists. */
.calsplit__off { padding:11px 12px; border-top:1px solid var(--hair);
                 background:var(--amber-bg); }
.calsplit__off b { display:block; font-size:12px; color:var(--amber); }
.calsplit__off .sub { font-size:11px; color:var(--steel); margin-top:2px; }

/* Below a laptop there is no room for two of anything: the map goes under the
   list rather than beside it, and both get their own height back. */
@media (max-width: 900px) {
  .calsplit { grid-template-columns:1fr; height:auto; }
  .calsplit__list { max-height:340px; }
  .calsplit__right .calmap__canvas { height:min(50vh, 420px); flex:none; }
}

/* Google refusing the key is not a crash and not a transient failure — it is a
   setting somebody has to change. Given room to say which one. */
.calmap__fail--auth { flex-direction:column; gap:6px; max-width:460px;
                      margin:0 auto; line-height:1.5; }
.calmap__fail--auth b { color:var(--ink); font-size:13.5px; }

/* ---- A route that has already been applied -----------------------------------
   The toast's Undo is right for the ten seconds after pressing Apply. The
   route and its snapshot outlive it in the database, so the ability to put a
   day back must outlive it on screen too — otherwise somebody returning after
   lunch has a schedule no button can reverse. */
.calmap__bar { display:flex; align-items:center; gap:10px; flex-wrap:wrap;
               justify-content:flex-end; }
.calmap__routes { display:flex; align-items:center; gap:8px; flex-wrap:wrap;
                  margin-right:auto; }
.routechip { display:inline-flex; align-items:center; gap:7px;
             font-size:11.5px; color:var(--steel);
             border:1px solid var(--hair); border-radius:999px;
             padding:4px 10px; background:#fff; }
.routechip b { color:var(--ink); }
.routechip__dot { width:8px; height:8px; border-radius:50%; flex:none;
                  background:var(--steel); }
.linkish { background:none; border:0; padding:0; font:inherit; font-weight:600;
           color:var(--accent); cursor:pointer; }
.linkish:hover { text-decoration:underline; }
.linkish:disabled { color:var(--steel); cursor:default; text-decoration:none; }

/* ---- The week as a time canvas -----------------------------------------------
   Rebuilt from a list of chips into the same canvas the day view uses: a job's
   top is when it starts and its height is how long it runs, so a gap on screen
   is a gap in the day and two jobs at ten o'clock overlap instead of stacking.

   ONE SCROLLER FOR ALL SEVEN COLUMNS. Seven that scrolled independently would
   let Tuesday show 8am while Wednesday shows 2pm, and reading across is the
   whole point of a week. */
.calweekc { border:1px solid var(--hair); border-radius:var(--r);
            overflow:hidden; background:#fff; }
.calweekc__scroll { max-height:calc(100vh - 260px); overflow:auto; }
@supports (height:1dvh) {
  .calweekc__scroll { max-height:calc(100dvh - 260px); }
}
/* No min-width in the normal case. `min-width:100%` resolves against the
   scroll container INCLUDING the strip the vertical scrollbar sits in, so a
   week tall enough to scroll gained about fifteen pixels of horizontal
   overflow and a scrollbar it had no use for. The long-span rule below sets
   max-content instead, which is a real measurement rather than a percentage of
   the wrong box. */
.calweekc__track { min-width:0; }
/* Sticky, so the day you are looking at is always labeled however far down
   the day you have scrolled. */
.calweekc__head { display:flex; position:sticky; top:0; z-index:6;
                  background:var(--band); border-bottom:1px solid var(--hair); }
.calweekc__head > .calgutter__spacer { flex:none; width:64px;
                                       position:sticky; left:0; z-index:7;
                                       background:var(--band); }
.calweekc__day { flex:1 1 0; min-width:0; padding:7px 8px; text-align:center;
                 border-left:1px solid var(--hair); cursor:pointer; }
.calweekc__day:hover { background:var(--paper); }
.calweekc__day b { font-size:10.5px; letter-spacing:.06em;
                   text-transform:uppercase; color:var(--steel); display:block; }
.calweekc__num { font-size:16px; font-weight:600; line-height:1.2;
                 font-variant-numeric:tabular-nums; }
.calweekc__day .sub { display:block; font-size:10.5px; color:var(--mute);
                      min-height:13px; }
.calweekc__day.is-today .calweekc__num { color:#fff; background:var(--accent);
                                         border-radius:999px; padding:0 7px;
                                         display:inline-block; }
.calweekc__body { display:flex; align-items:stretch; }
.calweekc__body > .calcol { flex:1 1 0; }

/* The half hour, fainter than the hour. Most service work starts on one. */
.calline { position:relative; }
.calline__half { position:absolute; left:0; right:0; top:50%;
                 border-bottom:1px dashed var(--hair); opacity:.55; }

/* ---- A chip with room to say what the job is ---------------------------------
   In the canvas the block's height is its duration, so how many lines it can
   hold is decided in boxLines() from the pixel height rather than by letting
   overflow crop whatever happened to be last. */
.calchip--box { padding:4px 6px; }
.calchip__head { display:flex; align-items:baseline; gap:4px; min-width:0; }
/* min-width:0 is what makes the ellipsis work. A flex child's min-width
   defaults to auto — it refuses to shrink below its own text — so the title
   overflowed its chip and was hard-clipped mid-character by the chip's
   overflow:hidden instead of ellipsing. Only visible on a chip narrowed by an
   overlap, which is exactly where the name matters most. */
.calchip__head b { font-size:11.5px; line-height:1.25; color:var(--ink);
                   min-width:0; flex:0 1 auto;
                   white-space:nowrap; overflow:hidden; text-overflow:ellipsis; }
.calchip__row { font-size:10.5px; line-height:1.35; color:var(--steel);
                white-space:nowrap; overflow:hidden; text-overflow:ellipsis; }
.calchip__row.is-who { font-weight:600;
                       color:color-mix(in srgb, currentColor 78%, var(--ink)); }
.calchip__note { font-size:10px; line-height:1.3; color:var(--mute);
                 margin-top:2px;
                 display:-webkit-box; -webkit-line-clamp:2; -webkit-box-orient:vertical;
                 overflow:hidden; }

/* ---- A canvas chip is a solid block of its team's color ---------------------
   The tinted version read as a faint wash: at a glance a board of them was one
   color, which defeats coloring at all. Solid fill is what makes "that whole
   morning is Crew A" readable from across a desk.

   currentColor carries the crew color, so the element sets `color` and
   everything inside is painted against it — one value still decides the whole
   chip, which is what the tinted version got right. */
.calcol .calchip--box,
.calweekc .calchip--box {
  background:currentColor;
  border:0; border-left:3px solid rgba(0,0,0,.22);
  outline:1px solid #fff; outline-offset:-1px;
  border-radius:4px;
}
.calcol .calchip--box .calchip__head b,
.calweekc .calchip--box .calchip__head b { color:#fff; }
.calcol .calchip--box .calchip__row,
.calweekc .calchip--box .calchip__row { color:rgba(255,255,255,.92); }
.calcol .calchip--box .calchip__row.is-who,
.calweekc .calchip--box .calchip__row.is-who { color:#fff; }
.calcol .calchip--box .calchip__note,
.calweekc .calchip--box .calchip__note { color:rgba(255,255,255,.82); }
.calcol .calchip--box:hover,
.calweekc .calchip--box:hover { box-shadow:0 3px 10px rgba(19,41,75,.28); z-index:3; }

/* Finished work steps back rather than shouting in the same color as the work
   still to do — it is on the board for reference, not for action. */
.calcol .calchip--box.calchip--done,
.calweekc .calchip--box.calchip--done { opacity:.55; }
.calcol .calchip--box.calchip--done .calchip__head b,
.calweekc .calchip--box.calchip--done .calchip__head b { text-decoration:line-through; }

/* ---- Schedule stop, from the calendar ----------------------------------------
   Carries the same four things the stop editor does — where, what it charges,
   what the customer is told, what only the crew sees — so a stop booked from
   the calendar is the same record as one raised from the stops screen, rather
   than a stub somebody has to open again afterwards. */
.schedsec { margin:4px 0 2px; }
.schedsec h3 { font-size:12px; letter-spacing:.05em; text-transform:uppercase;
               color:var(--steel); margin:0 0 7px; }
.schedbill { border:1px solid var(--hair); border-radius:var(--r-sm);
             padding:10px 11px; background:var(--paper); }
.schedbill > p.muted { font-size:12px; margin:0 0 9px; }
.schedbill .joblines { margin-bottom:8px; }
/* A single address is stated rather than offered — a dropdown with one option
   is a decision nobody has to make. */
.schedaddr { font-size:13px; padding:8px 11px; background:var(--paper);
             border:1px solid var(--hair); border-radius:var(--r-sm); }

/* ---- Recurring work ----------------------------------------------------------
   Plans that put stops on the calendar. Cards rather than a table because the
   useful unit is "what does this plan do" — cadence, place, crew, money and
   whether it is actually running — and that is five facts, not five columns. */
.rwlist { display:flex; flex-direction:column; gap:12px; }
.rwcard { border:1px solid var(--hair); border-radius:var(--r); background:#fff;
          padding:13px 15px; display:flex; flex-direction:column; gap:10px; }
.rwcard.is-review { border-color:color-mix(in srgb, var(--amber) 45%, transparent); }
.rwcard__head { display:flex; align-items:baseline; justify-content:space-between;
                gap:12px; flex-wrap:wrap; }
.rwcard__title { display:flex; align-items:center; gap:8px; min-width:0; }
.rwcard__title b { font-size:14px; }
.rwcard__icon { font-size:15px; line-height:1; }
.rwcard__cust { font-size:12.5px; color:var(--steel); }
.rwcard__why { font-size:12px; }
/* Wraps rather than scrolls: on a phone these stack, and a plan you cannot
   read the crew off is a plan you cannot check. */
.rwcard__facts { display:grid; gap:9px 18px;
                 grid-template-columns:repeat(auto-fit, minmax(130px, 1fr)); }
.rwfact span { display:block; font-size:10px; letter-spacing:.06em;
               text-transform:uppercase; color:var(--mute); }
.rwfact b { font-size:12.5px; font-weight:600; }
.rwcard__foot { display:flex; align-items:center; justify-content:space-between;
                gap:12px; flex-wrap:wrap; padding-top:9px;
                border-top:1px solid var(--hair); font-size:12px; }
.rwcard__act { display:flex; gap:12px; }

/* ---- The property passport ----------------------------------------------------
   The address as a record. Cards rather than a form, because what makes an
   address worth knowing is five different KINDS of fact — what is installed,
   what to watch out for, who has owned it, what has been done, what it has
   been worth — and each has its own shape. */

/* Loud, and above everything. A dog nobody warned the technician about is an
   afternoon lost and possibly worse, so this is the one block on the page that
   is allowed to shout. */
.propalerts { display:flex; flex-direction:column; gap:5px; margin-bottom:14px; }
.propalerts__row { font-size:13px; }

/* The address, then what it has been worth. Stacked rather than side by side:
   the tiles beside it were a third of the width, which turned three figures
   into a vertical stripe with an empty half-page next to it. */
.propident { display:flex; flex-direction:column; gap:14px; margin-bottom:18px; }
.propident h2 { font-size:17px; margin:0 0 3px; }
.propident__owner { margin-top:7px; font-size:13px; }
.tiles--tight { margin:0; }

/* Equipment. The serial is the point of the row, so it is set in the mono face
   and given room; everything else is context around it. */
.assetlist { display:flex; flex-direction:column; gap:10px; }
.asset { border:1px solid var(--hair); border-radius:var(--r-sm); padding:10px 12px;
         display:flex; flex-direction:column; gap:6px; background:var(--white); }
.asset.is-gone { opacity:.62; }
.asset__head { display:flex; align-items:center; gap:8px; flex-wrap:wrap; }
.asset__id { font-size:13px; display:flex; align-items:center; gap:10px; flex-wrap:wrap; }
.asset__serial { font-size:12px; color:var(--steel); }
.asset__meta { display:flex; gap:14px; flex-wrap:wrap; font-size:12px; color:var(--steel); }
.asset__attrs { display:flex; gap:6px; flex-wrap:wrap; }

/* The attribute bag, shown as chips: a label nobody chose in advance and a
   value that has to stay readable next to it. */
.kvchip { font-size:11.5px; border:1px solid var(--hair); border-radius:999px;
          padding:2px 9px; background:var(--paper); }
.kvchip i { font-style:normal; color:var(--mute); margin-right:5px; }

/* The warranty clock. Three states because the answer is really three: it is
   covered, it is about to stop being covered — which is a job worth booking —
   or it already stopped. */
.warrpill { align-self:flex-start; font-size:11.5px; border-radius:999px;
            padding:2px 10px; border:1px solid transparent; }
.warrpill--ok   { background:var(--green-bg); color:#1F7A5A; }
.warrpill--warn { background:var(--amber-bg); color:var(--amber); }
.warrpill--dead { background:var(--paper); color:var(--mute); }

.factrow { display:flex; align-items:flex-start; justify-content:space-between;
           gap:12px; flex-wrap:wrap; }
.factrow__main { min-width:0; }
.factrow__tags { display:flex; gap:6px; flex-wrap:wrap; }

/* Ownership as a line rather than a table: it is a sequence, and the current
   owner is the end of it. */
.ownerline { list-style:none; margin:0; padding:0;
             display:flex; flex-direction:column; gap:11px; }
.ownerline__row { border-left:2px solid var(--hair); padding:0 0 0 12px; }
.ownerline__row.is-now { border-left-color:var(--accent); }
.ownerline__who { display:flex; align-items:center; gap:8px; flex-wrap:wrap;
                  font-size:13px; font-weight:600; }
.ownerline__when { font-size:12px; color:var(--steel); }

.assetgone { margin-top:10px; }
.assetgone > summary { font-size:12px; color:var(--steel); cursor:pointer; }
.assetgone > summary::marker { color:var(--mute); }

/* Said before the button is pressed, not after: "what happens to my data" is
   exactly the question somebody hesitates over here. */
.transfernote { border:1px solid var(--hair); border-radius:var(--r-sm);
                background:var(--paper); padding:11px 13px; font-size:12.5px; }
.transfernote b { display:block; margin:4px 0 4px; }
.transfernote ul { margin:0 0 8px; padding-left:18px; }
.transfernote li { margin:2px 0; }

.attrgrid { display:grid; gap:10px 14px;
            grid-template-columns:repeat(auto-fit, minmax(150px, 1fr)); }
.attrgrid__add { align-self:end; }
.formsect { font-size:11px; letter-spacing:.06em; text-transform:uppercase;
            color:var(--mute); margin:14px 0 0; }
/* A checkbox with a sentence beside it. The size and the case are stated
   because `label` in this stylesheet is otherwise a field caption — uppercase,
   gray, and stretched — which is right on a form and wrong on a control. */
.checkline { display:flex; align-items:flex-start; gap:9px; font-size:13px;
             text-transform:none; letter-spacing:0; font-weight:400;
             color:var(--ink); margin:0; cursor:pointer; }
.checkline input[type=checkbox] { margin-top:2px; width:15px; height:15px; flex:none; }

/* Files on the passport. The Add control is a label wrapping a hidden file
   input — the native button says "Choose File" and cannot be told otherwise,
   and this row already has two other links that look like links. */
.fileadd input[type=file] { display:none; }
.fileadd { cursor:pointer; }
.fileadd__type { display:flex; align-items:center; gap:8px; margin-bottom:8px; }
.fileadd__type select { max-width:220px; }
.filerow { display:flex; align-items:flex-start; justify-content:space-between; gap:12px; }
.filerow__main { min-width:0; }

/* Settings → Property records. Each row is a preset the business owns; a
   switched-off one is dimmed rather than hidden, because "we turned that off"
   and "that was never here" are different answers. */
.presetrow { display:flex; align-items:flex-start; justify-content:space-between; gap:12px; }
.presetrow.is-off { opacity:.6; }
.presetrow__main { min-width:0; }
.presetfields { display:flex; flex-direction:column; gap:12px; }
.presetfield { border:1px solid var(--hair); border-radius:var(--r-sm);
               padding:10px 12px; background:var(--paper); }

/* Files, wherever they are listed. The sharing switch sits with the delete
   button rather than in the row body: both are decisions about the file, and
   neither is what somebody came to the row to read. */
.filerow__acts { display:flex; align-items:center; gap:14px; flex-shrink:0; }
/* Same reason as the upload bar: this is a control, not a field caption. */
.sharetoggle { display:flex; align-items:center; gap:6px; font-size:12px;
               color:var(--steel); cursor:pointer; white-space:nowrap;
               text-transform:none; letter-spacing:0; font-weight:400; margin:0; }
.sharetoggle input[type=checkbox] { width:15px; height:15px; flex:none; }
.chip--share { background:var(--green-bg); color:#1F7A5A; border-color:transparent; }
.filehost { margin-bottom:16px; }

/* The upload bar: one line that reads left to right — pick a file, say what it
   is, decide who sees it. It wrapped into a column when the share label was a
   full sentence, which made three controls look like three sections. */
.uploadbar { display:flex; align-items:center; gap:16px; flex-wrap:wrap;
             padding:11px 13px; margin-bottom:12px; font-size:12.5px;
             border:1px solid var(--hair); border-radius:var(--r-sm);
             background:var(--paper); }
/* label inside the bar is a control, not a field caption — the global label
   rule is uppercase and gray, which is right on a form and wrong here. */
.uploadbar label, .uploadbar__type, .uploadbar__share {
  display:flex; align-items:center; gap:7px; text-transform:none;
  letter-spacing:0; font-size:12.5px; font-weight:400; color:var(--steel);
  margin:0; cursor:pointer; }
.uploadbar input[type=checkbox] { width:15px; height:15px; flex:none; }
.uploadbar__type { white-space:nowrap; }
.uploadbar__type select { max-width:210px; }
.uploadbar__share span { white-space:normal; }
.uploadbar__hint { color:var(--mute); margin-left:auto; }
.btn.fileadd { cursor:pointer; white-space:nowrap; }

/* ---- Customer notifications -------------------------------------------------
   A moment, its channels, and the words. The wording is edited in place: a
   modal per rule would mean opening two dozen of them to set a business up. */
.custnotify { margin-top:16px; }
/* ---- Notices, as a list of switches -----------------------------------------

   The old .noticeitem opened every notice at once — two message editors and a
   row of token chips each, twenty-nine times. These rows are the replacement:
   one line per notice, the switches on the right, and the writing behind Edit.
   .noticeitem and .chanrow below are still used by nothing else and are kept
   only until the announcement dialog is looked at again. */
.noticerows { display:flex; flex-direction:column; }
.noticerow { display:flex; align-items:center; gap:14px; padding:12px 2px;
             border-bottom:1px solid var(--hair); }
.noticerow:last-child { border-bottom:0; }
.noticerow__main { flex:1; min-width:0; }
.noticerow__main b { display:block; font-size:13.5px; color:var(--ink);
                     display:flex; align-items:center; gap:8px; }
.noticerow__main .sub { font-size:12px; color:var(--mute); margin-top:2px; }
.noticerow__switches { display:flex; gap:8px; flex:none; }
.noticerow__edit { flex:none; }
/* A business's own notice is set in from the standard one it sits under, so
   the list reads as "this moment, and the extra thing we added to it". */
.noticerow--custom { padding-left:18px; border-left:2px solid var(--band); }

/* ── What has not reached the books ──────────────────────────────────────────

   The accounting sync's problem list. Deliberately the SAME shape and the same
   numbers as .noticerow above rather than a near-copy with its own opinions —
   14px between a row's halves, 12px of row padding, one hairline between —
   because two lists of rows in one product that agree about everything except
   their gaps is exactly what "spacing is not uniform" looks like.

   The one difference from .noticerow is `align-items:flex-start`: a notice is
   one line and a fault is three, and centring a two-button column against a
   paragraph pushes the buttons into the middle of the sentence. */
.prows { display:flex; flex-direction:column; }
.prow { display:flex; align-items:flex-start; gap:14px; padding:12px 2px;
        border-bottom:1px solid var(--hair); }
.prow:last-child { border-bottom:0; }
.prow__main { flex:1; min-width:0; }
.prow__main b { display:flex; align-items:center; gap:8px;
                font-size:13.5px; color:var(--ink); }
/* The sentence a person reads. Not muted — it is the content of the row, and
   greying the only thing on the screen that explains the failure is how a
   problem list becomes wallpaper. */
.prow__main p { margin:6px 0 0; font-size:13px; }
/* And QuickBooks' own words underneath. `.sub` carries margin-top:2px, which is
   right against a heading and too tight under a paragraph. */
.prow__main .sub { margin-top:6px; }
/* 10px, the same as .acts — the row at the foot of a card — so buttons are
   never a different distance apart in two places. */
.prow__acts { display:flex; align-items:center; gap:10px; flex:none; }

/* Two groups of rows under one heading: "needs you", then "waiting".
   20px, which is what the stylesheet already uses to separate two SECTIONS
   (.tiles + .tabs), rather than a new number invented for this screen. */
.pgroup + .pgroup { margin-top:20px; }
/* 12px — the same as .card__head's own bottom margin, so a sentence sits above
   its list at exactly the distance a heading sits above its content. */
.pgroup > p + .prows { margin-top:12px; }

/* ⚠ AND NOT `.notice + .notice`. Measured first: `.notice` already carries
   margin-top:16px, so two stacked notices were ALREADY 16px apart, and the
   12px rule written here before measuring made the page LESS uniform rather
   than more. `.tiles` has no margin of its own, so that one is a real zero. */
.notice + .tiles { margin-top:14px; }

/* The switch is the row's whole point, so it is a target rather than a tick. */
.noticesw { display:inline-flex; align-items:center; gap:7px; cursor:pointer;
            border:1px solid var(--hair); border-radius:999px;
            padding:5px 12px 5px 9px; font-size:12px; font-weight:600;
            color:var(--mute); background:var(--white); user-select:none; }
.noticesw input { width:15px; height:15px; min-height:0; margin:0; flex:none; }
.noticesw.is-on { border-color:var(--accent); color:var(--accent);
                  background:rgba(75,156,211,.09); }
/* A channel with no rule at all — nothing to switch, and saying so is better
   than an empty gap somebody reads as a missing feature. */
.noticesw--none { opacity:.4; cursor:default; }

.tokenwrap { display:flex; gap:6px; flex-wrap:wrap; }
.timingrow { display:flex; align-items:center; gap:8px; }

.noticelist { display:flex; flex-direction:column; gap:14px; }
.noticeitem { border:1px solid var(--hair); border-radius:var(--r-sm);
              padding:12px 14px; background:var(--white); }
.noticeitem__head { display:flex; align-items:flex-start; justify-content:space-between;
                    gap:14px; flex-wrap:wrap; margin-bottom:10px; }
.noticeitem__tokens { display:flex; gap:5px; flex-wrap:wrap; max-width:55%; }
.tokenchip { font:400 11px/1 ui-monospace,SFMono-Regular,Menlo,monospace;
             border:1px dashed var(--hair); background:var(--paper); color:var(--steel);
             border-radius:5px; padding:4px 7px; cursor:pointer; }
.tokenchip:hover { border-color:var(--accent); color:var(--accent); }
.noticeitem__chans { display:flex; flex-direction:column; gap:10px; }
.chanrow { display:grid; grid-template-columns:96px 1fr; gap:12px; align-items:start;
           padding:10px; border-radius:var(--r-sm); background:var(--paper); opacity:.72; }
.chanrow.is-on { opacity:1; background:color-mix(in srgb, var(--accent-bright) 8%, #fff); }
.chanrow__on { display:flex; align-items:center; gap:7px; text-transform:none;
               letter-spacing:0; font-size:12.5px; font-weight:600; color:var(--ink);
               margin:0; cursor:pointer; }
.chanrow__on input { width:15px; height:15px; }
.chanrow__body { display:flex; flex-direction:column; gap:7px; min-width:0; }
.chanrow__body textarea { font-size:13px; }
.chanrow__foot { display:flex; align-items:center; justify-content:space-between;
                 gap:12px; flex-wrap:wrap; }
.chanrow__foot input[type=number] { display:inline-block; }
.announce__count { border:1px solid var(--hair); border-radius:var(--r-sm);
                   background:var(--paper); padding:10px 12px; margin-bottom:4px; }
.announce__arg { display:inline-flex; align-items:center; gap:6px; }
@media (max-width:700px) { .chanrow { grid-template-columns:1fr; }
                           .noticeitem__tokens { max-width:100%; } }

/* ---- Why an item ranks where it does --------------------------------------
   The recovery score is a number nobody has a reason to trust on sight, so the
   vault shows the components that made it. A ranking a merchant cannot audit
   is a ranking they will not act on. */
.scorebars { display:grid; gap:7px; }
.scorebar { display:grid; grid-template-columns:150px 1fr 38px; align-items:center; gap:10px; }
.scorebar__label { font-size:12px; color:var(--ink2); }
.scorebar__track { height:8px; border-radius:99px; background:var(--band); overflow:hidden; }
.scorebar__fill { height:100%; background:var(--accent); border-radius:99px; }
.scorebar__fill.is-neg { background:var(--amber); }
.scorebar__num { font-size:12px; color:var(--mute); text-align:right; font-variant-numeric:tabular-nums; }
@media (max-width:560px) {
  .scorebar { grid-template-columns:110px 1fr 34px; }
}

/* ---- Good, better, best ----------------------------------------------------
   Each option is a card of its own in the estimate editor, because a merchant
   building three tiers is thinking in tiers, not in a flat list of lines with
   a column that says which is which. */
.optioncard { border-left:3px solid var(--hair); }
.optioncard.is-rec { border-left-color:var(--accent); }
.optioncard__name { display:flex; align-items:center; gap:10px; flex:1; min-width:0; }
.optioncard__name h2 { margin:0; white-space:nowrap; }
.optioncard__name input { max-width:220px; }
.optioncard__acts { display:flex; align-items:center; gap:14px; }
.optioncard__acts .checkline { margin:0; font-size:12.5px; white-space:nowrap; }
.totals__opt { display:flex; justify-content:space-between; align-items:center;
               gap:12px; padding:6px 0; font-size:13.5px; }
.totals__opt .num { font-variant-numeric:tabular-nums; font-weight:600; }
.totals__opt.is-rec { font-weight:600; }
.totals__opt .chip { margin-left:7px; }
@media (max-width:560px) {
  .optioncard__acts { gap:8px; }
  .optioncard__name { flex-wrap:wrap; }
}

/* ---- The customer choosing a tier -----------------------------------------
   Radios, not three buttons: the question is which one, and a person must be
   able to change their mind before they type their name. */
.opts { display:grid; gap:10px; margin:14px 0; }
/* THESE ARE <label> ELEMENTS, so the global label rule applies: 10px, bold,
   gray, uppercase and letter-spaced. That is right for a field caption and
   completely wrong for a price card — it shouted every option name and every
   line of scope. Reset first, style second. The upload bar learned this the
   same way. */
.opt, .opt * { text-transform:none; letter-spacing:normal; }
.opt { display:block; border:1px solid var(--hair); border-radius:10px; padding:12px 14px;
       cursor:pointer; background:#fff; font-size:14px; font-weight:400;
       color:var(--ink); }
/* And the same for the radio, which inherits the full-width sizing every other
   input on these pages wants. */
.opt input[type=radio], .reason input[type=radio] {
  width:18px; height:18px; min-height:0; flex:0 0 18px; margin:0; padding:0; accent-color:var(--accent);
}
.opt.is-on { border-color:var(--accent); box-shadow:0 0 0 1px var(--accent) inset; }
.opt__head { display:flex; align-items:center; gap:10px; }
.opt__name { flex:1; min-width:0; display:flex; align-items:center; gap:8px; flex-wrap:wrap; }
.opt__name b { font-size:15px; font-weight:600; color:var(--ink); }
.opt__price b { font-size:17px; font-weight:600; color:var(--ink);
                font-variant-numeric:tabular-nums; }
.opt__price { text-align:right; white-space:nowrap; font-size:17px; }
.opt__price .sub { font-size:11.5px; }
.opt__lines { margin:8px 0 0; padding:0 0 0 26px; }
.opt__lines li { font-size:13px; color:var(--ink2); margin:2px 0; list-style:disc; }
.reasons { display:grid; gap:2px; margin:8px 0; }
.reason, .reason * { text-transform:none; letter-spacing:normal; }
.reason { display:flex; align-items:center; gap:9px; padding:5px 2px; font-size:13.5px;
          color:var(--ink); font-weight:400; cursor:pointer; }

/* ---- One-tap pickers ------------------------------------------------------
   A technician answering "what happens if nobody does this" is standing in a
   plant room holding a phone. Five chips beat a dropdown, and a dropdown beats
   a number they have to invent. */
.pickrow { display:flex; flex-wrap:wrap; gap:6px; }
.pickrow button.chipbtn.is-on { background:var(--accent); color:#fff; }
.pickrow button.chipbtn { min-height:34px; }

/* ---- Boarding: the vertical pills ------------------------------------------
   A category is picked by recognition, not by reading, so these are big enough
   to hit and spaced far enough apart to scan. The trade cards under them carry
   a sentence each, because "Detailing" and "Auto repair" are not obviously
   different until you say what each one does. */
/* `.vpill`, not `.pill`: that name was already the small crimson environment
   badge in the rail, and taking it over turned the word "Sandbox" beside the
   logo into a large white button. Two things called pill is one too many. */
.vpills { display:flex; flex-wrap:wrap; gap:8px; margin:6px 0 10px; }
.vpill { display:inline-flex; align-items:center; gap:8px; padding:9px 16px;
         border-radius:99px; border:1px solid var(--hair); background:#fff;
         color:var(--ink); font-size:13.5px; font-weight:600; min-height:40px;
         box-shadow:none; white-space:nowrap; }
.vpill:hover { background:var(--paper); border-color:var(--accent-lt); }
.vpill.is-on { background:var(--accent); border-color:var(--accent); color:#fff; }
.vpill.is-on:hover { background:var(--accent-dark); }
.vpill__icon { opacity:.8; font-size:14px; }

.tradegrid { display:grid; grid-template-columns:repeat(auto-fill, minmax(230px, 1fr)); gap:10px; }
/* A <button>, so the base button skin has to be undone before it is dressed
   as a card — the same lesson the option rows taught. */
.trade { display:block; text-align:left; padding:12px 14px; border-radius:var(--r-sm);
         border:1px solid var(--hair); background:#fff; color:var(--ink);
         font-weight:400; font-size:13.5px; min-height:0; box-shadow:none;
         white-space:normal; }
.trade:hover { background:var(--paper); border-color:var(--accent-lt); }
.trade.is-on { border-color:var(--accent); background:var(--paper);
               box-shadow:0 0 0 2px rgba(42,110,158,.14); }
.trade b { display:block; font-size:14px; font-weight:600; margin-bottom:3px; }
.trade .sub { font-size:12px; line-height:1.45; }

.featgrid { display:grid; grid-template-columns:repeat(auto-fill, minmax(280px, 1fr));
            gap:10px 18px; }
.featgrid .featrow { align-items:flex-start; }

.peoplelist { display:grid; gap:8px; }
.peoplerow { display:grid; grid-template-columns:1fr 1fr 150px auto; gap:8px; align-items:center; }
@media (max-width:720px) {
  .peoplerow { grid-template-columns:1fr 1fr; }
  .peoplerow select { grid-column:1 / -1; }
}

/* ---- Sign-in notices --------------------------------------------------------
   One dialog carrying everything unseen, each saying who it is from, because a
   merchant can hear from the platform and from their own ISO on the same
   morning and those are two different relationships. */
.updates { display:grid; gap:18px; }
.update + .update { border-top:1px solid var(--hair); padding-top:16px; }
.update__head { display:flex; align-items:center; gap:9px; flex-wrap:wrap; margin-bottom:8px; }
.update__title { font-size:15px; }
.update__from { font-size:12px; margin-left:auto; }
.update__body p { margin:0 0 8px; line-height:1.55; }
.update__body p:last-child { margin-bottom:0; }

/* ── Vehicles and inspections ─────────────────────────────────────────────

   The buttons on an inspection item are sized for a thumb in a nitrile glove
   on a tablet propped against a fender, which is why they are 44px tall and
   not the app's usual control height. Everything a technician taps forty
   times an hour gets that treatment; nothing else does. */

  background:var(--paper); border:1px solid var(--hair); border-radius:10px;
  padding:14px 16px; margin:0 0 14px; }
  background:#fff; border:1px solid var(--hair); border-radius:6px;
  padding:3px 8px; font-size:13px; }
  color:var(--mute); }

  flex-direction:column; align-items:flex-end; gap:2px; min-width:150px; }
  color:var(--mute); }
  font-variant-numeric:tabular-nums; line-height:1.1; }

/* The VIN box and its Decode button are ONE control, so they share a baseline
   whatever the hint underneath does. align-items:stretch rather than a nudge,
   so the button matches the input's height exactly instead of approximately. */
.vininput { display:flex; gap:8px; align-items:stretch; }
.vininput > input { flex:1 1 auto; min-width:0; }
.vininput > button { flex:0 0 auto; white-space:nowrap; }
.vehdecode { margin:-4px 0 10px; min-height:16px; }

.sheetlist__row { display:flex; gap:14px; align-items:center;
  justify-content:space-between; padding:10px 0;
  border-bottom:1px solid var(--hair); }
.sheetlist__row:last-child { border-bottom:0; }

.inspbar { display:flex; gap:14px; align-items:center; flex-wrap:wrap;
  background:var(--paper); border:1px solid var(--hair); border-radius:10px;
  padding:10px 14px; margin:0 0 14px; position:sticky; top:0; z-index:5; }
.inspbar__count b { font-size:18px; color:var(--navy); }
.inspbar__count { display:flex; gap:6px; align-items:baseline; }
.inspbar__tally { display:flex; gap:6px; flex-wrap:wrap; }

.inspsec h2 { margin-bottom:4px; }
.insplist { display:flex; flex-direction:column; }
.inspitem { border-top:1px solid var(--hair); padding:10px 0; }
.inspitem:first-child { border-top:0; }
.inspitem__head { display:flex; gap:12px; align-items:flex-start;
  justify-content:space-between; }
.inspitem__head > div:first-child { min-width:0; }
.inspitem__meas { font-variant-numeric:tabular-nums; color:var(--ink); }
.inspitem.is-red    { box-shadow:inset 3px 0 0 var(--red); padding-left:10px; }
.inspitem.is-yellow { box-shadow:inset 3px 0 0 #B8860B; padding-left:10px; }
.inspitem.is-green  { box-shadow:inset 3px 0 0 #1B7A5A; padding-left:10px; }

.inspbtns { display:flex; gap:6px; flex:0 0 auto; }
.inspbtn { min-height:44px; min-width:60px; padding:0 12px; border-radius:8px;
  font-size:13px; font-weight:650; background:#fff; color:var(--steel);
  border:1px solid var(--hair); box-shadow:none; }
.inspbtn--ok.is-on   { background:#1B7A5A; color:#fff; border-color:#1B7A5A; }
.inspbtn--warn.is-on { background:#B8860B; color:#fff; border-color:#B8860B; }
.inspbtn--bad.is-on  { background:var(--red); color:#fff; border-color:var(--red); }
.inspbtn--na.is-on   { background:var(--steel); color:#fff; border-color:var(--steel); }

.inspitem__badbox { margin:10px 0 2px; padding:10px 12px; background:var(--paper);
  border:1px solid var(--hair); border-radius:8px;
  display:flex; flex-direction:column; gap:8px; }
.inspmeasure { display:flex; gap:10px; align-items:center; flex-wrap:wrap; }
.inspmeasure label { margin:0; }
.inspmeasure__in { display:flex; align-items:center; gap:6px; }
.inspmeasure__in input { width:110px; }
.inspmeasure__unit { color:var(--steel); font-size:13px; }
.inspnote label { margin:0 0 2px; display:block; }
.inspnote textarea { width:100%; }
.inspphotos { display:flex; gap:8px; align-items:center; flex-wrap:wrap; }
.inspitem__needphoto { color:var(--red); }

@media (max-width:640px) {
  .inspitem__head { flex-direction:column; align-items:stretch; }
  .inspbtns { width:100%; }
  .inspbtn { flex:1 1 0; min-width:0; }
}

/* ── The inspection a customer opens ────────────────────────────────────── */

.insp_head h1 { margin:4px 0 6px; }
.insp_score { margin:14px 0 10px; padding:12px 14px; border-radius:10px;
  background:var(--band); border:1px solid var(--hair); }
.insp_score b { font-size:30px; color:var(--navy); line-height:1;
  font-variant-numeric:tabular-nums; }
.insp_score span { font-size:15px; color:var(--ink2); }
.insp_summary { font-size:16px; line-height:1.5; margin:10px 0 0; }
.insp_tally { display:flex; gap:8px; flex-wrap:wrap; margin-top:12px; }

.insp_item { border-top:1px solid var(--hair); padding:12px 0; }
.insp_item:first-of-type { border-top:0; }
.insp_item__head { display:flex; gap:10px; align-items:baseline;
  justify-content:space-between; flex-wrap:wrap; }
.insp_item__head b { font-size:16px; }
.insp_item__meas { font-variant-numeric:tabular-nums; color:var(--ink2);
  font-size:14px; margin-top:2px; }
.insp_item__rec { color:var(--ink2); }

.insp_photos { display:flex; gap:8px; flex-wrap:wrap; margin-top:8px; }
.insp_photo { width:calc(50% - 4px); max-width:280px; border-radius:8px;
  border:1px solid var(--hair); display:block; }
@media (min-width:720px) { .insp_photo { width:calc(33.333% - 6px); } }

.insp_greenlist { columns:2; column-gap:22px; margin:12px 0 0; padding-left:18px; }
.insp_greenlist li { break-inside:avoid; margin:0 0 4px; }
@media (max-width:560px) { .insp_greenlist { columns:1; } }
.insp_foot p { margin:0 0 6px; }

/* A photograph on the technician's sheet, not a filename. Small enough that
   six fit on a phone, big enough to tell whether it is in focus. */
.inspthumb { width:92px; height:70px; object-fit:cover; border-radius:8px;
  border:1px solid var(--hair); background:var(--paper); display:block; }

/* ── What has already been offered here ───────────────────────────────────

   Sits directly under the identity block on the passport, because "have we
   already quoted this?" is a question you want answered BEFORE you open your
   mouth, not three cards down the page. */

.offered__head { display:flex; align-items:baseline; justify-content:space-between;
  gap:12px; flex-wrap:wrap; }
.offered__head h2 { margin:0; }
.offered__sum { font-size:14px; color:var(--steel); }
.offered__sum b { color:var(--navy); font-variant-numeric:tabular-nums; }
.offered__why { margin:2px 0 10px; }

.offered { display:flex; flex-direction:column; }
.offered--quiet { opacity:.72; }
.offeredrow { display:flex; gap:16px; align-items:flex-start;
  justify-content:space-between; padding:10px 0; border-top:1px solid var(--hair); }
.offeredrow:first-child { border-top:0; }
.offeredrow__main { min-width:0; flex:1 1 auto; }
.offeredrow__title { display:flex; gap:8px; align-items:baseline; flex-wrap:wrap; }
.offeredrow__title b { font-size:15px; color:var(--navy); }
.offeredrow__when { color:var(--mute); }
.offeredrow__money { flex:0 0 auto; text-align:right; display:flex;
  flex-direction:column; align-items:flex-end; gap:1px; min-width:132px; }
.offeredrow__money > b { font-size:17px; color:var(--navy);
  font-variant-numeric:tabular-nums; }
.offered__more { margin-top:10px; padding-top:8px; border-top:1px solid var(--hair); }

@media (max-width:640px) {
  .offeredrow { flex-direction:column; gap:6px; }
  .offeredrow__money { text-align:left; align-items:flex-start; min-width:0;
    flex-direction:row; gap:10px; align-items:baseline; }
}

/* ---- Inspections on a repair order -------------------------------------------
   The order is where a car's day is read, so the sheets done on it live here
   rather than only on the inspections list. Three columns: what it was, what
   it found, and the one thing it is waiting for. The found-column is fixed
   rather than fluid so the counts line up down the list — scanning "3 now" in
   a column beats reading it inside a sentence. */
.jobins { display:flex; flex-direction:column; gap:2px; }
.jobins__row { display:flex; align-items:flex-start; gap:16px;
               padding:12px 0; border-top:1px solid var(--line); }
.jobins__row:first-child { border-top:0; }
.jobins__what { flex:1; min-width:0; }
.jobins__found { display:flex; flex-wrap:wrap; gap:6px; align-items:center;
                 flex-shrink:0; width:210px; }
@media (max-width:760px) {
  .jobins__row { flex-wrap:wrap; }
  .jobins__what { flex-basis:100%; }
  .jobins__found { width:auto; }
}

/* ---- The approval sheet ------------------------------------------------------
   What a customer sees when a technician has been over their car: one card per
   job, a price, and three answers. Read on a phone, one-handed, by somebody at
   work — so the tap targets are full-height and the chosen state is carried by
   colour AND by weight, because a bright ring alone disappears in a car park in
   sunlight. */
.jobsheet { margin:20px 0 8px; }
.jobsheet h2 { margin:0 0 4px; }
.jobsheet__list { display:flex; flex-direction:column; gap:12px; margin-top:14px; }

.jobcard { border:1px solid var(--line); border-radius:14px; padding:14px 16px;
           background:var(--white); transition:border-color .14s ease, box-shadow .14s ease; }
.jobcard.is-approved { border-color:rgba(31,122,74,.55); box-shadow:0 0 0 1px rgba(31,122,74,.25); }
.jobcard.is-deferred { border-color:rgba(176,124,18,.5); }
.jobcard.is-declined { opacity:.62; }

.jobcard__head { display:flex; align-items:flex-start; justify-content:space-between; gap:14px; }
.jobcard__price { text-align:right; white-space:nowrap; }
.jobcard__price b { font-size:19px; font-variant-numeric:tabular-nums; }

.jobcard__lines { margin:10px 0 0; padding:0 0 0 16px; color:var(--ink2); font-size:13px; }
.jobcard__lines li { margin:2px 0; }

.jobcard__pick { display:flex; gap:8px; margin-top:14px; }
.jobcard__pick .pick { flex:1; min-height:46px; font-size:13px; font-weight:600;
                       background:rgba(255,255,255,.72); color:var(--ink2);
                       border:1px solid rgba(19,41,75,.16); box-shadow:none; }
.jobcard__pick .pick:hover { background:#fff; }
.jobcard__pick .pick.is-on { color:#fff; border-color:transparent; }
.jobcard__pick .pick.is-on.is-yes   { background:#1f7a4a; }
.jobcard__pick .pick.is-on.is-later { background:#b07c12; }
.jobcard__pick .pick.is-on.is-no    { background:#8a1f24; }
.jobcard__said { margin-top:12px; }

@media (max-width:420px) {
  /* Three words across a narrow phone wrap mid-label; stacking keeps each
     answer on one line and each target a full thumb tall. */
  .jobcard__pick { flex-direction:column; }
}

/* ---- Taking the answer in the shop -------------------------------------------
   The advisor's version of the customer's sheet: denser, because it is read on
   a desktop with a phone against an ear, and the whole list has to be visible
   at once while the customer is talking. */
.rolist { display:flex; flex-direction:column; gap:8px; margin:14px 0; }
.rojob { display:flex; align-items:center; gap:14px; padding:10px 12px;
         border:1px solid var(--line); border-radius:12px; }
.rojob__what { flex:1; min-width:0; }
.rojob__what .sub { overflow:hidden; text-overflow:ellipsis; white-space:nowrap; }
.rojob__price { font-weight:700; font-variant-numeric:tabular-nums; white-space:nowrap; }
.rojob__pick { display:flex; gap:6px; flex-shrink:0; }
.rojob__pick .pick { min-height:34px; padding:6px 12px; font-size:12px;
                     background:rgba(255,255,255,.72); color:var(--ink2);
                     border:1px solid rgba(19,41,75,.16); box-shadow:none; }
.rojob__pick .pick.is-on { color:#fff; border-color:transparent; }
.rojob__pick .pick.is-on.is-approved { background:#1f7a4a; }
.rojob__pick .pick.is-on.is-deferred { background:#b07c12; }
.rojob__pick .pick.is-on.is-declined { background:#8a1f24; }

@media (max-width:720px) {
  .rojob { flex-wrap:wrap; }
  .rojob__what { flex-basis:100%; }
  .rojob__pick { flex:1; }
  .rojob__pick .pick { flex:1; }
}
.rojob.is-done { border-color:rgba(31,122,74,.45); background:rgba(31,122,74,.05); }

/* ---- The finding, inside the job card ----------------------------------------
   The evidence for one recommendation, sitting above its price. Tinted by
   severity so a red finding reads as urgent before a word of it is read, and
   restrained enough that it does not become the loudest thing on a page whose
   job is a decision. */
.finding { margin:12px 0 0; padding:10px 12px; border-radius:10px;
           border-left:3px solid var(--hair); background:rgba(19,41,75,.03); }
.finding.is-red    { border-left-color:#8a1f24; background:rgba(138,31,36,.05); }
.finding.is-yellow { border-left-color:#b07c12; background:rgba(176,124,18,.06); }
.finding__said { display:flex; align-items:baseline; justify-content:space-between;
                 gap:12px; font-size:13px; }
.finding__meas { font-variant-numeric:tabular-nums; font-weight:700;
                 white-space:nowrap; }
.finding__note { margin:6px 0 0; font-size:13px; color:var(--ink2); }

/* Scrolls sideways in its own box rather than pushing the card wide. */
.finding__shots { display:flex; gap:8px; margin-top:10px;
                  overflow-x:auto; padding-bottom:2px; }
.finding__fig { margin:0; flex:0 0 auto; max-width:180px; }
.finding__shot { display:block; width:180px; height:135px; object-fit:cover;
                 border-radius:8px; background:rgba(19,41,75,.06); }
/* The whole width, when there is only one. object-fit stays `cover` at a
   letterbox ratio rather than `contain`: a technician's photograph is taken
   close, and cropping the edges of it costs nothing while a pillarboxed shot
   floating in grey looks like a placeholder. */
.finding__shots.is-one .finding__fig { flex:1 1 auto; max-width:100%; }
.finding__shots.is-one .finding__shot { width:100%; height:260px; }
.finding__fig figcaption { margin-top:4px; font-size:11px; color:var(--ink2);
                           line-height:1.3; }

@media (max-width:420px) {
  .finding__fig, .finding__shot { max-width:150px; width:150px; }
  .finding__shot { height:112px; }
  .finding__shots.is-one .finding__fig,
  .finding__shots.is-one .finding__shot { max-width:100%; width:100%; }
  .finding__shots.is-one .finding__shot { height:190px; }
}

/* ---- The keys ----------------------------------------------------------------
   Where the car is, answered at the top of the order. The collected state is a
   single row rather than a card of its own: once the vehicle has gone the
   question is closed, and a closed question should take one line. */
.relout { display:flex; align-items:flex-start; justify-content:space-between;
          gap:16px; padding:12px 14px; border-radius:12px;
          border:1px solid rgba(31,122,74,.35); background:rgba(31,122,74,.05); }
.relout.is-owing { border-color:rgba(138,31,36,.4); background:rgba(138,31,36,.05); }
@media (max-width:600px) {
  .relout { flex-direction:column; gap:8px; }
}

/* ---- Book against the clock -------------------------------------------------
   One row per sold job: what it was, what it was sold at, what it has actually
   taken. The three figures are a fixed-width group so they line up down the
   card — an efficiency column that wanders with the length of a job name is
   unreadable, and comparing them is the entire point. */
.labor { display:flex; flex-direction:column; gap:2px; }
.labor__row { display:flex; align-items:center; gap:16px; padding:12px 0;
              border-top:1px solid var(--line); }
.labor__row:first-child { border-top:0; }
.labor__row.is-live { background:rgba(75,156,211,.06); border-radius:10px;
                      padding-left:10px; padding-right:10px; }
.labor__what { flex:1; min-width:0; }
.labor__figs { display:flex; gap:22px; flex-shrink:0; }
.labor__fig { min-width:64px; text-align:right; }
.labor__fig span { display:block; font-size:10px; letter-spacing:.06em;
                   text-transform:uppercase; color:var(--ink2); }
.labor__fig b { font-variant-numeric:tabular-nums; }
.labor__act { flex-shrink:0; }
.labor__loose { margin-top:10px; }

@media (max-width:760px) {
  .labor__row { flex-wrap:wrap; }
  .labor__what { flex-basis:100%; }
  .labor__figs { flex:1; gap:14px; }
  .labor__fig { min-width:0; text-align:left; }
}

/* ---- Writing an inspection sheet ---------------------------------------------
   A section per card, a row per point. The flags read as chips because they are
   the things somebody scans for when hunting the one point that is wrong —
   "which of these seventy-one is set to require a photo" is a real question and
   a sentence-per-row makes it unanswerable. */
.sheetsec { margin-bottom:18px; }
.sheetpoints { display:flex; flex-direction:column; gap:2px; }
.sheetpoint { display:flex; align-items:flex-start; gap:16px;
              padding:11px 0; border-top:1px solid var(--line); }
.sheetpoint:first-child { border-top:0; }
.sheetpoint__what { flex:1; min-width:0; }
.sheetpoint__flags { display:flex; flex-wrap:wrap; gap:6px; margin-top:6px; }
.sheetpoint__act { display:flex; align-items:center; gap:12px; flex-shrink:0; }
.sheetpoint__act button.link[disabled] { opacity:.3; cursor:default; }

@media (max-width:720px) {
  .sheetpoint { flex-wrap:wrap; }
  .sheetpoint__what { flex-basis:100%; }
}
.sheetedit { margin-left:10px; }

/* ---- The appointment wall ----------------------------------------------------
   A schedule, read the way a schedule is read: what kind of work, when, which
   car, whose, then who has it and where.

   Soft corners, hairline separation, one accent. The card is a surface rather
   than a box — no hard border, a 1px hairline plus a low soft shadow so it
   sits on the page instead of being drawn on it. Colour appears exactly twice
   and both times it means BAY: a rounded tab down the left edge so a wall of
   twenty is scannable from across the service drive, and a tint on the bay
   pill so one card alone still says where the car is. Nothing else is
   coloured; status and everything else earn attention through weight and
   spacing. */
.apptwall { display:grid; grid-template-columns:repeat(4, minmax(0, 1fr));
            gap:14px; margin-top:16px; }

.appt { --appt-tone: var(--hair);
        position:relative; display:flex; flex-direction:column; gap:0;
        align-items:stretch; text-align:left; min-height:0;
        padding:15px 16px 13px 18px;
        background:var(--white); color:var(--ink);
        border:0; border-radius:16px;
        box-shadow:0 0 0 1px rgba(19,41,75,.07), 0 1px 2px rgba(19,41,75,.05);
        font:inherit; cursor:pointer; overflow:hidden;
        transform-origin:center;
        transition:box-shadow .18s ease, transform .18s ease; }

/* The bay tab. Inset and rounded rather than a full-height slab: a hard stripe
   to the corners reads as a table row, which is the thing this stopped being. */
.appt::before { content:''; position:absolute; left:0; top:14px; bottom:14px;
                width:3px; border-radius:0 3px 3px 0; background:var(--appt-tone); }

/* ⚠ THE BACKGROUND HAS TO BE RESTATED HERE.

   `.appt` is a <button>, and the shared `button:hover` rule paints the accent
   over it — so a card that declared a shadow and a transform on hover and
   nothing else went navy under its own dark text and became unreadable. The
   base `.appt` rule wins at rest because a class beats a type selector; on
   hover it stopped competing because it had nothing to say.

   The highlight is a lift, not a colour change: a wash of the accent at 5%
   over white, which reads as "this one" without touching the text. */
.appt:hover { background:linear-gradient(rgba(75,156,211,.055), rgba(75,156,211,.055)), var(--white);
              box-shadow:0 0 0 1px rgba(19,41,75,.10), 0 14px 32px -12px rgba(19,41,75,.32);
              transform:scale(1.035); z-index:2; }
.appt:focus-visible { background:linear-gradient(rgba(75,156,211,.055), rgba(75,156,211,.055)), var(--white);
                      box-shadow:0 0 0 2px var(--accent),
                                 0 14px 32px -12px rgba(19,41,75,.32);
                      transform:scale(1.035); z-index:2; outline:none; }
/* Every piece of text keeps its own colour through the hover — nothing on
   this card inherits from the button rules it is nominally made of. */
.appt:hover .appt__what, .appt:focus-visible .appt__what { color:var(--ink); }
.appt:hover .appt__when, .appt:focus-visible .appt__when { color:var(--ink); }
.appt:hover .appt__kind, .appt:hover .appt__who,
.appt:focus-visible .appt__kind, .appt:focus-visible .appt__who { color:var(--ink2); }
.appt:active { transform:scale(1.005); }
/* Work happening right now — the one card somebody is hunting for. */
.appt.is-live { box-shadow:0 0 0 1.5px rgba(75,156,211,.55), 0 1px 2px rgba(19,41,75,.05); }
.appt.is-done { opacity:.55; }

@media (prefers-reduced-motion: reduce) { .appt { transition:none; } }

/* Row 1 — what kind of work, and where it has got to. */
.appt__meta { display:flex; align-items:center; justify-content:space-between;
              gap:8px; min-height:20px; }
.appt__kind { font-size:10.5px; font-weight:700; letter-spacing:.07em;
              text-transform:uppercase; color:var(--ink2);
              overflow:hidden; text-overflow:ellipsis; white-space:nowrap; }

/* Row 2 — when. The board is sorted by it and read by it, so it carries real
   weight rather than sitting in the small grey that dates usually get. */
.appt__when { margin-top:10px; font-size:15.5px; font-weight:700; color:var(--ink);
              letter-spacing:-.006em; font-variant-numeric:tabular-nums; }
.appt__when .appt__day { color:var(--ink2); font-weight:600; }

/* Row 3 — the car, which is what an appointment is called on the floor. */
.appt__what { margin-top:3px; font-size:16px; font-weight:700; line-height:1.25;
              letter-spacing:-.011em;
              overflow:hidden; text-overflow:ellipsis; white-space:nowrap; }

/* Row 4 — whose, and the reference nobody says out loud. */
.appt__who { margin-top:3px; display:flex; align-items:baseline; gap:8px;
             font-size:12.5px; color:var(--ink2); overflow:hidden; }
.appt__who > span { overflow:hidden; text-overflow:ellipsis; white-space:nowrap; }
.appt__plate { font-variant-numeric:tabular-nums; letter-spacing:.03em; flex-shrink:0; }
.appt__no { margin-left:auto; flex-shrink:0; opacity:.65; font-variant-numeric:tabular-nums; }

/* Row 5 — who has it and where. Its own band under a hairline, because it
   answers a different question from everything above. */
.appt__foot { display:flex; align-items:center; justify-content:space-between;
              gap:10px; margin-top:13px; padding-top:11px;
              border-top:1px solid rgba(19,41,75,.07); }
.appt__tech { display:flex; align-items:center; gap:8px; min-width:0; }
.appt__av { width:26px; height:26px; border-radius:50%; flex-shrink:0;
            display:inline-flex; align-items:center; justify-content:center;
            font-size:10.5px; font-weight:700; letter-spacing:.02em; }
.appt__av.is-none { background:rgba(19,41,75,.06); color:var(--ink2); font-weight:600; }
.appt__techname { font-size:13px; font-weight:600;
                  overflow:hidden; text-overflow:ellipsis; white-space:nowrap; }
.appt__tech.is-none .appt__techname { font-weight:500; color:var(--ink2); }
.appt__more { font-size:11px; color:var(--ink2); flex-shrink:0; }

/* The bay, tinted to match its tab. */
.appt__bay { flex-shrink:0; font-size:11.5px; font-weight:600; white-space:nowrap;
             padding:4px 10px; border-radius:99px;
             color:var(--appt-tone);
             background:color-mix(in srgb, var(--appt-tone) 12%, transparent); }
.appt__bay.is-none { color:var(--ink2); background:rgba(19,41,75,.05); }

.apptwall__foot { display:flex; align-items:center; justify-content:space-between;
                  gap:12px; margin-top:18px; }

@media (max-width:1100px) { .apptwall { grid-template-columns:repeat(3, minmax(0,1fr)); } }
@media (max-width:820px)  { .apptwall { grid-template-columns:repeat(2, minmax(0,1fr)); } }
@media (max-width:520px)  { .apptwall { grid-template-columns:1fr; } }

/* ---- Asking for work, from the inspection ------------------------------------
   Deliberately quieter than the findings above it: this is an offer, not the
   point of the page. The ticked state is carried by weight and a tinted ground
   rather than by colour alone — read on a phone, outdoors, one-handed. */
.insp_ask__list { display:flex; flex-direction:column; gap:2px; margin:12px 0; }
.insp_ask__row { display:flex; align-items:center; gap:10px; padding:11px 12px;
                 border:1px solid var(--line); border-radius:10px; cursor:pointer; }
.insp_ask__row + .insp_ask__row { margin-top:6px; }
.insp_ask__row.is-on { border-color:rgba(75,156,211,.5); background:rgba(75,156,211,.06); }
.insp_ask__label { flex:1; min-width:0; font-weight:600; font-size:14px; }
.insp_ask__out { margin-top:12px; }

/* ---- One appointment ---------------------------------------------------------
   The same surface language as the board: soft corners, a hairline and a low
   shadow rather than a drawn box, and every section its own panel so the eye
   finds the edges without reading the headings.

   It was one long column on the page ground — Charges ran into Time ran into
   Checklist, and the only thing separating them was a gap. Panels make each
   answerable question a thing you can point at. */
/* `.jobsplit > section` as well as `.jobpage > section`: the two-up row nests
   its panels one level deeper, and a child selector that stopped at the top
   level left them sitting flat on the page ground with no surface at all. */
.jobpage > section,
.jobpage .jobsplit > section { background:var(--white); border:0; border-radius:16px;
                     padding:16px 18px; margin-top:14px;
                     box-shadow:0 0 0 1px rgba(19,41,75,.07),
                                0 1px 2px rgba(19,41,75,.05); }

/* The header panel joins the family: same radius, same hairline-and-shadow,
   no hard border. */
.jobpage > .summary { border:0; border-radius:16px; padding:18px 20px;
                      margin-bottom:14px;
                      box-shadow:0 0 0 1px rgba(19,41,75,.07),
                                 0 1px 2px rgba(19,41,75,.05); }

/* The heading is the panel's label, so it sits tighter to its content than a
   free-standing section heading needs to. */
.jobpage section > h2,
.jobpage section > .card__head { margin-bottom:12px; }
.jobpage section > .card__head { padding-bottom:11px;
                                 border-bottom:1px solid rgba(19,41,75,.07); }

/* The one-line title a job may carry — an orphan between two panels, so it is
   spaced as a lead-in rather than left floating. */
.jobpage > h2 { margin:18px 2px 0; font-size:13px; letter-spacing:.08em; }

/* Notices are already surfaces; they only need the same rhythm. */
.jobpage > .notice, .jobpage > .notice--warn { margin-top:14px; border-radius:14px; }

.jobpage > .back { display:inline-block; margin-bottom:14px; }

/* Tables inside a panel have the panel's padding around them already. */
.jobpage > section > .tablewrap { margin:0 -4px; }

@media (max-width:700px) {
  .jobpage > section { padding:14px 15px; border-radius:14px; }
  .jobpage > .summary { padding:15px 16px; border-radius:14px; }
}

/* The appointment header, built to the board's hierarchy so the card somebody
   clicked and the page it opens are recognisably the same object. */
.jobhead__top { display:flex; align-items:center; justify-content:space-between;
                gap:12px; margin-bottom:10px; }
.jobhead__kind { font-size:10.5px; font-weight:700; letter-spacing:.07em;
                 text-transform:uppercase; color:var(--ink2);
                 overflow:hidden; text-overflow:ellipsis; white-space:nowrap; }
.jobhead__when { font-size:17px; font-weight:700; color:var(--ink);
                 letter-spacing:-.008em; font-variant-numeric:tabular-nums; }
.jobhead__when .appt__day { color:var(--ink2); font-weight:600; }
.jobhead__what { margin-top:3px; font-size:23px; font-weight:700; line-height:1.2;
                 letter-spacing:-.018em; }
.jobhead__who { margin-top:7px; display:flex; align-items:baseline; gap:12px;
                flex-wrap:wrap; font-size:13.5px; color:var(--ink2); }
.jobhead__who a { font-weight:600; }
.jobhead__plate { font-variant-numeric:tabular-nums; letter-spacing:.04em;
                  padding:2px 9px; border-radius:99px;
                  background:rgba(19,41,75,.06); font-size:12px; font-weight:600;
                  color:var(--ink2); }
.jobhead__link { font-weight:500; }

/* ---- The appointment, laid out ----------------------------------------------
   Money first, then the two things you glance at, then the work. */

/* CHARGES IS THE CENTREPIECE. It is what the page is for — everything else on
   it exists to justify or explain this number — so it gets the weight: a
   fuller surface, a heavier total, and the first position under the header. */
.jobpage > section.jobcharges { padding:18px 20px; }
.jobcharges > .card__head h2 { font-size:12px; color:var(--ink); }
.jobcharges .jobline__sum,
.jobcharges .jobtotal { font-size:19px; font-weight:800; }

/* Found, and taken — side by side. */
.jobsplit { display:grid; grid-template-columns:1fr 1fr; gap:14px;
            align-items:start; }
.jobsplit > section { margin-top:14px; }
.jobsplit.is-one { grid-template-columns:1fr; }
@media (max-width:900px) { .jobsplit { grid-template-columns:1fr; } }

/* The header's lower band: who has it, and whether it has been looked over. */
.jobhead__title { font-size:19px; font-weight:700; letter-spacing:-.012em;
                  margin:0; text-transform:none; color:var(--ink); }
.jobhead__foot { display:flex; align-items:center; justify-content:space-between;
                 gap:14px; flex-wrap:wrap; margin-top:14px; padding-top:13px;
                 border-top:1px solid rgba(19,41,75,.07); }
.jobhead__tech { display:flex; align-items:center; gap:14px; flex-wrap:wrap; min-width:0; }
.jobhead__person { display:inline-flex; align-items:center; gap:8px; min-width:0; }
.jobhead__person.is-none .jobhead__personname { color:var(--ink2); font-weight:500; }
.jobhead__personname { font-size:13.5px; font-weight:600;
                       overflow:hidden; text-overflow:ellipsis; white-space:nowrap; }
.jobhead__acts { display:flex; align-items:center; gap:12px; flex-shrink:0; }

/* One pill, one verb: inspected and viewable, or not and startable. */
.inspill { display:inline-flex; align-items:center; gap:7px;
           padding:5px 12px 5px 10px; border-radius:99px;
           font:inherit; font-size:12px; font-weight:600; line-height:1.4;
           text-decoration:none; white-space:nowrap; cursor:pointer;
           border:1px solid transparent; box-shadow:none; min-height:0; }
.inspill.is-done { background:rgba(31,122,74,.10); color:#1f7a4a; }
.inspill.is-live { background:rgba(75,156,211,.12); color:var(--accent-dark); }
.inspill.is-none { background:rgba(19,41,75,.05); color:var(--ink2); }
.inspill:hover { filter:brightness(.97); }
.inspill__tick { font-size:11px; }
.inspill__do { font-weight:700; opacity:.85;
               border-left:1px solid currentColor; padding-left:8px; margin-left:2px; }
span.inspill { cursor:default; }

/* ⚠ HALF A ROW IS NOT A NARROW WINDOW. The inspection row stacks below 760px
   of VIEWPORT, which is the wrong measurement once the panel is half of a
   wide screen: at 1400px the media query stays quiet and the row is squeezed
   into 570px, wrapping the sheet name over two lines and its date over three.
   Inside the split it always stacks. */
.jobsplit .jobins__row { flex-wrap:wrap; }
.jobsplit .jobins__what { flex-basis:100%; margin-bottom:10px; }
.jobsplit .jobins__found { width:auto; }
.jobsplit .rowline__act { flex-wrap:wrap; gap:8px; }
.jobsplit .timebar { flex-wrap:wrap; }
.jobsplit .tablewrap { overflow-x:auto; }

/* A choice between two whole models, not a checkbox. Each option carries the
   sentence that decides it, because "standard" and "per employee" mean
   nothing until you know what each does to a line. */
.modelist { display:flex; flex-direction:column; gap:10px; margin:12px 0; }
/* A bare `label` is styled as a 10px uppercase field caption app-wide, which
   is right for "Payment terms" above a select and wrong for a whole option
   somebody reads to make a decision. Reset it back to running text. */
.modeopt { display:flex; align-items:flex-start; gap:11px; padding:13px 14px;
           border:1px solid var(--line); border-radius:12px; cursor:pointer;
           font-size:13px; letter-spacing:normal; text-transform:none;
           color:var(--ink); font-weight:400; }
.modeopt.is-on { border-color:rgba(75,156,211,.55); background:rgba(75,156,211,.06); }
.modeopt input { margin-top:3px; flex-shrink:0; }
.modeopt b { display:block; font-size:14px; font-weight:700; color:var(--ink);
             text-transform:none; letter-spacing:normal; }
.modeopt .sub { margin-top:3px; }

/* Labor, inside the charge dialog. Folded away until it is asked for: most
   charges are parts, and an hours box in front of somebody adding wiper
   blades is a question with no answer. */
.chg__labortoggle { margin-top:4px; }
.chg__labor { margin-top:10px; padding:12px 13px; border-radius:12px;
              background:rgba(75,156,211,.06);
              border:1px solid rgba(75,156,211,.18); }
.chg__labor .field { margin:0; }
.chg__rate { margin-top:8px; }
.chg__rate .hint { margin:0; }

/* The posted rates. One row per kind of work, current figure on the right
   where the eye compares them, and what it was before underneath — "we went
   to $172 in July" is the sentence somebody is actually checking. */
.laborrates { margin-top:4px; }
.ratelist { display:flex; flex-direction:column; gap:2px; }
.raterow { display:flex; align-items:center; gap:16px; padding:11px 0;
           border-top:1px solid var(--line); }
.raterow:first-child { border-top:0; }
.raterow__what { flex:1; min-width:0; }
.raterow__what b { font-size:14px; }
.raterow__now { text-align:right; flex-shrink:0; min-width:150px; }
.raterow__now b { font-size:17px; font-variant-numeric:tabular-nums; }
@media (max-width:600px) {
  .raterow { flex-wrap:wrap; }
  .raterow__what { flex-basis:100%; }
  .raterow__now { text-align:left; min-width:0; }
}

/* ---- Picking from the price book ---------------------------------------------
   Two lines per row: what it is, then what it costs and where it is filed.
   The price is right-aligned and bold because "which of these two brake pads"
   is usually a question the money answers. */
.prodpick__row { padding:9px 13px; }
.prodpick__name { font-weight:600; font-size:13.5px; line-height:1.3;
                  overflow:hidden; text-overflow:ellipsis; white-space:nowrap; }
.prodpick__meta { display:flex; align-items:baseline; justify-content:space-between;
                  gap:12px; margin-top:2px; font-size:12px; color:var(--ink2); }
.prodpick__meta span { overflow:hidden; text-overflow:ellipsis; white-space:nowrap; }
.prodpick__meta b { flex-shrink:0; font-variant-numeric:tabular-nums; color:var(--ink); }
/* Ten rows is a screenful on a laptop and a scroll on a phone. */
.prodpick .acbox { max-height:340px; overflow-y:auto; }

/* The labor card carries two decisions — how an hour is priced, and what the
   rates are — so it gets a clear gap before the document-defaults row rather
   than the same rhythm as the cards beside each other. */
.laborcard { margin-bottom:22px; }
.laborcard > .card__head { padding-bottom:12px; margin-bottom:14px;
                           border-bottom:1px solid var(--hair); }

/* ---- What an appointment type always charges ---------------------------------
   The list row is money on the right and a stepper in the middle, which is the
   shape of every other line-item row in the app — a type's charges are a price
   list, so they are read like one. */
.jtchg { display:flex; align-items:baseline; gap:12px; }
.jtchg span { flex:1; min-width:0; }
.jtchg b { font-variant-numeric:tabular-nums; white-space:nowrap; }

.chglist { display:flex; flex-direction:column; gap:2px; margin:4px 0 18px; }
.chgrow { display:flex; align-items:center; gap:14px; padding:11px 0;
          border-top:1px solid var(--line); }
.chgrow:first-child { border-top:0; }
.chgrow__what { flex:1; min-width:0; display:flex; flex-direction:column; gap:2px; }
.chgrow__what b { font-size:14px; }
.chgrow__what span { font-size:12px; }
.chgrow__qty { width:82px; flex-shrink:0; text-align:right;
               font-variant-numeric:tabular-nums; }
.chgrow__amt { min-width:92px; text-align:right; flex-shrink:0;
               font-variant-numeric:tabular-nums; font-size:15px; }
/* The total is the same row shape with the borders doubled, so it reads as the
   sum of what is above it rather than one more charge. */
.chgrow--sum { border-top:2px solid var(--hair); padding-top:12px; }
.chgrow--sum .chgrow__amt b { font-size:17px; }

.chgadd { display:flex; flex-direction:column; gap:6px; }
.chgadd__lbl { font-size:12px; font-weight:600; letter-spacing:.03em;
               text-transform:uppercase; color:var(--muted); }

@media (max-width:600px) {
  .chgrow { flex-wrap:wrap; }
  .chgrow__what { flex-basis:100%; }
  .chgrow__qty { margin-left:auto; }
}
.chgadd__room { height:0; }
.chgadd__room.is-open { height:300px; }
/* What the chosen appointment type will bring, under the dropdown that
   chooses it. Empty until there is something to say. */
.typechg { display:block; margin-top:6px; }
.typechg b { font-variant-numeric:tabular-nums; }

/* ---- "When can it go in?" ----------------------------------------------------
   Calendar on the left is how you move around; the panel on the right is what
   you do when you get there. Two columns on a desk, stacked on a phone where
   the calendar comes first because it is the thing being browsed. */
.sched { display:grid; grid-template-columns:360px 1fr; gap:24px; align-items:start; }
/* Wider than a wide dialog: a half-hour block has to hold a job's name and
   its bay, and at 760px it held neither. */
.modal__panel--sched { max-width:940px; }
.sched__h { margin:0 0 4px; font-size:15px; }
.sched__h5 { margin:16px 0 8px; font-size:12px; font-weight:600;
             letter-spacing:.03em; text-transform:uppercase; color:var(--mute); }
.sched__back { margin-bottom:8px; }
.sched__go { margin-top:18px; }
.sched__go button { width:100%; }

.calmini { border:1px solid var(--hair); border-radius:var(--r);
           padding:12px; background:var(--white); }
.calmini__head { display:flex; align-items:center; justify-content:space-between;
                 gap:8px; margin-bottom:10px; }
.calmini__head b { font-size:14px; }
.calmini__head button.icon { width:30px; height:30px; font-size:17px; line-height:1; }
.calmini__dow { display:grid; grid-template-columns:repeat(7,1fr); gap:2px;
                margin-bottom:4px; }
.calmini__dow span { text-align:center; font-size:10.5px; font-weight:600;
                     letter-spacing:.04em; color:var(--mute); }
.calmini__grid { display:grid; grid-template-columns:repeat(7,1fr); gap:2px; }
.calmini__day { display:flex; flex-direction:column; align-items:center; gap:2px;
                justify-content:flex-start; padding:5px 0 4px; min-height:40px;
                border:1px solid transparent; border-radius:10px;
                background:transparent; cursor:pointer; color:var(--ink); }
.calmini__day:hover { background:var(--band); }
.calmini__n { font-size:13px; font-variant-numeric:tabular-nums; line-height:1; }
/* Days either side of the month are still bookable — a week that straddles
   the first is one week to whoever is looking at it. */
.calmini__day.is-out { color:var(--mute); }
.calmini__day.is-shut .calmini__n { text-decoration:line-through;
                                    text-decoration-thickness:1px; }
.calmini__day.is-today { border-color:var(--hair); font-weight:700; }
.calmini__day.is-on { background:var(--accent); color:var(--white);
                      border-color:var(--accent); }
.calmini__day.is-on .calmini__more { color:var(--white); }
.calmini__dots { display:flex; align-items:center; gap:2px; height:5px; }
.calmini__dots i { width:4px; height:4px; border-radius:50%;
                   background:var(--accent); display:block; }
.calmini__day.is-on .calmini__dots i { background:var(--white); }
.calmini__more { font-size:9px; color:var(--steel); line-height:1; }

/* ---- The day, laid out on an hour grid ---------------------------------------
   It replaces the month in the left column once a day is chosen: a list says
   what is on, a grid says where the gaps are, and the gap is what somebody
   placing a car is looking for. */
.dayview { border:1px solid var(--hair); border-radius:var(--r);
           background:var(--white); overflow:hidden; }
.dayview__head { display:flex; align-items:center; gap:6px; padding:10px 12px;
                 border-bottom:1px solid var(--hair); }
.dayview__head b { font-size:14px; }
.dayview__head button.icon { width:28px; height:28px; font-size:17px; line-height:1; }
.dayview__month { margin-left:auto; }
.dayview__scroll { max-height:392px; overflow-y:auto; padding:0 12px 14px; }
.dayview__body { position:relative; margin-left:52px; margin-top:9px; }

/* The hour line and its label, the label hanging in the gutter the body was
   given room for. */
.dayview__hr { position:absolute; left:0; right:0; height:0;
               border-top:1px solid var(--line); }
.dayview__hr span { position:absolute; left:-52px; top:-7px; width:46px;
                    text-align:right; font-size:10.5px; color:var(--mute);
                    font-variant-numeric:tabular-nums; }

.dayview__shut { position:absolute; left:0; right:0; background:var(--paper);
                 border-radius:6px; }

.dayview__job { position:absolute; overflow:hidden; padding:4px 7px;
                border-radius:8px; background:var(--band);
                border:1px solid var(--hair); border-left:3px solid var(--steel);
                font-size:11.5px; line-height:1.32; color:var(--ink); }
.dayview__job b { display:block; font-variant-numeric:tabular-nums; font-size:11px; }
.dayview__job.is-short { padding:2px 7px; }
.dayview__job.is-short b { display:inline; margin-right:5px; }
.dayview__job.is-short span { display:inline; }
.dayview__job span { display:block; overflow:hidden; text-overflow:ellipsis;
                     white-space:nowrap; }
.dayview__bay { color:var(--steel); }

/* Where the one being booked would land. Drawn as it is chosen, which is the
   reason the day and the times sit side by side. */
.dayview__new { position:absolute; left:2px; right:2px; z-index:2;
                border-radius:8px; background:rgba(42,110,158,.16);
                border:1.5px dashed var(--accent); }
.dayview__new b { display:block; padding:3px 7px; font-size:11px;
                  color:var(--accent); }

.timechips { display:flex; flex-wrap:wrap; gap:6px; }
.timechip { display:inline-flex; align-items:center; gap:5px; cursor:pointer;
            padding:7px 12px; border-radius:999px; font-size:13px;
            border:1px solid var(--hair); background:var(--white);
            color:var(--ink); font-variant-numeric:tabular-nums; }
.timechip:hover { border-color:var(--accent); color:var(--accent); }
.timechip.is-on { background:var(--accent); border-color:var(--accent);
                  color:var(--white); }
/* How many bays are still free then. A slot with none left is not offered at
   all — the custom time below is how a full day still takes a car. */
.timechip__free { font-size:10px; font-style:normal; line-height:1;
                  padding:2px 5px; border-radius:999px;
                  background:var(--band); color:var(--steel); }
.timechip.is-on .timechip__free { background:rgba(255,255,255,.24); color:var(--white); }
.sched__full { margin:8px 0 0; font-size:12px; }

.schedform { display:grid; grid-template-columns:1fr 1fr; gap:12px 16px;
             margin-top:16px; }
.schedform__time { font-variant-numeric:tabular-nums; }

@media (max-width:760px) {
  .sched { grid-template-columns:1fr; gap:18px; }
  .schedform { grid-template-columns:1fr; }
}
/* The bay being booked into, picked out of everything else on the day so the
   gap you are aiming at is the one that matters. */
.dayview__job.is-bay { background:rgba(42,110,158,.18);
                       border-left-color:var(--accent); }

/* On duty, on the roster. A checkbox with its state spelled out beside it —
   "On duty" / "Off duty" reads at a glance down a column of twelve people in
   a way a bare tick does not. */
.duty { display:inline-flex; align-items:center; gap:7px; cursor:pointer;
        font-size:12.5px; white-space:nowrap; }
.duty input { margin:0; }
.duty.is-off { color:var(--mute); }

/* The booking rule sits above the types it governs. */
.techrule { margin-bottom:22px; }
.techrule > .card__head { padding-bottom:12px; margin-bottom:14px;
                          border-bottom:1px solid var(--hair); }

/* ---- Scanning a VIN ----------------------------------------------------------
   The camera pane only exists where the browser can actually read a barcode;
   on iOS the box below it is the scanner, via Scan Text. */
.vinscan { display:flex; flex-direction:column; gap:14px; }
.vinscan__cam:not(:empty) { position:relative; border-radius:var(--r);
                            overflow:hidden; background:#000; aspect-ratio:4/3; }
.vinscan__cam video { width:100%; height:100%; object-fit:cover; display:block; }
/* A window to hold the jamb label in. A VIN barcode is long and short, so the
   frame is the shape of the thing being looked for. */
.vinscan__aim { position:absolute; left:8%; right:8%; top:50%; height:22%;
                transform:translateY(-50%); border:2px solid rgba(255,255,255,.9);
                border-radius:8px; box-shadow:0 0 0 100vmax rgba(0,0,0,.35); }
.vinscan__box { font-variant-numeric:tabular-nums; letter-spacing:.06em;
                text-transform:uppercase; }
.vinscan__vin { font-size:15px; font-weight:700; letter-spacing:.08em;
                font-variant-numeric:tabular-nums; }

/* Today's board, inside the dialog. Time on the left because a technician
   knows roughly when the car came in, even when they do not know whose it is. */
.vinday { display:flex; flex-direction:column; gap:6px; max-height:44vh;
          overflow-y:auto; }
.vinrow { display:flex; align-items:center; gap:12px; width:100%; text-align:left;
          padding:11px 12px; border:1px solid var(--hair); border-radius:var(--r);
          background:var(--white); color:var(--ink); cursor:pointer; }
.vinrow:hover:not([disabled]) { border-color:var(--accent); background:var(--paper); }
.vinrow__when { min-width:74px; display:flex; flex-direction:column; gap:2px; }
.vinrow__when b { font-size:13px; font-variant-numeric:tabular-nums; }
.vinrow__on { font-size:10px; text-transform:uppercase; letter-spacing:.05em;
              color:var(--accent); font-weight:700; }
.vinrow__what { flex:1; min-width:0; display:flex; flex-direction:column; gap:2px; }
.vinrow__what b { font-size:14px; }
.vinrow__what span { font-size:12px; color:var(--steel);
                     overflow:hidden; text-overflow:ellipsis; white-space:nowrap; }
.vinrow__no { font-size:12px; color:var(--mute); font-variant-numeric:tabular-nums;
              white-space:nowrap; }
/* Already has a VIN: shown, so the day reads whole, but not offered — putting
   this VIN on it would be claiming two cars are one car. */
.vinrow.is-taken { opacity:.5; cursor:default; }

/* ---- What the customer answered, waiting for the technician ------------------
   Top of the appointment, above everything: the answers arrive while nobody is
   looking at this screen, so they have to be the first thing read when someone
   comes back to it. Green down the edge when there is approved work, because
   that is the case that changes what happens next. */
.answers { border:1px solid var(--hair); border-left:4px solid var(--steel);
           border-radius:var(--r); background:var(--white); padding:14px 16px;
           margin-bottom:16px; }
.answers.is-yes { border-left-color:var(--good, #1C7A56); }
.answers__head { display:flex; align-items:baseline; gap:12px;
                 margin-bottom:10px; }
.answers__head b { font-size:15px; }
.answers__head a { margin-left:auto; white-space:nowrap; }
.answers__rows { display:flex; flex-direction:column; gap:10px; }
.answers__grp { display:grid; grid-template-columns:150px 1fr auto; gap:14px;
                align-items:start; padding-top:10px;
                border-top:1px solid var(--line); }
.answers__grp:first-child { border-top:0; padding-top:0; }
.answers__lbl b { display:block; font-size:12px; text-transform:uppercase;
                  letter-spacing:.05em; }
.answers__lbl span { font-size:11.5px; color:var(--mute); }
.answers__grp.is-yes   .answers__lbl b { color:#1C7A56; }
.answers__grp.is-no    .answers__lbl b { color:#B3261E; }
.answers__grp.is-later .answers__lbl b { color:var(--steel); }
.answers__grp ul { list-style:none; margin:0; padding:0; display:flex;
                   flex-direction:column; gap:4px; }
/* THE PRICE BELONGS TO THE ITEM, NOT TO THE COLUMN (Greg, 2026-08-27). Pushed
   to the right edge it landed alongside the group total, and two figures side
   by side on the same line read as one sum and its subtotal rather than as a
   line item and the total of the group it is in. Sat next to the name, each
   price is obviously that item's; the total is the only figure on the right,
   which is what makes it read as the total. */
.answers__grp li { display:flex; align-items:baseline; gap:10px;
                   font-size:13.5px; justify-content:flex-start; }
.answers__grp li span { flex:0 1 auto; min-width:0; overflow:hidden;
                        text-overflow:ellipsis; white-space:nowrap; }
.answers__grp li b { font-variant-numeric:tabular-nums; font-weight:600;
                     flex:none; }
.answers__sum { font-size:15px; font-weight:700; font-variant-numeric:tabular-nums;
                white-space:nowrap; }
@media (max-width:700px) {
  .answers__grp { grid-template-columns:1fr; gap:6px; }
  .answers__sum { text-align:right; }
}
/* Where the approved money went, said on the banner that announced it. */
.answers__note { margin-top:10px; padding-top:10px; font-size:13.5px;
                 border-top:1px solid var(--line); }
.approved__list { list-style:none; margin:10px 0; padding:0;
                  display:flex; flex-direction:column; gap:6px; }
.approved__list li { display:flex; align-items:baseline; gap:10px;
                     padding:8px 0; border-top:1px solid var(--line);
                     justify-content:flex-start; }
.approved__list li:first-child { border-top:0; }
/* Same rule as the banner: the price sits with the thing it prices. */
.approved__list li span { flex:0 1 auto; min-width:0; overflow:hidden;
                          text-overflow:ellipsis; white-space:nowrap; }
.approved__list li b { font-variant-numeric:tabular-nums; flex:none; }
.approved__list li:last-child { border-top:2px solid var(--hair);
                                margin-top:2px; padding-top:10px; }
.approved__sum { margin-left:auto !important; }

/* ---- A type's checklist, in its editor ---------------------------------------
   What has to be recorded before this kind of work can be closed. */
.ckhead { display:flex; align-items:baseline; gap:12px; margin-top:4px; }
.ckhead h4 { margin:0; font-size:13px; text-transform:uppercase;
             letter-spacing:.05em; color:var(--mute); }
.ckhead button { margin-left:auto; }
.cknote { margin:0; font-size:12px; }
.cklist { display:flex; flex-direction:column; gap:8px; }
.ckrow { display:flex; align-items:center; gap:12px; }
.ckrow input[type=text] { flex:1; min-width:0; }
.ckrow__req { display:inline-flex; align-items:center; gap:6px;
              font-size:12.5px; white-space:nowrap; }
.ckrow__req input { margin:0; }

/* The action links line up because they are the same three words on every
   row — the column just holds them to the right. */
.jtacts .rowline__act { justify-content:flex-end; }
/* The size control belongs to the crew above it, so it sits inside the group
   rather than under the whole board. */
.crewgrp .pager--size { padding:8px 14px 4px; justify-content:flex-end; }

/* ---- My day ---------------------------------------------------------------

   The crew's own screen: a date, a map, and the run in order. Sized for a
   tablet held in a truck — the map takes the top third and the stops below it
   are thumb-sized rows, not table cells. */

.dayhead { display:flex; align-items:center; gap:10px; flex-wrap:wrap;
           padding:2px 0 10px; }
.dayhead__main { flex:1; min-width:0; display:flex; flex-direction:column; }
.dayhead__main b { font-size:16px; }
.dayhead__arrow { display:inline-flex; align-items:center; justify-content:center;
                  width:38px; height:38px; flex:none; border-radius:var(--r);
                  border:1px solid var(--hair); background:var(--paper);
                  color:var(--ink); font-size:20px; line-height:1;
                  text-decoration:none; }
.dayhead__arrow:hover { border-color:var(--steel); }
.dayhead__today { flex:none; }

/* Shorter than the dispatcher's map: the list underneath is the thing being
   read, and a map filling the screen means scrolling past it to every stop. */
.stopmap { margin-bottom:12px; }
.stopmap__canvas { height:min(38vh, 380px); min-height:220px;
                   border:1px solid var(--hair); border-radius:var(--r);
                   overflow:hidden; background:var(--paper); }
@supports (height:1dvh) { .stopmap__canvas { height:min(38dvh, 380px); } }
.stopmap__note:empty { display:none; }

.stoplist { display:flex; flex-direction:column; gap:8px; }
.stoplist__split { font-size:11.5px; letter-spacing:.04em; text-transform:uppercase;
                   color:var(--steel); padding:10px 2px 2px; }

.stoprow { display:flex; align-items:center; gap:12px; padding:12px 14px;
           border:1px solid var(--hair); border-radius:var(--r);
           background:var(--paper); text-decoration:none; color:var(--ink); }
.stoprow:hover { border-color:var(--steel); }
/* The route number, in the crew's own color — the same number as the pin. */
.stoprow__n { flex:none; width:28px; height:28px; border-radius:50%;
              display:inline-flex; align-items:center; justify-content:center;
              background:var(--steel); color:#fff;
              font-size:12.5px; font-weight:700; font-variant-numeric:tabular-nums; }
.stoprow__main { flex:1; min-width:0; display:flex; flex-direction:column; gap:2px; }
.stoprow__main b { font-size:14.5px; }
.stoprow__main .sub,
.stoprow__when .sub { font-size:11.5px; color:var(--steel); }
/* One line only: an address that wraps to three pushes the time off the row. */
.stoprow__main .sub { white-space:nowrap; overflow:hidden; text-overflow:ellipsis; }
.stoprow__when { flex:none; text-align:right; display:flex; flex-direction:column; gap:2px; }
.stoprow__when b { font-size:13.5px; font-variant-numeric:tabular-nums; }
.stoprow.is-done { opacity:.62; }
.stoprow.is-unmapped .stoprow__n { background:var(--hair); color:var(--steel); }

/* Google owns the DOM inside an info window, so this is a plain descendant
   selector rather than anything scoped. */
.pinbub { font-size:12.5px; line-height:1.45; }
.pinbub b { font-variant-numeric:tabular-nums; }
.pinbub a { color:var(--accent); }

/* ---- Settings › My account ------------------------------------------------

   Unrelated settings that share a page, so they are cards rather than one long
   form: each saves itself, and the gaps say the pinpad has nothing to do with
   the bottom bar. Two columns where there is room, one where there is not —
   these are read on the same tablet the bar they configure appears on. */
.setgrid { display:grid; gap:14px; grid-template-columns:repeat(auto-fit, minmax(320px, 1fr));
           align-items:start; }

.barpick { display:flex; flex-direction:column; gap:8px; }
.barpick__slot { display:flex; align-items:center; gap:8px; }
.barpick__slot .field,
.barpick__slot select { flex:1; min-width:0; margin:0; }
.barpick__n { flex:none; width:22px; height:22px; border-radius:50%;
              display:inline-flex; align-items:center; justify-content:center;
              background:var(--hair); color:var(--steel);
              font-size:11px; font-weight:700; }
.barpick__x { flex:none; padding:6px 10px; line-height:1; }
.barpick__acts { display:flex; gap:8px; flex-wrap:wrap; margin-top:12px; }
.barpick__acts button:last-child { margin-left:auto; }

/* ---- Settings, as an index ------------------------------------------------

   Fifteen tabs was a line of words that wrapped twice and that nobody read to
   the end of. Three columns of pills, each with a sentence, is the difference
   between scanning and searching — and the sentence is the whole reason the
   index earns its extra click. */
/* ⚠ STILL IN USE — DO NOT DELETE AGAIN. The business Settings index moved to
   .setcards (see THE CONTROL ROOM at the end of this file) and these rules were
   deleted with the markup that used them. They were not the only markup that
   used them: the ISO console's reseller index still draws `a.setpill`, and
   removing the CSS left it as a column of raw underlined links with broken
   icons on iso.onsightpayments.com — a host the business login cannot reach,
   which is why nothing caught it for half a day. */
.setpills { display:grid; gap:12px; grid-template-columns:repeat(3, minmax(0,1fr)); }
@media (max-width:1100px) { .setpills { grid-template-columns:repeat(2, minmax(0,1fr)); } }
@media (max-width:680px)  { .setpills { grid-template-columns:1fr; } }

.setpill { display:flex; align-items:flex-start; gap:12px; padding:14px 16px;
           border:1px solid var(--hair); border-radius:12px; background:var(--paper);
           text-decoration:none; color:inherit; }
.setpill:hover { border-color:var(--accent); }
.setpill__icon { flex:none; width:32px; height:32px; border-radius:8px;
                 display:grid; place-items:center; font-size:15px;
                 color:var(--accent); background:var(--band); }
.setpill__main { min-width:0; display:flex; flex-direction:column; gap:3px; }
.setpill__main b { font-size:14.5px; }
.setpill__main .sub { font-size:12px; color:var(--steel); line-height:1.45; }

.setcrumb { display:flex; align-items:center; gap:10px; padding-bottom:14px; }
.setcrumb__back { font-size:13px; color:var(--steel); text-decoration:none; }
.setcrumb__back:hover { color:var(--accent); }
.setcrumb__here { font-size:13px; font-weight:600; }
.setcrumb__here::before { content:'/'; color:var(--hair); margin-right:10px; }

/* The three-bars menu on a roster row. Wide enough to be a thumb target on a
   tablet, quiet enough not to compete with the name beside it. */
.dd__bars { padding:4px 10px; font-size:17px; line-height:1.1; color:var(--steel);
            border:1px solid transparent; border-radius:var(--r); }
.dd__bars:hover { border-color:var(--hair); color:var(--ink); }
.dd__pop--acts { min-width:190px; padding:4px; }
.dd__pop--acts .dd__act { border-radius:6px; }
.dd__pop--acts a.dd__act { display:block; text-decoration:none; }
.dd__pop--acts .dd__act:hover { background:var(--band); }
/* ⚠ background:none IS LOAD-BEARING. The global `button.danger` rule paints a
   solid red fill for the big destructive buttons, and a menu item inheriting
   it came out as red text on a red block — a bar with nothing readable in it.
   Menu items are text, so the colour goes on the text and the fill is only for
   the hover state. */
.dd__pop--acts .dd__act.danger { background:none; color:var(--danger); box-shadow:none; }
.dd__pop--acts .dd__act.danger:hover { background:var(--danger); color:#fff; }

/* The crew picker on the staff page. `.swatches` and `.swatch` already exist
   for the crew dialog — reused rather than restyled, so a colour chosen here
   and a colour chosen there look like the same control. */
.crewdot { display:inline-block; width:9px; height:9px; border-radius:50%;
           margin-right:7px; vertical-align:baseline; flex:none; }
.newcrew { border:1px dashed var(--hair); border-radius:var(--r);
           padding:14px; margin-top:10px; display:flex; flex-direction:column; gap:4px; }
.newcrew > button { align-self:flex-start; margin-top:4px; }

/* A permission row: what it is on the left, the one action available on the
   right. No checkbox — these are states iOS owns, and a switch that cannot
   move is a lie about who is in charge. */
.featrow--act { align-items:center; gap:12px; }
.featrow--act .featrow__main { flex:1; min-width:0; }
.featrow--act > span:last-child { flex:none; }
.featrow--act button { flex:none; }
/* The card head can carry a chip beside the heading — "Mobile / Tablet" on the
   bottom bar. Pushed right so it sits at the far edge like the action buttons. */
.card__head .chip { margin-left:auto; }
.card__head button ~ .chip,
.card__head .chip ~ button { margin-left:8px; }

/* The portal's review ask. A favour, not a task — quieter than the balance
   above it, and it never competes with Pay.

   ⚠ IN app.css, NOT portal.css. portal.html loads app.css and nothing loads
   portal.css at all, so a rule put there does nothing and the button renders
   as bare text. */
.previewask { display:flex; align-items:center; gap:14px; flex-wrap:wrap; }
.previewask > div { flex:1; min-width:220px; }
.previewask a.button { flex:none; }

/* ---- Customer requests, portal side ---------------------------------------

   ⚠ IN app.css. portal.html loads this file and nothing loads portal.css at
   all — a rule put there does nothing. */
.psechead { display:flex; align-items:center; gap:12px; }
.psechead h2 { margin:0; flex:1; }

.preqs { list-style:none; margin:0; padding:0; display:flex; flex-direction:column; gap:10px; }
.preq { border:1px solid var(--hair); border-radius:12px; padding:14px 16px;
        background:#fff; display:flex; flex-direction:column; gap:6px; }
.preq__head { display:flex; align-items:center; gap:10px; }
.preq__head b { flex:1; min-width:0; font-size:14.5px; }
.preq__body { margin:0; font-size:13.5px; line-height:1.5; color:var(--ink); }
.preq__meta { font-size:12px; color:var(--steel); }
/* The answer, set apart from what was asked — they are two different voices. */
.preq__note { border-left:3px solid var(--hair); padding:2px 0 2px 10px;
              font-size:13px; color:var(--steel); }
.preq.is-approved .preq__note { border-left-color:var(--green); }
.preq.is-declined .preq__note { border-left-color:var(--danger); }
.preq__photos { display:flex; gap:8px; flex-wrap:wrap; margin-top:2px; }
.preq__ph { width:78px; height:78px; border-radius:8px; overflow:hidden;
            border:1px solid var(--hair); background:var(--band); cursor:pointer;
            display:flex; align-items:center; justify-content:center; }
.preq__ph img { width:100%; height:100%; object-fit:cover; display:block; }

.askform__shots { display:flex; gap:8px; flex-wrap:wrap; align-items:center; }
.askform__shot { position:relative; width:78px; height:78px; border-radius:8px;
                 overflow:hidden; border:1px solid var(--hair); }
.askform__shot img { width:100%; height:100%; object-fit:cover; display:block; }
.askform__x { position:absolute; top:2px; right:2px; width:22px; height:22px;
              padding:0; line-height:1; border-radius:50%; border:none;
              background:rgba(0,0,0,.55); color:#fff; font-size:15px; }
.askform__add { border:1px dashed var(--hair); background:none; color:var(--steel);
                border-radius:8px; height:78px; padding:0 14px; font-size:12.5px; }
.askform__add:hover { border-color:var(--accent); color:var(--accent); background:none; }

/* ---- Customer requests, staff side ---------------------------------------- */
.reqlist { display:flex; flex-direction:column; gap:10px; }
.reqcard { border:1px solid var(--hair); border-radius:var(--r); padding:14px 16px;
           background:var(--paper); display:flex; flex-direction:column; gap:7px; }
/* Waiting is not a problem, so it gets a mark rather than an alarm. */
.reqcard.is-open { border-left:3px solid var(--accent); }
.reqcard.is-approved { border-left:3px solid var(--green); }
.reqcard.is-declined { border-left:3px solid var(--danger); opacity:.85; }
.reqcard__head { display:flex; align-items:center; gap:10px; }
.reqcard__head b { flex:1; min-width:0; font-size:14.5px; }
.reqcard__body { margin:0; font-size:13.5px; line-height:1.5; }
.reqcard__meta { font-size:12px; color:var(--steel); }
.reqcard__shots { display:flex; gap:8px; flex-wrap:wrap; }
.reqcard__shots .gallery__tile { width:110px; height:110px; }
.reqcard__note { display:flex; flex-direction:column; gap:3px; font-size:12.5px;
                 color:var(--steel); border-top:1px solid var(--hair); padding-top:8px; }
.reqcard__note b { color:var(--ink); font-size:12.5px; }
.reqcard__acts { display:flex; gap:8px; flex-wrap:wrap; padding-top:2px; }

/* ── OnSight Copilot ────────────────────────────────────────────────────────

   Appended rather than spliced: this is a self-contained surface that sits on
   top of the app and shares nothing with the layout beneath it, so it has no
   business being threaded into the middle of the sheet.

   Fixed to the viewport and above everything. The z-index sits under modals on
   purpose — a dialog asking somebody to confirm a charge must not end up
   behind a chat panel. */

/* WHERE IT SITS IS THE PERSON'S DECISION, NOT OURS. Greg, 2026-09-08, trying
   to read a payment error underneath it: "I am getting a error - but cant tell
   what that error is cause the AI Agent button is in the way. We need the
   ability to move the ai agent button." A thing pinned over the bottom-right
   corner will eventually cover the one message somebody needs.

   Two numbers, both offsets from the bottom-right corner, both written by
   copilot.js onto :root and remembered per browser. Everything that has to
   follow the bubble — the panel above it, the panel's own height — is written
   in terms of them, so the drag moves one thing and the surface stays whole. */
:root { --cp-x:20px; --cp-y:20px; }

.cpbubble {
  position:fixed; right:var(--cp-x); bottom:var(--cp-y); z-index:900;
  width:56px; height:56px; border-radius:50%; border:0; cursor:grab;
  /* The base button rule's padding is inside this box: at 9px 17px the content
     area is 22px, exactly the icon, and the touch query widens it to 18px and
     starts clipping. Nothing about a 56px circle wants padding. */
  padding:0; min-height:0;
  display:flex; align-items:center; justify-content:center; color:#fff;
  background:linear-gradient(140deg, var(--accent-bright) 0%, var(--accent) 55%, var(--navy) 100%);
  box-shadow:0 6px 20px rgba(19,41,75,.28), 0 1px 3px rgba(19,41,75,.2);
  transition:transform .16s ease, box-shadow .16s ease;
}
.cpbubble:hover { transform:translateY(-2px); box-shadow:0 10px 26px rgba(19,41,75,.34); }
.cpbubble:active { transform:translateY(0); }
/* Mid-drag the hover lift would fight the pointer for two pixels, and the
   0.16s transition would make the button lag behind the finger. */
.cpbubble.is-dragging,
.cpbubble.is-dragging:hover { cursor:grabbing; transform:none; transition:none; }
.cpbubble:focus-visible { outline:3px solid var(--accent-lt); outline-offset:3px; }
.cpbubble__spark { display:flex; transition:transform .2s ease; }
.cpbubble.is-open .cpbubble__spark { transform:rotate(90deg) scale(.9); }

/* One slow breath, so the thing reads as awake without becoming a distraction
   on a screen somebody stares at all day. Off entirely for anyone who asked
   for less motion. */
.cpbubble__pulse {
  position:absolute; inset:0; border-radius:50%; pointer-events:none;
  box-shadow:0 0 0 0 rgba(75,156,211,.55); animation:cppulse 3.6s ease-out infinite;
}
.cpbubble.is-open .cpbubble__pulse { animation:none; }
@keyframes cppulse {
  0%   { box-shadow:0 0 0 0 rgba(75,156,211,.5); }
  70%  { box-shadow:0 0 0 14px rgba(75,156,211,0); }
  100% { box-shadow:0 0 0 0 rgba(75,156,211,0); }
}
@media (prefers-reduced-motion: reduce) {
  .cpbubble__pulse { animation:none; }
  .cpbubble, .cpbubble__spark { transition:none; }
}

/* The panel hangs off the bubble, above it by default and below it when the
   bubble has been dragged too near the top for 380px of chat to fit — that
   flip is `cp--down`, and copilot.js decides it by measuring, once, as the
   panel opens. The width narrows rather than sliding off the left edge, so a
   bubble parked at the far left still opens a usable panel. */
.cp {
  position:fixed; right:var(--cp-x); bottom:calc(var(--cp-y) + 68px); z-index:900;
  width:390px; max-width:calc(100vw - var(--cp-x) - 16px);
  height:min(620px, calc(100vh - var(--cp-y) - 110px));
  display:flex; flex-direction:column; overflow:hidden;
  background:var(--white); border:1px solid var(--hair);
  border-radius:18px; box-shadow:0 18px 50px rgba(19,41,75,.22);
  animation:cprise .16s ease-out;
}
.cp.cp--down {
  bottom:auto; top:calc(100vh - var(--cp-y) + 12px);
  height:min(620px, calc(var(--cp-y) - 34px));
}
@keyframes cprise { from { opacity:0; transform:translateY(8px); } to { opacity:1; transform:none; } }
@media (prefers-reduced-motion: reduce) { .cp { animation:none; } }

.cp__head {
  display:flex; align-items:center; justify-content:space-between; gap:10px;
  padding:12px 14px; color:#fff;
  background:linear-gradient(120deg, var(--navy) 0%, var(--accent) 130%);
}
.cp__title { display:flex; align-items:center; gap:8px; font-size:14px; }
.cp__mark { display:flex; opacity:.9; }
.cp__mark svg { width:16px; height:16px; }
.cp__badge {
  font-size:10px; font-weight:600; letter-spacing:.02em; padding:2px 7px;
  border-radius:99px; background:rgba(255,255,255,.16); color:rgba(255,255,255,.92);
}
/* box-shadow and min-height come from the base `button` rule. On a
   transparent button the inherited glow paints a shadow with nothing casting
   it, and the 40px floor makes a × taller than the bar it sits in. */
.cp__x {
  border:0; background:none; color:#fff; opacity:.8; cursor:pointer;
  font-size:22px; line-height:1; padding:0 6px;
  min-height:0; box-shadow:none;
  display:flex; align-items:center; justify-content:center;
}
.cp__x:hover { opacity:1; }

.cp__list { flex:1; overflow-y:auto; padding:14px; display:flex; flex-direction:column; gap:10px; }

.cp__hello { color:var(--ink2); }
.cp__hello b { display:block; font-size:15px; margin-bottom:4px; }
.cp__hello p { margin:0 0 12px; font-size:12.5px; color:var(--steel); line-height:1.6; }
.cp__starters { display:flex; flex-direction:column; gap:6px; }
/* ⚠ white-space HAS TO BE RESET. The base `button` rule sets nowrap — right
   for a button that says Save, wrong for a whole sentence, which then ran
   straight out of the side of the pill instead of wrapping inside it. The
   same rule's min-height would also hold a two-line chip open at one line's
   height, so that goes too and the padding sets the size. */
.cp__starter {
  text-align:left; cursor:pointer; font:inherit; font-size:12.5px;
  white-space:normal; overflow-wrap:anywhere; line-height:1.5;
  min-height:0; height:auto;
  padding:9px 12px; border-radius:12px; color:var(--ink2);
  border:1px solid var(--hair); background:var(--paper);
  box-shadow:none;
}
.cp__starter:hover { border-color:var(--accent); color:var(--accent);
                     background:var(--white); }

.cpmsg { display:flex; }
.cpmsg--user { justify-content:flex-end; }
.cpmsg__body {
  max-width:88%; padding:9px 12px; border-radius:14px;
  font-size:13px; line-height:1.6; white-space:pre-wrap;
}
.cpmsg--user .cpmsg__body { background:var(--accent); color:#fff; border-bottom-right-radius:5px; }
.cpmsg--assistant .cpmsg__body {
  background:var(--paper); color:var(--ink); border:1px solid var(--hair);
  border-bottom-left-radius:5px;
}
.cpmsg__body p { margin:0 0 8px; }
.cpmsg__body p:last-child { margin:0; }
.cp__route { color:var(--accent); font-weight:600; text-decoration:none; }
.cpmsg--assistant .cp__route:hover { text-decoration:underline; }

.cp__thinking { display:flex; gap:4px; align-items:center; }
.cp__thinking i {
  width:6px; height:6px; border-radius:50%; background:var(--mute);
  animation:cpdot 1.2s ease-in-out infinite;
}
.cp__thinking i:nth-child(2) { animation-delay:.15s; }
.cp__thinking i:nth-child(3) { animation-delay:.3s; }
@keyframes cpdot { 0%,60%,100% { opacity:.3; } 30% { opacity:1; } }
@media (prefers-reduced-motion: reduce) { .cp__thinking i { animation:none; opacity:.6; } }

/* A report or a proposal. Deliberately not a chat bubble: it is a thing you
   act on, so it gets an edge and a button rather than looking like something
   somebody said. */
.cpcard {
  border:1px solid var(--hair-strong); border-radius:13px; overflow:hidden;
  background:var(--white);
}
.cpcard__head { padding:11px 13px 9px; border-bottom:1px solid var(--hair); }
.cpcard__head b { display:block; font-size:13px; color:var(--ink); }
.cpcard__head .sub { font-size:11.5px; color:var(--steel); }
.cpcard--auto .cpcard__head { background:var(--band); }
.cpcard__steps { margin:0; padding:10px 13px 10px 30px; font-size:12.5px; line-height:1.65; color:var(--ink2); }
.cpcard__note { margin:0 13px 10px; font-size:11.5px; color:var(--steel); line-height:1.55; }
.cpcard__acts { display:flex; align-items:center; gap:10px; flex-wrap:wrap; padding:0 13px 12px; }
/* Each of these renders as a <button> in one place and an <a> in another —
   Create it versus Open the setting. Sized explicitly so the two agree: an <a>
   picks up none of the base button rule and a <button> picks up all of it, and
   left alone they came out different heights beside each other. */
.cpcard__go {
  display:inline-flex; align-items:center; cursor:pointer;
  font:inherit; font-size:12px; font-weight:600; text-decoration:none;
  padding:7px 13px; min-height:0; border-radius:9px; border:0;
  background:var(--accent); color:#fff; box-shadow:none; white-space:nowrap;
}
.cpcard__go:hover { background:var(--accent-dark); }
.cpcard__go[disabled] { opacity:.6; cursor:default; }
.cpcard__alt {
  display:inline-flex; align-items:center; cursor:pointer;
  font:inherit; font-size:12px; border:0; background:none; min-height:0;
  color:var(--accent); text-decoration:underline; padding:0; box-shadow:none;
}
.cpcard__manual { font-size:11px; color:var(--mute); }

.cp__foot {
  display:flex; align-items:flex-end; gap:8px; padding:10px 12px;
  border-top:1px solid var(--hair); background:var(--white);
}
.cp__input {
  flex:1; resize:none; font:inherit; font-size:13px; line-height:1.5;
  padding:9px 11px; border:1px solid var(--hair-strong); border-radius:11px;
  color:var(--ink); background:var(--white); max-height:120px;
}
.cp__input:focus { outline:none; border-color:var(--accent); }
/* ⚠ min-height AND padding BOTH HAVE TO BE RESET HERE. The base `button` rule
   sets min-height:40px and padding:9px 17px, so a 34px square button came out
   34 wide by 40 tall — a pill wearing border-radius:50% — with the glyph
   pushed off centre by padding it never asked for. Sized from one custom
   property so the circle stays a circle when the touch query enlarges it. */
.cp__send {
  flex:none;
  width:var(--cp-send, 36px); height:var(--cp-send, 36px);
  min-height:0; padding:0;
  display:flex; align-items:center; justify-content:center;
  border-radius:50%; border:0; cursor:pointer;
  background:var(--accent); color:#fff;
  /* The base rule's accent glow is sized for a wide button and reads as a halo
     under a small circle sitting on a flat white bar. */
  box-shadow:0 1px 2px rgba(19,41,75,.18);
}
.cp__send svg { width:17px; height:17px; display:block; }
.cp__send:hover { background:var(--accent-dark); }
.cp__send:disabled { opacity:.5; cursor:default; }
/* A finger needs 44, and the width has to come with it or it is an oval again
   on exactly the devices that are hardest to check. */
@media (pointer:coarse) { .cp__send { --cp-send:44px; } }

/* On a phone it is a sheet, not a floating card — a 390px panel on a 380px
   screen is a scrollbar with a chat in it. */
@media (max-width: 700px) {
  /* A sheet spans the screen, so it ignores the drag entirely — cp--down
     included, or a bubble dragged high would tear the sheet off the bottom
     edge it is anchored to. Only the bubble carries the position here. */
  .cp, .cp.cp--down {
    right:0; left:0; top:auto; bottom:0; width:100%; max-width:none;
    height:min(86vh, 100%); border-radius:18px 18px 0 0; border-bottom:0;
  }
  :root { --cp-x:16px; --cp-y:16px; }
}

/* The automation a proposal would create, shown before anything is created.
   Deliberately looks like a message and not like a form: what somebody is
   agreeing to is the words their customer will read. */
.cpauto { margin:0 13px 10px; border:1px solid var(--hair); border-radius:10px;
          background:var(--paper); overflow:hidden; }
.cpauto__meta { display:flex; flex-wrap:wrap; gap:5px; padding:9px 10px 0; }
.cpauto__subj { padding:8px 11px 0; font-size:12px; font-weight:700; color:var(--ink); }
.cpauto__body { margin:0; padding:7px 11px 11px; font:inherit; font-size:12px;
                line-height:1.6; color:var(--ink2); white-space:pre-wrap;
                word-break:break-word; }
.cpcard__note.is-bad { color:var(--red); }
.cpcard__done { font-size:12px; font-weight:600; color:var(--green); }

/* ── List or grid ───────────────────────────────────────────────────────────

   Written generically rather than as .prod*, because the next screen that
   wants this toggle should not need a second copy of it. Only the cards below
   are products-specific. */

.viewtog { display:inline-flex; padding:2px; gap:2px; border-radius:9px;
           background:var(--band); }
.viewtog__b {
  /* The base button rule sets min-height:40px, padding and a glow; a 28px
     icon button wants none of them. */
  min-height:0; padding:0; box-shadow:none; border:0; cursor:pointer;
  width:30px; height:26px; border-radius:7px; background:none;
  display:flex; align-items:center; justify-content:center; color:var(--steel);
}
.viewtog__b svg { width:14px; height:14px; display:block; }
.viewtog__b:hover { background:rgba(255,255,255,.6); color:var(--ink); }
.viewtog__b.is-on { background:var(--white); color:var(--accent);
                    box-shadow:0 1px 2px rgba(19,41,75,.10); }
.viewtog__b:focus-visible { outline:2px solid var(--accent); outline-offset:1px; }

/* ONE CARD SHAPE FOR EVERY SCREEN, so a customer card and an invoice card are
   recognisably the same object with different words in it. Seven lists offer a
   grid; seven slightly different cards would be seven jobs to keep in step. */
.gcards { display:grid; gap:12px; margin-top:14px;
          grid-template-columns:repeat(auto-fill, minmax(238px, 1fr)); }
.gcard {
  background:var(--white); border:1px solid var(--hair); border-radius:var(--r);
  padding:14px 15px; cursor:pointer; box-shadow:var(--shadow-sm);
  display:flex; flex-direction:column; gap:5px; text-align:left;
  transition:border-color .12s ease, transform .12s ease;
}
.gcard:hover { border-color:var(--accent); transform:translateY(-1px); }
.gcard:focus-visible { outline:none; border-color:var(--accent);
                       box-shadow:0 0 0 3.5px rgba(75,156,211,.22); }
.gcard__top { display:flex; gap:6px; flex-wrap:wrap; }
.gcard__title { font-size:14px; line-height:1.35; color:var(--ink);
                overflow-wrap:anywhere; }
.gcard__sub { font-size:11.5px; color:var(--mute); line-height:1.45;
              overflow-wrap:anywhere; }
.gcard__money { font-size:19px; font-weight:700; font-variant-numeric:tabular-nums;
                color:var(--ink); margin-top:2px; display:flex; align-items:baseline; gap:6px; }
.gcard__moneycap { font-size:11px; font-weight:600; color:var(--mute); }

/* A definition list, so a label and its figure stay paired when a card wraps —
   two columns of loose spans drift apart at narrow widths. */
.gcard__facts { display:grid; grid-template-columns:auto 1fr; gap:2px 10px;
                margin:5px 0 0; font-size:12px; }
.gcard__facts dt { color:var(--mute); white-space:nowrap; }
.gcard__facts dd { margin:0; text-align:right; font-variant-numeric:tabular-nums;
                   color:var(--ink2); overflow-wrap:anywhere; }
.gcard__foot { margin-top:7px; padding-top:8px; border-top:1px solid var(--hair);
               font-size:11.5px; color:var(--steel); }

@media (prefers-reduced-motion: reduce) {
  .gcard { transition:none; }
  .gcard:hover { transform:none; }
}

/* ── The card you type into, and the check ──────────────────────────────────

   .vcard is unchanged as a picture. .vcard--live is the same face with real
   inputs in the slots: transparent, no border, inheriting the card's own type
   so what is typed looks printed on it rather than dropped on top. */

/* ⚠ NOT max-width:none. The face keeps a card's aspect ratio, so letting it
   fill a 600px column makes it 380px tall and pushes the billing fields off
   the bottom of the screen. A little wider than the picture-only version,
   because four fields have to sit on it, and no wider. */
.vcard--live { max-width:430px; }
.vcard__in {
  /* Every one of these is fighting a global input rule. */
  appearance:none; -webkit-appearance:none;
  background:transparent; border:0; box-shadow:none; outline:none;
  min-height:0; padding:0; margin:0; border-radius:0;
  color:inherit; font:inherit; letter-spacing:inherit; width:100%;
}
.vcard__in::placeholder { color:rgba(255,255,255,.42); }
/* The global input:focus paints a 3.5px accent halo. On a navy card that reads
   as a pale box floating over the artwork, so the focus cue here is the
   underline going bright instead — see below. */
.vcard__in:focus { outline:none; box-shadow:none; background:transparent; }
/* The number is the one field somebody has to find at a counter, so it gets a
   real box — a well in the face with an edge round it. The three small ones
   keep the underline: four boxes on a card is a form with a picture behind it
   rather than a card you type into. */
.vcard--live .vcard__num {
  background:rgba(255,255,255,.10);
  border:1px solid rgba(255,255,255,.34);
  border-radius:9px; padding:9px 12px;
  /* ⚠ RESTATED HERE ON PURPOSE. .vcard__in sets `font:inherit` to strip the
     browser's input styling, and being a later single-class rule it also wiped
     the size and weight set on .vcard__num — the number was rendering at 14px
     regular inside a box built for 23px bold. Two classes beats one. */
  /* ⚠ AND THE SIZE HAS TO FIT SIXTEEN DIGITS. A flat 20px with the tracking
     set on .vcard__num makes "4111 1111 1111 1111" ~296px inside a 256px
     well, so the last four digits were clipped — on the terminal, the pay page
     and the vault alike. Sized against the CARD, so it is right at whatever
     width the card is given. */
  font-size:clamp(14px, 5.2cqi, 20px); font-weight:700;
  font-family:ui-monospace, SFMono-Regular, Menlo, monospace;
  /* Fills the space LEFT OF THE CHIP. width:100% plus a right margin is 100%
     of the parent pushed 64px right, which put the digits under the chip.

     The auto top and bottom margins centre it in the band above the footer:
     taking the chip out of the flow left the number sitting on the top edge
     with an empty middle, and a card carries its number across the middle. It
     now shares that line with the chip, which is the point of moving it. */
  /* The row owns the vertical placement now; this just takes the space left
     of the chip. */
  display:block; flex:1; min-width:0; width:auto; margin:0;
  transition:border-color .12s ease, background .12s ease, box-shadow .12s ease;
}
/* Same reason: the three small fields inherit their size from .vcard__in
   otherwise, and come out smaller than the captions above them. */
.vcard--live .vcard__val { font-size:13.5px; font-weight:600; }
.vcard--live .vcard__num:focus-within,
.vcard--live input.vcard__num:focus {
  background:rgba(255,255,255,.16);
  border-color:rgba(255,255,255,.92);
  box-shadow:0 0 0 3px rgba(255,255,255,.14);
}
.vcard--live .vcard__val {
  border-bottom:1px solid rgba(255,255,255,.28);
  padding-bottom:2px; transition:border-color .12s ease;
}
.vcard--live .vcard__val:focus-within,
.vcard--live input.vcard__val:focus { border-bottom-color:rgba(255,255,255,.85); }

.vcard--live .vcard__foot { gap:14px; }

/* THE CHIP, RIGHT-HAND SIDE, LEVEL WITH THE NUMBER (Greg, 2026-09-01). One
   flex row, so "centred" is a fact rather than a percentage that drifts when
   the card or the footer changes height. */
.vcard__mid { display:flex; align-items:center; gap:18px; margin:auto 0; }
.vcard--live .vcard__chip { flex:none; margin:0; }
.vcard__slot--exp { width:64px; flex:none; }
.vcard__slot--cvv { width:52px; flex:none; margin-left:auto; }

/* A chosen card on file: nothing to type, so the fields go and the masked
   number takes their place. */
.vcard.is-masked .vcard__num,
.vcard.is-masked .vcard__foot { visibility:hidden; }
.vcard__masked {
  position:absolute; left:22px; right:22px; top:50%; transform:translateY(-50%);
  font-size:19px; letter-spacing:.055em; font-variant-numeric:tabular-nums;
}

/* ── The check ───────────────────────────────────────────────────────────── */

.chk {
  position:relative; width:100%; border-radius:10px; overflow:hidden;
  background:
    repeating-linear-gradient(-45deg, rgba(19,41,75,.022) 0 8px, transparent 8px 16px),
    linear-gradient(160deg, #FBFCF7 0%, #F2F6EE 100%);
  border:1px solid #DCE3D6; box-shadow:var(--shadow-sm);
  padding:16px 18px 14px; color:var(--ink);
  font-variant-numeric:tabular-nums;
}
.chk__head { display:flex; align-items:flex-start; gap:16px; }
.chk__from { flex:1; min-width:0; }
.chk__no { width:120px; flex:none; text-align:right; }
.chk__date { font-size:12.5px; font-weight:600; border-bottom:1px solid #C9D3C2;
             padding-bottom:2px; }
.chk__cap { font-size:8.5px; letter-spacing:.13em; text-transform:uppercase;
            color:#7A8A72; margin-bottom:3px; }
.chk__pay { margin-top:14px; }
.chk__payrow { display:flex; align-items:flex-end; gap:12px; }
.chk__payto { flex:1; min-width:0; font-size:13.5px; font-weight:600;
              border-bottom:1px solid #C9D3C2; padding-bottom:3px;
              white-space:nowrap; overflow:hidden; text-overflow:ellipsis; }
.chk__box { flex:none; min-width:118px; text-align:right; padding:5px 10px;
            border:1px solid #C9D3C2; border-radius:5px; background:rgba(255,255,255,.7); }
.chk__amt { font-size:15px; font-weight:700; }
.chk__rule { height:1px; background:#DCE3D6; margin:14px 0 12px; }

.chk__micr { display:flex; align-items:flex-end; gap:8px;
             font-family:ui-monospace, SFMono-Regular, Menlo, monospace; }
.chk__sym { font-size:16px; color:#8A9884; padding-bottom:3px; }
.chk__micrslot { min-width:0; }
.chk__micrslot:first-of-type { width:118px; flex:none; }
.chk__micrslot:last-of-type { flex:1; }
.chk__in {
  appearance:none; -webkit-appearance:none;
  background:transparent; border:0; border-bottom:1px solid #C9D3C2;
  box-shadow:none; outline:none; min-height:0; padding:0 0 2px; margin:0;
  border-radius:0; color:inherit; font:inherit; width:100%;
  font-family:inherit; font-size:13px;
}
.chk__in::placeholder { color:#A9B5A2; }
.chk__in:focus { border-bottom-color:var(--accent); box-shadow:none; background:transparent; }
.chk__from .chk__in, .chk__payto { font-family:inherit; }

/* The billing pair sat against the bottom edge of the card and read as part of
   it. Given air so the card finishes before they start. */
.termbilling { margin-top:22px; }

/* The fields a business asks for, under whichever face is on screen. */
.termextra { margin-top:16px; padding-top:14px; border-top:1px solid var(--hair); }
.termextra__h { font-size:11px; letter-spacing:.1em; text-transform:uppercase;
                color:var(--steel); margin:0 0 10px; font-weight:700; }

@media (max-width: 560px) {
  .vcard__slot--cvv { margin-left:0; }
  .chk__no { width:96px; }
  .chk__micrslot:first-of-type { width:96px; }
}

/* ── The Vault's outbox ─────────────────────────────────────────────────────

   Sits above the Vault's own figures, because in review mode nothing goes out
   until somebody reads this — and a queue below the fold is a feature that
   quietly does nothing. */
.vaultbox { border-color:var(--accent-lt); margin-bottom:18px; }
.vaultbox__head { display:flex; align-items:flex-start; justify-content:space-between;
                  gap:16px; flex-wrap:wrap; padding-bottom:12px;
                  border-bottom:1px solid var(--hair); }
.vaultbox__head b { font-size:15px; }
.vaultbox__acts { display:flex; gap:8px; flex:none; }
.vaultbox__list { display:flex; flex-direction:column; }

/* Each message shown whole. Somebody approving outreach in their name is
   entitled to read the words, not a summary of them. */
/* ⚠ A <label> HERE INHERITS THE FORM-LABEL STYLING. This app draws field
   labels uppercase and letter-spaced, so wrapping a message in one rendered
   every customer's words in block capitals — the text was right and the CSS
   shouted it. label is still the right element (it makes the whole row toggle
   the checkbox), so the styling is put back rather than the element changed. */
.vaultmsg { display:flex; gap:11px; padding:12px 0; border-bottom:1px solid var(--paper);
            cursor:pointer; align-items:flex-start;
            text-transform:none; letter-spacing:normal; font-weight:400;
            font-size:13px; color:var(--ink); }
.vaultmsg:last-child { border-bottom:0; }
.vaultmsg input[type=checkbox] { margin-top:3px; width:16px; height:16px; flex:none; }
.vaultmsg__main { min-width:0; flex:1; }
.vaultmsg__who { display:flex; align-items:baseline; gap:8px; flex-wrap:wrap; }
.vaultmsg__who b { font-size:13.5px; }
.vaultmsg__subj { font-size:12.5px; font-weight:600; color:var(--ink2); margin-top:3px; }
.vaultmsg__body { font-size:12px; line-height:1.6; color:var(--steel); margin-top:3px;
                  white-space:pre-wrap; overflow-wrap:anywhere; }

/* ---- The item page ---------------------------------------------------------

   Products used to be edited in a modal. Level II and Level III data does not
   fit in one, so an item is a page with tabs now — see views/products.js. */

/* THE TABS ARE BUTTONS, NOT LINKS, and that costs a reset. Every tab strip
   elsewhere is anchors, because each tab is a separate load of settled data. On
   this page all three tabs are one half-finished form, so a link would throw
   away whatever was typed on the tab being left. A <button> instead — and the
   global `a.button, button` rule would give it a 40px pill of accent blue with
   a glow, so all of it is undone here and .tab is allowed to win. */
.tabs > button.tab {
  min-height:0; padding:6px 13px; font:500 12.5px inherit;
  box-shadow:none; background:var(--white); color:var(--steel);
  border:1px solid var(--hair); border-radius:99px; white-space:nowrap;
}
.tabs > button.tab:hover { background:var(--paper); }
.tabs > button.tab.is-on,
.tabs > button.tab.is-on:hover {
  background:var(--ink); color:var(--white); border-color:var(--ink);
}

/* A heading inside a form, which is a thing this app otherwise does not have:
   the network tab collects two unrelated sets of identifiers and running them
   together as one column of boxes made neither legible. */
.l3h {
  font:700 11.5px inherit; text-transform:uppercase; letter-spacing:.06em;
  color:var(--mute); margin:20px 0 0;
}
.l3note { margin:0 0 2px; }

/* What would actually travel with the sale. Set on the page rather than in a
   tooltip because the first question this tab gets is what happens if it is
   left blank, and an answer nobody can see is an answer nobody believes. */
.l3prev {
  margin-top:22px; padding:15px 17px; border-radius:12px;
  background:var(--paper); border:1px solid var(--hair);
}
.l3prev h3 {
  font:700 11.5px inherit; text-transform:uppercase; letter-spacing:.06em;
  color:var(--mute); margin:0 0 11px;
}
.l3prev .kv { margin:0; }
.l3prev .hint { margin-top:11px; }
/* Where a value came from, when it came from another field. Lighter than the
   value itself: it is a note about the figure, not part of it. */
.l3prev__from { margin-left:9px; font-size:11.5px; color:var(--mute); font-weight:400; }

/* Sits directly under the price and cost row it is arithmetic on. */
.margin { margin-top:-4px; }

/* An archived item still opens — it is on invoices, and how it was priced is
   part of how they came to say what they say. The page says so at the top
   rather than looking like an ordinary item that nobody can find. */
.archnote {
  margin:0 0 14px; padding:10px 14px; border-radius:11px;
  background:var(--amber-bg); color:var(--amber); font-weight:600; font-size:12.5px;
}

/* ---- The board, drawn as cards ---------------------------------------------

   The dispatch board can be columns of crews or of bays, and each group can be
   read as rows or as cards — see drawGroups() in views/jobs.js. The cards are
   the card wall's own, so there is one card in this app and not two.

   THE WALL GIVES ITSELF A TOP MARGIN because it normally stands on the page.
   Inside a group it sits under a heading in a bordered box, where that margin
   opens a gap below the heading and the cards run into the edges. Padding
   instead, and the margin off. */
.crewgrp .apptwall { margin:0; padding:14px; }

/* One group's worth of cards is a narrower measure than the whole page, so it
   steps down a column earlier than the flat wall does. Without this a board of
   four bays put four cards across inside each of them and every card was too
   narrow to read the plate. */
@media (max-width:1500px) {
  .crewgrp .apptwall { grid-template-columns:repeat(3, minmax(0,1fr)); }
}
@media (max-width:1100px) {
  .crewgrp .apptwall { grid-template-columns:repeat(2, minmax(0,1fr)); }
}
@media (max-width:700px) {
  .crewgrp .apptwall { grid-template-columns:1fr; }
}

/* The switch sits beside the mode segment on a row that already holds the
   search box; it must not stretch when the box does. */
.toolbar--wide > .viewtog { flex:0 0 auto; }

/* ---- What an automation will actually do ------------------------------------

   The assistant can create three things now, not one, and two of them are about
   money rather than words — a plan that books work and a schedule that bills
   somebody every month. The preview is the only thing standing between a
   sentence somebody typed and a standing instruction, so a repeating invoice
   shows its lines and its total the way an invoice would. */

.cpauto__note { padding:7px 11px 11px; font-size:11.5px; color:var(--steel); }
.cpauto__subj + .cpauto__note { padding-top:5px; }

.cpauto__lines { width:100%; border-collapse:collapse; margin:8px 0 0; font-size:12px; }
.cpauto__lines td { padding:5px 11px; border-top:1px solid var(--hair); color:var(--ink2); }
.cpauto__lines tr:first-child td { border-top:1px solid var(--hair); }
/* Figures line up, so a wrong one is visible without reading it. */
.cpauto__lines td.num { text-align:right; font-variant-numeric:tabular-nums; white-space:nowrap; }
.cpauto__tot td { font-weight:700; color:var(--ink); background:var(--paper); }

/* A chip that says something reaches a customer or a card without anybody
   looking. Amber rather than red: it is a deliberate choice, not a fault. */
.cpauto__meta .chip.warn { background:var(--amber-bg); color:var(--amber); }

/* ---- Refer a friend, in the customer portal ---------------------------------

   A customer is handing over somebody else's details, so the screen is quiet
   and plain rather than a promotion. The offer is stated once, in a sentence,
   and everything else is the form and what has happened since. */

.reflede { font-size:15px; color:var(--ink2); margin:0 0 16px; line-height:1.55; }
.reflede b { color:var(--ink); }

.refform .row2 { display:grid; grid-template-columns:1fr 1fr; gap:0 14px; }
@media (max-width:560px) { .refform .row2 { grid-template-columns:1fr; } }
.refform .hint { margin:2px 0 14px; }
.refmsg:empty { display:none; }
.refmsg { margin-bottom:12px; }

.reflist { list-style:none; margin:6px 0 0; padding:0; }
.reflist__row { padding:11px 0; border-top:1px solid var(--hair); }
.reflist__row:first-child { border-top:0; }
.reflist__who { display:flex; align-items:center; gap:9px; font-size:14px; color:var(--ink); }
/* The reward and the date, quieter than the name — the name is what the
   referrer is scanning for. */
.reflist__meta { display:flex; flex-wrap:wrap; gap:4px 12px; margin-top:3px;
                 font-size:12.5px; color:var(--steel); }

/* Referral rewards waiting on a customer, offered under the discount fields.
   Buttons rather than a dropdown: there are rarely more than two, and the
   whole point is to read what each one is worth without opening anything. */
.refrewards:empty { display:none; }
.refrewards { margin:-4px 0 4px; }
.refrewards__row { display:flex; flex-wrap:wrap; gap:8px; margin-top:7px; }
.refrewards__row button { min-height:32px; padding:6px 12px; font-size:12.5px; }

/* ---- The public booking link ------------------------------------------------

   A stranger's first sight of the business, on a phone, probably from a link
   on their website. Wide targets, one decision per card, and nothing that
   needs explaining. */

.bookwrap { max-width:560px; margin:0 auto; padding:22px 16px 48px; }

.bookhead { text-align:center; margin-bottom:20px; }
.bookhead__logo { max-height:56px; max-width:220px; object-fit:contain; }
.bookhead__name { font-size:20px; color:var(--ink); }
.bookhead__intro { margin:10px 0 0; color:var(--steel); font-size:14px; line-height:1.5; }

/* What can be booked. A column on a phone, two across when there is room. */
.bookopts { display:grid; gap:9px; margin-top:4px; }
@media (min-width:480px) { .bookopts { grid-template-columns:1fr 1fr; } }
.bookopt { display:flex; flex-direction:column; align-items:flex-start; gap:2px;
           text-align:left; min-height:56px; padding:11px 14px; border-radius:12px;
           background:var(--white); border:1px solid var(--hair); color:var(--ink);
           box-shadow:none; font-weight:600; }
.bookopt .sub { font-size:12px; color:var(--steel); font-weight:400; }
.bookopt.is-on { border-color:var(--accent); box-shadow:0 0 0 2px rgba(42,110,158,.16); }

.bookmode { margin:2px 0 14px; }

/* The days, as a strip that scrolls sideways rather than wrapping into a grid
   nobody scans. */
.bookdays { display:flex; gap:8px; overflow-x:auto; padding:2px 0 10px;
            scrollbar-width:none; }
.bookdays::-webkit-scrollbar { display:none; }
.bookday { flex:0 0 auto; display:flex; flex-direction:column; align-items:center;
           gap:1px; min-width:60px; min-height:64px; padding:8px 6px; border-radius:12px;
           background:var(--white); border:1px solid var(--hair); color:var(--ink);
           box-shadow:none; }
.bookday b { font-size:17px; line-height:1.1; }
.bookday__dow, .bookday__mon { font-size:11px; color:var(--steel); font-weight:500;
                               text-transform:uppercase; letter-spacing:.04em; }
.bookday.is-on { background:var(--ink); border-color:var(--ink); color:var(--white); }
.bookday.is-on .bookday__dow, .bookday.is-on .bookday__mon { color:rgba(255,255,255,.75); }

.bookslots { display:grid; grid-template-columns:repeat(auto-fill, minmax(112px, 1fr));
             gap:8px; margin-top:4px; }
.bookslot { display:flex; flex-direction:column; gap:1px; min-height:52px;
            padding:8px 10px; border-radius:11px; background:var(--white);
            border:1px solid var(--hair); color:var(--ink); box-shadow:none; }
.bookslot .sub { font-size:11px; color:var(--steel); font-weight:400; }
.bookslot.is-on { background:var(--ink); border-color:var(--ink); color:var(--white); }
.bookslot.is-on .sub { color:rgba(255,255,255,.75); }

.bookchosen { display:flex; flex-direction:column; gap:2px; margin:0 0 14px;
              padding:11px 13px; border-radius:11px; background:var(--paper);
              border:1px solid var(--hair); }
.bookchosen .sub { font-size:12.5px; color:var(--steel); }

.bookform .row2 { display:grid; grid-template-columns:1fr 1fr; gap:0 14px; }
@media (max-width:520px) { .bookform .row2 { grid-template-columns:1fr; } }
.bookmsg:empty { display:none; }
.bookmsg { margin-bottom:12px; }

.bookdone { text-align:center; }
.bookdone__tick { width:52px; height:52px; margin:2px auto 12px; border-radius:50%;
                  display:flex; align-items:center; justify-content:center;
                  background:var(--green-bg); color:var(--green); font-size:26px; }
.bookdone__when { display:flex; flex-direction:column; gap:3px; margin:0 0 6px; }
.bookdone__when .sub { font-size:13px; color:var(--steel); }
.bookdone .hint { margin-top:10px; }

/* ---- Booking page: text stays inside its pill -------------------------------

   Two rules were fighting the labels.

   `.segbtn` is `inline-flex; flex:none` and `.segbtn__b` is `white-space:nowrap`
   — correct for a toolbar switch on a laptop, wrong for a full-width control
   whose labels are sentences. On a narrow phone "Soonest available" and "Pick a
   day" together were wider than the card, and `overflow:hidden` on the pill cut
   them off.

   And every one of these is a <button>, so the global `a.button, button` rule
   is underneath it: `white-space:nowrap`, `min-height:40px`, `padding:9px 17px`
   and an accent glow. A service called "Fertilize / weed control" has no chance
   of fitting on one line, and nowrap meant it never tried. */

.segbtn.bookmode { display:flex; width:100%; flex:1 1 auto; }
.segbtn.bookmode .segbtn__b {
  flex:1 1 0; min-width:0; min-height:38px;
  padding:9px 10px; border-radius:0; box-shadow:none;
  white-space:normal; line-height:1.25; text-align:center;
}

/* The service and slot pills wrap rather than run out of their own edges.
   `overflow-wrap` as well as normal wrapping, so a single long unbroken word —
   an email address, a service named without spaces — breaks instead of
   widening the pill past the card. */
.bookopt, .bookslot, .bookday {
  white-space:normal; overflow-wrap:anywhere; text-align:left;
}
.bookslot, .bookday { text-align:center; }
.bookopt b, .bookslot b { line-height:1.25; }
/* Height follows the words now, so the fixed heights become floors. */
.bookopt { height:auto; }
.bookslot { height:auto; }
.bookday { height:auto; white-space:nowrap; }

/* Which services the booking link offers. A checkbox list rather than a
   multi-select: the question is "is this one bookable", asked once per row, and
   a multi-select answers it by making you hold a key down. */
.booktypes__h { font:700 11.5px inherit; text-transform:uppercase; letter-spacing:.06em;
                color:var(--mute); margin:20px 0 8px; }
.booktypes { display:grid; gap:2px; margin-bottom:8px; }
@media (min-width:620px) { .booktypes { grid-template-columns:1fr 1fr; gap:2px 18px; } }
.booktype { padding:5px 0; align-items:center; }
.booktype .sub { color:var(--steel); font-weight:400; font-size:12.5px; }

/* The tax rate moved to Settings › Business. What is left on the Invoices tab
   is the figure and the way there — a value, not a control, so it is styled to
   look like one rather than like a disabled input somebody should try to type
   into. */
.taxmoved { display:flex; align-items:baseline; gap:10px; min-height:40px;
            padding:9px 0; flex-wrap:wrap; }
.taxmoved b { font-size:15px; color:var(--ink); font-variant-numeric:tabular-nums; }
.taxmoved a { font-size:12.5px; }

/* ---- Follow-ups -------------------------------------------------------------

   The screen a rep opens first, and the card that tells a customer record what
   happens next. Rows are wide targets with the tick on the left, because the
   commonest action on this screen by a long way is "done". */

.tasktally { display:flex; gap:14px; flex-wrap:wrap; align-items:center; }
.tasktally__n { font-size:12.5px; color:var(--steel); white-space:nowrap; }
.tasktally__n b { color:var(--ink); font-size:15px; margin-right:3px;
                  font-variant-numeric:tabular-nums; }
.tasktally__n.is-bad b { color:var(--red); }

.tasksec { margin-top:22px; }
.tasksec > h2 { font:700 11.5px inherit; text-transform:uppercase; letter-spacing:.06em;
                color:var(--mute); margin:0 0 9px; }

.tasklist { list-style:none; margin:0; padding:0; }
.taskrow { display:flex; align-items:flex-start; gap:11px; padding:11px 13px;
           background:var(--white); border:1px solid var(--hair); border-radius:12px;
           margin-bottom:7px; }
.taskrow.is-late { border-color:var(--red); box-shadow:inset 3px 0 0 var(--red), var(--aura); }
.taskrow.is-idle { box-shadow:inset 3px 0 0 var(--amber); }
.taskrow.is-waiting { box-shadow:inset 3px 0 0 var(--accent-bright); }

/* The tick. A circle you fill in — the most-pressed control here, so it is a
   real target rather than a checkbox at default size. */
.taskrow__tick { flex:0 0 auto; width:26px; height:26px; min-height:0; padding:0;
                 border-radius:50%; background:var(--white); color:var(--mute);
                 border:1.5px solid var(--hair-strong); box-shadow:none;
                 font-size:13px; line-height:1; }
.taskrow__tick:hover { background:var(--green-bg); color:var(--green);
                       border-color:var(--green); }

.taskrow__main { flex:1 1 auto; min-width:0; }
.taskrow__top { display:flex; align-items:center; gap:8px; flex-wrap:wrap; }
.taskrow__top b, .taskrow__top a { font-size:14px; color:var(--ink); }
.taskrow__sub { display:flex; gap:4px 12px; flex-wrap:wrap; margin-top:3px;
                font-size:12.5px; color:var(--steel); }
.taskrow__due.is-late { color:var(--red); font-weight:600; }
.taskrow__tel { font-variant-numeric:tabular-nums; }
.taskrow__note { margin-top:4px; font-size:12.5px; color:var(--steel); }
.taskrow__acts { flex:0 0 auto; display:flex; gap:12px; align-items:center; }

.quickwhen { flex-wrap:wrap; }
.quickwhen button { min-height:32px; padding:6px 11px; font-size:12.5px; }

.speedrow { display:grid; grid-template-columns:repeat(auto-fit, minmax(150px, 1fr));
            gap:10px; margin-bottom:12px; }
.speedstat { padding:12px 14px; border-radius:12px; background:var(--paper);
             border:1px solid var(--hair); }
.speedstat b { display:block; font-size:22px; color:var(--ink);
               font-variant-numeric:tabular-nums; line-height:1.1; }
.speedstat span { font-size:12px; color:var(--steel); }
.speedstat.is-bad { background:var(--red-bg); border-color:var(--red); }
.speedstat.is-bad b { color:var(--red); }

/* On the customer record. The nudge state is tinted, because a lead with
   nothing planned is the one thing on that page worth interrupting for. */
.card--nudge { border-color:var(--amber); }
.nextstep { list-style:none; margin:0; padding:0; }
.nextstep__row { display:flex; align-items:center; gap:10px; padding:7px 0;
                 border-top:1px solid var(--hair); }
.nextstep__row:first-child { border-top:0; }
.nextstep__main { flex:1 1 auto; min-width:0; }
.nextstep__main .sub { font-size:12px; color:var(--steel); }
.nextstep__row.is-late .sub { color:var(--red); font-weight:600; }
.nextstep__none { padding:4px 0 2px; font-size:13px; color:var(--ink2); line-height:1.5; }
.nextstep__past { margin-top:9px; padding-top:8px; border-top:1px solid var(--hair);
                  font-size:12px; display:flex; gap:6px; flex-wrap:wrap; }
.nextstep__done { color:var(--steel); }
.nextstep__done + .nextstep__done::before { content:'· '; color:var(--mute); }

/* How much attention an estimate is getting. Quiet when it is one open, loud
   when somebody keeps coming back — that is the difference between "received"
   and "about to decide". */
.readsig { margin:0 0 14px; padding:9px 13px; border-radius:11px; font-size:13px;
           background:var(--band); color:var(--ink2); line-height:1.5; }
.readsig b { color:var(--ink); }
.readsig.is-hot { background:var(--amber-bg); color:var(--amber); }
.readsig.is-hot b { color:var(--amber); }
.readsig.is-cold { background:var(--paper); color:var(--steel); }
.readsig.is-cold:empty { display:none; }
.readsig__nudge { font-weight:600; }
.taskrow.is-hot { box-shadow:inset 3px 0 0 var(--amber); }

/* ---- The pipeline board -----------------------------------------------------

   Columns scroll sideways rather than squeezing: five stages will not fit on a
   phone at a readable width, and a card you cannot read is not a board. */

.pipehead { display:grid; grid-template-columns:repeat(auto-fit, minmax(160px, 1fr));
            gap:10px; margin-bottom:16px; }

.pipe { display:flex; gap:12px; overflow-x:auto; padding-bottom:10px;
        align-items:flex-start; }
.pipecol { flex:0 0 260px; background:var(--paper); border:1px solid var(--hair);
           border-radius:14px; padding:10px; min-height:140px; }
.pipecol.is-dropping { border-color:var(--accent); background:var(--band);
                       box-shadow:0 0 0 2px rgba(42,110,158,.18); }
.pipecol__head { display:flex; align-items:baseline; gap:8px; padding:2px 4px 10px;
                 flex-wrap:wrap; }
.pipecol__head b { font-size:13px; color:var(--ink); }
.pipecol__n { font-size:12px; color:var(--steel); font-variant-numeric:tabular-nums; }
.pipecol__v { margin-left:auto; font-size:12.5px; font-weight:700; color:var(--ink2);
              font-variant-numeric:tabular-nums; }
.pipecol__body { display:flex; flex-direction:column; gap:7px; }
.pipecol__more { padding:8px 4px 2px; font-size:11.5px; color:var(--mute); }

.pipecard { background:var(--white); border:1px solid var(--hair); border-radius:11px;
            padding:9px 11px; cursor:pointer; }
.pipecard:hover { border-color:var(--hair-strong); }
.pipecard.is-dragging { opacity:.45; }
/* Nothing planned is the state the board exists to surface. */
.pipecard.is-nonext { box-shadow:inset 3px 0 0 var(--amber); }
.pipecard__top { display:flex; align-items:baseline; gap:8px; }
.pipecard__top b { font-size:13.5px; color:var(--ink); min-width:0;
                   overflow-wrap:anywhere; }
.pipecard__v { margin-left:auto; font-size:12.5px; font-weight:700; color:var(--ink2);
               font-variant-numeric:tabular-nums; white-space:nowrap; }
.pipecard__sub { display:flex; gap:4px 10px; flex-wrap:wrap; margin-top:3px;
                 font-size:11.5px; color:var(--steel); }
.pipecard__next { margin-top:5px; font-size:11.5px; color:var(--steel); }
.pipecard__next.is-late { color:var(--red); font-weight:600; }
.pipecard__next.is-none { color:var(--amber); font-weight:600; }

/* What a lead asked about. Given its own line above the notes because it is
   the sentence somebody reads before dialling, not part of a diary. */
.lookingfor { display:flex; gap:9px; align-items:baseline; flex-wrap:wrap;
              margin:12px 0 0; padding:9px 12px; border-radius:10px;
              background:var(--band); font-size:13.5px; color:var(--ink); line-height:1.5; }
.lookingfor__k { font:700 10.5px inherit; text-transform:uppercase; letter-spacing:.06em;
                 color:var(--steel); flex:0 0 auto; }
/* On a won account it is history rather than a prompt. */
.lookingfor.is-quiet { background:var(--paper); color:var(--steel); }
.lookingfor.is-empty { background:var(--amber-bg); }
.lookingfor.is-empty .lookingfor__k { color:var(--amber); }

/* ---- Reaching a customer from a phone or a tablet ---------------------------

   Two targets at the top of the record. The person pressing them is holding the
   device in one hand, so they clear 40px everywhere and take the whole row on a
   phone.

   ⚠ NOT `1fr 1fr`. A lead with a phone number and no address has ONE button,
   and a fixed two-column grid gave it half a 1200px page and left the other
   half blank — a stray box floating in white.

   AND NOT TWO PANELS EITHER (Greg, 2026-09-02). At 52px and two lines they
   were card-sized, so the record opened with two boxes that were not part of
   the record. A pill on one line clears 40px — comfortably a touch target —
   sizes to its own content, and reads as a toolbar under the tabs. */
.reach { display:flex; flex-wrap:wrap; gap:8px; margin:0 0 16px; }
.reach__b { display:inline-flex; align-items:center; gap:9px; min-height:40px;
            max-width:100%; padding:0 15px 0 8px; border-radius:999px;
            text-decoration:none; background:var(--white);
            border:1px solid var(--hair); color:var(--ink); font-size:13.5px; }
.reach__b:hover { border-color:var(--accent); background:var(--paper); }
.reach__b b { flex:0 0 auto; font-size:13.5px; }
/* The number or the street. It yields first when the row is tight, down to an
   ellipsis, so the verb never truncates — "Direc…" would be useless. */
.reach__v { min-width:0; flex:0 1 auto; color:var(--steel);
            overflow:hidden; text-overflow:ellipsis; white-space:nowrap; }
.reach__i { flex:0 0 auto; width:26px; height:26px; border-radius:50%;
            display:flex; align-items:center; justify-content:center;
            background:var(--band); color:var(--accent); font-size:13px; }
/* On a phone each pill takes the row, because a thumb should not have to aim
   between two pills that ended up 4px apart. */
@media (max-width:520px) {
  .reach { gap:9px; }
  .reach__b { width:100%; }
}

/* ---- Notes on a customer ----------------------------------------------------
   Written between other things, usually straight after a call, so the cost of
   writing one is a box and a button — no dialog, no title, no category. */
.notewrite textarea { width:100%; }
.notewrite__foot { display:flex; align-items:center; justify-content:space-between;
                   gap:10px; margin-top:7px; }
.notewrite__foot .hint { margin:0; }

.notelist { list-style:none; margin:12px 0 0; padding:0; }
.notelist .note { padding:10px 0; border-top:1px solid var(--hair);
                  background:none; border-radius:0; }
.notelist .note:first-child { border-top:0; }
.notelist .note.is-pinned { padding-left:11px; box-shadow:inset 3px 0 0 var(--amber); }
.note__body { font-size:13.5px; color:var(--ink); line-height:1.55;
              white-space:pre-wrap; overflow-wrap:anywhere; }
.note__foot { display:flex; gap:4px 12px; flex-wrap:wrap; margin-top:4px;
              font-size:11.5px; color:var(--steel); align-items:center; }

/* ---- A customer record, as two columns --------------------------------------

   Identity on the left — reference, barely changes, and the thing you check.
   What is happening on the right — the reason anybody opened the page.

   Each column is a STACK, not a grid: nothing stretches to match a neighbour,
   nothing can land in an empty middle cell, and a card with two lines in it is
   two lines tall. That is the whole fix. */
.reccols { display:grid; grid-template-columns:340px minmax(0, 1fr);
           gap:14px; align-items:start; }
.reccols__side, .reccols__main { display:flex; flex-direction:column; gap:14px;
                                 min-width:0; }

/* On a tablet held portrait, and on any phone, identity goes back on top. */
@media (max-width:900px) { .reccols { grid-template-columns:1fr; } }

/* ⚠ THE STACKS OVERRIDE THE GLOBAL CARD MARGIN. `.card + .card` adds 14px of
   top margin, which on top of the flex `gap` would double every space between
   cards in a column — the same rule that put the terminal's right column 14px
   low. The gap owns the spacing here. */
.reccols__side > .card + .card,
.reccols__main > .card + .card { margin-top:0; }

/* A host that has not loaded, or has nothing to show, must take no room at all
   — an empty div in a flex column still eats a `gap`. */
.reccols__main > :empty, .reccols__side > :empty { display:none; }

/* The floor that made an empty card 200px tall applies to the old grids, not
   to these stacks: here a card is as tall as what is in it. */
.reccols .card { min-height:0; }

/* A pair row INSIDE a stack — addresses beside contacts at the top of the
   record. `.cols--2` carries its own 14px top margin for use as a standalone
   row; inside a flex column that doubles the gap above it, the same way
   `.card + .card` did. The stack owns the spacing. */
.reccols .cols--2 { margin-top:0; align-items:start; }
/* Below a laptop the pair stacks before the whole record does, so an address
   never gets squeezed into a column narrower than the words in it. */
@media (max-width:1180px) { .reccols .cols--2 { grid-template-columns:1fr; } }

/* ---- THE CUSTOMER RECORD, REDRAWN -------------------------------------------

   Greg, 2026-09-02: "act like a world class web designer and redo the customer
   page all together — keep the same pill look but resize, replace whatever to
   make both prospective customers and existing customers in a clean flowing
   page, right now its like tetris board."

   The page was not missing a style. It was missing a HIERARCHY: nine boxes of
   equal visual weight, each with the same border, the same 18px padding and the
   same 15px heading, so nothing told the eye where to start and every block
   competed with every other. That is what reads as tetris — not the colours.

   So the record now has three levels, and only three:

     1  THE STRIP — figures, glass tiles, the same component the dashboard
        opens with. Read in one glance, never read twice.
     2  THE PILLS — the two things you DO from a van. A toolbar, not cards.
     3  THE CARDS — everything else, and they are quieter than they were:
        smaller headings, a hairline under each one, less padding. A card is
        now a container for content rather than an object competing with it.

   Nothing here is new vocabulary. It is the existing tiles, pills and cards
   given different weights so they stop shouting over each other. */

/* The strip sits under the tabs and above the pills, and owns both spaces —
   `.tabs` has margin-bottom for a page that goes straight to content. */
.tabs + .tiles { margin-top:4px; }
.tiles + .reach { margin-top:14px; }
/* When a record has no phone and no address there are no pills, and the strip
   must still clear the columns below it. */
.tiles + .reccols, .reach + .reccols { margin-top:16px; }

/* A record's tiles are reference, not a dashboard's headline. Smaller figures
   and a tighter box, so the strip informs the page instead of dominating it. */
.reccols-strip { grid-template-columns:repeat(auto-fit, minmax(158px, 1fr)); gap:10px; }
.reccols-strip .tile { padding:11px 13px; }
.reccols-strip .tile__value { font-size:18px; margin-top:2px; }
.reccols-strip .tile__sub { display:block; margin-top:1px; }

/* ---- Cards at record weight -------------------------------------------------
   18px of padding and a 15px heading is right for a card that IS the page. Nine
   of them in two columns needs less of both, and a rule under each heading so
   the head reads as a label on its contents rather than as the first line. */
.reccols .card { padding:15px 16px; }
.reccols .card__head { margin-bottom:11px; padding-bottom:9px;
                       border-bottom:1px solid var(--hair); }
.reccols .card__head h2 { font-size:12px; letter-spacing:.07em;
                          text-transform:uppercase; color:var(--steel); font-weight:700; }
/* The head's action shrinks with it — an "Add" link at body size beside a small
   caps label reads as the loudest thing in the card. */
.reccols .card__head .link { font-size:12px; }

/* A shelf inside a card: two related lists under one heading, with the second
   separated by air rather than by another border. */
/* ⚠ LONGHAND, NOT THE `font:` SHORTHAND. `font:700 10.5px/1.4 inherit` is not a
   valid shorthand — `inherit` cannot stand in for the family there — so Chrome
   drops the WHOLE declaration and an `h3` renders at its browser default. The
   shelves came back 17px bold, which is bigger than the card heading above
   them. Set the properties one at a time. */
.shelf { font-size:10.5px; font-weight:700; line-height:1.4; text-transform:uppercase;
         letter-spacing:.08em; color:var(--mute); margin:15px 0 7px; }
/* The first shelf in a card sits directly under the head, which has already
   provided the space. */
.card__head + .shelf { margin-top:0; }
.shelf ~ .hint { margin-top:6px; }

/* ---- Key and value as a spec sheet ------------------------------------------
   Label left, value hard right, a hairline between rows. In a 340px column a
   left-aligned value column leaves a ragged gutter down the middle of the card
   and the eye has to hunt for where each answer begins; ranged right they form
   a column of their own and the dividers give the block a rhythm.

   ⚠ The dividers go on the ROWS, which in a two-column grid means both cells —
   a border on the `dt` alone stops halfway across the card. */
/* ⚠ AND THE COLUMN GAP HAS TO GO. A border-top on two grid cells with a 14px
   gap between them draws TWO dashes with a hole in the middle, not a rule —
   which is worse than no rule at all. The gap becomes padding on the label, so
   the two borders meet and the line is continuous. */
dl.kv--spec { grid-template-columns:auto minmax(0, 1fr); gap:0; }
dl.kv--spec > dt { padding-right:16px; }
/* ⚠ AND THEY MUST STRETCH, NOT CENTRE. `align-self:center` sizes each cell to
   its own content and centres it in the row, so the two border-tops land at
   different heights — a label at 11.5px beside a value at 13px was already
   1px out, and a two-line billing address put the label's rule 14px below the
   value's. The row is the divider; both cells fill it. */
dl.kv--spec > dt, dl.kv--spec > dd { padding:7px 0; border-top:1px solid var(--hair);
                                     align-self:stretch; }
dl.kv--spec > dt:first-child, dl.kv--spec > dt:first-child + dd {
  border-top:0; padding-top:0; }
dl.kv--spec > dt { font-size:11.5px; }
dl.kv--spec > dd { text-align:right; font-size:13px; min-width:0;
                   overflow-wrap:anywhere; }
/* Long free text — a billing address, a note — is unreadable ranged right, so
   anything over one line falls back to the left edge of its own column. */
dl.kv--spec > dd { text-wrap:pretty; }

/* `.lookingfor` and `.note` sit at the foot of the Details card, below the spec
   rows. They carried a 12px top margin against a list that used to have padded
   rows; against a divided list they need a real gap and a rule of their own. */
.kv--spec + .lookingfor { margin-top:14px; padding-top:0; }
.kv--spec + .note, .lookingfor + .note { margin-top:10px; }

/* A CARD INSIDE A CARD. The portal invite ships with the same border, radius,
   ground and padding as the card it now sits inside, so Contacts drew a box,
   then drew another box 16px in from it — the single loudest bit of nesting
   left on the record. Inside the two columns it becomes a band: same content,
   same button, one frame instead of two. */
.reccols .portalcta { border:0; border-radius:10px; background:var(--band);
                      padding:12px 14px; }

/* Below the two-column breakpoint the identity card becomes full width, and a
   spec sheet stretched across 800px puts the label and its value at opposite
   ends of a tablet — the eye cannot carry a row that long. The rows keep a
   readable measure and sit at the left of the card. */
@media (max-width:900px) { dl.kv--spec { max-width:520px; } }

/* ============================================================================
   THE CUSTOMER CONSOLE                                     Greg, 2026-09-02
   "go with the two pane console."

   WHAT THE MEASUREMENT SAID. The record carried 89 rounded elements in 32
   distinct shapes across seven radii — 8, 10, 11, 14, 50, 99, 999 — and pill
   heights from 14px to 40px. `--r-sm/--r/--r-lg` already existed and almost
   nothing used them. Moving boxes around could never fix that; the count of
   SHAPES was the problem, not their positions.

   So: two surfaces. A filled panel on the left holding identity, one flat
   sheet on the right holding the record. Inside the sheet nothing draws a
   frame — the flattening rules at the bottom of this block strip the border,
   radius and shadow off every `.card` that lands there, so the existing view
   code keeps working unchanged and simply stops drawing boxes.
   ========================================================================= */

/* The bar above the console: back link left, page actions right. Replaces
   `.page__head` for this screen, because the customer's NAME now lives in the
   panel and an <h1> above it would say it twice. */
.recbar { display:flex; align-items:center; justify-content:space-between;
          gap:14px; flex-wrap:wrap; margin-bottom:14px; }
.recbar .back { margin:0; }
.recbar__acts { display:flex; gap:8px; flex-wrap:wrap; margin-left:auto; }

.console { display:grid; grid-template-columns:330px minmax(0, 1fr);
           gap:16px; align-items:start; }

/* ---- The panel -------------------------------------------------------------
   Filled navy so it reads as furniture rather than as the first and loudest
   card in a stack, which is what the old Details card had become. */
.who { background:linear-gradient(168deg, var(--ink2), var(--navy));
       color:#fff; border-radius:var(--r); padding:20px;
       box-shadow:var(--shadow); position:sticky; top:var(--stick); }
.who__name { font-size:20px; line-height:1.25; letter-spacing:-.01em; margin:0; color:#fff; }
.who__sub { font-size:12.5px; color:#A9C2DC; margin-top:5px; }
.who__dim { color:#8FAECC; }

.who__money { display:grid; grid-template-columns:1fr 1fr; gap:14px; margin:17px 0;
              padding:14px 0; border-top:1px solid rgba(255,255,255,.16);
              border-bottom:1px solid rgba(255,255,255,.16); }
.who__money dt { font-size:10px; letter-spacing:.09em; text-transform:uppercase;
                 color:#8FAECC; font-weight:700; }
.who__money dd { font-size:19px; margin:3px 0 0; font-variant-numeric:tabular-nums; }
.who__money dd.is-warn { color:#FFC98A; }

/* The spec sheet, on the dark ground. Rows own the divider, so both cells
   carry the rule and it runs the full width — a border on the `dt` alone
   stops halfway across the panel. And they STRETCH: `align-self:center` sizes
   each cell to its own content, which puts a 12px label's rule and a 13px
   value's rule at different heights on every row. */
.who__k { display:grid; grid-template-columns:auto minmax(0, 1fr); gap:0; margin:0; }
.who__k dt, .who__k dd { padding:7px 0; font-size:12.5px; align-self:stretch;
                         border-top:1px solid rgba(255,255,255,.10); margin:0; }
.who__k dt { color:#8FAECC; padding-right:14px; }

.who__k dd { text-align:right; color:#EAF2FA; min-width:0; overflow-wrap:anywhere; }
.who__k > dt:first-child, .who__k > dt:first-child + dd { border-top:0; padding-top:0; }

.who__want { display:flex; gap:8px; align-items:baseline; flex-wrap:wrap;
             margin-top:14px; padding:9px 11px; border-radius:var(--r-sm);
             background:rgba(255,255,255,.08); font-size:13px; line-height:1.5; }
.who__want.is-empty { background:rgba(255,201,138,.13); }
.who__wantk { font-size:10px; font-weight:700; letter-spacing:.07em;
              text-transform:uppercase; color:#8FAECC; flex:0 0 auto; }
.who__want.is-empty .who__wantk { color:#FFC98A; }

/* ⚠ THE GLOBAL BUTTON RULE REACHES IN HERE. `a.button, button` sets
   min-height:40px, its own padding and an accent glow; every control on this
   panel has to reset all three or it inherits a shape from the light theme. */
/* ⚠ NOT `1fr 1fr`. A lead with a phone number and no address has ONE button,
   and a fixed two-column grid gives it half the panel and leaves the other
   half empty — the same fault the old pill row had. Flex, so one button fills
   the row and two share it. */
.who__do { display:flex; gap:8px; margin-top:16px; }
.who__do a { flex:1 1 0; }
.who__do a { display:flex; align-items:center; justify-content:center; gap:7px;
             min-height:40px; padding:10px 12px; border-radius:var(--r-sm);
             background:rgba(255,255,255,.10); border:1px solid rgba(255,255,255,.18);
             color:#fff; text-decoration:none; font-weight:600; font-size:13px;
             white-space:nowrap; box-shadow:none; }
.who__do a:hover { background:rgba(255,255,255,.18); border-color:rgba(255,255,255,.30); }
.who__link { background:none; border:0; padding:0; min-height:0; box-shadow:none;
             color:#9CC6E8; font:600 12.5px inherit; cursor:pointer; text-align:left; }
.who__link:hover { color:#C6E0F5; text-decoration:underline; }
.who__edit { display:block; margin-top:12px; }
.who__note { margin-top:14px; padding-top:12px; border-top:1px solid rgba(255,255,255,.10);
             font-size:12.5px; color:#C3D6E8; line-height:1.55; white-space:pre-wrap; }

/* ---- The sheet ------------------------------------------------------------- */
.rec { background:var(--white); border:1px solid var(--hair); border-radius:var(--r);
       box-shadow:var(--shadow); overflow:hidden; min-width:0; }
.rectabs { display:flex; gap:0; padding:0 18px; overflow-x:auto;
           border-bottom:1px solid var(--hair); }
.rectab { font:600 13px inherit; color:var(--steel); text-decoration:none;
          padding:13px 13px 11px; border-bottom:2px solid transparent;
          white-space:nowrap; }
.rectab:hover { color:var(--ink); }
.rectab.is-on { color:var(--ink); border-bottom-color:var(--accent); }
.recbody { padding:18px 20px 20px; min-width:0; }

/* ---- Flattening -----------------------------------------------------------
   The whole point of the console: inside the sheet, a card is a heading and a
   rule. Done here rather than by rewriting every view so that Invoices,
   Estimates, Activity and Recurring — which build their own cards and
   sections — land correctly without being touched. */
.recbody .card {
  background:none; border:0; border-radius:0; box-shadow:none; padding:0;
  backdrop-filter:none; -webkit-backdrop-filter:none;
}
.recflow { display:flex; flex-direction:column; }
/* A rule between siblings, never above the first or below the last — and
   `:empty` hosts take no room at all, so a section that has not loaded yet
   cannot leave a stray divider behind. */
.recflow > *:not(:empty) ~ *:not(:empty) {
  margin-top:18px; padding-top:18px; border-top:1px solid var(--hair);
}
.recflow > :empty { display:none; }
/* `.card + .card` and `.cols--2`'s own top margin would double the spacing the
   rule above already owns. */
.recbody .card + .card, .recbody .cols--2 { margin-top:0; }
.recbody .cols--2 { align-items:start; }
@media (max-width:1180px) { .recbody .cols--2 { grid-template-columns:1fr; } }

/* One heading style for everything in the sheet, whatever built it. */
.recbody .card__head { margin-bottom:11px; padding-bottom:0; border-bottom:0; }
.recbody .card__head h2,
.recbody > section > h2, .recbody .card > h2 {
  font-size:12px; letter-spacing:.08em; text-transform:uppercase;
  color:var(--steel); font-weight:700;
}
.recbody > section { margin:0; }
.recbody .card__head .link, .recbody .card__head .ghost { font-size:12px; }
/* The map, the upload bar and the message column keep a frame — they are
   pictures and inputs, not sections — but they answer to the shared token. */
.recbody .propmap, .recbody .mapbox, .recbody .uploadbar,
.recbody .msgbox-scroll { border-radius:var(--r-sm); }

/* A card inside the sheet must not draw a second frame around itself. */
.recbody .portalcta { border:0; border-radius:var(--r-sm); background:var(--band);
                      padding:12px 14px; }

@media (max-width:900px) {
  .console { grid-template-columns:1fr; }
  .who { position:static; }
  .recbody { padding:16px 14px 18px; }
  .rectabs { padding:0 10px; }
}

/* ---- ONE SHAPE LANGUAGE INSIDE THE SHEET -----------------------------------

   Moving to the console took the record from 89 rounded elements in 32 shapes
   down to 46 in 23 — better, but still five radii inside one surface (8, 10,
   11, 99) because the chips, the small buttons and the message bubbles each
   still carried their own. The console cannot look uniform while its contents
   disagree, so inside `.recbody` there are exactly three radii and no others:

     var(--r)      14px   the two outer surfaces, and nothing else
     var(--r-sm)   10px   every panel, control, map, bubble and small button
     5px                  chips, which need to read as labels, not as buttons

   Scoped to `.recbody` on purpose. These components appear all over the
   product and re-shaping them globally is a separate decision on a separate
   day — this is the customer record answering for itself. */
.recbody .chip,
.recbody .chip--dual_pricing,
.recbody .chip--warn {
  border-radius:5px; font-size:10px; font-weight:700; letter-spacing:.06em;
  text-transform:uppercase; padding:3px 7px; min-height:0; line-height:1.4;
}
.recbody .btnlink,
.recbody .mapbox__go {
  border-radius:var(--r-sm); min-height:30px; padding:6px 12px;
  font-size:12.5px; font-weight:600;
}
.recbody .msg, .recbody .msgbox, .recbody .msgbox-scroll, .recbody .msgstate,
.recbody .uploadbar, .recbody .propmap, .recbody .mapbox,
.recbody button, .recbody .ghost, .recbody input, .recbody select,
.recbody textarea, .recbody a.button {
  border-radius:var(--r-sm);
}

/* ⚠ A DESTRUCTIVE ACTION WAS THE LOUDEST THING ON THE PAGE. `button.link.danger`
   picked up a SOLID red fill at 99px — so "Remove" beside a contact drew more
   attention than the contact's own name, three times over in one list. A
   destructive control should be findable and quiet, never advertised: red
   text, no fill, and it only fills in on hover once you are actually on it. */
.recbody button.link.danger,
.recbody .rowline__act button.link.danger {
  background:none; color:var(--red); border:0; box-shadow:none;
  min-height:0; padding:2px 4px; border-radius:6px;
  font:600 12.5px inherit; text-transform:none; letter-spacing:0;
}
.recbody button.link.danger:hover { background:var(--red-bg); color:var(--danger-dark); }

/* ---- The console, reused ----------------------------------------------------
   Greg, 2026-09-02: "lets use this style across the site where it works."
   The passport carries three lifetime figures rather than two, and they need
   a caption each — an "Invoiced" with no "every owner" under it is a number
   whose scope you have to guess. */
.who__money--three { grid-template-columns:repeat(3, minmax(0, 1fr)); gap:10px; }
.who__money--three dd { font-size:16px; }
.who__figsub { display:block; font-size:10.5px; color:#8FAECC; margin-top:2px;
               line-height:1.35; }

/* A warning that has to survive every scroll position and every tab. Loud on
   the navy, but it is a warning — it is meant to be. */
.who__alert { margin-top:13px; padding:9px 11px; border-radius:var(--r-sm);
              background:rgba(255,201,138,.16); border:1px solid rgba(255,201,138,.34);
              color:#FFE2BC; font-size:12.5px; line-height:1.5; }
.who__alert b { color:#FFC98A; }

/* An anchor inside the panel's key/value rows is a link, not a button — the
   global `a.button` rule does not reach it, but `.who__link` was written for
   a <button> and needs to behave the same as an <a>. */
a.who__link { text-decoration:none; }
a.who__link:hover { text-decoration:underline; }
.who__k .who__link { font-size:12.5px; }

/* A record with no facets gets no tab bar, so the sheet needs its own top
   padding back — otherwise the first section sits against the rounded edge. */
.recbody--notabs { padding-top:20px; }

/* The passport's alert banner is the one thing in the sheet that keeps a
   fill, and it must not also keep a shape of its own. */
.recbody .notice--warn, .recbody .notice, .recbody .notice--bad {
  border-radius:var(--r-sm);
}
/* A three-up figure row in a 330px panel: a date like "Aug 12, 2026" needs to
   fit its column or it takes two lines and the row goes ragged. */
.who__money--three dd { font-size:15px; line-height:1.3; }

/* An eyebrow above the name: the project's reference number, which identifies
   it in conversation but is not what anybody calls it. */
.who__eyebrow { font-size:10.5px; font-weight:700; letter-spacing:.1em;
                text-transform:uppercase; color:#8FAECC; margin-bottom:3px; }
/* A status badge sitting in the subtitle line beside the description. The
   global badge colours are built for a white ground, so on the panel it gets
   a translucent white fill and keeps only its text colour's brightness. */
.who__sub { display:flex; gap:8px; align-items:baseline; flex-wrap:wrap; }
.who__badge .badge { background:rgba(255,255,255,.14); color:#EAF2FA;
                     border-color:rgba(255,255,255,.2); }
/* A crew chip on the dark ground. */
.who__k .crewchip { color:#EAF2FA; background:none; padding:0; }

/* A value in the panel that is not good news — a merchant that is suspended
   rather than active. Amber, and only ever on the value: colouring the label
   too would make the row itself look like a warning. */
.who__warnval { color:#FFC98A; }
/* The pricing program chip on the dark ground. It carries its own colours for
   a white card, so on the panel it keeps only its shape and takes the ink. */
.who__k .progchip, .who__k .chip { background:rgba(255,255,255,.14); color:#EAF2FA;
                                   border-color:rgba(255,255,255,.22); }
/* The merchant's "ready to trade" checklist reuses the boarding steps, which
   are buttons there and a report here. Inside the sheet they do not invite a
   click, so no pointer and no hover highlight. */
.recbody .obstep { cursor:default; }
.recbody .obstep:hover { border-color:var(--hair); }
.recbody .obverdict { margin-bottom:12px; }
.recbody .progbox { margin-bottom:8px; }

/* The merchant's address on its Details sheet (Greg, 2026-09-05). An address
   is read as lines, not as a label and a value, so it stands on its own
   rather than ranged right in a spec row; the directions link sits under it
   the way the customer panel's does, quiet. */
.mdet-addr { font-size:13px; line-height:1.5; color:var(--ink); }
.recbody a.mdet-dir { display:inline-block; margin-top:8px; font-size:12.5px;
                      font-weight:600; color:var(--accent); text-decoration:none; }
.recbody a.mdet-dir:hover { text-decoration:underline; }

/* ============================================================================
   THE STOP'S WORKFLOW BAR                                  Greg, 2026-09-02
   "go with option D" — A's console under C's topline workflow.

   Where the job IS on the left, and the one thing that happens next on the
   right. It spans the full width above both columns because it belongs to the
   whole record, and because squeezing a five-step sequence plus a clock plus a
   verb into a 330px panel would truncate all three.
   ========================================================================= */
.jobflow { display:flex; align-items:stretch; margin-bottom:14px;
           background:var(--solid-bg); border:1px solid var(--hair);
           border-radius:var(--r); box-shadow:var(--shadow-sm); overflow:hidden; }
.jobflow__steps { display:flex; flex:1; min-width:0; }
.jobstep { flex:1; min-width:0; padding:11px 14px; border-left:1px solid var(--hair);
           display:flex; flex-direction:column; justify-content:center; }
.jobstep:first-child { border-left:0; }
.jobstep__k { display:block; font-size:9.5px; font-weight:700; letter-spacing:.08em;
              text-transform:uppercase; color:var(--mute); }
.jobstep__v { display:block; font-size:13px; margin-top:2px;
              white-space:nowrap; overflow:hidden; text-overflow:ellipsis; }
.jobstep.is-done { background:var(--green-bg); }
.jobstep.is-done .jobstep__k { color:#1C7A57; }
.jobstep.is-todo .jobstep__v { color:var(--mute); }
/* The step that is true right now. Navy, for the same reason the identity
   panel is navy: the eye should land on it before it reads any of the others.

   ⚠ AND IT IS WIDER THAN THE REST. Five equal cells beside a clock and two
   buttons squeezed the live step to ~190px, so the one date on the bar that
   anybody actually needs — "Wed 24 Dec, 7:30am" — was the one truncating. The
   finished and not-yet steps can afford to be narrow; this one cannot. */
.jobstep.is-now { background:var(--navy); color:#fff; flex:1.9; }
.jobstep.is-now .jobstep__k { color:#8FAECC; }
.jobstep.is-bad { background:var(--red-bg); color:var(--ink); }
.jobstep.is-bad .jobstep__k { color:var(--red); }
.jobflow.is-dead .jobstep.is-now { background:var(--red-bg); color:var(--ink); }

/* The verb, at the end of the sequence it advances. */
.jobflow__next { display:flex; align-items:center; gap:8px; padding:9px 12px; flex:0 0 auto;
                 border-left:1px solid var(--hair); background:var(--paper); }
.jobflow__clock { padding-right:11px; border-right:1px solid var(--hair); line-height:1.2; }
.jobflow__clock span { display:block; font-size:9px; font-weight:700; letter-spacing:.08em;
                       text-transform:uppercase; color:var(--mute); }
.jobflow__clock b { font-size:16px; font-variant-numeric:tabular-nums; }
/* The global button rule gives these 40px and their own padding, which is what
   we want here — they are the page's primary controls. Only the radius is
   brought into line with everything else in the console. */
.jobflow__next button { border-radius:var(--r-sm); }

/* THIS PAGE IS OPERATED, NOT READ — one-handed, in a van or at a counter. So
   the bar stacks before the action can be crushed: steps wrap first, then the
   clock and buttons take a row of their own and go full width. */
@media (max-width:1220px) {
  .jobflow { flex-direction:column; }
  .jobstep { border-left:0; border-top:1px solid var(--hair); }
  .jobflow__steps { flex-wrap:wrap; }
  .jobstep { flex:1 0 33%; }
  .jobflow__next { border-left:0; border-top:1px solid var(--hair); justify-content:flex-end; }
}
@media (max-width:560px) {
  .jobstep { flex:1 0 50%; }
  .jobflow__next { flex-wrap:wrap; }
  .jobflow__next button { flex:1 1 auto; }
}

/* The stop's own blocks inside the sheet: the split that paired inspections
   with time is gone (they are tabs now), so its grid must not still apply. */
.recbody .jobsplit { display:block; }
.recbody .jobcharges { margin:0; }

/* The scope guard's warning in the log-time dialog. Amber, inline, and it
   never blocks the save — the hour happened; the point is that somebody sees
   the question while the answer is still worth having. */
.scopewarn { margin-top:2px; }
.scopewarn[hidden] { display:none; }

/* ---- The billing run's row actions -----------------------------------------

   Greg, 2026-09-02: "Billing run has the hold, decide, write off buttons all
   off alignment."

   TWO FAULTS, AND ONLY ONE OF THEM WAS ALIGNMENT.

   The first: only an out-of-scope line gets a Decide button, so a flex row
   shuffled Hold and Write off left and right depending on the line above it.
   Three fixed slots, sized to the longest label each holds, and the Decide slot
   is reserved even when empty — the same verb now sits at the same x on every
   row and the column can be read straight down.

   The second, and the worse one: `button.link.danger` picked up a SOLID RED
   FILL, so "Write off" — the destructive action, the one that quietly loses the
   firm money — was the loudest object on the screen, once per row. That was
   fixed for the customer record but the fix was scoped to `.recbody`, and this
   screen is not inside one. Scoped properly now: a destructive control should
   be findable and quiet, and fill in only once you are actually on it. */
td.wipacts { vertical-align:middle; }
.wipacts__grid { display:grid; grid-template-columns:58px 40px 72px; gap:8px;
                 align-items:center; justify-content:end; }
.wipacts__grid > button { width:100%; text-align:center; white-space:nowrap;
                          min-height:0; padding:5px 4px; border-radius:6px;
                          font:600 12.5px inherit; }
.wipacts__grid button.link { background:none; border:0; box-shadow:none;
                             color:var(--accent); cursor:pointer; }
.wipacts__grid button.link:hover { background:var(--band); }
.wipacts__grid button.link.danger { color:var(--red); background:none; }
.wipacts__grid button.link.danger:hover { background:var(--red-bg); color:var(--danger-dark); }

/* ============================================================================
   TIME: PICK A CLIENT, THEN ADD TO WHAT IS UNBILLED     Greg, 2026-09-02
   "one box on the left hand side that is scrollable where the user can select
   or search for the client. When they select the client on the right pane
   opens their billable hours listed out with the option to log more time."

   The left column scrolls on its OWN, not with the page: a firm with 300
   clients should be able to run down the list while the client they have open
   stays where it is. Sticky, capped to the viewport, and the search box stays
   pinned at the top of it so it never scrolls out of reach.
   ========================================================================= */
.timepick, .pickpane { display:grid; grid-template-columns:320px minmax(0, 1fr);
            gap:16px; align-items:start; }

.timepick__list, .pickpane__list { background:var(--solid-bg); border:1px solid var(--hair);
                  border-radius:var(--r); box-shadow:var(--shadow);
                  position:sticky; top:var(--stick); overflow:hidden;
                  display:flex; flex-direction:column;
                  max-height:calc(100vh - var(--stick) - 14px); }
.timepick__search, .pickpane__search { padding:12px; border-bottom:1px solid var(--hair); flex:0 0 auto; }
.timepick__search input, .pickpane__search input { width:100%; }
.timepick__rows, .pickpane__rows { overflow-y:auto; flex:1 1 auto; min-height:120px; }
.timepick__empty, .pickpane__empty { padding:18px; }

/* A row is a button, so it is reachable by keyboard and reads as pressable.
   ⚠ The global `button` rule gives it 40px, its own padding, a radius and an
   accent glow — all four have to go or every row draws as a pill. */
.timerow { display:flex; width:100%; align-items:baseline; justify-content:space-between;
           gap:12px; text-align:left; background:none; border:0; box-shadow:none;
           border-radius:0; min-height:0; padding:11px 14px; cursor:pointer;
           border-bottom:1px solid var(--hair); font:14px inherit; color:var(--ink); }
.timerow:last-child { border-bottom:0; }
.timerow:hover { background:var(--paper); }
.timerow.is-on { background:var(--band); box-shadow:inset 3px 0 0 var(--accent); }
/* ⚠ THE CHIPS MUST OUTLIVE THE TRUNCATION. This was one nowrap ellipsis box
   with the name and the chips inside it, so "no rate" and "scope" — the two
   things on the row worth reading — were the first things clipped off the end
   of a long client name. The name truncates; the chips are their own flex
   items and cannot be eaten. */
.timerow__name { display:flex; align-items:center; gap:6px; min-width:0;
                 font-weight:600; }
.timerow__nm { min-width:0; overflow:hidden; text-overflow:ellipsis;
               white-space:nowrap; }
.timerow__name > .chip { flex:0 0 auto; }
.timerow__val { flex:0 0 auto; text-align:right; font-size:12px; color:var(--steel);
                line-height:1.3; }
.timerow__val b { display:block; font-size:13px; color:var(--ink);
                  font-variant-numeric:tabular-nums; }
/* A client with nothing outstanding is still selectable — you log the first
   hour against them from here — but it must not compete with the ones that
   have money sitting on them. */
.timerow__val.is-none { color:var(--mute); font-size:11.5px; }

.timepick__body, .pickpane__body { background:var(--solid-bg); border:1px solid var(--hair);
                  border-radius:var(--r); box-shadow:var(--shadow);
                  padding:18px 20px 20px; min-width:0; }
.timepick__head { display:flex; align-items:flex-start; justify-content:space-between;
                  gap:14px; flex-wrap:wrap; padding-bottom:14px; margin-bottom:6px;
                  border-bottom:1px solid var(--hair); }
.timepick__head h2 { font-size:19px; }
.timepick__head .sub { font-size:13px; color:var(--steel); margin-top:3px; }
.timepick__acts { display:flex; gap:8px; flex-wrap:wrap; }
.timepick__blank, .pickpane__blank { padding:34px 8px; text-align:center; }
.timepick__blank h2, .pickpane__blank h2 { font-size:17px; margin-bottom:6px; }
.timepick__blank p, .pickpane__blank p { max-width:52ch; margin:0 auto; }
/* Same quiet-danger rule as the billing run: Remove must not be the loudest
   thing in a table of hours somebody is reading for money. */
.timedel { background:none; border:0; box-shadow:none; color:var(--red);
           min-height:0; padding:3px 6px; border-radius:6px; font:600 12.5px inherit; }
.timedel:hover { background:var(--red-bg); color:var(--danger-dark); }



/* ---- Engagements: the list, grouped by client ------------------------------
   Greg, 2026-09-02: "same model for engagements — the scrollable business list
   with a pane to the right … we can add more context now to this."

   The client is a heading, not a row: a practice thinks "Bayou Brewing, and the
   three things we do for them", so the businesses are the spine and the work is
   indented under each one. */
.engclient { position:sticky; top:0; z-index:1; padding:9px 14px 6px;
             font:700 10px/1.3 inherit; letter-spacing:.09em; text-transform:uppercase;
             color:var(--steel); background:var(--paper);
             border-bottom:1px solid var(--hair); }
/* ⚠ align-items COMES DOWN FROM .timerow__name AND MEANS THE OPPOSITE HERE.
   The base row is a flex ROW, where centring is vertical; this one is a
   COLUMN, where the same declaration centres the name and the model label
   horizontally — which showed up as one engagement sitting a few pixels
   further right than its neighbours. Restated, not inherited. */
.engrow .timerow__name { display:flex; flex-direction:column;
                         align-items:flex-start; gap:2px; }
.engrow__model { font-size:11.5px; font-weight:400; color:var(--mute); }
.engrow__state { font-size:10px; font-weight:700; letter-spacing:.06em;
                 text-transform:uppercase; color:var(--amber); }
.engrow .timerow__val .is-warn { color:var(--amber); font-weight:600; }

/* The pane's own header carries the client above the engagement, because the
   list heading it came from scrolls away. */
.engpane__client { font:700 10.5px/1.3 inherit; letter-spacing:.09em;
                   text-transform:uppercase; color:var(--steel); margin-bottom:3px; }

.engfigs { display:grid; grid-template-columns:repeat(auto-fit, minmax(140px, 1fr));
           gap:1px; background:var(--hair); border:1px solid var(--hair);
           border-radius:var(--r-sm); overflow:hidden; margin-top:16px; }
.engfig { background:var(--white); padding:12px 14px; }
.engfig__k { display:block; font-size:9.5px; font-weight:700; letter-spacing:.09em;
             text-transform:uppercase; color:var(--mute); }
.engfig b { display:block; font-size:19px; margin-top:3px; font-variant-numeric:tabular-nums; }
.engfig__s { display:block; font-size:11.5px; color:var(--mute); margin-top:2px; }
.engfig.is-warn b { color:var(--amber); }
.engfig.is-bad b { color:var(--red); }

/* A percentage is a number; a budget is a position. The bar keeps filling past
   the line rather than capping, because an engagement at 160% should look like
   one rather than look identical to one at 100%. */
.engbar { margin-top:14px; }
.engbar__track { height:7px; border-radius:99px; background:var(--band); overflow:hidden; }
.engbar__fill { height:100%; background:var(--accent); border-radius:99px;
                transition:width .25s ease; }
.engbar__fill.is-warn { background:var(--amber); }
.engbar__fill.is-over { background:var(--red); }
.engbar__note { margin-top:7px; font-size:12.5px; color:var(--steel); }

.engblock { margin-top:22px; padding-top:18px; border-top:1px solid var(--hair); }
.engblock__head { display:flex; align-items:baseline; justify-content:space-between;
                  gap:12px; margin-bottom:11px; }
.engblock__head h3 { font-size:12px; letter-spacing:.08em; text-transform:uppercase;
                     color:var(--steel); font-weight:700; }
.engblock .tablewrap, .engblock table { margin-top:0; }
.engnote { font-size:13.5px; color:var(--ink); line-height:1.55; margin-bottom:10px; }
.engchips { display:flex; gap:6px; flex-wrap:wrap; }
.engwarn { margin-top:12px; padding:11px 13px; border-radius:var(--r-sm);
           background:var(--amber-bg); font-size:13px; line-height:1.5; }
.engwarn b { color:var(--amber); }

/* ---- Two fields side by side inside a 480px modal --------------------------
   Greg, 2026-09-02: "the TO box is almost falling out of the iframe."

   A `datetime-local` input has a wide intrinsic minimum — it has to fit
   "09/02/2026, 04:30 PM" plus a picker icon — and a grid track will happily be
   overflowed by it rather than shrinking it. Two of them in a 480px modal do
   not fit, so the second one runs out of the panel.

   Two fixes, and both are needed: the tracks are allowed to shrink (`min-width:0`
   is not the default in a grid item), and the date fields stack below the width
   where they would start truncating their own text rather than being squeezed
   into a box too small to read. */
.modal .cols--2 > * { min-width:0; }
.modal input { max-width:100%; }
.modal input[type="datetime-local"], .modal input[type="date"],
.modal input[type="time"] { width:100%; min-width:0; }
.modal .cols--2:has(input[type="datetime-local"]) { grid-template-columns:1fr; gap:0; }
/* Fallback for engines without :has() — the pair simply stacks on any narrow
   viewport, which is where the squeeze is worst anyway. */
@media (max-width:620px) { .modal .cols--2 { grid-template-columns:1fr; gap:0; } }

/* ============================================================================
   "THIS ONE IS YOURS"                                     Greg, 2026-09-02
   "a popup … in the top right hand corner of a user's screen, or just front
   and center on the user's tablet or phone."

   TWO PLACEMENTS, BECAUSE THEY ARE TWO DIFFERENT SCREENS. On a desktop the
   top-right corner is where a toast belongs and nothing important sits under
   it. On a phone that corner is out of a thumb's reach and gets scrolled past,
   so the card comes to the middle and takes the width — a ticket landing on
   somebody is worth interrupting for, which is the whole difference between
   this and a "Saved." toast.

   ⚠ ABOVE THE COPILOT BUBBLE (z 900) AND THE TOASTS (80), because it is the
   one message that is about work somebody now owns. Below a modal, because a
   person mid-dialog should finish it.
   ========================================================================= */
.alertstack { position:fixed; z-index:950; top:14px; right:14px;
              display:flex; flex-direction:column; gap:10px;
              width:min(360px, calc(100vw - 28px)); pointer-events:none; }
.alertstack > * { pointer-events:auto; }

.alertcard { background:var(--white); border:1px solid var(--hair-strong);
             border-left:4px solid var(--accent); border-radius:var(--r);
             box-shadow:var(--shadow-lift); padding:14px 16px; cursor:pointer;
             animation:alertin .22s ease-out; }
.alertcard:hover { border-color:var(--accent); }
.alertcard.is-going { opacity:0; transform:translateY(-6px); transition:.2s ease; }
@keyframes alertin { from { opacity:0; transform:translateY(-10px); } }
@media (prefers-reduced-motion:reduce) {
  .alertcard { animation:none; }
  .alertcard.is-going { transition:none; }
}
.alertcard__kind { font:700 9.5px/1 inherit; letter-spacing:.1em; text-transform:uppercase;
                   color:var(--accent); }
.alertcard__title { display:block; font-size:14.5px; margin-top:5px; }
.alertcard__body { font-size:12.5px; color:var(--steel); margin-top:3px; line-height:1.45; }
.alertcard__foot { display:flex; align-items:center; justify-content:space-between;
                   margin-top:9px; }
.alertcard__go { font:600 12.5px inherit; color:var(--accent); }
/* ⚠ The close button has to shed the global button rule — 40px tall with an
   accent glow would be the biggest thing in the card. */
.alertcard__x { background:none; border:0; box-shadow:none; min-height:0;
                padding:2px 6px; border-radius:6px; color:var(--mute);
                font:400 17px/1 inherit; cursor:pointer; }
.alertcard__x:hover { background:var(--band); color:var(--ink); }
.alertcard__ask { display:block; width:100%; margin-top:10px; background:none;
                  border:1px dashed var(--hair-strong); box-shadow:none;
                  min-height:0; padding:7px; border-radius:var(--r-sm);
                  color:var(--steel); font:600 12px inherit; cursor:pointer; }
.alertcard__ask:hover { border-color:var(--accent); color:var(--accent); }

@media (max-width:760px) {
  .alertstack { top:auto; right:14px; left:14px; bottom:auto;
                top:50%; transform:translateY(-50%);
                width:auto; align-items:stretch; }
  .alertcard { padding:18px 18px 16px; border-left-width:5px; }
  .alertcard__title { font-size:16px; }
  .alertcard__body { font-size:13.5px; }
}

/* ---- Room to breathe on a tab full of figures ------------------------------
   Greg, 2026-09-02: "Engagements and Time tab on customer is too close
   together, the different pills text and boxes need to be spread out."

   Measured before changing anything: every block on that tab began exactly
   where the one above it ended — four sections, three gaps, 0px each. The
   flattening rules that make a console sheet read as one surface had done
   their job a little too well.

   `.recflow` restores the rhythm; these three add the air that a tab of
   dense figures needs beyond it — the strip of tiles is a different kind of
   object from the tables under it and should not sit flush against them. */
.recflow > .tiles + * { margin-top:20px; padding-top:20px; }
.recbody .cols--2 { gap:16px; }
/* A heading needs more room from the rule above it than from its own content,
   or the section reads as belonging to what came before. */
.recflow > * > .card__head:first-child { margin-top:2px; }
/* Two tables side by side, each with its own heading: give the rows a little
   more height so a column of figures does not read as a wall. */
.workhost table td, .workhost table th { padding-top:11px; padding-bottom:11px; }

/* ---- The portal's supplier switcher ----------------------------------------
   Greg, 2026-09-02: a customer may buy from several businesses on this
   platform and needs to toggle between them.

   Sits beside their name rather than in the branded block on the left: that
   block is the SUPPLIER's identity — logo, name, accent — and putting a
   control inside it invites the reading that you are editing the business
   rather than choosing which one you are looking at. */
.ptop__switchwrap { display:flex; align-items:center; gap:8px; margin-right:14px; }
.ptop__switchcap { font:700 9.5px/1 inherit; letter-spacing:.09em;
                   text-transform:uppercase; color:var(--mute); }
.ptop__switch { min-height:34px; padding:6px 30px 6px 10px; border-radius:var(--r-sm);
                border:1px solid var(--hair-strong); background:var(--white);
                font:600 13px inherit; color:var(--ink); max-width:230px; }
.ptop__switch:disabled { opacity:.6; }
@media (max-width:640px) {
  .ptop__switchwrap { width:100%; margin:8px 0 0; }
  .ptop__switchcap { display:none; }
  .ptop__switch { flex:1; max-width:none; }
}

/* ============================================================================
   THE TIMESHEET PANEL                                     Greg, 2026-09-02
   Built to match onsightpayments.com/business-services — "The clock belongs to
   a client, not to a day." The marketing page is a promise about how the
   product behaves; a product that looks like something else when you sign in
   makes a liar of it. Measurements taken off the live page rather than guessed.
   ========================================================================= */
.tsheet { background:var(--white); border:1px solid var(--hair);
          border-radius:var(--r); box-shadow:var(--shadow); overflow:hidden; }
.tsheet__bar { display:flex; align-items:baseline; justify-content:space-between;
               gap:14px; padding:16px 20px; border-bottom:1px solid var(--hair);
               flex-wrap:wrap; }
.tsheet__bar b { font-size:17px; }
/* The total belongs at the top, in the accent — it is the number the whole
   panel exists to produce, and it should be readable before any single row. */
.tsheet__wip { font-size:12.8px; font-weight:500; color:var(--accent);
               font-variant-numeric:tabular-nums; }

.tsheet__row { display:grid; grid-template-columns:14px minmax(0, 1fr) 62px 92px;
               align-items:center; gap:12px; padding:13px 20px;
               border-top:1px solid var(--rule, var(--hair)); }
.tsheet__row:first-child { border-top:0; }
.tsheet__row:hover { background:var(--paper); }

/* ⚠ THE DOT IS A STATE, NOT DECORATION. Green is recorded and chargeable,
   grey is recorded and deliberately not — "on the record, off the invoice".
   The accent is reserved for time still on the clock. */
.tsheet__dot { width:9px; height:9px; border-radius:50%; background:var(--accent); }
.tsheet__dot.is-done { background:var(--green); }
.tsheet__dot.is-nb { background:var(--hair-strong); }

.tsheet__w { min-width:0; font-size:14.4px; }
.tsheet__w b { display:block; font-weight:600; }
/* Who did it, and when — under the description rather than in a column of its
   own, because it qualifies the line rather than standing beside it. */
.tsheet__w > span { display:block; font-size:12.16px; color:var(--steel);
                    margin-top:1px; overflow:hidden; text-overflow:ellipsis;
                    white-space:nowrap; }
.tsheet__note { color:var(--mute) !important; font-style:italic; }

/* H:MM, because that is what a timer reads and what a person recognises. */
.tsheet__h { font-size:14.72px; font-weight:700; text-align:right;
             font-variant-numeric:tabular-nums; }
.tsheet__v { font-size:12.8px; color:var(--steel); text-align:right;
             font-variant-numeric:tabular-nums; }

.tsheet__none { padding:34px 20px; color:var(--steel); font-size:13.5px;
                text-align:center; max-width:56ch; margin:0 auto; }
.tsheet__foot { display:flex; align-items:center; justify-content:space-between;
                gap:14px; flex-wrap:wrap; padding:14px 20px;
                border-top:1px solid var(--hair); background:var(--paper); }
.tsheet__acts { display:flex; gap:8px; flex-wrap:wrap; }
.tsheet__foothint { font-size:12.5px; color:var(--mute); }

/* ⚠ THE ROW ANSWERS TO ITS PANE, NOT TO THE WINDOW. A viewport media query is
   the wrong instrument for a panel inside a two-column split: the window can be
   1200px while this pane is 300px, and the row kept its four fixed columns and
   collapsed the description to a single letter. The container query is the
   whole fix; the @media below stays as the fallback for anything that does not
   support one. */
.tsheet { container-type:inline-size; }

@container (max-width:470px) {
  .tsheet__row { grid-template-columns:14px minmax(0, 1fr) auto; row-gap:2px; }
  .tsheet__v { grid-column:2 / -1; text-align:left; }
  .tsheet__row--pick .tsheet__v { display:flex; gap:8px; align-items:baseline; }
  .tsheet__row--pick .tsheet__v b, .tsheet__row--pick .tsheet__v span { display:inline; }
  .tsheet__bar { flex-direction:column; align-items:flex-start; gap:2px; }
  .tsheet__foot { flex-direction:column; align-items:stretch; }
  .tsheet__acts { width:100%; }
  .tsheet__acts > button, .tsheet__acts > a.button { flex:1 1 auto; }
}

/* ---- The same sheet, on the billing run ------------------------------------

   /time draws these rows to be READ; /billing draws them to be DECIDED. Same
   grid, three differences: the dot becomes a checkbox (the row is a <label>,
   so the whole line is the hit target), the value column carries the money in
   bold with the rate under it, and the per-line verbs sit under the
   description where they cannot push the money column around. */
.tsheet__row--pick { cursor:pointer; align-items:start; }
.tsheet__row--pick > input[type=checkbox] { margin:3px 0 0; width:14px; height:14px;
                                            accent-color:var(--accent); cursor:pointer; }
/* ⚠ justify-content:end on the shared grid was right in a table, where the
   verbs sat in their own right-hand column. Under a description they have to
   start where the description starts, or they float in the middle of the row
   attached to nothing. The three fixed slots stay — that is what keeps the
   same verb at the same x on every line. */
.tsheet__row--pick .wipacts__grid { margin-top:7px; justify-content:start; }
.tsheet__row--pick .wipacts__grid > button { text-align:left; padding-left:0; }
.tsheet__row--pick .wipacts__grid button.link:hover { background:none;
                                                      text-decoration:underline; }
.tsheet__row--pick .tsheet__v { line-height:1.35; }
.tsheet__row--pick .tsheet__v b { display:block; font-size:14px; color:var(--ink);
                                  font-weight:700; }
.tsheet__row--pick .tsheet__v span { display:block; font-size:11.5px; color:var(--mute); }
.tsheet__row--pick .tsheet__v .is-warn { color:var(--amber); font-weight:600; }

/* ---- Running every client ------------------------------------------------

   A batch that says only "working…" is a batch nobody trusts enough to leave
   running. Each client's outcome is written down as it happens, failures
   included, so what is on screen when it stops IS the record of what
   happened. */
.runlog { margin-top:14px; }
.runlog__bar { display:flex; justify-content:space-between; align-items:baseline;
               gap:12px; font-size:13px; color:var(--ink2); font-weight:600;
               padding-bottom:8px; border-bottom:1px solid var(--hair); }
.runlog__bar .num { font-variant-numeric:tabular-nums; color:var(--steel);
                    font-weight:500; }
.runlog__rows { max-height:230px; overflow-y:auto; }
.runlog__row { display:flex; justify-content:space-between; align-items:baseline;
               gap:12px; font-size:12.5px; padding:7px 0;
               border-bottom:1px solid var(--hair); }
.runlog__row:last-child { border-bottom:0; }
.runlog__row > span:first-child { font-weight:600; color:var(--ink2);
                                  min-width:0; overflow:hidden;
                                  text-overflow:ellipsis; white-space:nowrap; }
.runlog__row > span:last-child { flex:0 0 auto; color:var(--green); }
.runlog__row.is-bad > span:last-child { color:var(--red); text-align:right;
                                        max-width:58%; }

@media (max-width:620px) {
  .tsheet__row { grid-template-columns:14px minmax(0, 1fr) auto; row-gap:2px; }
  .tsheet__v { grid-column:2 / -1; text-align:left; }
  .tsheet__row--pick .tsheet__v { display:flex; gap:8px; align-items:baseline; }
  .tsheet__row--pick .tsheet__v b, .tsheet__row--pick .tsheet__v span { display:inline; }
}

/* ---- Where the two panes give up being two panes ---------------------------

   ⚠ ALL FOUR STEPS LIVE HERE, IN ORDER, AND NOTHING ABOVE MAY SET THIS. Every
   one of these is the same selector at the same specificity, so the cascade is
   decided by position alone — the stacking rule written earlier in the file
   was silently beaten by the 1100px rule written later, and a 900px window
   kept a 308px detail pane that had no business existing.

   ⚠ AND THE NUMBERS ARE VIEWPORT, WHILE THE PANES LIVE INSIDE A 224px RAIL.
   The list wants 360 and the detail wants 420 or it is not a detail pane;
   224 + 360 + 16 + 420 = 1020. So the split ends at 1040, not at the 900 it
   used to, which is why the description column was collapsing to one letter
   on a window most people would call wide.

   The client column is wider than it was — Greg, 2026-09-02: a firm's names
   are longer than "Bayou Brewing Co", and the value beside each was squeezing
   the name into an ellipsis on the very rows worth reading. */
.timepick, .pickpane { grid-template-columns:360px minmax(0, 1fr); }
@media (max-width:1180px) {
  .timepick, .pickpane { grid-template-columns:300px minmax(0, 1fr); }
}
@media (max-width:1040px) {
  .timepick, .pickpane { grid-template-columns:1fr; }
  .timepick__list, .pickpane__list { position:static; max-height:340px; }
}

/* The way back in, under the sign-in form. Quiet — it is the exception, not
   the path — but present, which it was not. */
.signin__forgot { margin-top:12px; text-align:center; }
.signin__forgot .link { background:none; border:0; box-shadow:none; min-height:0;
                        padding:4px 8px; border-radius:6px; color:var(--accent);
                        font:600 13px inherit; cursor:pointer; }
.signin__forgot .link:hover { background:var(--band); }

/* The embed snippet: code somebody is going to select and paste, so it wraps
   rather than scrolls and keeps its indentation. */
.embedbox { white-space:pre-wrap; word-break:break-all; line-height:1.5;
            max-height:none; padding:12px 14px; }

/* ── Blueprint Assemble — the sign-in animation ──────────────────────────────

   3.6 seconds, once per device per day. The mark draws itself over a blueprint
   grid, then fills and takes a check.

   ⚠ THE TIMELINE HERE AND `MS` IN signin-animation.js MUST AGREE. The JS
   removes the overlay on a timer rather than on animationend, because a
   dismissed overlay must come away instantly and animationend would never fire
   for it. If this timeline grows past MS the ending is cut off; if it shrinks,
   the user stares at a finished picture. */
.bpwrap{
  position:fixed; inset:0; z-index:9000;
  background:var(--navy-deep,#0A1730);
  display:grid; place-items:center;
  opacity:0; transition:opacity .22s ease;
  cursor:pointer;
}
.bpwrap.is-in{opacity:1}
.bpwrap.is-out{opacity:0}
.bpwrap__inner{text-align:center; padding:24px}
.bpwrap__art{width:min(62vw,320px); height:auto; display:block; margin:0 auto}

.bpwrap__word{
  margin:18px 0 0; color:#EAF2FA; font-size:19px; font-weight:800;
  letter-spacing:.01em;
  opacity:0; animation:bpWord .7s cubic-bezier(.2,.9,.3,1) 2.9s forwards;
}
.bpwrap__skip{
  margin:10px 0 0; color:rgba(234,242,250,.45); font-size:12px;
  letter-spacing:.06em;
  opacity:0; animation:bpWord .5s ease 2.2s forwards;
}
@keyframes bpWord{from{opacity:0;transform:translateY(14px)}to{opacity:1;transform:none}}

/* The grid: present immediately but faint, so the frame has something to be
   drawn *onto* rather than appearing in empty space. */
.bpwrap .bp-grid path{
  fill:none; stroke:var(--accent,#2A6E9E); stroke-width:.7; opacity:0;
  animation:bpFade .6s ease-out forwards;
}
@keyframes bpFade{to{opacity:.5}}

.bpwrap .bp-frame{
  fill:none; stroke:var(--accent-bright,#4B9CD3); stroke-width:1.8;
  stroke-dasharray:352; stroke-dashoffset:352;
  animation:bpDraw 1.5s ease-in-out .3s forwards;
}

/* Every shape in the mark draws itself. 260 comfortably exceeds the longest
   path in the set — a dasharray SHORTER than the path leaves a permanent gap,
   which is why this is generous rather than measured. */
.bpwrap .bp-mark{transform:scale(1.35)}
.bpwrap .bp-draw{
  stroke-dasharray:260; stroke-dashoffset:260;
  animation:bpDraw 1.6s ease-in-out .6s forwards;
}
@keyframes bpDraw{to{stroke-dashoffset:0}}

.bpwrap .bp-check circle{
  fill:var(--green,#2FA37A);
  opacity:0; transform-origin:center;
  animation:bpPop .5s cubic-bezier(.2,1.4,.4,1) 2.5s forwards;
}
.bpwrap .bp-check .bp-tick{
  fill:none; stroke:#fff; stroke-width:2.4;
  stroke-linecap:round; stroke-linejoin:round;
  stroke-dasharray:22; stroke-dashoffset:22;
  animation:bpDraw .35s ease-out 2.75s forwards;
}
@keyframes bpPop{
  from{opacity:0;transform:scale(.4)}
  60%{opacity:1;transform:scale(1.12)}
  to{opacity:1;transform:scale(1)}
}

/* Reduced motion never reaches here — maybePlay() returns before building the
   overlay — but if anything ever renders it directly, show the finished frame
   rather than a blank navy screen. */
@media (prefers-reduced-motion:reduce){
  .bpwrap :is(.bp-frame,.bp-draw,.bp-tick){stroke-dashoffset:0 !important; animation:none !important}
  .bpwrap :is(.bpwrap__word,.bpwrap__skip,.bp-check circle){opacity:1 !important; animation:none !important}
  .bpwrap .bp-grid path{opacity:.5 !important; animation:none !important}
}

/* ── The encouragement card — Carolina Rail ───────────────────────────────── */
.qcard{
  position:relative; overflow:hidden;
  background:var(--navy,#13294B); color:#fff;
  border-radius:14px; border-left:5px solid var(--accent-bright,#4B9CD3);
  padding:26px 28px;
  display:flex; flex-direction:column; justify-content:center;
  min-height:172px;
}
/* The watermark quote mark. aria-hidden by being a pseudo-element: it is
   decoration, and a screen reader announcing a stray quotation mark before the
   sentence is noise. */
.qcard::after{
  content:"\201C";
  position:absolute; right:10px; top:-38px;
  font-size:170px; line-height:1;
  color:rgba(123,175,212,.15);
  pointer-events:none;
}
.qcard__q{
  position:relative; z-index:1;
  margin:0; font-size:21px; font-weight:600; line-height:1.36;
  text-wrap:balance;
}
.qcard__by{
  position:relative; z-index:1;
  margin:14px 0 0; font-size:11px; font-weight:700;
  letter-spacing:.12em; text-transform:uppercase;
  color:var(--accent-lt,#7BAFD4);
}
@media (max-width:680px){
  .qcard{min-height:0; padding:22px}
  .qcard__q{font-size:18px}
}

/* ── The consolidated dashboard ──────────────────────────────────────────────

   One hero number with the only chart on the page, the navy card beside it,
   the aging row as the supporting figures, and today's work. Nine sections
   became four.

   ⚠ THE HERO OWNS THE ONLY CHART. If a second chart is ever added to this
   screen, the layout stops meaning anything — the whole point is that one
   metric is visualized and the rest are figures. */
.herorow{
  display:grid; gap:16px; align-items:stretch;
  grid-template-columns:1.35fr .65fr;
  margin-bottom:16px;
}
@container view (max-width:720px){ .herorow{grid-template-columns:1fr} }

.hero{
  background:var(--white,#fff); border:1px solid var(--hair,#D6E1EC);
  border-radius:14px; padding:20px 22px;
  display:flex; flex-direction:column;
}
.hero__lab{
  font-size:11px; font-weight:700; letter-spacing:.09em; text-transform:uppercase;
  color:var(--mute,#8A99AC);
}
.hero__n{
  font-size:clamp(34px,5vw,44px); font-weight:800; line-height:1.02;
  margin-top:6px; font-variant-numeric:tabular-nums; color:var(--ink,#13294B);
}
.hero__sub{margin:7px 0 0; font-size:13px; color:var(--steel,#5A6B80)}
.hero__sub .up{color:var(--green,#2FA37A); font-weight:700}
.hero__sub .down{color:var(--red,#C93B40); font-weight:700}
.hero__sparkwrap{margin-top:auto; padding-top:16px}
.hero__spark{display:block; width:100%; height:48px}
.hero__line{fill:none; stroke:var(--accent-bright,#4B9CD3); stroke-width:2.5;
  stroke-linecap:round; stroke-linejoin:round}
.hero__area{fill:var(--accent-bright,#4B9CD3); opacity:.13}
.hero__dot{fill:var(--accent,#2A6E9E)}

/* ── The aging row — Underglow ───────────────────────────────────────────────

   Greg picked C: the light sits BENEATH each tile, so it reads as lifted onto
   colored light rather than tinted.

   ⚠ THE GLOW CARRIES THE SAME MEANING AS THE NUMBER — blue is fine, amber is
   getting old, red is chase it today. It is a second channel for a fact the
   tile already states, which is what keeps it from being decoration.

   ⚠ AND THE AMBER GLOW IS NOT --amber. That token (#8A6100) was chosen to pass
   contrast as TEXT on white; as a light source it goes muddy brown. The text
   in the tile keeps the accessible token, the glow uses a brighter one. */
.agerow{
  display:grid; grid-template-columns:repeat(3,1fr); gap:16px;
  /* Room for the glow to fall without being clipped by the next block. */
  margin:0 0 26px;
}
@container view (max-width:760px){ .agerow{grid-template-columns:repeat(2,minmax(0,1fr))} }
@container view (max-width:460px){ .agerow{grid-template-columns:1fr; gap:22px} }

.age{
  --h:var(--accent-bright,#4B9CD3);
  position:relative; isolation:isolate; cursor:pointer;
  background:var(--white,#fff); border-radius:14px; padding:18px 20px;
  border:1px solid color-mix(in srgb, var(--h) 34%, var(--hair,#D6E1EC));
  box-shadow:
    0 10px 22px -10px color-mix(in srgb, var(--h) 70%, transparent),
    0 2px 5px -2px  color-mix(in srgb, var(--h) 45%, transparent);
  transition:transform .14s ease, box-shadow .14s ease;
}
/* The light source itself: a blurred ellipse tucked under the card. */
.age::after{
  content:""; position:absolute; z-index:-1;
  left:14%; right:14%; bottom:-12px; height:22px;
  border-radius:50%; background:var(--h); opacity:.34; filter:blur(13px);
  transition:opacity .14s ease;
}
.age:hover{transform:translateY(-2px)}
.age:hover::after{opacity:.46}

.age--ok  {--h:#4B9CD3}
.age--warn{--h:#C08A1E}
.age--bad {--h:#C93B40}

.age__lab{
  font-size:11px; font-weight:700; letter-spacing:.09em; text-transform:uppercase;
  color:var(--mute,#8A99AC); display:block;
}
.age__n{
  display:block; margin-top:6px; font-size:26px; font-weight:800; line-height:1.05;
  font-variant-numeric:tabular-nums;
}
.age--ok   .age__n{color:var(--accent,#2A6E9E)}
.age--warn .age__n{color:var(--amber,#8A6100)}   /* the accessible token, not the glow */
.age--bad  .age__n{color:var(--red,#C93B40)}
.age__sub{display:block; margin-top:5px; font-size:12.5px; color:var(--steel,#5A6B80)}

/* color-mix is not everywhere yet. Without this the border and shadow resolve
   to nothing and the tiles lose their edge entirely — the fallback is a plain
   card, which is worse-looking but never broken. */
@supports not (color: color-mix(in srgb, red 50%, blue)){
  .age{border-color:var(--hair,#D6E1EC); box-shadow:0 8px 18px -10px rgba(19,41,75,.4)}
}
@media (prefers-reduced-motion:reduce){
  .age{transition:none} .age::after{filter:none; opacity:.22}
}

/* ── Today ──────────────────────────────────────────────────────────────── */
.todaylist{display:flex; flex-direction:column; gap:2px; margin-top:6px}
.todayrow{
  display:flex; align-items:baseline; gap:12px;
  padding:9px 10px; border-radius:9px; cursor:pointer; transition:background .12s;
}
.todayrow:hover{background:var(--band,#E6F0F8)}
.todayrow__n{
  font-size:22px; font-weight:800; font-variant-numeric:tabular-nums;
  color:var(--accent,#2A6E9E); min-width:1.5em;
}
.todayrow__n--warn{color:var(--amber,#8A6100)}
.todayrow__m{display:flex; gap:8px; align-items:baseline; flex-wrap:wrap}
.todayrow__m b{font-weight:600; color:var(--ink,#13294B); font-size:14.5px}
.todayrow__s{font-size:12.5px; color:var(--mute,#8A99AC)}

/* ── Where the rest of the page went ────────────────────────────────────── */
.dashmore{
  display:flex; align-items:center; gap:16px; flex-wrap:wrap;
  margin-top:26px; padding-top:16px; border-top:1px solid var(--hair,#D6E1EC);
}
.dashmore__t{
  font-size:11px; font-weight:700; letter-spacing:.09em; text-transform:uppercase;
  color:var(--mute,#8A99AC);
}
.dashmore__l{font-size:14px; font-weight:600; color:var(--accent,#2A6E9E); text-decoration:none}
.dashmore__l:hover{text-decoration:underline}

/* ── Follow-ups: the Heat Board and the Call Queue ───────────────────────────

   Greg, 2026-09-03: "Combine the 1 with 4".

   The board is the overview, the queue is the work, and they share one card.
   `--h` is the item's temperature and EVERYTHING here reads from it — border,
   shadow, glow, kicker, and the tint behind the reason line in the queue. One
   variable per card is what keeps a red item red all the way from the grid
   into the call. */
.hcard{
  --h:var(--accent-bright,#4B9CD3);
  position:relative; isolation:isolate;
  background:var(--white,#fff); border-radius:14px; padding:16px;
  border:1px solid color-mix(in srgb, var(--h) 34%, var(--hair,#D6E1EC));
  box-shadow:
    0 10px 22px -10px color-mix(in srgb, var(--h) 70%, transparent),
    0 2px 5px -2px  color-mix(in srgb, var(--h) 45%, transparent);
  transition:transform .14s ease, box-shadow .14s ease;
}
.hcard::after{
  content:""; position:absolute; z-index:-1;
  left:14%; right:14%; bottom:-12px; height:22px;
  border-radius:50%; background:var(--h); opacity:.34; filter:blur(13px);
  transition:opacity .14s ease;
}
.hcard:hover{transform:translateY(-2px)}
.hcard:hover::after{opacity:.46}

.hcard--cold{--h:#7BAFD4}
.hcard--warm{--h:#C08A1E}   /* lit amber, not --amber: see the dashboard note */
.hcard--hot {--h:#C93B40}
.hcard--live{--h:#2FA37A}
/* ⚠ ONLY ONE THING ON THIS PAGE MOVES. The pulse is reserved for a customer
   who has your quote open right now — the single signal worth interrupting
   yourself for. Give it to a second state and it stops meaning anything. */
.hcard--live::after{animation:hcPulse 2.6s ease-in-out infinite}
@keyframes hcPulse{0%,100%{opacity:.28} 50%{opacity:.55}}

.hcard__k{
  display:block; font-size:10.5px; font-weight:800; letter-spacing:.09em;
  text-transform:uppercase; color:var(--h);
}
.hcard__n{
  display:block; margin-top:5px; font-size:17px; font-weight:700; line-height:1.2;
  color:var(--ink,#13294B); text-decoration:none;
}
a.hcard__n:hover{text-decoration:underline}
.hcard__s{display:block; margin-top:5px; font-size:12.5px; color:var(--steel,#5A6B80)}
.hcard__f{margin-top:12px; display:flex; gap:7px; flex-wrap:wrap}

/* ⚠ A FIXED COLUMN COUNT, NOT auto-fit. With auto-fit the number of columns
   depends on the viewport, so no page size divides evenly at every width — at
   five across, a page of six left one card alone on the second row looking
   like a bug. The counts below are 4 / 3 / 2 / 1 and BOARD_SIZES in tasks.js
   is 12 / 24 / 48, every one of which divides by 4, 3, 2 and 1. Change either
   and you must change the other, or the orphan comes back. */
.heatboard{
  display:grid; grid-template-columns:repeat(4, minmax(0, 1fr));
  gap:22px;                /* room for the glow to fall without being clipped */
  margin:4px 0 30px;
}
@container view (max-width:900px){ .heatboard{grid-template-columns:repeat(3, minmax(0,1fr))} }
@container view (max-width:640px){ .heatboard{grid-template-columns:repeat(2, minmax(0,1fr))} }
@container view (max-width:380px){ .heatboard{grid-template-columns:1fr} }

/* ── Queue mode ─────────────────────────────────────────────────────────── */
.queue{max-width:580px; margin:0 auto}
.q__top{display:flex; align-items:center; gap:12px; margin-bottom:16px}
.q__prog{flex:1; height:6px; border-radius:3px; background:var(--band,#E6F0F8); overflow:hidden}
.q__prog i{
  display:block; height:100%; background:var(--accent,#2A6E9E); border-radius:3px;
  transition:width .3s cubic-bezier(.3,.9,.3,1);
}
.q__count{
  font-size:12px; font-weight:700; letter-spacing:.06em; text-transform:uppercase;
  color:var(--mute,#8A99AC); font-variant-numeric:tabular-nums; white-space:nowrap;
}
.qcard{padding:28px; border-radius:18px; animation:qIn .3s cubic-bezier(.2,.9,.3,1)}
.qcard:hover{transform:none}
@keyframes qIn{from{opacity:0; transform:translateY(14px) scale(.985)} to{opacity:1; transform:none}}
.q__name{
  display:block; font-size:26px; font-weight:800; line-height:1.1; margin-top:7px;
  color:var(--ink,#13294B);
}
.q__why{
  margin:15px 0 0; padding:11px 14px; border-radius:10px; font-size:13.5px; font-weight:600;
  background:color-mix(in srgb, var(--h) 13%, transparent); color:var(--h);
}
.q__ctx{
  margin-top:15px; padding-top:14px; border-top:1px solid var(--hair,#D6E1EC);
  display:grid; gap:6px; font-size:13px; color:var(--steel,#5A6B80);
}
.q__acts{display:flex; gap:9px; margin-top:19px; flex-wrap:wrap}
.q__keys{margin-top:15px; font-size:11.5px; color:var(--mute,#8A99AC); text-align:center}
.q__keys kbd{
  font-family:ui-monospace,SFMono-Regular,Menlo,monospace; font-size:11px;
  background:var(--band,#E6F0F8); border:1px solid var(--hair-strong,#BACCDE);
  border-radius:5px; padding:1px 5px;
}

.qdone{text-align:center; padding:56px 20px}
.qdone__t{display:block; font-size:52px; line-height:1; color:var(--green,#2FA37A)}
.qdone__h{display:block; font-size:25px; font-weight:800; margin-top:14px}
.qdone__s{color:var(--steel,#5A6B80); margin:8px 0 20px}

.chip--live{background:var(--green-bg,#E4F5EE); color:#1E7A5A}
.chip--late{background:var(--red-bg,#FCECEC); color:var(--red,#C93B40)}
.chip--none{background:var(--amber-bg,#FFF6E5); color:var(--amber,#8A6100)}

/* Same guard as the dashboard: without color-mix the borders and shadows
   resolve to nothing and every card loses its edge. A plain card is worse
   looking and never broken. */
@supports not (color: color-mix(in srgb, red 50%, blue)){
  .hcard{border-color:var(--hair,#D6E1EC); box-shadow:0 8px 18px -10px rgba(19,41,75,.4)}
  .q__why{background:var(--band,#E6F0F8); color:var(--steel,#5A6B80)}
}
@media (prefers-reduced-motion:reduce){
  .hcard,.hcard::after,.q__prog i,.qcard{transition:none!important; animation:none!important}
  .hcard::after{filter:none}
}
/* Sizing only — the app already owns what a button looks like. Inventing a
   .btn system here would leave two button vocabularies in one codebase. */
.qbtn{font-size:12.5px; padding:6px 12px}

@media (max-width:600px){
  .heatboard{grid-template-columns:1fr}
  .q__acts > *{flex:1 1 auto; text-align:center}
}


/* ── The small aura, product-wide ────────────────────────────────────────────

   Greg, 2026-09-03: "a small aura".

   On the old blue field, panels were separated from the ground by TINT. White
   took that away, so each panel needs its own quiet shadow to sit on or the
   page flattens into one sheet of paper with lines drawn on it.

   ⚠ THIS IS A SHADOW, NOT A GLOW. Navy at 7% under every card reads as depth;
   the coloured glows stay where they mean something — the aging tiles, the
   follow-up temperatures, the segment tiles. A product-wide colour glow would
   spend the signal those three screens rely on.

   ⚠ AND IT IS DELIBERATELY BELOW THE CARD, NOT AROUND IT. A symmetrical glow
   reads as a selected state; an offset one reads as a surface lifted a
   millimetre off the page, which is what a white ground is missing. */
.card, .dashcard, .panel, .solid{ box-shadow:var(--aura); }
/* Follow-ups sit quieter than the cards around them; the late row alone is
   allowed to glow (see .taskrow.is-late). */
.tasksec{ box-shadow:var(--aura-soft); }

/* The band fill loses its job on white — it used to read as "slightly darker
   than the page" and now needs to be slightly darker than WHITE, which is what
   #F2F7FC above does. Hover states inherit it, so nothing else changes. */

/* ── Wave 1: the money screens ───────────────────────────────────────────────

   Invoices and Estimates now open the way the dashboard does — one figure that
   matters, then supporting figures that are figures rather than another row of
   identical boxes.

   ⚠ .hero AND .stat ARE SHARED. The dashboard defined .hero first; these two
   screens use the same class rather than a near-copy, so a change to the hero
   moves all three together. .stat is the aging tile's shape with a neutral
   name, because "aging" means nothing on an estimates page. */
.hero__n--sm{font-size:clamp(26px,3.4vw,32px)}

.statrow{
  display:grid; grid-template-columns:repeat(3,minmax(0,1fr));
  gap:18px; margin:18px 0 26px;
}
@container view (max-width:760px){ .statrow{grid-template-columns:repeat(2,minmax(0,1fr))} }
@container view (max-width:460px){ .statrow{grid-template-columns:1fr; gap:22px} }

.stat{
  --h:var(--accent-bright);
  position:relative; isolation:isolate; cursor:pointer; border-radius:var(--r);
  padding:15px 17px;
  background:linear-gradient(180deg,
    color-mix(in srgb, var(--h) 5%, #fff),
    color-mix(in srgb, var(--h) 2%, #fff));
  border:1px solid color-mix(in srgb, var(--h) 40%, var(--hair));
  box-shadow:var(--aura);
  transition:transform .14s ease, border-color .14s ease;
}
.stat::after{
  content:""; position:absolute; z-index:-1;
  left:16%; right:16%; bottom:-8px; height:16px; border-radius:50%;
  background:var(--h); opacity:.12; filter:blur(11px);
}
.stat:hover{transform:translateY(-2px)}
.stat--bad{--h:#C93B40} .stat--warn{--h:#C08A1E}
.stat--ok{--h:#2FA37A}  .stat--info{--h:#8A99AC}
.stat__n{
  display:block; margin-top:5px; font-size:23px; font-weight:800; line-height:1.05;
  font-variant-numeric:tabular-nums;
}
.stat--bad .stat__n{color:var(--red)}
.stat--warn .stat__n{color:var(--amber)}   /* the accessible token, not the glow */
.stat--ok .stat__n{color:#1E7A5A}
.stat--info .stat__n{color:var(--ink2)}
.stat__s{display:block; margin-top:3px; font-size:12.5px; color:var(--steel)}
@supports not (color: color-mix(in srgb, red 50%, blue)){
  .stat{background:var(--white); border-color:var(--hair)}
}

/* Estimates' win rate. A bar, not a sparkline — there is no history to draw,
   and this is a proportion rather than a trend. */
.winbar{
  height:7px; border-radius:4px; background:var(--band); overflow:hidden;
  margin-top:11px;
}
.winbar i{display:block; height:100%; background:var(--accent-bright); border-radius:4px}

/* ── Reports: six charts, one family ────────────────────────────────────────
   The dashboard shed its charts onto this page, so the job here is that they
   stop looking like six separate ideas. Equal cards, one chart each. */
.repgrid{display:grid; grid-template-columns:1fr 1fr; gap:18px; margin-top:14px}
/* The two wide charts keep the full width — a six-month bar chart in half a
   screen stops being readable, and readable is the whole job of this page. */
.repgrid > .card:nth-child(1),
.repgrid > .card:nth-child(2){grid-column:1 / -1}
/* ⚠ AND ANY WRAPPER THAT IS ITSELF A GRID. Four of the six charts are not
   direct children — they live inside a .cols2, which is a two-column grid of
   its own. As an ordinary grid item it was squeezed into ONE 581px column and
   drew its two columns inside that, which is why aging sat at half width with
   the right-hand side blank. Spanning it lets it lay its own pair out across
   the full width, which is the layout that was approved. */
.repgrid > .cols2{grid-column:1 / -1}
@container view (max-width:720px){ .repgrid{grid-template-columns:1fr} }

/* ── Transactions: a ledger, and treated as one ─────────────────────────────
   No hero. You come here to find one payment, not to learn a total — that
   lives on Reports. The tab row picks up the segment-control look so the
   page belongs to the same product. */
.subtabs .tab{
  border-radius:999px; border:1px solid var(--hair); background:var(--white);
  color:var(--ink2); font-size:12.5px; font-weight:600; padding:6px 13px;
}
.subtabs .tab.is-on{background:var(--navy); border-color:var(--navy); color:#fff}


/* Greg, 2026-09-03: "add a little more aura to the transaction tab". The
   ledger is one big flat panel with no tiles to carry the glow, so it was the
   thinnest-looking page in the set. It gets its own step up rather than a
   product-wide change — everywhere else is already where it should be. */
/* ⚠ EVERY REPORTS TAB, NOT JUST THE LEDGER. Greg, 2026-09-03: "add a little
   more aura to the other tabs like transactions". Overview, Transactions,
   Schedules, Vault, Sales and Expiring cards are one screen with six tabs, and
   only one of them having a heavier shadow read as a bug rather than a choice.

   Selected by the sub-tab bar rather than by a class on each view: those six
   render from four different files, and a class somebody has to remember to add
   is a class the seventh tab will not have. `.subtabs` is what makes a page a
   Reports page, so it is what the rule asks about. */
.view:has(> .subtabs) .card,
.view:has(> .subtabs) .panel,
.ledger .card,
.ledger .panel,
.ledger .tablewrap{
  box-shadow:
    0 1px 2px rgba(19,41,75,.07),
    0 16px 38px -14px rgba(19,41,75,.52),
    0 3px 12px -5px rgba(42,110,158,.26);
}

/* ── Reports · the headline strip (option B) ─────────────────────────────────

   Four figures that answer the page's common question in two seconds, so the
   six charts below become the explanation rather than the first thing you meet.

   ⚠ SMALLER THAN A .stat, ON PURPOSE. These are context for the charts, not
   the subject of the page — at tile size they would compete with the very
   panels they are supposed to introduce. No hue wash and no coloured glow
   either; only the overdue one takes a colour, because it is the only one of
   the four that can be bad news. */
.repstrip{
  display:grid; grid-template-columns:repeat(4,minmax(0,1fr));
  gap:14px; margin-bottom:4px;
}
@container view (max-width:760px){ .repstrip{grid-template-columns:repeat(2,minmax(0,1fr))} }
@container view (max-width:380px){ .repstrip{grid-template-columns:1fr} }

.kpi{
  border:1px solid var(--hair); border-radius:12px; padding:12px 14px;
  background:var(--white); box-shadow:var(--aura);
}
.kpi__l{
  display:block; font-size:10px; font-weight:700; letter-spacing:.09em;
  text-transform:uppercase; color:var(--mute);
}
.kpi__v{
  display:block; margin-top:3px; font-size:21px; font-weight:800; line-height:1.1;
  font-variant-numeric:tabular-nums; color:var(--ink);
}
.kpi__s{display:block; margin-top:2px; font-size:11.5px; color:var(--steel)}
.kpi--bad .kpi__v{color:var(--red)}
.kpi--bad{border-color:color-mix(in srgb, var(--red) 34%, var(--hair))}
.kpi--ok .kpi__v{color:#1E7A5A}
@supports not (color: color-mix(in srgb, red 50%, blue)){
  .kpi--bad{border-color:var(--hair)}
}

/* ── Accounts receivable aging, as one divided bar ───────────────────────────
   Five rows of figures give the numbers; one bar gives the shape. */
.agebar{
  display:flex; height:12px; border-radius:6px; overflow:hidden;
  background:var(--band); margin-top:14px;
}
.agebar i{display:block; min-width:2px}
.agebar i:first-child{border-radius:6px 0 0 6px}
.agebar i:last-child{border-radius:0 6px 6px 0}

.agekey{
  display:flex; flex-wrap:wrap; gap:6px 18px; margin-top:12px;
  font-size:12.5px; color:var(--steel);
}
.agekey__i{display:flex; align-items:center; gap:7px}
/* A band with no money keeps its place in the key but stops competing for
   attention — dropping it entirely would make the ramp change shape every
   time a bucket emptied, and the colours would stop meaning one thing. */
.agekey__i.is-zero{opacity:.42}
.agekey__sw{width:10px; height:10px; border-radius:3px; flex:none}
.agekey__v{font-variant-numeric:tabular-nums; color:var(--ink)}

.agetot{
  display:flex; justify-content:space-between; align-items:baseline;
  margin-top:14px; padding-top:12px; border-top:1px solid var(--hair);
  font-size:13.5px; color:var(--steel);
}
.agetot b{font-size:16px; font-variant-numeric:tabular-nums; color:var(--ink)}

/* One more step of aura on the reports tabs, as asked. */
.view:has(> .subtabs) .card,
.view:has(> .subtabs) .panel{
  box-shadow:
    0 1px 2px rgba(19,41,75,.08),
    0 22px 48px -16px rgba(19,41,75,.58),
    0 4px 14px -6px rgba(42,110,158,.30);
}

/* The window picker sitting inside a hero panel rather than as a second row of
   sub-tabs. Same pills, but clearly a control on this page instead of
   navigation away from it. */
.hero--pick{justify-content:flex-start}
.pickrow{display:flex; gap:6px; flex-wrap:wrap; margin-top:10px}

/* Data-dense panels: two roomy columns rather than three tight ones.
   .cols2 asks for 340px and therefore lays three columns at portal width —
   right for a two-column list, wrong for a table of four figures under a
   sentence-length label. */
.cols2--wide{grid-template-columns:repeat(auto-fit, minmax(430px, 1fr))}
/* A wide table inside any card scrolls itself rather than clipping. Nothing
   should ever lose its last column silently. */
.card table{max-width:100%}
.card .tablewrap{overflow-x:auto}

/* ── Transactions · banded ledger (option A) ─────────────────────────────────

   Greg, 2026-09-03: more texture and feel, keeping the scheme.

   The texture is the rows themselves — a stripe for the outcome and a faint
   band for the rhythm — rather than anything added around them. This page
   exists to find one payment among a hundred, so nothing here costs width or
   rows. */

/* The stripe. ⚠ INSET, NOT A BORDER: a real left border would shift every
   cell by 3px and misalign the first column against the header. */
.tx--paid    td:first-child{box-shadow:inset 3px 0 0 var(--green,#2FA37A)}
.tx--overdue td:first-child{box-shadow:inset 3px 0 0 var(--red,#C93B40)}
.tx--partial td:first-child{box-shadow:inset 3px 0 0 var(--amber-lit,#C08A1E)}
.tx--draft   td:first-child{box-shadow:inset 3px 0 0 var(--hair-strong)}

/* The band. Carolina at 3.5% — enough for the eye to track a long row across
   eight columns, far too little to read as a highlight or fight the stripe. */
.txtable tbody tr:nth-child(odd) td{background:rgba(42,110,158,.035)}
.txtable tbody tr:hover td{background:var(--band)}
.txtable tbody tr.is-open td{background:var(--band)}
/* The amount is what the eye comes for on a ledger row. */
.txtable td.num{font-weight:600}

/* The summary tiles that replaced the grey sentence. */
.txsums{
  display:grid; grid-template-columns:repeat(4,minmax(0,1fr));
  gap:16px; margin:4px 0 18px;
}
@container view (max-width:760px){ .txsums{grid-template-columns:repeat(2,minmax(0,1fr))} }
@container view (max-width:380px){ .txsums{grid-template-columns:1fr} }
.txsum{
  --h:var(--accent-bright); position:relative; isolation:isolate;
  border-radius:12px; padding:13px 15px;
  background:linear-gradient(180deg,
    color-mix(in srgb, var(--h) 5%, #fff), color-mix(in srgb, var(--h) 2%, #fff));
  border:1px solid color-mix(in srgb, var(--h) 40%, var(--hair));
  box-shadow:var(--aura);
}
.txsum::after{
  content:""; position:absolute; z-index:-1; left:16%; right:16%; bottom:-8px;
  height:16px; border-radius:50%; background:var(--h); opacity:.12; filter:blur(11px);
}
.txsum--ok{--h:#2FA37A} .txsum--bad{--h:#C93B40}
.txsum--warn{--h:#C08A1E} .txsum--info{--h:#8A99AC}
.txsum__l{display:block; font-size:10px; font-weight:700; letter-spacing:.09em;
  text-transform:uppercase; color:var(--mute)}
.txsum__v{display:block; margin-top:3px; font-size:21px; font-weight:800; line-height:1.1;
  font-variant-numeric:tabular-nums; color:var(--ink)}
.txsum--ok .txsum__v{color:#1E7A5A}
.txsum--bad .txsum__v{color:var(--red)}
.txsum--warn .txsum__v{color:var(--amber)}
.txsum__s{display:block; margin-top:2px; font-size:11.5px; color:var(--steel)}
@supports not (color: color-mix(in srgb, red 50%, blue)){
  .txsum{background:var(--white); border-color:var(--hair)}
}

/* The search group now lives inside the filter bar instead of below it. */
.txbar .txsearch{display:flex; gap:8px; align-items:center; flex:1; min-width:240px}
.txbar .txsearch input[type="search"]{flex:1; min-width:130px}

/* ── Wave 2 · work screens ───────────────────────────────────────────────────

   The money screens lead with a figure; these lead with what is NOT happening
   — finished work nobody billed, a stop with no date. So the hero can be bad
   news, and says so. */
.hero__n--bad{color:var(--red)}
.hero{cursor:default}
.hero[role],.herorow .hero:hover{cursor:pointer}

/* Project cards take their status as a tint and the shared aura, so the board
   reads at a glance the way the follow-ups heat board does. */
.ptile{
  --h:var(--accent-bright);
  position:relative; isolation:isolate;
  border:1px solid color-mix(in srgb, var(--h) 30%, var(--hair));
  box-shadow:var(--aura);
}
.ptile::after{
  content:""; position:absolute; z-index:-1; left:16%; right:16%; bottom:-8px;
  height:16px; border-radius:50%; background:var(--h); opacity:.12; filter:blur(11px);
}
.ptile.is-planning{--h:#8A99AC}
.ptile.is-active  {--h:#4B9CD3}
.ptile.is-on_hold {--h:#C08A1E}
.ptile.is-complete{--h:#2FA37A}
.ptile.is-cancelled{--h:#BACCDE}
.ptile .bar i{background:var(--h)}
/* ⚠ .ptile__figs IS FLEX, NOT GRID — a grid-template-columns here would have
   been silently inert. Three figures in a card this narrow need a tighter gap
   and permission to wrap; two keep the roomier original. */
.ptile__figs--three{gap:12px; flex-wrap:wrap}
.ptile__figs--three .ptile__fig{min-width:72px}
@supports not (color: color-mix(in srgb, red 50%, blue)){
  .ptile{border-color:var(--hair)}
}

/* ── Calendar · the wave 2 pass ──────────────────────────────────────────────
   ⚠ MOST OF WHAT THE DESIGN PROPOSED WAS ALREADY THERE. Chips already carry a
   crew-coloured left bar (chip() sets border-left-color from the crew), and the
   unmapped banner already separates addresses that can be looked up from stops
   with no address at all — only the first kind gets a button. Re-implementing
   either would have replaced better thinking with mine.

   What was missing is depth: the grid sat flat on the new white ground with
   nothing lifting it. */
.calgrid, .calcanvas, .calsplit > *{
  border-radius:var(--r);
  box-shadow:var(--aura);
}
.calcell:hover{background:var(--band)}
/* The unscheduled tray reads as a place to drag FROM rather than a list that
   happens to be last. */
.caltray{
  border:1.5px dashed var(--hair-strong); border-radius:12px; padding:12px;
  background:transparent; box-shadow:none;
}
.calmap__notice{
  border-radius:12px; border:1px solid color-mix(in srgb, var(--amber) 34%, var(--hair));
  box-shadow:var(--aura);
}
@supports not (color: color-mix(in srgb, red 50%, blue)){
  .calmap__notice{border-color:var(--hair)}
}

/* The calendar's view strip, as two groups rather than seven equal pills.
   The gap between the groups is what does the work — it says these answer
   different questions — so it is wider than the gap inside either one. */
.calviews{display:flex; align-items:center; gap:18px; flex-wrap:wrap}
.calviews__grp{
  display:flex; gap:4px; align-items:center;
  padding:3px; border-radius:999px;
  background:var(--band); border:1px solid var(--hair);
}
.calviews__grp .tab{border:0; background:transparent; padding:5px 12px}
.calviews__grp .tab.is-on{
  background:var(--white); color:var(--ink);
  box-shadow:0 1px 2px rgba(19,41,75,.10), 0 2px 6px -2px rgba(19,41,75,.22);
}
@media (max-width:820px){ .calviews{gap:10px} }

/* ── Properties · wave 3 ─────────────────────────────────────────────────────
   Option 1's header with option 2's list, and a state that means the same
   thing in both. toneOf() decides it once; these two rules render it. */
.hero__n--warn{color:var(--amber)}

/* Grid: the card is tinted. */
.gcard.pcard{--h:var(--accent-bright); position:relative; isolation:isolate}
.gcard.pcard--alert{--h:#C93B40} .gcard.pcard--warr{--h:#C08A1E}
.gcard.pcard--none {--h:#8A99AC}
.gcard.pcard--alert,.gcard.pcard--warr,.gcard.pcard--none{
  border-color:color-mix(in srgb, var(--h) 34%, var(--hair));
  box-shadow:var(--aura);
}
.gcard.pcard--alert::after,.gcard.pcard--warr::after,.gcard.pcard--none::after{
  content:""; position:absolute; z-index:-1; left:16%; right:16%; bottom:-8px;
  height:16px; border-radius:50%; background:var(--h); opacity:.12; filter:blur(11px);
}

/* List: the same state as a stripe. ⚠ inset, not a border — a real border
   would shift the first cell and misalign it against the header. */
.prow--alert td:first-child{box-shadow:inset 3px 0 0 #C93B40}
.prow--warr  td:first-child{box-shadow:inset 3px 0 0 #C08A1E}
.prow--none  td:first-child{box-shadow:inset 3px 0 0 var(--hair-strong)}

@supports not (color: color-mix(in srgb, red 50%, blue)){
  .gcard.pcard--alert,.gcard.pcard--warr,.gcard.pcard--none{border-color:var(--hair)}
}

/* ── Products · wave 3 ───────────────────────────────────────────────────────
   Same contract as Properties: one toneOf() upstream, two renderings here.
   The hue scale is margin health — green clears, amber is thin, red is sold
   below cost, grey is unknowable because no cost was ever recorded. Grey is
   NOT a mild version of amber; it means the page cannot say. */
.hero__n--none{color:var(--mute)}

/* The figure itself, wherever it appears — a card fact, a table cell. */
.mchip{
  display:inline-block; padding:2px 8px; border-radius:999px;
  font-size:11.5px; font-weight:650; letter-spacing:.01em;
  font-variant-numeric:tabular-nums; line-height:1.55;
  border:1px solid transparent;
}
.mchip--good{color:#1F6B50; background:rgba(47,163,122,.13); border-color:rgba(47,163,122,.30)}
.mchip--thin{color:#8A6100; background:rgba(192,138,30,.15); border-color:rgba(192,138,30,.34)}
.mchip--loss{color:#9E2A2E; background:rgba(201,59,64,.13); border-color:rgba(201,59,64,.32)}
.mchip--none{color:var(--steel); background:rgba(19,41,75,.05); border-color:var(--hair);
             font-weight:600; letter-spacing:.03em}

/* Grid: the card is tinted, and glows the colour it is tinted. */
.gcard.mcard{--h:var(--accent-bright); position:relative; isolation:isolate}
.gcard.mcard--loss{--h:#C93B40} .gcard.mcard--thin{--h:#C08A1E}
.gcard.mcard--nocost{--h:#8A99AC}
.gcard.mcard--loss,.gcard.mcard--thin,.gcard.mcard--nocost{
  border-color:color-mix(in srgb, var(--h) 34%, var(--hair));
  box-shadow:var(--aura);
}
.gcard.mcard--loss::after,.gcard.mcard--thin::after,.gcard.mcard--nocost::after{
  content:""; position:absolute; z-index:-1; left:16%; right:16%; bottom:-8px;
  height:16px; border-radius:50%; background:var(--h); opacity:.12; filter:blur(11px);
}

/* List: the same state as a stripe. ⚠ inset, not a border — a real border
   widens the first cell and knocks every row out of line with the header. */
.mrow--loss   td:first-child{box-shadow:inset 3px 0 0 #C93B40}
.mrow--thin   td:first-child{box-shadow:inset 3px 0 0 #C08A1E}
.mrow--nocost td:first-child{box-shadow:inset 3px 0 0 var(--hair-strong)}

@supports not (color: color-mix(in srgb, red 50%, blue)){
  .gcard.mcard--loss,.gcard.mcard--thin,.gcard.mcard--nocost{border-color:var(--hair)}
}

/* ── Product report · the earnings ladder ────────────────────────────────────
   Greg picked option 1, 2026-09-04. Bar LENGTH is revenue, so the ranking is
   visible before a single figure is read; the fill inside it splits kept from
   spent. Everything is prefixed pr- because `.bar`, `.key`, `.tag` and `.dead`
   are all already taken elsewhere in this file and a collision here would
   silently restyle the project tiles. */
.hint--warn{
  border-left:3px solid var(--amber);
  background:rgba(192,138,30,.06);
  padding:11px 14px; border-radius:8px;
}

.prladder{
  border:1px solid var(--hair); border-radius:14px; background:var(--paper);
  box-shadow:var(--aura); padding:6px 18px; margin-bottom:6px;
}
.prrung{
  display:grid; grid-template-columns:minmax(160px,220px) 1fr minmax(118px,148px);
  gap:16px; align-items:center; padding:11px 0;
  border-bottom:1px solid var(--hair); cursor:pointer; border-radius:8px;
}
.prrung:last-child{border-bottom:0}
.prrung:hover{background:rgba(19,41,75,.028)}
.prrung:focus-visible{outline:2px solid var(--accent); outline-offset:2px}
.prrung--thin{background:rgba(192,138,30,.05); padding-left:10px; padding-right:10px}
.prrung--thin:hover{background:rgba(192,138,30,.09)}

.prrung__n{font-size:14px; font-weight:600; line-height:1.3; color:var(--ink)}
.prrung__s{font-size:11.5px; color:var(--mute); margin-top:2px}
.prrung__flag{color:var(--amber); font-weight:600}

/* ⚠ THE WRAPPER IS THE TRACK, THE BAR IS THE VALUE. The width percentage goes
   on .prbar and is read against .prbarwrap, so every bar on the page is
   measured against the same full width — put the width on a grid child
   directly and the column, not the value, decides how long it looks. */
.prbarwrap{min-width:0}
.prbar{
  height:22px; border-radius:6px; display:flex; overflow:hidden;
  background:rgba(19,41,75,.05); min-width:14px;
}
.prbar__prof{display:block; height:100%; background:linear-gradient(90deg, var(--accent), var(--accent-bright))}
.prbar__cost{display:block; height:100%;
  background:repeating-linear-gradient(135deg, rgba(19,41,75,.17) 0 4px, rgba(19,41,75,.07) 4px 8px)}
.prbar--unk{
  background:repeating-linear-gradient(135deg, rgba(138,153,172,.34) 0 4px, rgba(138,153,172,.13) 4px 8px);
}

.prrung__f{text-align:right; font-variant-numeric:tabular-nums}
.prrung__f b{display:block; font-size:14.5px; font-weight:700; color:var(--ink)}
.prrung__f span{font-size:11.5px; color:var(--steel)}

.prkey{display:flex; gap:20px; flex-wrap:wrap; font-size:12px; color:var(--steel);
       margin:12px 2px 20px}
.prkey i{display:inline-block; width:11px; height:11px; border-radius:3px;
         margin-right:7px; vertical-align:-1px}
.prkey__prof{background:linear-gradient(90deg, var(--accent), var(--accent-bright))}
.prkey__cost{background:repeating-linear-gradient(135deg, rgba(19,41,75,.17) 0 3px, rgba(19,41,75,.07) 3px 6px)}
.prkey__unk{background:repeating-linear-gradient(135deg, rgba(138,153,172,.34) 0 3px, rgba(138,153,172,.13) 3px 6px)}

.prdead{
  border:1px dashed var(--hair-strong); border-radius:13px;
  padding:16px 18px; background:rgba(19,41,75,.02); margin-bottom:16px;
}
.prdead h3{margin:0 0 3px; font-size:14.5px; font-weight:700; color:var(--ink)}
.prdead p{margin:0 0 12px; font-size:12.5px; color:var(--steel); max-width:72ch}
.prtags{display:flex; gap:9px; flex-wrap:wrap}
.prtag{
  font:inherit; font-size:13px; padding:7px 12px; border-radius:9px; cursor:pointer;
  border:1px solid var(--hair); background:var(--paper); color:var(--steel);
}
.prtag b{color:var(--ink); font-weight:600}
.prtag:hover{border-color:var(--accent); box-shadow:var(--aura)}
.prtag__ever{color:var(--mute)}

.prfoot{font-size:12px; color:var(--mute); max-width:80ch; margin:4px 2px 0; line-height:1.6}

@container view (max-width:700px){
  .prrung{grid-template-columns:1fr; gap:7px}
  .prrung__f{text-align:left; display:flex; align-items:baseline; gap:10px}
  .prrung__f b{display:inline}
}

/* ── Vault · wave 3 ──────────────────────────────────────────────────────────
   Greg, 2026-09-04: option 1 with option 3 inside it, the way Follow-ups
   works. The board is banded by consequence; the round reuses .queue /
   .q__top / .q__prog / .q__acts / .q__keys / .qdone from Follow-ups rather
   than growing a second set of nearly-identical classes.
   --h is set per band by the view; nothing here hard-codes a hue twice. */

/* The outbox, collapsed. Eighteen drafts printed in full ran to 4,800px. */
.vaultmsg{
  display:grid; grid-template-columns:26px 1fr auto auto; gap:10px;
  align-items:start; padding:9px 4px; border-top:1px solid var(--hair);
}
.vaultmsg:first-child{border-top:0}
.vaultmsg__pick{display:flex; align-items:center; justify-content:center; padding-top:2px}
.vaultmsg__p{font-variant-numeric:tabular-nums; font-weight:650; white-space:nowrap;
             align-self:center}
.vaultmsg__read{align-self:center; font-size:12.5px; white-space:nowrap}
.vaultmsg__body{margin-top:7px; font-size:13px; color:var(--steel); line-height:1.6;
                white-space:pre-wrap; border-left:2px solid var(--hair); padding-left:12px}
.vaultmsg__body p{margin:0}
.vaultmsg__subj{font-weight:650; color:var(--ink); margin-bottom:5px}

/* The bands. Safety gets the aura; the quiet tiers stay quiet, which is the
   whole point of ordering them. */
.vband{
  border:1px solid var(--hair); border-radius:13px; margin-bottom:12px;
  overflow:hidden; background:var(--paper);
}
.vband--bad{
  border-color:color-mix(in srgb, var(--h) 42%, var(--hair));
  box-shadow:var(--aura);
}
.vband--warn{border-color:color-mix(in srgb, var(--h) 30%, var(--hair))}
.vband__h{
  display:flex; align-items:center; gap:11px; flex-wrap:wrap;
  padding:11px 16px; background:color-mix(in srgb, var(--h) 6%, transparent);
}
.vband__dot{width:9px; height:9px; border-radius:50%; background:var(--h); flex:none}
.vband__n{font-size:14px; font-weight:700; color:var(--ink)}
.vband__c{font-size:12.5px; color:var(--steel)}
.vband__v{margin-left:auto; font-variant-numeric:tabular-nums; font-weight:700;
          color:var(--ink)}
.vband__more{
  width:100%; border:0; border-top:1px solid var(--hair); background:transparent;
  padding:10px; font:inherit; font-size:12.5px; color:var(--accent); cursor:pointer;
}
.vband__more:hover{background:rgba(19,41,75,.03)}

.vrow{
  display:grid; grid-template-columns:1fr 190px 104px 132px; gap:14px;
  align-items:center; padding:10px 16px; border-top:1px solid var(--hair);
  cursor:pointer;
}
.vrow:hover{background:rgba(19,41,75,.028)}
.vrow:focus-visible{outline:2px solid var(--accent); outline-offset:-2px}
.vrow__m b{font-weight:600}
.vrow__w{font-size:13px; color:var(--steel); min-width:0}
.vrow__a{font-size:12.5px; color:var(--steel); font-variant-numeric:tabular-nums}
.vrow__a.is-old > span{color:var(--red, #C93B40); font-weight:650}
.vrow__p{text-align:right; font-variant-numeric:tabular-nums}
.vrow__p b{font-size:14.5px}

/* The round. Wider than the Follow-ups queue because a drafted email has to
   fit on it — a 580px column turns four sentences into twelve lines. */
.queue--wide{max-width:720px}
.vround{
  border:1px solid color-mix(in srgb, var(--h) 34%, var(--hair));
  border-radius:18px; padding:26px; background:var(--paper);
  box-shadow:var(--aura); position:relative; overflow:hidden;
  animation:qIn .3s cubic-bezier(.2,.9,.3,1);
}
.vround::before{
  content:""; position:absolute; inset:0 auto 0 0; width:4px; background:var(--h);
}
.vround__k{display:flex; gap:8px; flex-wrap:wrap; margin-bottom:11px}
.vround__t{display:block; font-size:20px; font-weight:750; letter-spacing:-.01em;
           line-height:1.3; color:var(--ink)}
.vround__w{margin:5px 0 17px; font-size:13px; color:var(--steel)}
.vround__g{display:flex; gap:26px; flex-wrap:wrap; margin-bottom:16px}
.vround__g span{display:block; font-size:9.5px; letter-spacing:.1em;
                text-transform:uppercase; color:var(--mute); margin-bottom:3px}
.vround__g b{font-size:17px; font-variant-numeric:tabular-nums; color:var(--ink)}
.vround__why{font-size:12.5px; margin-bottom:14px}
.vround__draft{
  border:1px solid var(--hair); border-radius:11px; padding:14px 16px;
  background:rgba(19,41,75,.025); font-size:13.5px; color:var(--steel);
  line-height:1.65; white-space:pre-wrap; margin-bottom:4px;
}
.vround__draft p{margin:0 0 9px}
.vround__subj{font-weight:650; color:var(--ink); margin-bottom:7px; white-space:normal}

@container view (max-width:760px){
  .vrow{grid-template-columns:1fr auto; gap:6px}
  .vrow__w,.vrow__a{grid-column:1}
  .vrow__p{grid-row:1; grid-column:2; align-self:start}
  .vaultmsg{grid-template-columns:26px 1fr auto}
  .vaultmsg__read{grid-column:2}
}

/* ── The subject's face · wave 3 ─────────────────────────────────────────────
   Greg, 2026-09-04: option 1 — the picture at the top of the navy panel. A
   vehicle gets the model's photograph from Wikipedia; an address gets the
   front of the house from Street View. ONE set of classes, because they are
   the same box doing the same job and two would drift apart.

   TWO LAYERS. The silhouette is always drawn; the photograph lands on top of
   it when one resolves. So this box is never empty, never a grey rectangle
   waiting on a network call, and a 404 on a cached URL simply reveals the
   shape again. */
.subjface{
  position:relative; border-radius:11px; overflow:hidden; margin:0 0 15px;
  background:linear-gradient(160deg, rgba(255,255,255,.10), rgba(255,255,255,.03));
  aspect-ratio:16/9; display:flex; align-items:center; justify-content:center;
}
.subjface__sil{
  width:76%; color:rgba(200,220,242,.42);
  display:flex; align-items:center; justify-content:center;
}
/* A house is taller than a car and reads as tiny at the same width. */
.subjface__sil:has(svg[aria-label="property"]){width:44%}
.subjface__sil svg{width:100%; height:auto; display:block}
.subjface__img{
  position:absolute; inset:0; width:100%; height:100%; object-fit:cover; display:block;
}
/* ⚠ The caption sits OVER the photograph, not under it. Under it, the credit
   line pushed the car's name down the panel on every vehicle that had a
   picture and not on the ones that did not — two different panel layouts for
   what is meant to be one screen. */
.subjface__cap{
  position:absolute; left:0; right:0; bottom:0; z-index:2;
  display:flex; justify-content:space-between; gap:8px; align-items:baseline;
  padding:14px 10px 6px;
  font-size:9.5px; line-height:1.35; color:rgba(233,240,248,.82);
  background:linear-gradient(to top, rgba(6,15,28,.86), rgba(6,15,28,0));
}
.subjface__cap a{color:rgba(233,240,248,.82); text-decoration:underline;
                text-underline-offset:2px}
.subjface__cap a:hover{color:#fff}
.subjface__cap span:first-child{white-space:nowrap; overflow:hidden; text-overflow:ellipsis}
.subjface__cap > :last-child{text-align:right; flex:none; max-width:58%;
  overflow:hidden; text-overflow:ellipsis; white-space:nowrap}

/* ── Files · wave 3 ──────────────────────────────────────────────────────────
   Greg, 2026-09-04: "go on with 1 with 2s warnings". Every scope on one page,
   with the broken files saying so on themselves rather than in a section of
   their own. */
.fgrid{display:grid; grid-template-columns:repeat(auto-fill, minmax(212px, 1fr)); gap:14px}
.fcard{
  border:1px solid var(--hair); border-radius:12px; overflow:hidden;
  background:var(--paper); cursor:pointer; transition:transform .12s, box-shadow .12s;
}
.fcard:hover{transform:translateY(-2px); box-shadow:var(--shadow-lift, var(--aura))}
.fcard:focus-visible{outline:2px solid var(--accent); outline-offset:2px}
/* ⚠ The tint means "this file is broken", not "this file is selected".
   Anything else red on this page would dilute it. */
.fcard--bad{
  border-color:color-mix(in srgb, #C93B40 40%, var(--hair));
  box-shadow:var(--aura);
}
.fcard__pic{
  aspect-ratio:4/3; display:flex; align-items:center; justify-content:center;
  background:linear-gradient(160deg, rgba(19,41,75,.055), rgba(19,41,75,.02));
  color:var(--mute); overflow:hidden;
}
.fcard__pic svg{width:46px; height:auto; display:block}
.fcard__pic img{width:100%; height:100%; object-fit:cover; display:block}
.fcard__pic--img{background:linear-gradient(140deg, #A9C2D8, #7794B0)}
.fcard__b{padding:11px 13px}
.fcard__n{font-size:12.5px; font-weight:600; line-height:1.35; color:var(--ink);
          word-break:break-word}
.fcard__m{display:flex; gap:5px; flex-wrap:wrap; margin-top:6px}
.fcard__w{display:flex; justify-content:space-between; gap:8px; align-items:baseline;
          margin-top:7px; font-size:11px; color:var(--steel)}
.fcard__w a{color:var(--accent)}
.fcard__d{color:var(--mute); white-space:nowrap; font-variant-numeric:tabular-nums}

/* ⚠ inset, not a border — a real border widens the first cell and knocks
   every row out of line with the header. Same rule as Properties and Products. */
.frow--bad td:first-child{box-shadow:inset 3px 0 0 #C93B40}

@container view (max-width:560px){
  .fgrid{grid-template-columns:1fr 1fr}
}

/* ── The terminal · The Counter ──────────────────────────────────────────────
   Greg picked design 3 of five with design 1's surcharge ledger, 2026-09-04,
   after a research pass that changed what this screen has to say.

   SPLIT DOWN THE MIDDLE, AND THE SPLIT IS THE POINT. The left panel is
   customer-facing — you turn the screen around — and carries the amount, who
   it is for, and the price or prices. The right is only ever touched by the
   person taking the payment.

   ⚠ IT IS THE ONLY ARRANGEMENT THAT SOLVES THE DUAL-PRICING RULE
   STRUCTURALLY. Both prices must be shown at equal prominence before the
   customer chooses; that equality IS the disclosure, and it is why dual
   pricing needs no card-brand registration. Give the prices their own surface,
   sized for somebody on the other side of a counter, and equality becomes the
   layout rather than a rule nobody must later break.

   ⚠ THIS IS .vterm, NOT .termface. `.termface` is ALREADY the old design's
   sticky card band (`position:sticky; top:62px; margin:0 -18px 14px`), and
   reusing the name gave the surface a 62px offset that belonged to no element
   on the page and survived three attempts to find it.

   ⚠ THE HOSTS ARE MOVED, NEVER REBUILT — FreedomToDesign finds its inputs by
   id when it tokenizes, so a copy would look right and send nothing. */

/* Who and what for, across the top. Answered once, then out of the way. */
.termtop{
  display:grid; grid-template-columns:minmax(0,1.1fr) minmax(0,1fr) minmax(0,1.3fr);
  gap:14px 18px; align-items:start;
  background:var(--paper); border:1px solid var(--hair); border-radius:14px;
  padding:16px 18px; margin-bottom:16px;
}
.termtop .field{margin:0}
.termtop textarea{min-height:64px}
.termtop__wide{grid-column:1 / -1}
.termtop__wide:empty{display:none}
.termtop__wide .field:empty{display:none}

.vterm.counter{
  display:grid; grid-template-columns:minmax(0,1fr) minmax(0,1.12fr); gap:20px;
  align-items:start; background:transparent; border:0; box-shadow:none; padding:0; margin:0;
}

/* ── The customer's side ─────────────────────────────────────────────────── */
.vcust{
  background:linear-gradient(168deg, rgba(42,110,158,.075), transparent 70%), var(--paper);
  border:1px solid var(--hair); border-radius:18px; padding:26px 24px 24px;
  box-shadow:var(--aura); text-align:center;
}
.vcust__k{
  display:block; font-size:9.5px; font-weight:700; letter-spacing:.13em;
  text-transform:uppercase; color:var(--mute); margin-bottom:14px;
}
.vcust__who{margin:2px 0 4px}
.vcust__who b{display:block; font-size:14.5px; font-weight:650; color:var(--ink)}
.vcust__inv{display:block; font-size:12.5px; color:var(--steel); margin-top:2px}
.vcust__inv:empty{display:none}

/* The amount, centred and large.
   ⚠ `.moneybox__sign` is position:absolute at 13px with the input padded 25px
   to clear it — at this size the figure ran straight over the top of it. Here
   the box is a flex row and the sign takes its own space. */
.vterm__amt .moneybox{
  position:static; display:flex; align-items:baseline; justify-content:center; gap:11px;
  border:0; background:transparent; padding:0; box-shadow:none; max-width:none;
}
.vterm__amt .moneybox__sign{
  position:static; transform:none; font-size:28px; font-weight:700; color:var(--mute); flex:none;
}
/* ⚠ AN <input> WITH width:auto IS ~460px WIDE, WHATEVER IS IN IT. The row was
   centred, so the box was centred — but the text inside sat hard against its
   left edge, 215px left of where the panel's middle is. Centring the container
   does nothing when the thing being centred is a 460px box holding "0.00".

   `field-sizing:content` shrinks the input to its own text, so the $ and the
   figure centre as one object and stay centred as digits are typed.
   `text-align:center` is the fallback for browsers without it (Safari) — the
   number is then centred inside its own box, which is most of the way there.

   ⚠ AND IT HAS TO BEAT `.moneybox__in:placeholder-shown{text-align:left}`,
   which exists so an empty right-aligned field does not read as a value
   already entered. On this panel there is no right alignment to undo. */
.vcust .vterm__amt input.moneybox__in,
.vcust .vterm__amt input.moneybox__in:placeholder-shown{
  border:0; outline:0; background:transparent; color:var(--ink); box-shadow:none;
  padding:0; height:auto; flex:0 1 auto;
  field-sizing:content; width:auto; min-width:3.2ch; max-width:100%;
  font:inherit; font-size:48px; font-weight:800; letter-spacing:-.035em;
  font-variant-numeric:tabular-nums; text-align:center;
}
.vcust .vterm__amt input.moneybox__in::placeholder{color:var(--hair-strong)}
.vcust .hint{margin-top:10px; text-align:center}

/* ── The operator's side ─────────────────────────────────────────────────── */
.vops{
  background:var(--paper); border:1px solid var(--hair); border-radius:18px;
  padding:22px 24px 24px; box-shadow:var(--aura);
}
.vops__k{
  display:block; font-size:9.5px; font-weight:700; letter-spacing:.13em;
  text-transform:uppercase; color:var(--mute); margin-bottom:12px;
}
.vops .termmethods{display:grid; grid-template-columns:repeat(4,1fr); gap:9px; margin:0}
.vops .termmethods button{width:100%; padding:13px 0; font-size:13.5px; border-radius:11px}

/* Whatever the method needs.
   ⚠ .cardblock is a white card by default — inside a white panel it drew a
   second border round everything. Flattened, never re-parented: the node has to
   stay where it is for the gateway's inputs to be found. */
.vterm__body{margin-top:20px}
.vterm__body:empty{margin-top:0}
.vops .cardblock:not(:empty){border:0; background:transparent; padding:0; margin:0; border-radius:0}

/* ⚠ width:max-content, or .field's column flexbox stretches this two-way
   switch to the full width of the panel. */
.vops .cardblock .termmethods{
  display:inline-grid; grid-template-columns:auto auto; gap:4px; margin:0 0 15px;
  border:1px solid var(--hair); border-radius:10px; padding:3px;
  align-self:start; justify-self:start; width:max-content;
}
.vops .cardblock .termmethods button{
  padding:8px 20px; font-size:12.5px; border:0; background:transparent; border-radius:7px;
}
.vops .cardblock .termmethods button:not(.ghost){background:var(--navy); color:var(--paper)}
.vops .row2{display:grid; grid-template-columns:1fr 1fr; gap:12px; margin-top:14px}

/* ── The card · Aurora ──────────────────────────────────────────────────────
   Deep navy with the product's own aura beneath it, the chip drawn as a thin
   outline rather than a gold sticker. Scoped to the terminal: .vcard is shared
   with the pay page and the vault, and rolling it out there is a one-line
   change and a deliberate one. */
.vops .vcard{
  max-width:340px; margin:0 auto; position:relative; isolation:isolate;
  background:linear-gradient(152deg, #1A3358 0%, #0C1A2E 100%);
  border:0; box-shadow:0 12px 32px -16px rgba(42,110,158,.7);
}
.vops .vcard::before{
  content:""; position:absolute; z-index:-1; left:12%; right:12%; bottom:-10px; height:20px;
  border-radius:50%; background:var(--accent-bright); opacity:.3; filter:blur(14px);
}
/* ⚠ THE CHIP HAS TO READ AS A CHIP. The first pass at Aurora stripped the gold
   fill AND the contact strip, leaving a bare outlined rectangle — Greg: "the
   chip on the card is not showing". It was rendering; it just looked like an
   empty box. Muted gold rather than none: quiet enough for the navy, still
   unmistakably the thing it is. */
.vops .vcard__chip{
  background:linear-gradient(140deg, #D8C08C, #A98F5E 55%, #E2D3A6);
  box-shadow:inset 0 0 0 1px rgba(255,255,255,.30); border:0; opacity:.92;
}
.vops .vcard__chip::after{
  display:block; border-color:rgba(0,0,0,.28); border-left:0; border-right:0;
}
/* ⚠ TRACKING IS PART OF THE FIT. .115em on a 20px number adds 2.3px per
   character across 19 of them — 44px, which is most of the overflow on its
   own. Aurora keeps a lighter weight and settles for .04em. */
.vops .vcard__num{font-weight:500; letter-spacing:.04em}

/* The button ends the operator's column and names the figure. */
.vops button.termrecord{
  width:100%; padding:16px; font-size:16.5px; font-weight:750; border-radius:13px; margin-top:20px;
}
/* A declined or unavailable charge stays with the action until the next try.
   The 12px gap is the operator panel's standard control-to-feedback rhythm. */
.termrecord__notice{margin-top:12px}

/* While the device has the card, the customer's panel is the only thing that
   matters — so the animation takes it, and the operator's side steps back. */
.vterm.is-waiting .vops{display:none}
.vterm.is-waiting{grid-template-columns:1fr}
.vterm__wait{background:none; padding:44px 20px; border-radius:0}

.termdocswrap{margin-top:14px}

@media (max-width:1000px){
  .termtop{grid-template-columns:1fr}
  .vterm.counter{grid-template-columns:1fr}
}
@media (max-width:620px){
  .vcust,.vops{padding:18px}
  .vcust .vterm__amt input.moneybox__in{font-size:38px}
  .vops .termmethods{grid-template-columns:1fr 1fr}
}

/* ── The price zone, one shape per program ──────────────────────────────── */
.pzone{border:1px solid var(--hair); border-radius:14px; padding:16px 18px; margin-top:20px}
.pzone__k{
  display:block; font-size:9.5px; font-weight:700; letter-spacing:.12em;
  text-transform:uppercase; color:var(--mute); margin-bottom:11px; text-align:center;
}
/* traditional — one number */
.pzone__one{display:flex; align-items:baseline; justify-content:center; gap:12px}
.pzone__one b{font-size:34px; font-weight:800; letter-spacing:-.03em; font-variant-numeric:tabular-nums}
.pzone__one span{font-size:12.5px; color:var(--steel)}

/* dual pricing — EQUAL PROMINENCE. Both cells are identical in size, weight
   and colour; the chosen one is ringed, never the other dimmed. Making one
   quieter is what turns dual pricing into an unregistered surcharge. */
.pdual{display:grid; grid-template-columns:1fr 1fr; gap:12px}
.pdual__c{
  border:1.5px solid var(--hair); border-radius:12px; padding:13px 14px; text-align:center;
  transition:border-color .16s, box-shadow .16s;
}
.pdual__c.is-chosen{border-color:var(--accent); box-shadow:0 0 0 3px rgba(42,110,158,.10)}
.pdual__c em{
  display:block; font-style:normal; font-size:9.5px; font-weight:700; letter-spacing:.1em;
  text-transform:uppercase; color:var(--mute);
}
.pdual__c b{
  display:block; font-size:30px; font-weight:800; letter-spacing:-.03em; margin:5px 0 2px;
  line-height:1.02; font-variant-numeric:tabular-nums; color:var(--ink);
}
.pdual__c i{display:block; font-style:normal; font-size:11.5px; color:var(--steel)}
.pdual__tick{
  display:inline-block; margin-top:7px; font-size:10px; font-weight:700;
  letter-spacing:.09em; text-transform:uppercase; color:var(--accent);
}

/* surcharge — the ledger the receipt will print */
.pled__row{
  display:flex; justify-content:space-between; gap:12px; padding:8px 0; font-size:13.5px;
  border-top:1px solid var(--hair);
}
.pled__row:first-child{border-top:0}
.pled__row span{color:var(--steel)}
.pled__row b{font-variant-numeric:tabular-nums; font-weight:600}
.pled__row.is-total{border-top:1.5px solid var(--hair-strong); margin-top:4px; padding-top:11px}
.pled__row.is-total span{color:var(--ink); font-weight:600}
.pled__row.is-total b{font-size:25px; font-weight:800; letter-spacing:-.025em}
.pzone__disc{font-size:11.5px; color:var(--mute); margin:11px 0 0; line-height:1.55; text-align:center}
/* The consequence, not the rule — worth a shade more weight than the boilerplate. */
.pzone__prov{color:var(--steel)}

/* Four methods. */
.vterm .termmethods{display:grid; grid-template-columns:repeat(4,1fr); gap:10px; margin:22px 0 0}
.vterm .termmethods button{width:100%; padding:14px 0; font-size:14px; border-radius:12px}

/* Whatever the method needs, centred in the measure.
   ⚠ .cardblock is a white card by default — inside a white slate it drew a
   second border round everything. Flattened, never re-parented: the node has
   to stay where it is for the gateway's inputs to be found. */
.vterm__body{margin-top:22px}
.vterm__body:empty{margin-top:0}
.vterm .cardblock:not(:empty){border:0; background:transparent; padding:0; margin:0; border-radius:0}
.vterm .cardblock .field,
.vterm .cardblock .notice,
.vterm .cardblock .hint{text-align:left}

/* ⚠ align-self:start, or it is full width — .field is a column flexbox and
   stretches its children, which turned a two-way switch into a full-width bar. */
.vterm .cardblock .termmethods{
  display:inline-grid; grid-template-columns:auto auto; gap:4px; margin:0 auto 16px;
  border:1px solid var(--hair); border-radius:10px; padding:3px;
  align-self:center; justify-self:center; width:max-content;
}
.vterm .cardblock .termmethods button{
  padding:8px 20px; font-size:12.5px; border:0; background:transparent; border-radius:7px;
}
.vterm .cardblock .termmethods button:not(.ghost){background:var(--navy); color:var(--paper)}
.vterm .cardblock > .field:has(.termmethods){text-align:center}
.vterm .row2{display:grid; grid-template-columns:1fr 1fr; gap:12px; margin-top:14px}

/* ── The card · Aurora ──────────────────────────────────────────────────────
   Deep navy with the product's own aura bleeding out beneath it, the number in
   a light mono, the chip drawn as a thin outline rather than a gold sticker.
   Scoped to the terminal: .vcard is shared with the pay page and the vault, and
   this is a one-line change away from being rolled out there too. */
/* ⚠ THE SLATE'S CARD AND BUTTON RULES LIVED HERE AND WERE NEVER DELETED when
   the layout became The Counter — a second Aurora block on `.vterm`, later in
   the file than the `.vops` one and at the same specificity, so it quietly won
   every tie. That is why the chip vanished: this copy set
   `background:none` and `::after{display:none}` and the .vops copy above never
   got a look in. */
/* While the device has the card, nothing else on the slate matters. */
.vterm.is-waiting > *:not(.vterm__wait){display:none}
.vterm.is-waiting{align-items:center; justify-content:center; min-height:420px}
.vterm__wait{background:none; padding:40px 20px; border-radius:0}

.termdocswrap{margin-top:14px}

@media (max-width:900px){
  .termtop{grid-template-columns:1fr}
  .vterm{padding:24px 18px}
  .vterm .vterm__amt input.moneybox__in{font-size:40px}
  .vterm .termmethods{grid-template-columns:1fr 1fr}
  .pdual{grid-template-columns:1fr}
}

/* ── The waiting mark ────────────────────────────────────────────────────────
   Greg picked "The Signal" (2026-09-04) from three, then: "i want that
   animation to run in the middle of the screen anytime the website, tablet or
   phone is loading something." So this IS the loading state — ui.spinner()
   returns it and nothing else in the product draws a spinner.

   A sweep, not a fill. Every wait it covers is indeterminate: a query, a
   gateway round trip, a customer hunting for their card. Anything that fills
   up either finishes early and looks stuck or finishes repeatedly and looks
   like failed attempts. */
.waitmark{
  display:flex; flex-direction:column; align-items:center; justify-content:center;
  text-align:center; gap:2px; padding:26px 16px;
}
/* Inside a card body or a dialog the mark is furniture, not the subject —
   44px keeps it readable without turning a small panel into a logo. */
.waitmark:not(.waitmark--page):not(.waitmark--dark) .waitmark__svg{ width:48px; height:48px }
.waitmark:not(.waitmark--page) .waitmark__t{ margin-top:11px; font-size:12.5px; font-weight:600 }
/* ⚠ IN THE MIDDLE OF THE SCREEN, which is what was asked for — not pinned to
   the top of an empty column. min() so it still centres on a phone in
   landscape, where 60vh is barely 200px. */
.waitmark--page{ padding:40px 20px; min-height:min(58vh, 440px); justify-content:center; }
.waitmark__svg{ width:76px; height:76px; display:block; }
.waitmark--page .waitmark__svg{ width:88px; height:88px; }

/* On the navy pinpad panel the ring is white; on a page it is navy. Inherited
   from the surface rather than set twice — .waitmark--dark is the only switch. */
/* The comet, in three tokens: a transparent tail, a bright middle, a head that
   contrasts with whatever it is drawn on. */
.waitmark{ --wm-0:var(--accent-bright); --wm-1:var(--accent-bright); --wm-2:var(--navy); }
.waitmark__g0{ stop-color:var(--wm-0) }
.waitmark__g1{ stop-color:var(--wm-1) }
.waitmark__g2{ stop-color:var(--wm-2) }

.waitmark__ring{ fill:none; stroke:var(--navy); stroke-width:11.2; stroke-linecap:round; opacity:.22; }
/* ⚠ NO stroke HERE. The gradient id is unique per mark and is set as a
   presentation attribute on the element itself — a stroke in this rule would
   have higher priority than that attribute and would point every mark back at
   one gradient again. */
.waitmark__sweep{
  fill:none; stroke-width:11.2; stroke-linecap:round;
  stroke-dasharray:32 68; animation:waitSweep 2s cubic-bezier(.4,0,.6,1) infinite;
}
.waitmark__dot{ fill:var(--accent-bright); animation:waitDot 2s cubic-bezier(.4,0,.6,1) infinite; }
.waitmark__halo{ fill:none; stroke:var(--accent-bright); stroke-width:2; opacity:0;
                 animation:waitHalo 2s cubic-bezier(.4,0,.6,1) infinite; }

@keyframes waitSweep{ from{ stroke-dashoffset:100 } to{ stroke-dashoffset:0 } }
@keyframes waitDot{ 0%,70%{ opacity:.4 } 85%{ opacity:1 } 100%{ opacity:.4 } }
@keyframes waitHalo{ 0%,72%{ opacity:0; r:8 } 88%{ opacity:.5; r:19 } 100%{ opacity:0; r:24 } }

.waitmark__t{ margin-top:14px; font-size:13.5px; font-weight:650; color:var(--ink); }
.waitmark__s{ font-size:12px; color:var(--steel); }

/* On navy — the pinpad panel, and anywhere else the surface is dark. */
.waitmark--dark{
  background:linear-gradient(165deg, #16305A, #0C1A2E);
  border-radius:13px; padding:26px 18px; margin:4px 0 10px;
}
.waitmark--dark{ --wm-0:#4B9CD3; --wm-1:#8CC4EA; --wm-2:#FFFFFF; }
.waitmark--dark .waitmark__ring{ stroke:#FFFFFF; opacity:.30 }
.waitmark--dark .waitmark__t{ color:#DCE6F2 }
.waitmark--dark .waitmark__s{ color:#9FB4CE }

/* ⚠ Reduced motion does not mean no feedback. The mark holds still and the
   sweep becomes a steady dimmed ring — legibly the logo, visibly a wait. */
@media (prefers-reduced-motion:reduce){
  .waitmark__sweep,.waitmark__dot,.waitmark__halo{ animation:none }
  .waitmark__sweep{ stroke-dasharray:none; opacity:.45 }
  .waitmark__halo{ display:none }
}

/* ── "A new version is ready" ────────────────────────────────────────────────
   See OSP.buildWatch. Bottom-centre, above everything, and dismissible —
   somebody halfway through keying a card must be able to make it go away
   without reloading. */
.newbuild{
  position:fixed; left:50%; bottom:22px; transform:translateX(-50%);
  z-index:9000; display:flex; align-items:center; gap:12px;
  background:var(--navy); color:#E8EEF6; border-radius:12px;
  padding:11px 12px 11px 18px; font-size:13.5px;
  box-shadow:0 8px 28px -10px rgba(19,41,75,.55), 0 2px 8px rgba(19,41,75,.30);
  animation:newbuildIn .32s cubic-bezier(.2,.9,.3,1);
  max-width:calc(100vw - 32px);
}
@keyframes newbuildIn{ from{opacity:0; transform:translate(-50%,14px)} to{opacity:1; transform:translate(-50%,0)} }
.newbuild button{
  font:inherit; font-weight:650; padding:7px 14px; border-radius:8px;
  border:0; background:var(--accent-bright); color:#0B1728; cursor:pointer;
}
.newbuild button:hover{ background:#6FB4DF }
.newbuild__x{
  background:transparent !important; color:rgba(232,238,246,.65) !important;
  padding:6px 9px !important; font-size:17px; line-height:1;
}
.newbuild__x:hover{ color:#fff !important }
@media (prefers-reduced-motion:reduce){ .newbuild{animation:none} }

/* ── Where the day's money went · wave 4 ─────────────────────────────────────
   Greg picked design 4 of six, 2026-09-04: the deposit view, with checks
   included and a line per transaction.

   ⚠ TWO DESTINATIONS, AND THEY ARE NOT THE SAME EVENT. Card and ACH go to the
   processor and land days later minus fees; cash and checks go to the bank in
   a deposit bag. They are drawn as two panels rather than one total because
   adding them is what makes a batch screen impossible to reconcile — on one of
   Delgado's days 87% of the takings was a single check the processor never
   sees. */
.deprow{
  display:grid; grid-template-columns:repeat(3, minmax(0,1fr)); gap:14px; margin-bottom:16px;
}
.dep{
  border:1px solid var(--hair); border-radius:14px; padding:16px 18px;
  background:var(--paper); display:flex; flex-direction:column;
}
.dep--proc{box-shadow:var(--aura)}
/* Agreement is quiet; disagreement is not. */
.dep--proc.is-ok{border-color:color-mix(in srgb, var(--green, #2FA37A) 40%, var(--hair))}
.dep--proc.is-off{
  border-color:color-mix(in srgb, #C93B40 46%, var(--hair));
  box-shadow:var(--aura), 0 0 0 3px rgba(201,59,64,.07);
}
.dep--all{background:linear-gradient(170deg, rgba(19,41,75,.03), transparent)}
.dep__k{
  display:block; font-size:9.5px; font-weight:700; letter-spacing:.12em;
  text-transform:uppercase; color:var(--mute);
}
.dep__n{
  display:block; font-size:30px; font-weight:800; letter-spacing:-.03em;
  margin:7px 0 2px; font-variant-numeric:tabular-nums; color:var(--ink); line-height:1.05;
}
.dep__s{display:block; font-size:12px; color:var(--steel)}
.dep__line{
  display:flex; justify-content:space-between; gap:10px; align-items:baseline;
  margin-top:auto; padding-top:13px; font-size:12.5px; color:var(--steel);
}
.dep__line b{font-variant-numeric:tabular-nums; color:var(--ink); font-weight:650; text-align:right}
/* "not settled yet" is a real answer, not a missing one — it reads as pending
   rather than as an error. */
.dep__line.is-wait b{color:var(--mute); font-weight:500; font-style:italic}
.dep__line.is-ok b{color:#1F6B50}
.dep__line.is-off b{color:#9E2A2E}
/* Checks and cash are two different piles and two different errands — one
   line each rather than one line reading "$0.00 · 0 · cash $0.00". */
.dep__split{margin-top:auto; padding-top:13px}
.dep__split .dep__line{margin-top:0; padding-top:0}
.dep__split .dep__line + .dep__line{padding-top:7px}
.dep__ref{
  display:block; margin-top:8px; font-size:11px; color:var(--mute);
  font-family:ui-monospace, SFMono-Regular, Menlo, monospace;
}
@media (max-width:900px){ .deprow{grid-template-columns:1fr} }

/* ── The billing gate · wave 4 ───────────────────────────────────────────────
   Greg took the recommendation from three designs: exceptions as a gate, the
   review as the page, the builder as the client. This is the gate.

   ⚠ THESE ARE MISTAKES, NOT BILLING DECISIONS. A timer nobody stopped and an
   entry nobody could have worked have to be dealt with before anything reaches
   an invoice — on this database one entry is 12,244 minutes, 204.1 hours,
   started on 12 August, and because it carries no rate the summary valued that
   client at 205.5 hours and $183.33. The biggest problem on the screen was its
   quietest number, which is exactly what a band of prose at the top fixes. */
.wipx{
  border:1px solid color-mix(in srgb, #C93B40 40%, var(--hair));
  border-radius:13px; overflow:hidden; margin-bottom:16px; box-shadow:var(--aura);
  background:var(--paper);
}
.wipx__head{
  padding:12px 16px; background:rgba(201,59,64,.06);
  font-size:13.5px; font-weight:700; color:var(--ink);
}
.wipx__row{
  display:flex; align-items:center; justify-content:space-between; gap:14px;
  padding:12px 16px; border-top:1px solid var(--hair);
}
.wipx__row b{font-size:13.5px; font-weight:650; display:block}
.wipx__row .sub{font-size:12px; color:var(--mute); margin-top:2px; max-width:76ch}

/* ── Engagements · the portfolio board · wave 4 ──────────────────────────────
   Greg took the recommendation from three designs: the portfolio as the page,
   the agreement as the engagement, and the overrun stated in money everywhere
   a budget appears.

   ⚠ FOUR LANES, AND "NEVER STARTED" IS THE ONE NOTHING HAD. Work agreed and
   not begun records $0.00, so on a list sorted by value it sits at the bottom
   looking like a quiet month rather than a job nobody has picked up. Two of the
   seven engagements on this database are exactly that. */
.engboard{display:grid; grid-template-columns:repeat(4, minmax(0,1fr)); gap:12px}
.englane{border:1px solid var(--hair); border-radius:13px; padding:12px; background:var(--paper)}
.englane__head{
  display:flex; align-items:baseline; justify-content:space-between; gap:8px; margin-bottom:11px;
  font-size:10px; font-weight:700; letter-spacing:.11em; text-transform:uppercase; color:var(--mute);
}
.englane__head b{font-size:13px; letter-spacing:0; color:var(--ink)}
.englane--bad{border-color:color-mix(in srgb,#C93B40 40%,var(--hair)); box-shadow:var(--aura)}
.englane--bad .englane__head{color:#9E2A2E}
.englane--warn .englane__head{color:#8A6100}
.englane--ok .englane__head{color:#1F6B50}
.englane__none{font-size:11.5px; color:var(--mute); margin:0; padding:14px 4px; text-align:center}

.engcard{
  display:block; width:100%; text-align:left; font:inherit; cursor:pointer;
  border:1px solid var(--hair); border-left:3px solid var(--mute);
  border-radius:11px; padding:11px 12px; background:var(--paper); margin-bottom:9px;
}
.engcard:hover{border-color:var(--accent2, var(--hair2))}
.engcard.is-on{box-shadow:0 0 0 3px rgba(42,110,158,.12); border-color:var(--accent)}
.engcard--over{border-left-color:#C93B40}
.engcard--near{border-left-color:#C08A1E}
.engcard--ok{border-left-color:#2FA37A}
.engcard--idle{border-left-color:var(--hair-strong)}
.engcard__n{font-size:13px; font-weight:650; line-height:1.3; color:var(--ink)}
.engcard__c{font-size:11.5px; color:var(--mute); margin-top:2px}
.engcard__f{display:flex; justify-content:space-between; align-items:baseline; gap:8px; margin-top:9px}
.engcard__f b{font-size:13px; font-variant-numeric:tabular-nums}
.engcard__p{font-size:12px; color:var(--steel); font-variant-numeric:tabular-nums}
.engcard--over .engcard__p{color:#9E2A2E; font-weight:650}
.engcard--near .engcard__p{color:#8A6100; font-weight:650}
/* The whole point of the pick: the money, not the percentage. */
.engcard__over{
  margin-top:8px; padding-top:8px; border-top:1px solid var(--hair);
  font-size:11.5px; font-weight:650; color:#9E2A2E;
}

/* The agreement, against what happened. */
.engover{
  border:1px solid color-mix(in srgb,#C93B40 40%,var(--hair));
  background:rgba(201,59,64,.05); border-radius:12px; padding:14px 16px; margin-bottom:12px;
}
.engover__n{font-size:29px; font-weight:800; letter-spacing:-.03em; color:#9E2A2E;
            font-variant-numeric:tabular-nums; line-height:1.05}
.engover__s{font-size:12.5px; color:var(--steel); margin-top:4px}
.engacts{display:flex; gap:9px; flex-wrap:wrap; margin-top:10px}

@media (max-width:1100px){ .engboard{grid-template-columns:1fr 1fr} }
@media (max-width:640px){ .engboard{grid-template-columns:1fr} }

/* The board is the page; the engagement opens beneath it. */
.engpage__search{max-width:420px; margin-bottom:14px}
.engpage .engboard{margin-bottom:16px}
.engpage .pickpane__body{
  background:var(--paper); border:1px solid var(--hair); border-radius:14px; padding:20px;
}
.engpage .pickpane__blank{padding:40px 20px; text-align:center}

/* ══ THE DATED SPINE ═══════════════════════════════════════════════════════
   Greg, 2026-09-04: "go with Option 1 and 2s money column."

   Obligations, the records chase and the service desk. One time axis, today
   drawn across it, late work above the line — and, in its own right-hand
   column, what the client is worth.

   ⚠ THE AXIS IS POSITIONED OFF THE SAME THREE VARIABLES THE GRID USES. The
   line is absolute and the rows are grid, so the only thing keeping the pips
   on the axis is that both derive from --gutter/--gap/--pipw. Change one in a
   media query and both move together; hard-code the left offset and they part
   company at the first breakpoint. */
.spine{
  --gutter:88px; --gap:10px; --pipw:20px; --moneyw:0px;
  position:relative; margin-top:14px;
}
.spine::before{
  content:''; position:absolute; top:12px; bottom:12px; width:2px;
  left:calc(var(--gutter) + var(--gap) + var(--pipw) / 2 - 1px);
  background:var(--hair);
}

.spx{
  display:grid; align-items:start; gap:0 var(--gap);
  grid-template-columns:var(--gutter) var(--pipw) minmax(0,1fr) var(--moneyw);
  padding:11px 12px 11px 0; border-radius:10px;
  border:1px solid transparent;
}
.spx:hover{background:var(--band)}
.spx.clickable{cursor:pointer}

.spx__when{
  grid-column:1; text-align:right; padding-top:2px; line-height:1.3;
  font-size:12px; font-weight:600; color:var(--steel);
  font-variant-numeric:tabular-nums;
}
.spx__when small{display:block; font-size:11px; font-weight:500; color:var(--mute)}

.spx__pip{
  grid-column:2; width:9px; height:9px; border-radius:50%; justify-self:center;
  margin-top:6px; background:var(--hair-strong);
  box-shadow:0 0 0 3px var(--paper);
}

.spx__body{grid-column:3; min-width:0}
.spx__row{display:flex; align-items:baseline; gap:9px; flex-wrap:wrap}
.spx__what{font-size:15px; font-weight:700; color:var(--ink); letter-spacing:-.01em}
.spx__who{font-size:13.5px; color:var(--accent); text-decoration:none}
.spx__who:hover{text-decoration:underline}
.spx__meta{font-size:12.5px; color:var(--mute); margin-top:3px}

/* Late colours the date and the pip. It does NOT ground the row — see the note
   in ui.spine(): a page where four rows are shouting is a page where nothing
   is, and the records chase produced exactly that on its first build. */
.spx--late .spx__pip{background:var(--red)}
.spx--late .spx__when{color:var(--red)}
.spx--late .spx__when small{color:var(--red)}
.spx--soon .spx__pip{background:var(--amber)}
.spx--soon .spx__when{color:var(--amber)}

/* The ground, and the only thing that raises its voice. Set for a statutory
   date that has passed, or a request nobody ever followed up — both of which
   are ours, not the client's. */
.spx--alarm{background:var(--red-bg); border-color:color-mix(in srgb,var(--red) 22%,transparent)}
.spx--alarm:hover{background:var(--red-bg)}
.spx--alarm .spx__pip{background:var(--red); box-shadow:0 0 0 3px var(--red-bg)}

/* Today. Three real cells in the row grid — see the note in ui.spine(). */
.spine__now{
  display:grid; align-items:center; gap:0 var(--gap);
  grid-template-columns:var(--gutter) var(--pipw) minmax(0,1fr);
  margin:12px 0 14px;
}
.spine__nowl{
  grid-column:1; text-align:right; font-size:10.5px; font-weight:800;
  letter-spacing:.12em; text-transform:uppercase; color:var(--ink);
}
.spine__nowp{
  grid-column:2; width:9px; height:9px; border-radius:50%; justify-self:center;
  background:var(--ink); box-shadow:0 0 0 3px var(--paper);
}
.spine__nowr{grid-column:3; height:1px; background:var(--hair-strong)}

/* What is stopping it, under the thing it is stopping. */
.spblock{
  margin-top:10px; padding-left:12px; display:grid; gap:7px;
  border-left:2px solid var(--hair-strong);
}
.spblock__r{display:flex; align-items:center; gap:9px; flex-wrap:wrap; font-size:13px}
.spblock__t{font-weight:600; color:var(--ink)}
.spblock__a{font-size:11.5px; color:var(--mute)}
.spblock__a.is-warn{color:var(--red); font-weight:600}
.spblock__r button.link{font-size:12px}

/* Option 2's column. Absent entirely without the money permission, and the
   grid track collapses on its own when nothing fills it. */
.spx__money{
  grid-column:4; text-align:right; padding-top:1px; padding-left:14px; min-width:0;
}
.spx__moneyn{
  font-size:15.5px; font-weight:800; letter-spacing:-.02em; color:var(--ink);
  font-variant-numeric:tabular-nums; line-height:1.15;
}
.spx__moneyn.is-warn{color:var(--red)}
.spx__moneyn.is-good{color:var(--green)}
.spx__moneyn.is-none{font-size:12.5px; font-weight:600; color:var(--mute); letter-spacing:0}
.spx__moneys{font-size:11px; color:var(--mute); margin-top:3px}

/* The money column is the first thing to go: on a tablet the date, the thing
   and the blockers are the job, and a realization figure is a partner's
   question asked at a desk. */
/* Set only when a row actually carries money. 136px holds "nothing written
   off" at 11px without wrapping, which is the longest sub-line either screen
   produces. */
.spine--money{--moneyw:136px}

@media (max-width:900px){
  .spine--money{--moneyw:0px}
  .spx{grid-template-columns:var(--gutter) var(--pipw) minmax(0,1fr)}
  .spx__money{
    grid-column:3; text-align:left; padding-left:0; margin-top:7px;
    display:flex; align-items:baseline; gap:8px;
  }
  .spx__moneys{margin-top:0}
}
@media (max-width:560px){
  .spine{--gutter:62px; --gap:8px}
  .spx__what{font-size:14.5px}
}

/* ══ THE SENTENCE A SCREEN OPENS WITH ═════════════════════════════════════
   A count says how many. This says what to do about it, and it is the only
   thing on the page written as prose. */
.saidfirst{
  margin:2px 0 18px; padding:15px 18px; border-radius:12px;
  border:1px solid var(--hair); background:var(--band);
}
.saidfirst__k{
  font-size:10.5px; font-weight:800; letter-spacing:.14em; text-transform:uppercase;
  color:var(--mute); margin-bottom:7px;
}
.saidfirst__v{font-size:19px; line-height:1.4; font-weight:500; color:var(--ink)}
.saidfirst__v b{font-weight:800}
.saidfirst__do{display:flex; gap:9px; flex-wrap:wrap; margin-top:12px}
.saidfirst--bad{
  background:var(--red-bg); border-color:color-mix(in srgb,var(--red) 26%,transparent);
}
.saidfirst--bad .saidfirst__k{color:var(--red)}
.saidfirst--warn{
  background:var(--amber-bg); border-color:color-mix(in srgb,var(--amber) 30%,transparent);
}
.saidfirst--warn .saidfirst__k{color:var(--amber)}
.saidfirst--good{
  background:var(--green-bg); border-color:color-mix(in srgb,var(--green) 30%,transparent);
}
.saidfirst--good .saidfirst__k{color:var(--green)}

/* Files and the two verbs, under the request they belong to. */
.spacts{display:flex; gap:14px; flex-wrap:wrap; align-items:center; margin-top:8px}
.spacts button.link{font-size:12px}

/* ══ SETTINGS · THE CONTROL ROOM ═══════════════════════════════════════════
   Greg, 2026-09-04: "1 with 3s search."

   Fourteen identical pills became five bands of cards that each say what they
   are currently set to. The old .setpill rules are gone with the markup that
   used them — a leftover block at equal specificity is how a renamed layout
   gets quietly overruled. */
.setfind{margin-bottom:20px}
.setfind input{
  width:100%; max-width:560px; padding:11px 14px; font-size:14.5px;
  border-radius:9px; border:1px solid var(--hair-strong); background:var(--paper);
}
.setfind input:focus{border-color:var(--accent); outline:none;
  box-shadow:0 0 0 3px rgba(75,156,211,.16)}

.setband{margin-bottom:26px}
.setband__h{
  display:flex; align-items:baseline; gap:10px; flex-wrap:wrap;
  margin-bottom:11px; padding-bottom:7px; border-bottom:1px solid var(--hair);
}
.setband__t{font-size:11px; font-weight:800; letter-spacing:.13em;
  text-transform:uppercase; color:var(--ink)}
.setband__s{font-size:12.5px; color:var(--mute)}

.setcards{display:grid; gap:10px; grid-template-columns:repeat(3, minmax(0,1fr))}
@media (max-width:1100px){ .setcards{grid-template-columns:repeat(2, minmax(0,1fr))} }
@media (max-width:680px){ .setcards{grid-template-columns:1fr} }

.setcard{
  display:flex; flex-direction:column; gap:5px; min-height:104px;
  padding:13px 15px; border:1px solid var(--hair); border-radius:11px;
  background:var(--paper); text-decoration:none; color:inherit;
}
.setcard:hover{border-color:var(--accent)}
.setcard__top{display:flex; align-items:center; gap:9px}
.setcard__icon{
  flex:none; width:26px; height:26px; border-radius:7px; display:grid;
  place-items:center; font-size:13px; color:var(--accent); background:var(--band);
}
.setcard__n{font-size:14.5px; font-weight:700; letter-spacing:-.01em; color:var(--ink)}
.setcard__q{font-size:11.5px; line-height:1.45; color:var(--mute)}

/* The answer. Pushed to the bottom so the values line up across a row however
   long the questions above them run. */
.setcard__v{
  margin-top:auto; padding-top:7px; font-size:13px; font-weight:600; color:var(--ink);
}
.setcard__v:empty{display:none}

/* ⚠ AMBER, NOT RED, AND ONLY FOR THINGS THAT ARE ACTUALLY UNSET. A business
   that has decided it wants no fraud rules is not in error, and a settings page
   that shouts at somebody for a deliberate choice is a page they stop reading.
   Nothing here uses --red. */
.setcard--amber{background:var(--amber-bg);
  border-color:color-mix(in srgb, var(--amber) 30%, transparent)}
.setcard--amber .setcard__v{color:var(--amber)}
.setcard--amber .setcard__icon{background:rgba(138,97,0,.10); color:var(--amber)}
.setcard--warn{background:var(--red-bg);
  border-color:color-mix(in srgb, var(--red) 26%, transparent)}
.setcard--warn .setcard__v{color:var(--red)}
.setcard--warn .setcard__icon{background:rgba(201,59,64,.10); color:var(--red)}

/* Option 3's search results, without its permanent rail. */
.sethits{border:1px solid var(--hair); border-radius:11px; overflow:hidden;
  background:var(--paper); margin-bottom:26px}
.sethit{
  display:flex; align-items:baseline; gap:12px; flex-wrap:wrap;
  padding:11px 15px; border-bottom:1px solid var(--hair);
  text-decoration:none; color:inherit;
}
.sethit:last-child{border-bottom:0}
.sethit:hover{background:var(--band)}
.sethit b{font-size:14px; font-weight:700}
.sethit__p{font-size:12px; color:var(--mute)}
.sethit__none{padding:15px; font-size:13.5px; color:var(--ink)}

/* ══ BOARDING · ONE QUESTION AT A TIME ═════════════════════════════════════
   Greg, 2026-09-04: "Board with option 3, then shows as option 1."

   The interview is deliberately narrow and centred. A merchant is usually on
   the phone while this is filled in, and a single question in the middle of an
   empty screen is the only layout where nothing else can be skipped by
   accident. */
.iv{max-width:660px; margin:0 auto; padding:26px 0 8px; text-align:center}
.iv--review{max-width:860px; text-align:left}

.iv__step{
  font-size:10.5px; font-weight:700; letter-spacing:.15em; text-transform:uppercase;
  color:var(--accent); margin-bottom:14px;
}
/* ⚠ IT IS AN <h2>, AND THE BASE h2 IS AN EYEBROW: 11px, uppercase, .1em
   tracking, steel. Every card heading in the product relies on that, so the
   question has to undo all four properties by name — the first build read
   "WHAT IS THE BUSINESS CALLED?" in 11px caps. */
.iv__q{
  font-size:32px; font-weight:400; line-height:1.18; letter-spacing:-.015em;
  text-transform:none; color:var(--ink); max-width:22ch; margin:0 auto 10px;
  text-wrap:balance;
}
.iv__hint{font-size:14px; color:var(--steel); max-width:52ch; margin:0 auto 22px;
          line-height:1.55}

/* The fields go back to reading left to right — a centred form is unfillable. */
.iv__body{text-align:left; margin-bottom:18px}
.iv__body .card{margin-top:12px}

.iv__acts{display:flex; gap:10px; justify-content:center; align-items:center;
          flex-wrap:wrap; margin-top:6px}
.iv--review .iv__acts{justify-content:flex-start}
/* ⚠ QUIETER THAN "NEXT", AND NEVER BESIDE IT IN WEIGHT. Deferring is a real
   answer, but it is the second-best one — a merchant who can take a card on
   Monday is the point of the screen. */
.iv__defer{font-size:12.5px; color:var(--mute)}
.iv__defer:hover{color:var(--accent)}

.iv__dots{display:flex; gap:7px; justify-content:center; margin-top:26px}
.iv__d{
  width:7px; height:7px; border-radius:50%; background:var(--hair-strong);
  cursor:pointer; transition:transform .12s ease, background .12s ease;
}
.iv__d.is-done{background:var(--green)}
.iv__d.is-on{background:var(--accent); transform:scale(1.5)}

/* ══ AND THEN IT SHOWS AS A CHECKLIST ═════════════════════════════════════ */
.obverdict{
  padding:15px 17px; border-radius:11px; margin-bottom:16px;
  border:1px solid color-mix(in srgb, var(--amber) 30%, transparent);
  background:var(--amber-bg);
}
.obverdict__k{
  font-size:10.5px; font-weight:800; letter-spacing:.14em; text-transform:uppercase;
  color:var(--amber); margin-bottom:7px;
}
.obverdict__v{font-size:16.5px; line-height:1.45; color:var(--ink)}
.obverdict__v b{font-weight:800}
.obverdict--stop{
  background:var(--red-bg); border-color:color-mix(in srgb, var(--red) 28%, transparent);
}
.obverdict--stop .obverdict__k{color:var(--red)}
.obverdict--go{
  background:var(--green-bg); border-color:color-mix(in srgb, var(--green) 30%, transparent);
}
.obverdict--go .obverdict__k{color:var(--green)}

.obcheck{display:grid; gap:6px}
.obstep{
  display:flex; gap:12px; align-items:flex-start; padding:11px 13px;
  border:1px solid var(--hair); border-radius:10px; background:var(--paper);
  cursor:pointer;
}
.obstep:hover{border-color:var(--accent)}
.obstep__m{
  width:21px; height:21px; border-radius:50%; flex:none; display:grid;
  place-items:center; font-size:10px; font-weight:800; color:#fff;
  background:var(--green); margin-top:1px;
}
.obstep__b{min-width:0}
.obstep__t{font-size:14px; font-weight:700; display:block; line-height:1.3}
.obstep__s{font-size:12.5px; color:var(--steel); margin-top:2px}

/* Needed, left-for-later and simply empty are three different things, and the
   whole point of the interview is that the screen can tell them apart. */
.obstep.is-needed{background:var(--red-bg);
  border-color:color-mix(in srgb, var(--red) 24%, transparent)}
.obstep.is-needed .obstep__m{background:var(--red)}
.obstep.is-empty{background:var(--amber-bg);
  border-color:color-mix(in srgb, var(--amber) 26%, transparent)}
.obstep.is-empty .obstep__m{background:var(--amber)}
.obstep.is-later .obstep__m{
  background:var(--paper); color:var(--mute); border:1px dashed var(--hair-strong);
}

/* The same checklist again after boarding, so the leftovers leave with them. */
.obafter{margin-top:18px; padding-top:16px; border-top:1px solid var(--hair)}
.obafter h3{font-size:12px; font-weight:800; letter-spacing:.12em;
            text-transform:uppercase; color:var(--mute); margin-bottom:11px}

@media (max-width:640px){
  .iv__q{font-size:26px}
  .iv{padding-top:16px}
}

/* ══ THE PHONE AND TABLET HOME ═════════════════════════════════════════════
   Greg, 2026-09-05: "number 4 as design with number 2 as spine and 8 as
   switch mode." The Signal is the look, the Day is the spine, Site Mode is
   the switch. See views/mobile-home.js for which idea does which job.

   ⚠ THE WHOLE SCREEN IS DARK, on a product whose desktop is light. That is
   deliberate and not a theme: the aura only exists against navy, the ring only
   glows on a dark field, and a phone held at a bay door at dusk is easier to
   read dark. It is scoped to .mh so nothing else in the product inherits it. */
.mh{
  margin:-16px -16px 0; color:#fff; position:relative; background:transparent;
  min-height:calc(100vh - 120px);
  /* ⚠ .main ALREADY RESERVES THE THUMB BAR, and it does it OUTSIDE this box —
     .mh sits in #view and the reserve is below it. This is breathing room under
     the last row, not the reserve itself; paying the full 56px again here left
     a hand's width of empty field above the bar. */
  padding:0 0 16px;
}

/* ---- The field ──────────────────────────────────────────────────────────

   ⚠ FIXED TO THE VIEWPORT, AND THAT IS THE WHOLE DESIGN. This was .mh's own
   background plus a pseudo-element stretched past the bottom of the box, and
   every version of that was arithmetic against something the device decides:
   the 56px thumb bar, a safe-area inset that is 0 in a desktop window, a box
   bottom landing on a fractional pixel and antialiasing a white hairline
   across the field. A fixed layer at inset:0 IS the display. Nothing to
   measure, nothing to keep in step, no seam — and it reaches up behind the
   status bar and around a notch, an island or a punch hole, which is what it
   is for (Greg, 2026-09-05).

   z-index:-1 puts it behind the content. .mh opens no stacking context, so it
   lands in front of the page background and behind everything else. */
.mh__ground{
  position:fixed; inset:0; z-index:-1; pointer-events:none;
  background:var(--navy-deep, #0A1730);
}
/* The aura. Three soft lights, no hard edges — the same field the terminal and
   the sign-in screen sit on. Anchored to the display now, so the bottom light
   sits above the thumb bar wherever the list happens to end. */
.mh__ground::before{
  content:''; position:absolute; inset:0;
  background:
    radial-gradient(52% 30% at 18% 6%, rgba(75,156,211,.34) 0%, transparent 64%),
    radial-gradient(46% 26% at 88% 18%, rgba(42,110,158,.30) 0%, transparent 62%),
    radial-gradient(72% 36% at 50% 100%, rgba(123,175,212,.14) 0%, transparent 68%);
}
/* ⚠ NOT THE GROUND. This lifts the content above the field, and it is written
   at the same specificity as .mh__ground but LATER in the file — so it quietly
   won, turned the fixed layer into a 0px-tall relative box, and the whole home
   screen came up white. */
.mh > *:not(.mh__ground){position:relative}

/* ---- 04 · the Signal ---------------------------------------------------- */
.mh__sig{padding:18px 18px 20px; text-align:center}
.mh__org{
  font-size:11px; font-weight:700; letter-spacing:.14em; text-transform:uppercase;
  color:rgba(255,255,255,.56); margin-bottom:14px;
}
.mh__ring{
  -webkit-appearance:none; appearance:none; background:none; border:0; padding:0;
  cursor:pointer; -webkit-tap-highlight-color:transparent; display:block;
  margin:0 auto;
}
.mh__ring:active .sigring{transform:scale(.97)}

.sigring{
  position:relative; width:190px; height:190px; display:grid; place-items:center;
  transition:transform .12s ease;
}
/* The glow behind the ring — this is the "aura" doing literal work rather than
   decorating an edge. */
.sigring::before{
  content:''; position:absolute; inset:6px; border-radius:50%;
  background:radial-gradient(circle, rgba(75,156,211,.34) 0%, transparent 68%);
  filter:blur(14px);
}
.sigring__svg{position:absolute; inset:0}
.sigring__svg svg{width:100%; height:100%; display:block;
  filter:drop-shadow(0 0 10px rgba(75,156,211,.55))}
.sigring__mid{position:relative; text-align:center; padding:0 26px}
.sigring__big{
  display:block; font-size:31px; font-weight:800; letter-spacing:-.03em;
  line-height:1.05; font-variant-numeric:tabular-nums; color:#fff;
}
/* Set by the view from the string's own length — see the note beside
   .sigring__big in mobile-home.js. */
.sigring__big.is-l2{ font-size:26px; letter-spacing:-.035em; }
.sigring__big.is-l3{ font-size:22px; letter-spacing:-.04em; }
.sigring__small{
  display:block; margin-top:5px; font-size:11px; line-height:1.35;
  color:rgba(255,255,255,.66);
}

.mh__chips{display:flex; gap:7px; justify-content:center; flex-wrap:wrap; margin-top:16px}
/* ⚠ .mhchip, NOT .mchip. The latter is the products margin chip — light
   background, dark text — and reusing it here put grey-on-grey pills on a navy
   field. Different surface, different component. */
.mhchip{
  font-size:11px; font-weight:700; letter-spacing:.04em; padding:5px 10px;
  border-radius:20px; background:rgba(75,156,211,.20); color:#CBE3F5;
  border:1px solid rgba(75,156,211,.30);
}
.mhchip--go{background:rgba(47,163,122,.20); color:#9EE0C4; border-color:rgba(47,163,122,.34)}
.mhchip--no{background:rgba(201,59,64,.20); color:#F3B4B6; border-color:rgba(201,59,64,.34)}
.mh__tap{margin-top:12px; font-size:11.5px; color:rgba(255,255,255,.48)}

/* ---- 02 · the Day ------------------------------------------------------- */
.mh__day{padding:4px 14px 0}
.mh__dayhead{
  display:flex; align-items:baseline; justify-content:space-between;
  padding:0 3px 9px; border-bottom:1px solid rgba(255,255,255,.12); margin-bottom:11px;
}
.mh__daylabel{font-size:11px; font-weight:800; letter-spacing:.14em;
  text-transform:uppercase; color:#fff}
.mh__daymeta{font-size:11.5px; color:rgba(255,255,255,.55)}

/* The next stop is the one thing on this screen that is NOT glass. It is what
   the person is about to do, so it gets the readable surface — the same rule
   the stylesheet has always had: glass for chrome, solid for content. */
.mhnow{
  background:rgba(255,255,255,.95); color:var(--ink); border-radius:15px;
  padding:13px 14px; margin-bottom:9px;
  box-shadow:0 10px 26px -12px rgba(0,0,0,.6);
}
.mhnow__k{font-size:9.5px; font-weight:800; letter-spacing:.14em;
  text-transform:uppercase; color:var(--accent)}
.mhnow__t{display:block; font-size:16px; font-weight:700; margin-top:5px;
  letter-spacing:-.01em}
.mhnow__s{font-size:12px; color:var(--steel); margin-top:3px}
.mhnow__do{display:flex; gap:8px; margin-top:12px}
.mhbtn{
  flex:1; -webkit-appearance:none; appearance:none; border:0; cursor:pointer;
  padding:12px 10px; border-radius:11px; background:var(--accent); color:#fff;
  font:700 13px/1 inherit; min-height:44px;
  -webkit-tap-highlight-color:transparent;
}
.mhbtn--ghost{background:transparent; color:var(--accent);
  border:1px solid rgba(42,110,158,.36)}
.mhbtn:active{transform:scale(.98)}

.mhlist{display:grid; gap:7px}
.mhstop{
  -webkit-appearance:none; appearance:none; cursor:pointer; text-align:left;
  display:grid; grid-template-columns:52px minmax(0,1fr); gap:10px;
  align-items:center; padding:11px 12px; min-height:52px;
  border-radius:13px; color:#fff;
  background:rgba(255,255,255,.07); border:1px solid rgba(255,255,255,.14);
  backdrop-filter:blur(18px) saturate(150%);
  -webkit-backdrop-filter:blur(18px) saturate(150%);
  -webkit-tap-highlight-color:transparent;
}
.mhstop:active{background:rgba(255,255,255,.12)}
.mhstop.is-done{opacity:.5}
.mhstop.is-done .mhstop__b b{text-decoration:line-through;
  text-decoration-color:rgba(255,255,255,.4)}
.mhstop__t{font-size:11.5px; font-weight:700; color:rgba(255,255,255,.6);
  font-variant-numeric:tabular-nums}
.mhstop__b{min-width:0}
.mhstop__b b{display:block; font-size:13.5px; font-weight:700; overflow:hidden;
  text-overflow:ellipsis; white-space:nowrap}
.mhstop__c{display:block; font-size:11px; color:rgba(255,255,255,.55);
  overflow:hidden; text-overflow:ellipsis; white-space:nowrap}
.mhempty{padding:20px 4px; text-align:center; font-size:13px;
  color:rgba(255,255,255,.5)}

.mhsite{
  display:block; width:calc(100% - 28px); margin:16px 14px 4px;
  -webkit-appearance:none; appearance:none; cursor:pointer;
  padding:13px; min-height:46px; border-radius:13px;
  background:transparent; border:1px dashed rgba(255,255,255,.26);
  color:rgba(255,255,255,.7); font:600 12.5px/1 inherit;
  -webkit-tap-highlight-color:transparent;
}
.mhsite:active{background:rgba(255,255,255,.06)}

/* ---- 08 · SITE MODE ─────────────────────────────────────────────────────
   A forecourt at noon.

   ⚠ IT KEEPS THE GROUND. The first build painted everything black, and Greg,
   2026-09-05: "needs to keep same background as the signal but change to site
   info." He is right — the navy field and the aura are the product's face, and
   walking outside is no reason to lose it. What goes is the GLASS: blur and
   translucency cost contrast and battery to look pretty at somebody wearing
   gloves. Cards go solid white, targets go to 64px, and the SCREEN CHANGES —
   see siteScreen() in mobile-home.js.

   ⚠ THIS IS ALSO THE Reduce Transparency STORY. Everything switched off below
   is the same set of properties iOS drops when a person turns that on, so the
   mode doubles as proof the screen works without any of it. */
body.is-site .sigring::before{display:none}
body.is-site .sigring__svg svg{filter:none}
/* The unfilled track at 14% white barely registers outdoors, so the ring reads
   as a floating arc with nothing to measure it against. */
body.is-site .sigring__svg circle:first-of-type{stroke:rgba(255,255,255,.34)}
body.is-site .sigring__small{color:#D8E2EC}
body.is-site .mh__tap{display:none}

body.is-site .mhchip{
  background:#fff; color:#000; border-color:#fff; font-weight:800;
}
body.is-site .mhchip--go{background:#2FA37A; color:#fff; border-color:#2FA37A}
body.is-site .mhchip--no{background:#C93B40; color:#fff; border-color:#C93B40}

body.is-site .mhnow{background:#fff; color:#000; border-radius:8px; box-shadow:none}
body.is-site .mhnow__t{font-size:20px}
body.is-site .mhnow__s{color:#333; font-size:13.5px}
body.is-site .mhbtn{min-height:64px; font-size:16px; border-radius:8px; background:#2FA37A}
body.is-site .mhbtn--ghost{background:#fff; color:#000; border:2px solid #000}

body.is-site .mhstop{
  background:#fff; color:#000; border:0; border-radius:8px;
  backdrop-filter:none; -webkit-backdrop-filter:none; min-height:64px;
}
body.is-site .mhstop__t{color:#22597F; font-size:13px}
body.is-site .mhstop__b b{font-size:15.5px}
body.is-site .mhstop__c{color:#333}
body.is-site .mhstop.is-done{opacity:.45}
body.is-site .mh__daymeta,
body.is-site .mhempty{color:#D8E2EC}
body.is-site .mhsite{border:2px solid #fff; color:#fff; font-weight:800}
/* The bar loses its blur — not its colour. A blurred bar over a bright outdoor
   screen is a smear; the navy is the same navy it always was. */
body.is-site .tabbar{background:var(--navy); backdrop-filter:none;
  -webkit-backdrop-filter:none; border-top:1px solid rgba(255,255,255,.3)}

/* ⚠ AND THE SAME THING WITHOUT ANYONE ASKING. A person who has turned on
   Reduce Transparency has told the OS they cannot read blurred surfaces; the
   home screen must honour that whether or not they found site mode. */
@media (prefers-reduced-transparency: reduce){
  .mh__ground::before{display:none}
  .mhstop{backdrop-filter:none; -webkit-backdrop-filter:none;
          background:rgba(255,255,255,.14)}
  .sigring::before{filter:none; opacity:.5}
}
@media (prefers-reduced-motion: reduce){
  .sigring{transition:none}
}

/* A tablet has room to put the day beside the ring rather than under it. */
@media (min-width:701px){
  .mh__sig{padding:22px 18px 24px}
  .sigring{width:214px; height:214px}
  .sigring__big{font-size:35px}
}
@media (min-width:701px) and (orientation:landscape){
  .mh{display:grid; grid-template-columns:minmax(0,42%) minmax(0,58%);
      align-items:start; column-gap:6px}
  .mh__sig{padding-top:26px}
  .mh__day{padding-top:22px; padding-right:18px}
  .mhsite{grid-column:1 / -1; width:calc(100% - 32px); margin-left:16px}
}

/* ---- 08 · SITE MODE — the site screen ───────────────────────────────────
   Not a restyle of the Signal: a different composition on the same ground.
   The ring shrinks to a mark beside "Stop 2 of 4" instead of owning the top
   third, because on site the question is never "how was the month". */
.mh--site .mh__sitehead{
  display:flex; align-items:center; gap:14px; padding:16px 18px 10px;
}
.mh__sigsmall{flex:0 0 auto}
.mh__sigsmall .sigring{width:84px; height:84px}
.mh__sigsmall .sigring__big{font-size:19px}
.mh__sitewho{min-width:0}
.mh__sitek{
  font-size:11px; font-weight:800; letter-spacing:.09em; text-transform:uppercase;
  color:rgba(255,255,255,.72);
}
.mh__sitet{display:block; font-size:23px; line-height:1.15; margin-top:2px; color:#fff}
.mh__sites{font-size:14px; color:rgba(255,255,255,.78); margin-top:3px}

/* One solid sheet. Glass is for chrome and this is the thing being read —
   outdoors, at arm's length, through a screen protector. */
.mhsite__card{
  margin:8px 14px 0; padding:16px; border-radius:12px;
  background:#fff; color:#12233B; display:grid; gap:14px;
}

.mhaddr{
  display:block; font-size:19px; line-height:1.3; color:#12233B;
  text-decoration:none; font-weight:600;
}
.mhaddr b{display:block; font-size:22px; font-weight:800}
.mhaddr span{display:block; color:#40566F}
.mhaddr--none{color:#6B7A8C; font-weight:600; font-size:16px}

.mhfacts{display:grid; gap:10px}
.mhfact{border-top:1px solid #E2E8EF; padding-top:10px}
.mhfact__k{
  font-size:11px; font-weight:800; letter-spacing:.09em; text-transform:uppercase;
  color:#5B6B7E;
}
.mhfact__v{font-size:16px; line-height:1.4; margin-top:3px; color:#12233B}
/* The one number somebody reads out loud at a keypad while holding a ladder. */
.mhfact--code .mhfact__v{
  font-size:34px; font-weight:800; letter-spacing:.12em;
  font-variant-numeric:tabular-nums;
}

/* The buttons inside the sheet are on white, so the ghost needs its own edge. */
/* One weight for all four, links and buttons alike — see the note below. */
.mhsite__card .mhbtn{min-height:60px; font-size:16px; border-radius:10px; font-weight:800}
.mhsite__card .mhbtn--ghost{
  background:#fff; color:#12233B; border:2px solid #12233B;
}
/* ⚠ AN ANCHOR IS NOT A BUTTON. Navigate and Call have to be links — only the
   browser can hand an address to a maps app or a number to the dialler — but
   an <a> takes none of the UA's button typography, so the two of them rendered
   in a lighter weight than the two real buttons beside them and read as a
   different class of control. */
.mhsite__card a.mhbtn{
  display:flex; align-items:center; justify-content:center; text-decoration:none;
  font:inherit; font-weight:800;
}

/* ---- The home runs edge to edge ─────────────────────────────────────────
   Greg, 2026-09-05: "I want to use the whole screen, but make sure logo stays
   fully visible on all devices."

   The field is already the whole display (.mh__ground above). This is the bar
   that sits on it: glass and a hairline border over a navy ground read as a
   grey smear, so on the home the bar carries nothing but the name. */
body.mh-open .topbar{
  background:transparent; border-bottom:0;
  backdrop-filter:none; -webkit-backdrop-filter:none;
}
body.mh-open .topbar__org b{ color:#fff; }
body.mh-open .topbar .chip{
  background:rgba(255,255,255,.16); color:#fff; border-color:rgba(255,255,255,.28);
}

/* ⚠ AND THE NAME CLEARS THE HARDWARE EVEN IF THE MEASUREMENT DID NOT LAND.
   --statuspad above is the real answer and it is exact. This is the floor
   under it for the one case that would be unrecoverable: a shell where both
   the measurement and env() came back as nothing, on a phone with a camera
   punched through the top of the screen. 34px clears a 24dp Android status
   bar; a deeper island reports itself through --statuspad and max() takes the
   larger. In a browser this never applies — is-native is only set by the
   Capacitor bridge — so nobody pays for it who does not have the hardware. */
body.is-native .topbar{
  padding-top:calc(10px + max(var(--statuspad, 0px),
                              env(safe-area-inset-top, 0px), 34px));
}



/* ---- Skeleton tiles ---------------------------------------------------------
   Drawn by OSP.tileSkeleton() while the invoice and estimate lists wait for
   their figures. Sized to the real hero and stat so nothing moves when the
   numbers land. */
.hero.is-skel{ min-height:150px; opacity:.55; animation:skelpulse 1.4s ease-in-out infinite; cursor:default; }
.stat.is-skel{ min-height:88px; opacity:.55; animation:skelpulse 1.4s ease-in-out infinite; cursor:default; }
@keyframes skelpulse{ 50%{ opacity:.28; } }
@media (prefers-reduced-motion:reduce){ .is-skel{ animation:none; } }

/* ---- Paying with a card on file (portal) ----------------------------------- */
/* Scoped to .paycards: the page-level .paycard (pay, estimate, receipt) must
   keep stacking; an unscoped rule here turned the Statement card into one row. */
.paycards{ display:flex; flex-direction:column; gap:8px; margin:6px 0 10px; }
/* Scoped to the picker: .paycard alone is the pay/estimate page's card, and an
   unscoped rule here squashed those pages into one row (Greg, 2026-09-05). The
   picker rows must also shed that card's max-width. */
.paycards .paycard{ display:flex; align-items:center; gap:12px; padding:11px 13px; cursor:pointer;
          background:var(--white); border:1px solid var(--hair); border-radius:12px; max-width:none; }
.paycards .paycard.is-on{ border-color:var(--accent); box-shadow:0 0 0 3px var(--tint, rgba(75,156,211,.16)); }
.paycards .paycard input{ margin:0; }
.paycard__main{ flex:1 1 auto; min-width:0; }
.paycard__main b{ display:block; font-size:14px; }
.paycard__main .sub{ font-size:12.5px; color:var(--steel); }

/* ---- Idle sign-out warning (OSP.idleGuard) -------------------------------- */
.idlebox{ position:fixed; inset:0; z-index:400; display:flex; align-items:center; justify-content:center;
          background:rgba(19,41,75,.38); backdrop-filter:blur(3px); }
.idlebox__card{ background:var(--white); color:var(--ink); border-radius:16px; padding:22px 26px;
                max-width:360px; width:calc(100% - 32px); box-shadow:var(--aura); text-align:center; }
.idlebox__card h3{ margin:0 0 6px; font-size:18px; }
.idlebox__card p{ margin:0 0 16px; color:var(--steel); }
.idlebox__card p b{ color:var(--ink); font-variant-numeric:tabular-nums; }

/* ---- Closing out a visit (jobs.js) ---------------------------------------- */
.jobclose{ margin-top:18px; }
.jobphotos{ margin:8px 0 12px; }
.gallery__tile{ position:relative; }
.gallery__x{ position:absolute; top:6px; right:6px; width:26px; height:26px; min-height:0; padding:0;
             border-radius:50%; background:rgba(19,41,75,.72); color:#fff; border:0; box-shadow:none;
             font-size:15px; line-height:1; }
.sigblock{ display:flex; flex-direction:column; gap:6px; margin:8px 0 12px; }
.sigblock__who{ font-size:13.5px; }
.sigimg{ max-width:320px; width:100%; border:1px solid var(--hair); border-radius:10px; background:#fff; }
/* ---- Walk-around clips (inspections) -------------------------------------- */
.inspclips{ margin:0 0 12px; }
.inspclips__t{ display:block; font-size:12px; text-transform:uppercase; letter-spacing:.06em; color:var(--mute); margin-bottom:6px; }
.inspclips__row{ display:flex; gap:10px; flex-wrap:wrap; }
.inspclip{ width:min(100%, 360px); border-radius:12px; background:#000; }
.insp_clips{ margin:12px 0 4px; }
.insp_clips__t{ display:block; font-size:12px; text-transform:uppercase; letter-spacing:.06em; color:var(--mute); margin-bottom:6px; }

/* ---- Inspection sheet, verdict first (inspection.js) ---------------------- */
.insp_ring{ display:flex; align-items:center; gap:14px; margin:14px 0 6px; }
.insp_ring__c{ width:64px; height:64px; border-radius:50%; display:grid; place-items:center; flex:0 0 auto; }
.insp_ring__c span{ width:48px; height:48px; border-radius:50%; background:var(--white); display:grid; place-items:center;
                    font:700 13px/1 ui-monospace,SFMono-Regular,Menlo,monospace; color:var(--ink); }
.insp_ring__t b{ display:block; font-size:17px; }
.insp_ring__t small{ color:var(--steel); font-size:13px; }
.insp_offer__list{ margin:4px 0 8px; }
.insp_offer__row{ display:flex; align-items:flex-start; gap:10px; padding:9px 0; border-top:1px solid var(--hair); }
.insp_offer__row:first-child{ border-top:0; }
.insp_offer__pip{ width:10px; height:10px; border-radius:50%; margin-top:6px; flex:0 0 auto; }
.insp_offer__pip.is-red{ background:var(--red); } .insp_offer__pip.is-amber{ background:var(--amber); }
.insp_offer__what{ flex:1 1 auto; min-width:0; }
.insp_offer__what b{ display:block; font-size:14.5px; }
.insp_offer__what small{ color:var(--steel); font-size:12.5px; }
.insp_offer__money{ font:600 14px ui-monospace,SFMono-Regular,Menlo,monospace; white-space:nowrap; }
.insp_offer__total{ display:flex; align-items:baseline; gap:10px; flex-wrap:wrap; padding:10px 0 12px; border-top:2px solid var(--hair-strong); }
.insp_offer__total span{ color:var(--steel); }
.insp_offer__total b{ font-size:22px; margin-left:auto; font-variant-numeric:tabular-nums; }
.insp_offer__total small{ width:100%; text-align:right; color:var(--steel); }
.insp_offer .button.block{ margin-top:6px; }

/* ---- The statement card (pay.js, estimate.js, portal home) -----------------

   Greg, 2026-09-05, sending the first cut back: "All the pills and tiles sit
   right on top of each other. those need to be spaced out." The first cut
   put the hero in and left every block under it with the margins it had
   before there was a hero — which was none, on the portal home, where the
   balance, the review ask, the estimate nudge and the three tiles were
   siblings touching edge to edge. One rhythm now: 16px between every block
   under the hero on the links, 16px between every card on the home, and
   air inside the hero between the figure, the date and the button. */
.stmt__hero{ background:var(--white); border:1px solid var(--hair); border-radius:14px; padding:22px 22px 20px;
             margin:18px 0 0; box-shadow:var(--aura); display:flex; flex-direction:column; gap:6px; }
.stmt__hero small{ font-size:11px; letter-spacing:.09em; text-transform:uppercase; color:var(--mute); font-weight:700; }
.stmt__n{ font-size:38px; line-height:1.1; font-variant-numeric:tabular-nums; letter-spacing:-.01em; }
.stmt__hero.is-paid .stmt__n{ color:var(--green); font-size:30px; }
.stmt__due{ font-size:13.5px; color:var(--steel); margin-top:2px; }
.stmt__due.is-late{ color:var(--red); font-weight:600; }
.stmt__pay{ margin-top:14px; font-size:16px; padding:13px; }
/* Decline sits under Approve, a step back from it. */
.stmt__hero > button.ghost.block{ margin-top:8px; }
/* The two ways to pay, as quiet pills under the button rather than a run of
   text. Wrapping onto two lines on a phone, each keeps its own air. */
.stmt__ways{ display:flex; flex-wrap:wrap; gap:8px; margin-top:12px; }
.stmt__ways span{ font-size:12.5px; color:var(--steel); font-variant-numeric:tabular-nums;
                  background:var(--paper); border:1px solid var(--hair); border-radius:99px; padding:5px 11px; }
.stmt .pay__deposit{ margin-top:6px; }
/* Two amounts due on a dual-priced invoice (task #19): the card figure and
   the cash / ACH / check figure side by side, each named underneath, the
   same size — neither is the "real" one, the customer picks. On a narrow
   phone the pair wraps and each keeps its name with it. */
.stmt__two{ display:flex; flex-wrap:wrap; gap:4px 28px; align-items:flex-end; }
.stmt__basis{ display:flex; flex-direction:column; gap:2px; min-width:0; }
.stmt__basis .stmt__n{ font-size:32px; }
.stmt__basis span{ font-size:11px; letter-spacing:.09em; text-transform:uppercase;
                   color:var(--steel); font-weight:700; }
.psummary__main .stmt__two{ margin:4px 0 2px; }

/* Under the hero on a link, every block is 16px from the last. The rules the
   blocks brought with them (a notice's own margin, a table butting a button)
   are set aside here so the page has one spacing and not six. */
.paycard.stmt > * + *{ margin-top:16px; }
.paycard.stmt > .pay__to{ padding:0; }
.paycard.stmt > .pay__to + .pay__to{ margin-top:14px; }
.paycard.stmt > .docfields{ padding:14px 0 0; }
.paycard.stmt > .jobsheet{ margin:20px 0 0; }
.paycard.stmt > .opts, .paycard.stmt > .rec__scope, .paycard.stmt > .note{ margin:16px 0 0; }
.paycard.stmt > .pay__foot{ margin-top:24px; }

/* The fold: what is under the figure, one tap away (OSP.fold, lib.js). */
.stmt__fold{ border:1px solid var(--hair); border-radius:12px; background:var(--white); overflow:hidden; }
.stmt__fold > summary{ list-style:none; cursor:pointer; display:flex; align-items:center; gap:12px;
                       padding:14px 16px; font-size:14px; font-weight:600; user-select:none; }
.stmt__fold > summary::-webkit-details-marker{ display:none; }
.stmt__fold > summary:hover{ background:var(--paper); }
.stmt__fold > summary:focus-visible{ outline:2px solid var(--accent); outline-offset:-2px; }
.stmt__fold__t{ flex:1 1 auto; min-width:0; }
.stmt__fold__n{ color:var(--steel); font-weight:400; font-size:13px; white-space:nowrap; font-variant-numeric:tabular-nums; }
.stmt__fold__chev{ color:var(--mute); font-size:14px; line-height:1; transition:transform .15s ease; }
.stmt__fold[open] > summary .stmt__fold__chev{ transform:rotate(180deg); }
.stmt__fold__body{ border-top:1px solid var(--hair); }
.stmt__fold .tablewrap{ border:0; border-radius:0; box-shadow:none; }
.stmt__fold .irows .irow:first-child{ border-top:0; }

/* The portal home: the same card, then everything under it as cards on one
   grid with one gap. The home is wide, so the button sits beside the figure
   rather than stretching under it; on a phone it drops back underneath. */
.phome{ display:grid; gap:16px; }
.phome > .stmt__hero{ margin:0; border-radius:18px; border-color:rgba(16,24,32,.06);
                      display:grid; grid-template-columns:minmax(0,1fr) auto; column-gap:28px; row-gap:6px;
                      align-items:center; }
.phome > .stmt__hero.is-paid{ grid-template-columns:1fr; }
.phome > .stmt__hero > *{ grid-column:1; }
.phome > .stmt__hero > .stmt__pay{ grid-column:2; grid-row:1 / span 3; margin:0; width:auto; min-width:220px;
                                   padding:13px 26px; }
.phome > .stmt__fold{ border-radius:18px; border-color:rgba(16,24,32,.06); box-shadow:0 1px 3px rgba(16,24,32,.05); }
.phome .pgrid{ gap:16px; }
.phome .pcard h2{ margin-bottom:12px; }
.phome .pcard .plink{ margin-top:14px; }
@media (max-width:640px){
  .phome > .stmt__hero{ grid-template-columns:1fr; }
  .phome > .stmt__hero > .stmt__pay{ grid-column:1; grid-row:auto; width:100%; margin-top:10px; }
}

/* ---- The ISO desk (platform.js) --------------------------------------------
   Greg, 2026-09-05: "option 2 with more aura". The merchant list on the left,
   the book's numbers on the right, and every card on this host lifted a
   little further off the page. A merchant opens as the ordinary record
   console — Greg, later the same day, on the merchant page: "the record
   console, because it is the shape every other record already has" — so
   nothing below reshapes `.console` or `.who`. */
:root[data-host="iso"]{
  --aura:
    0 1px 2px rgba(19,41,75,.08),
    0 14px 34px -12px rgba(19,41,75,.55),
    0 4px 12px -4px rgba(42,110,158,.28);
  --aura-soft:
    0 1px 2px rgba(19,41,75,.06),
    0 8px 22px -12px rgba(19,41,75,.32);
}
:root[data-host="iso"][data-theme="dark"], :root[data-host="iso"][data-theme="system-dark"]{
  --aura:0 1px 2px rgba(0,0,0,.4), 0 14px 34px -12px rgba(0,0,0,.85), 0 4px 12px -4px rgba(75,156,211,.35);
}
.isodesk{ display:grid; grid-template-columns:320px minmax(0,1fr); gap:18px; align-items:start; }
.isodesk__list{ position:sticky; top:var(--stick); background:var(--white); border:1px solid var(--hair);
                border-radius:var(--r); box-shadow:var(--aura); padding:12px; display:flex; flex-direction:column; gap:8px;
                max-height:calc(100vh - var(--stick) - 16px); }
.isodesk__head{ display:flex; align-items:center; justify-content:space-between; gap:8px; }
.isodesk__head h1{ font-size:18px; margin:0; }
.isodesk__chips{ margin:0; flex-wrap:wrap; }
.isodesk__chips .tab{ padding:4px 10px; font-size:12px; }
/* The three selects under the chips (#26): under whom, which program, which
   trade. Quiet until one is set, then the border picks up the accent so a
   narrowed list says why it is short without a banner. */
.isodesk__filters{ display:flex; gap:6px; flex-wrap:wrap; }
.isodesk__filters select{ flex:1 1 90px; min-width:0; width:auto; padding:5px 26px 5px 8px; font-size:12px;
                          background-position:right 8px center; }
.isodesk__filters select.is-set{ border-color:var(--accent); color:var(--ink); }
.isodesk__count{ font-size:12px; color:var(--mute); display:flex; justify-content:space-between; align-items:center; }
.isodesk__clear{ font-size:12px; padding:0; }
.isodesk__rows{ overflow:auto; flex:1 1 auto; min-height:120px; display:flex; flex-direction:column; gap:6px; }
.mrow{ display:flex; align-items:center; gap:10px; padding:9px 10px; border-radius:11px; cursor:pointer;
       border:1px solid transparent; }
.mrow:hover{ background:var(--band); }
.mrow.is-on{ background:var(--band); border-color:var(--accent); box-shadow:inset 3px 0 0 var(--accent); }
.mrow__main{ flex:1 1 auto; min-width:0; }
.mrow__main b{ display:block; font-size:14px; white-space:nowrap; overflow:hidden; text-overflow:ellipsis; }
.mrow__main .sub{ font-size:12px; color:var(--steel); white-space:nowrap; overflow:hidden; text-overflow:ellipsis; }
.mrow__side{ display:flex; flex-direction:column; align-items:flex-end; gap:3px; flex:0 0 auto; }
/* This month's processed volume, on the row. Steel, not amber: what a
   merchant ran is a fact to read, not a fault to chase (#25 — the amber
   figure here used to be what the merchant was owed). */
.mrow__vol{ font-size:12px; font-variant-numeric:tabular-nums; color:var(--steel); font-weight:600; }
.isodesk__main{ min-width:0; }
@container view (max-width:1120px){
  .isodesk{ grid-template-columns:1fr; }
  .isodesk__list{ position:static; max-height:none; }
  .isodesk__rows{ max-height:260px; }
}

/* ── What's new — the platform's changelog in the partner console ──────────
   (views/whats-new.js). A day is a card with its date down the left and its
   entries beside it, so a week of work reads as a week and not as a wall.
   "New" is --accent, not --red: it is something to look at, not a warning. */
.whatsnew{ display:grid; gap:14px; max-width:880px; }
.whatsnew__lead{ margin:-6px 0 14px; font-size:13px; }
.whatsnew__day{ display:grid; grid-template-columns:150px minmax(0,1fr); gap:18px;
                background:var(--solid-bg); border:1px solid var(--hair); border-radius:var(--r);
                box-shadow:var(--aura-soft); padding:16px 18px; }
.whatsnew__date{ display:flex; flex-direction:column; gap:2px; padding-top:2px; }
.whatsnew__date b{ font-size:14px; }
.whatsnew__items{ display:grid; gap:14px; min-width:0; }
.whatsnew__item + .whatsnew__item{ border-top:1px solid var(--hair); padding-top:14px; }
.whatsnew__head{ display:flex; align-items:center; gap:9px; flex-wrap:wrap; margin-bottom:6px; }
.whatsnew__title{ font-size:14.5px; }
.whatsnew__act{ display:flex; gap:10px; margin-left:auto; }
.whatsnew__body p{ margin:0 0 6px; line-height:1.55; font-size:13.5px; color:var(--ink2); }
.whatsnew__body p:last-child{ margin-bottom:0; }
.chip.whatsnew__new{ background:rgba(75,156,211,.16); color:var(--accent); font-weight:700; }
.whatsnew-card .rowline .chip{ flex-shrink:0; margin-top:2px; }
@container view (max-width:640px){
  .whatsnew__day{ grid-template-columns:1fr; gap:10px; }
  .whatsnew__date{ flex-direction:row; align-items:baseline; gap:8px; }
}

/* ── ISO dashboard: the period pills and what sits under them ─────────────
   Greg, 2026-09-05 (#25): "text under pills at the top is touching the pills."
   Two things touched. `.tabs` carries a 14px bottom margin for a strip that
   stands above content; inside the centred flex toolbar that margin sat the
   caption low against the pills' bottom edge, and the tiles' distance from the
   pills was borrowed from it. The margin comes off, the caption centres on the
   pills, the rows get 12px when the caption wraps under them, and the toolbar
   owns a real 18px before the tiles. The cash-and-check note under the tiles
   was pulled up 4px into them; it stands 12px clear now and 16px above the
   cards. */
.isodash__bar{ gap:12px 16px; margin-bottom:18px; }
.isodash__bar > .tabs{ margin-bottom:0; }
.isodash__scope{ margin-left:auto; font-size:12px; line-height:1.4; }
.tiles + .isodash__offline{ margin:12px 0 16px; line-height:1.5; }

/* ---- The site diagram (#43, Greg 2026-09-06: Design B holding A) ------------

   The drawing is navy on the band, because it is a document rather than a
   photograph: hairlines read as walls, the accent is reserved for the stations
   because those are the things you press, and red is kept for a finding that
   is actually urgent. Everything is sized in FEET through the viewBox, so a
   rule here in px would fight the scale — hence stroke-width in feet-ish
   fractions and font-size on the text elements rather than a class. */

.dgm { display:block; width:100%; height:auto; background:var(--white);
       border-radius:10px; }
.dgm__base { stroke:none; }
.dgm__grid { fill:none; stroke:var(--hair); stroke-width:.12; }
.dgm__structure { fill:rgba(19,41,75,.05); stroke:var(--ink); stroke-width:.9;
                  stroke-linejoin:round; }
.dgm__zone { stroke-width:.5; stroke-dasharray:2 1.4; }
.dgm__zone--treated { fill:rgba(86,169,220,.16); stroke:var(--accent); }
.dgm__zone--notreat { fill:rgba(180,35,24,.10); stroke:var(--red); }
.dgm__line { fill:none; stroke:var(--steel); stroke-width:.6; stroke-dasharray:1.8 1.2; }
.dgm__len { font:600 2.4px "Source Sans 3", system-ui, sans-serif; fill:var(--ink2);
            paint-order:stroke; stroke:var(--white); stroke-width:.7; stroke-linejoin:round; }
.dgm__note { font:500 2.6px "Source Sans 3", system-ui, sans-serif; fill:var(--ink2);
             paint-order:stroke; stroke:var(--white); stroke-width:.7; }
.dgm__draft { fill:rgba(86,169,220,.12); stroke:var(--accent); stroke-width:.7;
              stroke-dasharray:1.6 1.2; }
.dgm__handle { fill:var(--white); stroke:var(--accent); stroke-width:.5; }

.dgm__devshape { fill:var(--accent); stroke:var(--white); stroke-width:.55; }
.dgm__devnum { font:700 2.2px "IBM Plex Mono", ui-monospace, monospace; fill:var(--white); }
.dgm__dev.is-on .dgm__devshape { fill:var(--ink); stroke:var(--accent); stroke-width:.8; }
.dgm__find rect { fill:var(--amber, #8A6100); stroke:var(--white); stroke-width:.45; }
.dgm__find--urgent rect { fill:var(--red); }
.dgm__find--note rect { fill:var(--steel); }
.dgm__north, .dgm__scale { fill:none; stroke:var(--ink2); stroke-width:.5; }
.dgm__scaletext { font:600 2.2px "Source Sans 3", system-ui, sans-serif; fill:var(--ink2); }

/* ---- The tab on the property record ---------------------------------------- */
.dgmtab__head { display:flex; align-items:center; justify-content:space-between;
                gap:12px; flex-wrap:wrap; margin-bottom:10px; }
.dgmtab__stats { display:flex; gap:22px; flex-wrap:wrap; margin-bottom:12px; }
.dgmstat { display:flex; flex-direction:column; gap:1px; }
.dgmstat__k { font-size:11px; letter-spacing:.06em; text-transform:uppercase; color:var(--mute); font-weight:700; }
.dgmstat__v { font-size:19px; font-variant-numeric:tabular-nums; }
.dgmtab__map { border:1px solid var(--hair); border-radius:12px; overflow:hidden;
               background:var(--white); }
.dgmlegend { display:flex; gap:16px; flex-wrap:wrap; margin-top:10px; font-size:12.5px; color:var(--steel); }
.dgmlegend__i { display:inline-flex; align-items:center; gap:6px; }
.dgmlegend__g { width:14px; height:14px; }

/* ---- The full-screen editor ------------------------------------------------- */
.dgmed { position:fixed; inset:0; z-index:60; background:var(--band);
         display:flex; flex-direction:column; }
.dgmed__bar { display:flex; align-items:center; gap:14px; padding:10px 16px;
              background:var(--white); border-bottom:1px solid var(--hair); }
.dgmed__title { flex:1; min-width:0; overflow:hidden; text-overflow:ellipsis; white-space:nowrap; }
.dgmed__acts { display:flex; gap:8px; }
.dgmed__body { flex:1; display:grid; grid-template-columns:172px minmax(0,1fr) 250px;
               min-height:0; }
.dgmed__pal, .dgmed__side { background:var(--white); padding:12px 14px; overflow:auto; }
.dgmed__pal { border-right:1px solid var(--hair); }
.dgmed__side { border-left:1px solid var(--hair); }
.dgmed__palhead { font-size:11px; letter-spacing:.07em; text-transform:uppercase;
                  color:var(--mute); font-weight:700; margin:0 0 8px; }
.dgmed__tools { display:flex; flex-direction:column; gap:4px; }
.dgmtool { text-align:left; background:transparent; border:1px solid transparent;
           border-radius:8px; padding:7px 10px; font-size:13.5px; font-weight:600;
           color:var(--ink2); cursor:pointer; }
.dgmtool:hover { background:var(--band); }
.dgmtool.is-on { background:var(--ink); color:var(--white); border-color:var(--ink); }
.dgmed__kinds { margin-top:14px; }
.dgmed__hint { font-size:12.5px; color:var(--mute); margin-top:12px; }
.dgmed__canvas { padding:16px; overflow:auto; display:flex; align-items:center;
                 justify-content:center; }
.dgmed__canvas .dgm { max-height:calc(100vh - 110px); }
.dgmed__do { margin-top:14px; display:flex; flex-direction:column; gap:6px; }

@media (max-width:900px) {
  .dgmed__body { grid-template-columns:1fr; grid-template-rows:auto minmax(0,1fr) auto; }
  .dgmed__pal { border-right:0; border-bottom:1px solid var(--hair); }
  .dgmed__tools { flex-direction:row; flex-wrap:wrap; }
  .dgmed__side { border-left:0; border-top:1px solid var(--hair); max-height:38vh; }
}

/* Undo, delete and the keys that do them (#43, second pass). */
.dgm__structure.is-on, .dgm__zone.is-on { stroke:var(--accent); stroke-width:1.4; }
.dgm__line.is-on { stroke:var(--accent); stroke-width:1; }
.dgm__note.is-on { fill:var(--accent); }
.dgmed__keys { margin-top:14px; display:flex; flex-direction:column; gap:5px;
               font-size:12px; color:var(--mute); }
.dgmed__keys kbd { font:600 11px "IBM Plex Mono", ui-monospace, monospace; background:var(--band);
                   border:1px solid var(--hair); border-radius:5px; padding:1px 5px; color:var(--ink2); }
.dgmed__acts button:disabled { opacity:.4; cursor:default; }

/* ── The price book ─────────────────────────────────────────────────────────

   Two views of one book. The MENU is what a technician turns round to show a
   homeowner, so it is set larger than anything else in the product and the
   price is the loudest thing on the card. The LIST is a table like every other
   table here and needs almost nothing of its own.

   The art is a hairline drawing in the accent, on a band tile — glass would be
   wrong under it, because this is content rather than chrome and it gets read
   on a tablet in a driveway. */

.bookmenu { display:flex; flex-direction:column; gap:26px; }
.booksec__h {
  font-size:13px; letter-spacing:.08em; text-transform:uppercase;
  color:var(--steel); font-weight:700; margin:0 0 10px;
  padding-bottom:6px; border-bottom:1px solid var(--hair);
}
.bookcards {
  display:grid; gap:12px;
  grid-template-columns:repeat(auto-fill, minmax(320px, 1fr));
}

.bookcard {
  display:flex; gap:14px; align-items:flex-start;
  padding:14px; border:1px solid var(--hair); border-radius:var(--r);
  background:var(--paper); box-shadow:var(--shadow-sm);
  cursor:pointer; text-align:left;
  transition:box-shadow .14s ease, border-color .14s ease;
}
.bookcard:hover, .bookcard:focus-visible {
  border-color:var(--hair-strong); box-shadow:var(--shadow);
}
.bookcard--off { opacity:.55; }
/* A tier card is the comparison, so it gets the full width of the grid rather
   than being squeezed into a column beside single tasks. */
.bookcard--tiers { grid-column:1 / -1; }
.bookcard__t { flex:1; min-width:0; }
.bookcard__top {
  display:flex; align-items:center; gap:8px; margin-bottom:4px; min-height:20px;
}
.bookcard__n { display:block; font-size:16px; line-height:1.25; color:var(--ink); }
.bookcard__s {
  margin:4px 0 0; font-size:12.5px; line-height:1.45; color:var(--steel);
}
.bookcard__foot {
  display:flex; align-items:baseline; gap:10px; margin-top:10px; flex-wrap:wrap;
}

.bookart {
  flex:none; width:46px; height:46px; border-radius:var(--r-sm);
  background:var(--band); color:var(--accent);
  display:flex; align-items:center; justify-content:center;
}
.bookart--photo { overflow:hidden; background:var(--band); }
.bookart--photo img { width:100%; height:100%; object-fit:cover; display:block; }
.bookart__i { width:24px; height:24px; }

.bookcode {
  font-family:ui-monospace, SFMono-Regular, Menlo, monospace;
  font-size:11px; letter-spacing:.04em; font-weight:600;
  color:var(--steel); background:var(--band);
  padding:2px 6px; border-radius:6px; white-space:nowrap;
}

.bookprice { font-size:18px; font-variant-numeric:tabular-nums; color:var(--ink); }
/* A derived price is the GOOD state, so the mark is quiet: a dotted rule
   under it that says "ask me where this came from", not a warning colour. */
.bookprice--derived {
  border-bottom:1px dotted var(--hair-strong); cursor:help;
}

.booktiers { display:flex; gap:8px; margin-top:10px; flex-wrap:wrap; }
.booktier {
  flex:1 1 160px; min-width:0; text-align:left; cursor:pointer;
  padding:10px 12px; border:1px solid var(--hair); border-radius:var(--r-sm);
  background:var(--band); color:inherit; font:inherit;
}
.booktier:hover, .booktier:focus-visible {
  border-color:var(--accent); background:var(--paper);
}
.booktier__lab {
  display:block; font-size:11px; letter-spacing:.07em; text-transform:uppercase;
  color:var(--accent); font-weight:700;
}
.booktier__p {
  display:block; font-size:17px; margin-top:2px;
  font-variant-numeric:tabular-nums; color:var(--ink);
}
.booktier__n {
  display:block; font-size:11.5px; color:var(--steel); margin-top:2px;
  overflow:hidden; text-overflow:ellipsis; white-space:nowrap;
}

.bookrowacts { display:flex; gap:6px; justify-content:flex-end; }

/* The empty book. The only moment the screen gets to say what it is. */
.bookzero {
  max-width:620px; margin:24px auto; text-align:center;
  padding:30px 24px; border:1px solid var(--hair); border-radius:var(--r-lg);
  background:var(--paper); box-shadow:var(--shadow-sm);
}
.bookzero h2 { margin:0 0 10px; font-size:21px; color:var(--ink); }
.bookzero p { margin:0 0 10px; line-height:1.55; color:var(--ink2); }
.bookzero p.muted { font-size:12.5px; }
.bookzero__acts {
  display:flex; gap:10px; justify-content:center; margin-top:18px; flex-wrap:wrap;
}

/* The editor. A form of short fields over a list of lines, and the lines are
   a grid so the four columns line up down the dialog rather than each row
   finding its own widths. */
.bookform {
  display:grid; gap:12px; grid-template-columns:repeat(auto-fit, minmax(190px, 1fr));
}
.booklines { display:flex; flex-direction:column; gap:6px; margin-top:6px; }
.bookline {
  display:grid; gap:6px; align-items:center;
  grid-template-columns:104px 1fr 74px 88px 32px;
}
.bookline input, .bookline select { margin:0; }
.booktotal { margin-top:8px; font-size:12px; color:var(--steel); }

@media (max-width:640px) {
  .bookline { grid-template-columns:1fr 1fr; }
  .bookline > :nth-child(2) { grid-column:1 / -1; }
  .bookcards { grid-template-columns:1fr; }
}

/* The line that says which rate the whole book was priced from. Quiet when it
   is fine, amber when it is the reason everything reads zero. */
.bookrate {
  margin:0 0 14px; padding:9px 13px; border-radius:var(--r-sm);
  background:var(--band); border:1px solid var(--hair);
  font-size:12.5px; color:var(--steel);
}
.bookrate b { color:var(--ink); font-variant-numeric:tabular-nums; }
.bookrate__warn { color:var(--amber); }
.bookrate--warn {
  background:var(--amber-bg); border-color:var(--amber); color:var(--amber);
}
.bookrate--warn b { color:var(--amber); }

/* The way out of the "no rate" strip. A button rather than a sentence naming
   a tab: a signpost that is nearly right sends somebody looking and then
   convinces them the screen is broken. */
.bookrate__go { margin-left:8px; font-size:12.5px; }
.bookrate--warn .bookrate__go { color:var(--amber); text-decoration:underline; }

/* The two halves of the price book. The tab strip is ui.recordPage's, reused
   rather than reinvented so a tab here and a tab on a customer record are
   recognisably the same control. */
.booktabs { margin: 0 0 14px; }
.rectab__n {
  margin-left: 7px; font-size: 11px; font-variant-numeric: tabular-nums;
  color: var(--mute); background: var(--band);
  padding: 1px 6px; border-radius: 99px;
}
.rectab.is-on .rectab__n { color: var(--accent); }

/* What the merge hid, and the way to look behind it. Deliberately quiet: it
   reports a decision the software made, it is not a warning. */
.bookhidden {
  display: flex; align-items: baseline; gap: 10px; flex-wrap: wrap;
  margin: 0 0 12px; padding: 8px 13px; border-radius: var(--r-sm);
  background: var(--band); border: 1px solid var(--hair);
  font-size: 12.5px; color: var(--steel);
}
.chip--inside { background: var(--tint); color: var(--accent); }

/* ── Stock ──────────────────────────────────────────────────────────────────

   The count is the loudest thing on the row because it is the only number
   anybody opened the screen for. Amber at or below the reorder level, red
   under zero — which is allowed, and is the one worth chasing. */
.stockn { font-size:15px; font-variant-numeric:tabular-nums; color:var(--ink); }
.stockn__u { font-size:11px; font-weight:400; color:var(--mute); margin-left:2px; }
.stockn--low { color:var(--amber); }
.stockn--short { color:var(--red); }

.chip--short { background:var(--red-bg); color:var(--red); }
.chip--onorder { background:var(--tint); color:var(--accent); }
/* Quiet, because a delivery driving the cost is the good state — but present,
   because a supplier's rise reaching a customer silently is the failure. */
.chip--fromdelivery {
  background:var(--band); color:var(--steel); margin-left:6px; cursor:help;
}
.chip--po-draft { background:var(--band); color:var(--steel); }
.chip--po-sent { background:var(--tint); color:var(--accent); }
.chip--po-partial { background:var(--amber-bg); color:var(--amber); }
.chip--po-received { background:rgba(47,163,122,.14); color:var(--green); }
.chip--po-cancelled { background:var(--band); color:var(--mute); }


/* A SPEC SHEET ON A WHITE CARD — its own class, not .who__k with an override.
   The plan facts first borrowed the dark panel's list and came out pale blue
   and right-aligned on white, because `.who__k dd` sets a colour and an
   alignment further down the file and wins on source order. Borrowing a
   component written for one ground and patching it for another is how that
   happens twice, so this is a list of its own. */
.factlist { display:grid; grid-template-columns:150px minmax(0, 1fr); gap:0; margin:8px 0 12px; }
.factlist dt, .factlist dd { padding:7px 0; font-size:13px; margin:0; align-self:stretch;
                             border-top:1px solid var(--hair); }
.factlist dt { color:var(--muted); padding-right:14px; }
.factlist dd { color:var(--ink); min-width:0; overflow-wrap:anywhere; }
.factlist > dt:first-child, .factlist > dt:first-child + dd { border-top:0; padding-top:0; }
@media (max-width:560px) { .factlist { grid-template-columns:1fr; }
  .factlist dd { padding-top:0; border-top:0; } }

/* ---- Documents and signatures (F9, 2026-09-06) --------------------------

   Two surfaces: the office's placement editor, and the page a customer signs
   on. They share one idea — a page image with absolutely-positioned boxes on
   top of it, sized in PERCENTAGES so the same fractions land in the same place
   on a laptop, a tablet and a phone. If a rule here ever uses px for a field
   box, the box has stopped being the thing the office placed. */

.segbtns { display:flex; gap:0; }
.segbtns button { border-radius:0; border:1px solid var(--hair-strong); margin-left:-1px;
  background:var(--paper); color:var(--ink); font-weight:600; }
.segbtns button:first-child { border-radius:8px 0 0 8px; margin-left:0; }
.segbtns button:last-child { border-radius:0 8px 8px 0; }
.segbtns button.is-on { background:var(--accent); border-color:var(--accent); color:#fff; }

.doc__body { width:100%; font:13px/1.55 ui-monospace, SFMono-Regular, Menlo, monospace;
  color:var(--ink); background:var(--paper); border:1px solid var(--hair-strong);
  border-radius:8px; padding:12px; resize:vertical; }

.fieldbar { position:sticky; top:0; z-index:3; display:flex; flex-wrap:wrap; gap:6px;
  align-items:center; padding:10px 0; background:var(--paper); }
.fieldbar__hint { color:var(--mute); font-size:12px; margin-right:4px; }
.fieldbar__k { font-size:12px; padding:5px 10px; border-radius:999px;
  border:1px solid var(--hair-strong); background:transparent; color:var(--ink); }
.fieldbar__k.is-on { background:var(--accent); border-color:var(--accent); color:#fff; }

.doceditor { margin-top:8px; }
.docpage, .signpage { position:relative; margin:0 auto 18px; max-width:820px;
  border:1px solid var(--hair-strong); border-radius:6px; overflow:hidden;
  background:#fff; }
.docpage__img, .signpage__img { display:block; width:100%; height:auto; }
.docpage__no { position:absolute; top:6px; right:8px; font-size:11px; color:var(--mute);
  background:rgba(255,255,255,.85); padding:1px 6px; border-radius:4px; }

/* A placed field, in the editor. Percentages throughout — see the note above. */
.fieldbox { position:absolute; border:1.5px solid var(--accent);
  background:rgba(75,156,211,.14); border-radius:3px; cursor:move;
  display:flex; align-items:center; padding:0 4px; touch-action:none; }
.fieldbox--signature { border-style:solid; background:rgba(75,156,211,.20); }
.fieldbox--checkbox { padding:0; }
.fieldbox__label { font-size:10px; color:var(--ink); font-weight:600;
  white-space:nowrap; overflow:hidden; text-overflow:ellipsis; pointer-events:none; }
.fieldbox__x { position:absolute; top:-9px; right:-9px; width:18px; height:18px;
  line-height:1; border-radius:50%; border:0; background:var(--red, #B3153A); color:#fff;
  font-size:13px; padding:0; cursor:pointer; }
.fieldbox__grip { position:absolute; right:-4px; bottom:-4px; width:12px; height:12px;
  background:var(--accent); border-radius:2px; cursor:nwse-resize; }

.seal { font:11px/1.4 ui-monospace, SFMono-Regular, Menlo, monospace;
  overflow-wrap:anywhere; color:var(--ink2); }
.rechead { display:flex; align-items:center; gap:10px; flex-wrap:wrap; margin-bottom:12px; }
.rechead h3 { margin:0; }

/* ---- The page a customer signs on --------------------------------------- */
.signwrap { max-width:880px; margin:0 auto; padding:20px 16px 64px; }
.signhead h1 { margin:0 0 2px; font-size:22px; }
.signdoc { margin:18px 0 26px; }
.signdoc--text { border:1px solid var(--hair-strong); border-radius:8px;
  background:#fff; padding:22px 24px; }
.signdoc__text { margin:0; white-space:pre-wrap; overflow-wrap:anywhere;
  font:14px/1.65 -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
  color:#13294B; }

/* A field on the signing page. Same percentage geometry as the editor's box,
   which is the whole reason a template built on a laptop signs on a phone. */
.signfield { position:absolute; font-size:12px; padding:0 4px; border-radius:3px;
  border:1px solid var(--accent); background:rgba(255,255,255,.92); color:#13294B; }
.signfield--sig { display:flex; align-items:center; justify-content:center;
  font-size:11px; color:var(--accent); cursor:pointer;
  background:rgba(75,156,211,.10); border-style:dashed; }
.signfield--check { padding:0; }
.signfield__ink { max-width:100%; max-height:100%; object-fit:contain; }

.signfoot { border-top:2px solid var(--hair-strong); padding-top:20px; }
.signfoot h2 { margin:0 0 12px; font-size:17px; }
.esign { border:1px solid var(--hair-strong); border-radius:8px; padding:14px 16px;
  background:var(--band); margin-bottom:16px; }
.esign__body { margin:0 0 12px; white-space:pre-wrap; overflow-wrap:anywhere;
  font:12.5px/1.6 -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
  color:var(--ink2); max-height:220px; overflow-y:auto; }
button.big { font-size:16px; padding:12px 22px; }

/* The evidence table: the moment is one line, because a date broken across
   three lines is harder to read out than it is to read. The device string
   stays whole and wraps — it is evidence, not decoration, and truncating it
   would mean the screen and the certificate disagree. */
.evidence td:first-child { white-space:nowrap; }
.evidence td:last-child { font-size:11.5px; line-height:1.45; }

/* ---- Pay runs (F4, 2026-09-07) ------------------------------------------

   Two surfaces with opposite jobs. The run is a dense list somebody scans for
   the wrong one; the person's own statement is one number with its working
   under it. So the run gets a table and the statement gets a figure. */

.paytotal { font-size:15px; }
.paytotal strong { font-size:19px; font-variant-numeric:tabular-nums; }
.toolbar .sp { flex:1; }

.payperson { border:1px solid var(--hair); border-radius:10px; background:var(--paper);
  margin-bottom:10px; padding:0 14px; }
.payperson > summary { display:flex; align-items:center; gap:10px; cursor:pointer;
  padding:13px 0; list-style:none; font-size:15px; }
.payperson > summary::-webkit-details-marker { display:none; }
.payperson > summary::before { content:"▸"; color:var(--mute); font-size:11px;
  transition:transform .12s ease; }
.payperson[open] > summary::before { transform:rotate(90deg); }
.payperson__who { font-weight:600; }
.payperson__total { margin-left:auto; font-weight:700; font-variant-numeric:tabular-nums; }
.payperson .tablewrap { margin-bottom:12px; }

/* The blockers. Amber rather than red: nothing is broken, something is
   unfinished, and the difference matters to whoever reads it at 6pm. */
.blockers { border-color:var(--amber, #B7791F); }
.blockers__list { margin:0; padding-left:20px; }
.blockers__list li { margin:6px 0; line-height:1.5; }
.blockers__list strong { color:var(--ink); }
.blockers__list span { color:var(--muted); }

/* One number, and the working under it. */
.paybig { padding:6px 0 18px; }
.paybig__amt { font-size:40px; font-weight:700; letter-spacing:-.02em; line-height:1.05;
  font-variant-numeric:tabular-nums; }
.paybig__sub { color:var(--muted); font-size:14px; margin-top:3px; }
@media (max-width:560px) { .paybig__amt { font-size:32px; } }

/* The rules that stack on a pay plan. Each is a small card because a rule is
   one idea with four settings in it, and a flat list of sixteen fields reads
   as a form nobody finishes. */
.payrules__h { margin:22px 0 2px; font-size:15px; }
.payrules { display:flex; flex-direction:column; gap:10px; margin:10px 0; }
.payrule { border:1px solid var(--hair-strong); border-radius:10px; padding:12px 14px;
  background:var(--band); }
.payrule__head { display:flex; align-items:center; gap:8px; margin-bottom:8px; }
.payrule__n { color:var(--muted); font-size:12.5px; flex:none; }
.payrule__head select { flex:1; }
.payrule__x { flex:none; padding:2px 9px; font-size:16px; line-height:1.2; }
.payrule .field { margin:8px 0 0; }
.payrule .inline { display:flex; align-items:center; gap:8px; flex-wrap:wrap; }
.payrule .inline select { flex:1; min-width:150px; }

/* ---- Licences and credentials (2026-09-07) ------------------------------

   The warning is amber, never red. Greg chose warn over refuse, and a red
   banner on something that is not being stopped teaches people that red means
   nothing. */
.credwarn { border-left:3px solid var(--amber, #B7791F); background:var(--band);
  border-radius:0 8px 8px 0; padding:10px 14px; margin:10px 0; }
.credwarn__row { line-height:1.5; }
.credwarn__row + .credwarn__row { margin-top:6px; }
.credwarn__row strong { color:var(--ink); }
.credwarn__row span { color:var(--muted); }

.credpicks { display:flex; flex-wrap:wrap; gap:6px 16px; }
.credpicks .check { margin:0; }

/* On the roster, beside the duty switch, because that row already answers
   "can this person be sent out". */
.credchip { display:inline-flex; align-items:center; gap:5px; font-size:11.5px; }
.credwarn__who { font-size:12px; font-weight:600; color:var(--muted); margin-top:8px; }

/* ── The application record (2026-09-07) ─────────────────────────────────────
   Three small things, modelled on .credwarn, which is the nearest thing in the
   product: a strip that says whose rule this visit is under, a warning block
   that never looks like an error because it never blocks anything, and a card
   per state on the reporting screen. */
.appstate { display:flex; align-items:baseline; gap:8px; flex-wrap:wrap;
            margin:0 0 10px; font-size:13px; color:var(--muted); }
.appstate strong { font-size:14px; color:var(--ink); letter-spacing:.02em; }

.appwarn { border-left:3px solid var(--amber, #B7791F); background:var(--band);
           border-radius:0 8px 8px 0; padding:10px 14px; margin:0 0 12px; }
.appwarn__row { line-height:1.5; }
.appwarn__row + .appwarn__row { margin-top:8px; }
.appwarn__row.is-bad { color:var(--bad, #B42318); }
.appwarn__cite { font-size:11.5px; color:var(--muted); margin-top:2px;
                 font-variant-numeric:tabular-nums; }

.statecard { border:1px solid var(--hair); border-radius:12px; padding:14px 16px;
             background:var(--paper); }
.statecard__h { display:flex; align-items:baseline; gap:8px; flex-wrap:wrap;
                margin-bottom:4px; }
.statecard__h strong { font-size:17px; letter-spacing:.04em; }
.statecard__cite { font-size:11.5px; color:var(--muted); margin:2px 0 6px; }

/* Two fields on one line inside a modal form, which .form has no shape for. */
.pairrow { display:flex; gap:12px; flex-wrap:wrap; }
.pairrow > * { flex:1 1 150px; min-width:0; }
