/* Forge Field — app shell stylesheet (REWRITE R1).
   Extracted mechanically from handoff-kit/mockups/forge-field-receipts.html <style>.
   Source revision ported: sha256 5e77171a46684c58e4c09eb753824a904c5ee3ccce50091772bf2284a66d61f5

   WHAT WAS DELIBERATELY LEFT BEHIND
   - The demo-backdrop token set. MASTER-HANDOFF section 2.2: those six tokens style the dark
     PAGE the mockup sits on, not the app. Everything inside .screen is LIGHT.
   - .head / .mk / h1 / .sub / .demo / .foot — demo page chrome.
   - .phone — the 398px device bezel. In the real app the screen IS the viewport.
   - .sbar and children — a FAKE iOS status bar + notch pill. The OS draws the real one; the
     space it occupied is now var(--sa-top) on .abar — env(safe-area-inset-top) where the
     browser reports one, and a native floor where the iOS binary reports 0px (see the head
     script in index.html).

   WHAT WAS ADDED
   - --copper:#c9832f. The mockup uses var(--copper) at three sites and never defines it, so
     those three rendered with no color at all. A real defect, fixed on port.
   - The port-hardening block at the bottom, carried forward from the old public/styles.css.
     Those are paid-for Android bug fixes, not preferences. Do not delete them. */

:root{
  /* ---- app tokens (MASTER-HANDOFF section 2.2) — the screen is light, always ---- */
  --scr:#f3f5f7;      /* screen background */
  --card:#ffffff;
  --border:#e3e7ec;   /* hairlines */
  --strong:#cfd6dc;   /* emphasized dividers, input borders */
  --text:#1a2026;     /* primary ink */
  --tmut:#69727b;     /* secondary */
  --tfaint:#9aa2aa;   /* tertiary / placeholder */
  --brand:#2f6a3a;    /* OVRT green — app bar gradient origin */
  --lime:#7DBA3D;     /* primary action / positive */
  /* --lime AT INK WEIGHT, and the exact precedent is --copper-ink twelve lines down. #7DBA3D is
     a FILL colour — a button, a dot, a meter — and it does not carry 8.5px type on a tinted
     chip. Every POSITIVE chip in this app is already written in this darker step, and it was a
     hand-copied hex at four sites (the approved time-off chip, a job's ON chip, the on-the-clock
     count, the met-requirement chip) before this token existed. It lives here beside its parent
     so the two cannot drift the way four copies three sections apart do (eval, `palette-drift`:
     "no raw hex outside :root"). Zero pixel change: rgb(77,122,31) before and after, measured. */
  --lime-ink:#4d7a1f;
  /* FOUR MEANINGS, ENUMERATED — and the fourth was added by a round that did not touch this
     line (eval 2026-08-23, `doc-drift`). The Get Set Up card's next-move panel, its target ring,
     its progress track, its send-off disc and the pay-week ask's tint and dot are all copper:
     PROGRESS, which was not on the list a designer reads first. No screen shows two of these
     meanings at once (Home carries no money; Office's copper is materials and unpriced
     warnings), so it is one vocabulary, written down. */
  --copper:#c9832f;   /* materials, warnings, foreman accent, setup progress */
  /* --copper AT INK WEIGHT. #c9832f is a FILL colour — 4.9px of meter, a chip background, a
     progress segment — and it does not carry small type on white (2.5:1). Every warning
     SENTENCE in this app is already written in this darker step, and it was a hand-copied hex at
     six sites before f-6 put it at a seventh (eval, `palette-drift`: "no raw hex outside :root").
     It lives here beside its parent so the two cannot drift the way a copy three sections down
     does. 6.0:1 on --card, which is why it is the one that carries words. */
  --copper-ink:#8a5a17;
  --amber:#b97f1c;    /* pending */
  --red:#c2412e;      /* OT, damage, deny */
  --red-line:rgba(194,65,46,.35); /* --red at 35% — the destructive OUTLINE (.signout). Lives
                                     here beside its parent so the two cannot drift apart the
                                     way a hand-copied hex in a rule three sections down does. */

  /* ---- RADIUS SCALE (2026-08-13, Wes: '9/5'). Four stops, and nothing between them. ----
     Before this the app carried 154 border-radius declarations across NINETEEN distinct
     values, ten of them between 5px and 16px — which is why nothing read as decided. The
     control stop is deliberately about half the card stop: a control sits INSIDE a card, and
     matching radii make the inner shape look rounder than the outer one.
     Pills (99px) and circles (50%) are shapes, not radii — they are not on this scale. */
  --r-micro:2px;   /* marks inside controls: bar caps, legend swatches */
  --r-ctrl:5px;    /* buttons, inputs, rows, chips — anything you touch */
  --r-card:9px;    /* cards and panels */
  --r-sheet:14px;  /* bottom sheets and other full-width surfaces */
  --blue:#3f6fae;     /* manager accent, fuel category */
  --font:'Segoe UI',-apple-system,BlinkMacSystemFont,Helvetica,Arial,sans-serif;

  /* ---- FERN PRESENCE TOKENS (2026-08-22, the fern-alive wave). Ported from the Claude
     Design motion mockup (fern-alive.html); the mockup's own DECISIONS comment contradicted
     itself three times on these numbers (a second copy in its <style>, a third in its
     in-page <details>) — these values are the ones the mockup's CODE actually ran on, which
     PORT-SPEC.md names canonical. --fern-stagger in particular used to be DECLARED (150ms in
     one place) and never REFERENCED — the JS hardcoded 125ms for citations and 60ms for
     chips, three numbers for one stagger. It is 125ms here and views-fern.js reads this
     token via calc(), not a repeated literal, so there is exactly one source now. */
  --fern-sway:5.2s;    /* the idle frond's wake sway, one direction */
  --fern-rest:9s;      /* the frond's infinite rest cycle after the wake sway ends */
  --fern-settle:600ms; /* greeting/answer riseIn — one motion language for both */
  --fern-stagger:125ms;/* per-item delay step for citations and chips settling in */
  --fern-typing:1.2s;  /* the typing-dot wave loop */
  --fern-curve:cubic-bezier(.2,.7,.3,1);
  --fern-breath:rgba(125,186,61,.16); /* the frond's idle aura, lime-derived */

  /* ---- GET SET UP TOKENS (2026-08-22, the Get Set Up round). Ported from Wes's refined
     mockups (getsetup-a-fresh…e-payweek); these are the values the mockups' CODE ran on.
     COPPER ON PURPOSE, and it is the one colour decision this card makes: brand green is the
     primary action and the active state and nothing else, and the ONE green thing on Home is
     Clock in. A checklist that also wore green would be a second thing on that screen asking
     to be pressed. The durations are read from these tokens by views-team.js (getMs), never
     re-typed as literals — the fern round's --fern-stagger was declared in one place and
     hardcoded in two others, and that is the shape this avoids. */
  --setup-tint:rgba(201,131,47,.07);  /* the next-move panel's fill — copper, barely there  */
  --setup-line:rgba(201,131,47,.30);  /* the next-move panel's edge, and the arrival flash   */
  --setup-check-ms:500ms;             /* the check draws itself                              */
  --setup-beat-ms:380ms;              /* the hold after the check, before the row sinks      */
  --setup-sink-ms:450ms;              /* the done row compresses and sinks into the ledger   */
  --setup-rise-ms:400ms;              /* the next row inflates into the panel                */
  --setup-bye-hold:1600ms;            /* the send-off holds before the card leaves           */
  --setup-bye-ms:520ms;               /* the card collapses away                             */
  --setup-curve:cubic-bezier(.2,.7,.3,1);
}

/* This is a hand-tuned light theme. Declaring the scheme stops Samsung One UI dark mode and
   Chrome's "Darken websites" from algorithmically inverting it — an auto-darkened UI is
   materially harder to read at 6am in direct sun, which is the only lighting that matters. */
:root{color-scheme:light}

*{margin:0;box-sizing:border-box}

/* overscroll-behavior-y:none kills Chrome-for-Android's pull-to-refresh. Without it, a glove
   flicking the Home tab downward — or a time wheel hitting its scroll limit and chaining the
   gesture up to the document — RELOADS THE APP, taking any staged photo or half-filled receipt
   with it. Per-scroller `contain` rules live in the hardening block at the bottom. */
html{overscroll-behavior-y:none}
html,body{height:100%}
/* ---------- THE GREY BAND, AND WHY THIS IS SCOPED ----------
   Wes, on real glass 2026-08-18: a home-screen iPhone showed the tab bar's white ending short
   of the screen with a strip of content-grey beneath it. Root cause is WebKit 301994 (open,
   reopened 2026-08-04, reproduces on the iOS 27 beta): an installed home-screen web app gets a
   window of screen.height MINUS safe-area-inset-top, and its CSS length units disagree with the
   window it actually owns. His numbers -- 874 screen / 812 window / 62 short -- match a comment
   on that bug report exactly. There is no vendor workaround posted.

   THIS BLOCK IS SCOPED TO display-mode:standalone ON PURPOSE, and the reason is the whole
   lesson of 2026-08-19. Unscoped, these two rules break LAW 19 at 375x407 with the keyboard up:
   min-height lets the shell grow past a shrunken viewport instead of folding, and a fixed nav
   sits above the keyboard covering what is under it -- ten assertions, every one a control the
   crew could not reach. Scoped, they apply exactly where the bug is (an installed app) and are
   invisible where the law is measured (a browser tab, which is what test/laws-v2.cjs drives).

   THE HONEST LIMIT: emulated Chrome CANNOT reproduce this bug -- it measures flush at 390x844 --
   so a green suite is NOT evidence about the band, and nothing in here ships without a real
   device look. I shipped a wholesale revert of this file on 2026-08-19 with every gate green and
   the band was back on his phone within the hour. Revert the line you proved, never the file it
   lives in. */
/* TWO ROADS TO THE SAME RULE, and the duplication is the point. A media query is not a promise
   on WebKit: nothing anywhere proves a real iPhone matches (display-mode: standalone), and the
   2026-08-19 regression shipped on exactly that unproven assumption. `html.is-installed` is set
   by the six-line script in index.html's <head> from navigator.standalone — the iOS-native
   signal, a decade older than display-mode support — before first paint. If either road is
   open the rule applies; if the query is a dead letter on the phone, the class still is not.
   Neither road opens in a browser tab, so LAW 19's world is untouched. */
@media all and (display-mode: standalone){ html,body{min-height:100%} }
html.is-installed, html.is-installed body{ min-height:100% }
body{
  font-family:var(--font);
  /* WHITE, unconditional. This is the belt-and-braces line lost in the 2026-08-19 wholesale
     revert, and it is the only one of the band fixes that needs no query and no class to work.
     Under WebKit 301994 an installed app's window can be shorter than the shell resolves to,
     and whatever residual sliver shows through renders as an invisible WHITE SEAM against the
     glass instead of a grey stripe that reads as broken chrome. Nothing INSIDE the app changes
     colour: .screen paints var(--scr) over its own box (see `.screen{background:var(--scr)}`
     just below) and covers the whole viewport.
     Was: background:var(--scr). */
  background:#fff;
  color:var(--text);
  font-size:13px;
  /* dvh tracks Android's collapsing URL bar; the vh line above it is the fallback. 100vh
     resolves against the LARGE viewport, so the body sits ~56px taller than the screen — a
     constant slop-scroll, which is exactly the gesture that fires pull-to-refresh. */
  min-height:100vh;
  min-height:100dvh;
  overscroll-behavior-y:none;
  display:flex;
  flex-direction:column;
}

/* ---------- the screen ----------
   Was: .phone{width:398px} wrapping .screen{height:812px; border-radius:var(--r-sheet)}.
   Now: the screen IS the viewport. The proportions and the flex column are unchanged, and
   position:relative is KEPT — .nav, .toast, .sheetovl, .sysfull, #onbScr and #bootScr all
   position themselves absolutely against it, exactly as they did inside the bezel. */
.screen{
  position:relative;
  background:var(--scr);
  color:var(--text);
  overflow:hidden;
  flex:1;
  min-height:100vh;
  min-height:100dvh;
  display:flex;
  flex-direction:column;
}

/* ================= verbatim from the mockup, .abar through .aitem .pchip ================= */
  .abar{display:flex;align-items:center;gap:10px;padding:10px 16px 12px;background:linear-gradient(160deg,#2f6a3a,#24512c);color:#fff}
  /* THE MARK — his logo, no words (2026-08-15). It is an <img> now rather than an inline
     SVG, so the tinted rounded tile the old glyph sat in is gone with it: a logo does not
     wear a swatch behind it. Sized to the same 26px optical height so nothing else in the bar
     moved. object-fit:contain because the crop is near-square but not exactly. */
  .abar .bmk{width:28px;height:28px;flex:none;object-fit:contain;display:block}
  .abar .t{font-weight:800;font-size:15px;letter-spacing:.01em}
  .abar .s{font-size:10.5px;color:rgba(255,255,255,.75);font-weight:600;margin-top:1px}
  .avatar{margin-left:auto;width:34px;height:34px;border-radius:50%;background:var(--lime);color:#16240b;font-weight:800;font-size:12.5px;display:flex;align-items:center;justify-content:center;border:2px solid rgba(255,255,255,.5);cursor:pointer}
  .rolechip{font-size:9px;font-weight:800;background:rgba(255,255,255,.16);border-radius:var(--r-ctrl);padding:2px 6px;letter-spacing:.05em}
  /* THE GEAR — your account, from any screen. margin-left:auto pushes it hard right, where
     the avatar used to be, so the bar's shape is unchanged. 38px is above the gloved-thumb
     floor (test/android.cjs measured 44px as the floor for a primary control; this is chrome
     at the edge of the screen, where the thumb lands generously by construction) and the
     negative margin keeps it from adding height to the bar. */
  /* 56px, NOT 38 (eval 2026-08-15, `touch-target`, third occurrence and now a standing
     check). This is the round's "genuinely always reachable" control and it shipped as the
     SMALLEST tap target in the app bar. The two prior instances of this class — a 34px eye
     and a 44px #pwOpen — were both raised to 56, which is the standard this app already
     holds every other primary control to; a gloved thumb in a gravel lot does not care that
     the third one is chrome. The negative margins keep the bigger box from adding height to
     the bar or pushing the icon off the right edge: the button grows around the glyph, which
     stays 22px. */
  .gearbtn{margin-left:auto;flex:none;width:56px;height:56px;margin-right:-14px;margin-top:-9px;margin-bottom:-9px;
    border:0;background:none;color:rgba(255,255,255,.92);display:flex;align-items:center;justify-content:center;
    border-radius:50%;cursor:pointer;-webkit-tap-highlight-color:transparent}
  .gearbtn svg{width:22px;height:22px}
  .gearbtn:active{background:rgba(255,255,255,.16)}
  /* padding-bottom IS NOT DECLARED HERE, and that is the fix. It used to be the third
     value of this shorthand (124px), and the safe-area block near the end of the file set
     it again — one constant, two jobs, and the later rule silently cut the clearance to
     96px while this line still read 124. The bottom bar is 111px, so the last 15px of
     every scrolled view lived under it. ONE OWNER NOW: the .content padding-bottom in the
     safe-area block (search `--- nav clearance ---`). Longhands here so a shorthand cannot
     quietly re-take it. */
  .content{flex:1;overflow-y:auto;padding-top:14px;padding-right:14px;padding-left:14px}
  .content::-webkit-scrollbar{display:none}

  /* in-app camera */
  .cam{position:relative;height:560px;border-radius:var(--r-card);overflow:hidden;background:radial-gradient(120% 90% at 50% 30%,#2b2e31,#141618 70%);display:flex;flex-direction:column}
  /* PERMISSION CARD ONLY. The stage is sized for a viewfinder that is not there: with .vf and
     .camrow correctly hidden, the fixed 560px left ~470px of black under the card -- a stage
     reserved for absent content, which is exactly what the .cam-shutter:disabled{display:none}
     rule further down already gets right. Height goes to content; nothing else moves. */
  .cam.permonly{height:auto}
  .cam .privacy{position:absolute;top:12px;left:50%;transform:translateX(-50%);display:flex;align-items:center;gap:6px;background:rgba(20,24,20,.78);border:1px solid rgba(125,186,61,.5);color:#cfe8b4;font-weight:700;font-size:10px;border-radius:99px;padding:6px 11px;white-space:nowrap;z-index:3}
  .cam .privacy svg{width:11px;height:11px;color:var(--lime)}
  .privx{border:0;background:none;color:#8fa07f;font-size:10px;font-weight:800;cursor:pointer;padding:0 0 0 3px;line-height:1}
  /* 44, not 30 (2026-09-04 sweep, lane 4 F9, `touch-target`, and its own words: "the SMALL one
     is always the entry point to the thing that was made big, so measure the door as well as the
     room"). This is the way OFF the camera stage and it was the only control on that stage under
     the floor — .flashbtn and .libbtn on the row below it are both 56x56. It takes its floor at
     its own rule rather than joining the shared 44px min-height list, for the reason .signout and
     .adjout each left that list: a min-height alone would have left it 30 WIDE, which is a second
     number for one control and the exact shape those two moved out to avoid. camera-roll.cjs
     measures the box. It keeps its ::before hit expansion further down the file — inert at 44 and
     harmless, and it is what covers the button if this drawn size is ever reduced again. */
  .camx{position:absolute;top:10px;left:10px;z-index:4;width:44px;height:44px;border-radius:99px;border:1px solid rgba(255,255,255,.2);background:rgba(20,24,20,.72);color:#dfe5da;font-size:13px;font-weight:800;cursor:pointer;display:flex;align-items:center;justify-content:center}
  .vf{flex:1;position:relative;margin:52px 40px 18px}
  .vf .guide{position:absolute;inset:0;border:2px dashed rgba(255,255,255,.55);border-radius:var(--r-card)}
  .vf .ck{position:absolute;width:22px;height:22px;border:3px solid var(--lime);z-index:2}
  .vf .ck.tl{top:-3px;left:-3px;border-right:0;border-bottom:0;border-radius:var(--r-ctrl) 0 0 0}
  .vf .ck.tr{top:-3px;right:-3px;border-left:0;border-bottom:0;border-radius:0 var(--r-ctrl) 0 0}
  .vf .ck.bl{bottom:-3px;left:-3px;border-right:0;border-top:0;border-radius:0 0 0 var(--r-ctrl)}
  .vf .ck.br{bottom:-3px;right:-3px;border-left:0;border-top:0;border-radius:0 0 var(--r-ctrl) 0}
  .vf .paper{position:absolute;left:16%;right:16%;top:9%;bottom:9%;background:#f4f2ec;border-radius:var(--r-ctrl);box-shadow:0 10px 30px rgba(0,0,0,.5);transform:rotate(-2deg)}
  .vf .paper i{position:absolute;left:12%;right:12%;height:3px;border-radius:var(--r-micro);background:#c9cdd2}
  .vf .hinttx{position:absolute;bottom:-30px;left:0;right:0;text-align:center;color:rgba(255,255,255,.75);font-size:11px;font-weight:600}
  .camrow{display:flex;align-items:center;justify-content:space-around;padding:16px 30px 22px}
  /* 56, not the 44 these carried while .norollbtn was a decoration. It is a real control now
     (the library import), and the touch floor moved onto the class — so its sibling on the same
     row moved with it rather than being left as the one 44px target beside a 56px one. */
  .flashbtn,.libbtn{width:56px;height:56px;border-radius:50%;background:rgba(255,255,255,.12);border:0;color:#fff;display:flex;align-items:center;justify-content:center;cursor:pointer}
  .flashbtn svg,.libbtn svg{width:22px;height:22px}
  .shutter{width:70px;height:70px;border-radius:50%;background:#fff;border:5px solid rgba(255,255,255,.35);cursor:pointer;position:relative}
  .shutter:active{transform:scale(.94)}
  .shutter::after{content:'';position:absolute;inset:6px;border-radius:50%;border:2.5px solid #16240b;background:var(--lime)}
  .reading{position:absolute;inset:0;background:rgba(13,15,14,.82);display:none;align-items:center;justify-content:center;flex-direction:column;gap:12px;z-index:5;color:#fff;font-weight:800}
  .reading.on{display:flex}
  .spin{width:34px;height:34px;border-radius:50%;border:3.5px solid rgba(125,186,61,.25);border-top-color:var(--lime);animation:sp 0.9s linear infinite}
  @keyframes sp{to{transform:rotate(360deg)}}
  .reading small{font-weight:600;color:rgba(255,255,255,.7);font-size:11px}

  /* capture screen */
  .snap{border:1.5px dashed var(--strong);border-radius:var(--r-card);background:var(--card);padding:12px;display:flex;gap:12px;align-items:center}
  .rimg{width:64px;height:84px;border-radius:var(--r-ctrl);background:#fff;border:1px solid var(--border);box-shadow:0 3px 10px rgba(20,30,25,.14);position:relative;overflow:hidden;flex:none}
  .rimg .ln{position:absolute;left:8px;right:8px;height:3px;border-radius:var(--r-micro);background:#d7dbe0}
  .rimg .tot{position:absolute;left:8px;right:8px;bottom:7px;height:5px;border-radius:var(--r-micro);background:#8a929a}
  .snap .t1{font-weight:800;font-size:13px;display:flex;align-items:center;gap:7px}
  /* NEUTRAL, NOT GREEN AT ALL (2026-09-04 sweep lane 4 F12, corrected by eval r2 —
     `green-overload`). This tick and "Add to job costs" shipped in the SAME fill, so the confirm
     screen gave the eye two identical greens and only one of them does anything. The first fix
     moved it to --brand, and that was still wrong by the rule it was quoting: Wes's standing
     rule gives brand green to the primary action AND THE ACTIVE STATE, and this tick is neither
     — it is a passive confirmation that a photo arrived. A passive mark wearing the active-state
     colour is the same confusion one shade quieter. --tmut is the app's muted ink, which is what
     this actually is. receipt-form.cjs counts solid brand fills inside #v-add by computed colour
     and requires exactly one: #submitBtn. */
  .snap .ok{width:17px;height:17px;border-radius:50%;background:var(--tmut);color:#fff;display:inline-flex;align-items:center;justify-content:center;flex:none}
  .snap .ok svg{width:9px;height:9px}
  .snap .t2{color:var(--tmut);font-size:11px;margin-top:3px;line-height:1.4}
  .relink{background:none;border:0;color:var(--blue);font:inherit;font-weight:700;font-size:11px;cursor:pointer;padding:0;margin-top:6px}
  label{display:block;font-size:10px;font-weight:800;letter-spacing:.06em;text-transform:uppercase;color:var(--tfaint);margin:14px 2px 6px}
  .ocr{font-size:9px;font-weight:800;color:var(--brand);background:rgba(125,186,61,.14);border:1px solid rgba(125,186,61,.45);border-radius:var(--r-ctrl);padding:1.5px 6px;margin-left:5px}
  /* THE 56px TOUCH FLOOR LIVES ON THE BASE CLASS (f-7 fix round 3 — `touch-target`, 6th
     occurrence, and the class's own words are the reason: "when a floor moves onto ONE class,
     measure every sibling wearing the old one").
     WHAT WENT WRONG: the 8-27 round put `min-height:56px` on `.g2 .inp` — the Date/Job row —
     and the four boxes measured there all came out at 56. On the SAME form, #catOther and
     #note measured 46 with a computed min-height of 0px, and two screens over #logIssues and
     #dmgNote did too. #catOther is the control that round was opened for.
     WHY THE BASE AND NOT ANOTHER SELECTOR: padding 12px + 15px type is ~46px, so EVERY .inp
     that no other rule reaches is under the floor by construction. A floor written per-screen
     is a floor that is one new screen behind, permanently — which is what the last five
     occurrences of this class were. THE FIVE RULES THAT ONLY RESTATED 56px ARE DELETED rather
     than left as five places to change one number: `.g2 .inp`; the onboarding steps 1/5/9 with
     #pwovl and #delovl; `.agbar .inp`; and `#coovl .inp,#bkovl .inp`. `.homeline .inp,.hok`
     stays because .hok is a BUTTON and still needs the number.
     WHERE IT IS DELIBERATELY EXCEEDED, not overridden: textarea.inp is 88px, .stepin is its
     own control at 56px, and #onbScr's primary buttons are 60px. Nothing sets it lower. */
  .inp{width:100%;font:inherit;font-size:15px;border:1px solid var(--strong);border-radius:var(--r-card);padding:12px 13px;background:var(--card);min-height:56px}
  .inp:focus{outline:2px solid rgba(125,186,61,.4);border-color:var(--lime)}
  /* iOS ZOOMS THE WHOLE VIEWPORT the moment a focused input is under 16px, and it does not
     reliably zoom back — Wes, 2026-09-01: "sometimes when i get out of pages like the edit job
     details page I get like zoomed in and cant get zoomed out or recentered within the app."
     The edit-job screen is almost entirely .inp, which is why it is the one he named.
     THERE IS NO OTHER LEVER. iOS 10+ deliberately ignores maximum-scale and user-scalable for
     accessibility, and inside a WKWebView a pinch does not restore scale the way it does in
     Safari — so 16px is not a preference, it is the only thing that prevents the zoom.
     SCOPED TO iOS, not global: the 15px above is a deliberate part of this screen's type scale
     (see the block at the top of this section) and bumping it everywhere would relay out every
     form in the app to fix a bug that only exists on one platform. */
  /* THE RULE ITSELF IS NOT HERE ANY MORE — search `-webkit-touch-callout` and read the block
     near the end of this file. It shipped here on 2026-09-01 and LOST THE CASCADE the same
     day: five later rules — textarea.inp, .srch input, .stepin, .ratebox input and
     .icard textarea.inp — either out-specify it or simply come after it, so the scope box on
     the very edit-job screen Wes named still computed 14px and still trapped the zoom. A guard
     that greps the stylesheet for the rule's TEXT stayed green through all of it. Source order
     is load-bearing in this file (the grey band's own comment at the bottom says so about
     .nav), so the fix is to declare it after every rule it has to beat, naming each of them at
     equal-or-higher specificity. */
  /* minmax(0,1fr), NOT 1fr. A `1fr` TRACK has an automatic minimum of its content's
     min-content size, so the track itself grows past its share before the item's own
     min-width ever gets a say. Measured in a 375px column with an unbreakable 38-character
     child: `1fr 1fr` lays the two columns out at 34px and 288px — which IS Wes's screenshot,
     one box squeezed to nothing and the other riding over it — and `minmax(0,1fr)` lays them
     out at 161px and 161px. Both halves of the pair below are needed and neither is
     redundant: this one fixes the TRACK, `.g2>*{min-width:0}` fixes the ITEM. */
  .g2{display:grid;grid-template-columns:minmax(0,1fr) minmax(0,1fr);gap:9px}
  /* THE SECOND HALF OF `1fr 1fr`, AND WITHOUT IT THE FIRST HALF IS A SUGGESTION (2026-08-25,
     off Wes's Confirm-receipt screenshot: the Date box and the Job box printed on top of one
     another). A grid item's default is `min-width:auto`, which means "never shrink below my
     content's min-content width" — and a <select>'s min-content width is its LONGEST OPTION.
     ON WEBKIT — see the measured note at the end of this comment; Chrome shrinks selects and
     WebKit does not. One job called "Florkowski PH 02" therefore widened its own column past
     1fr on Wes's phone, pushed the row past the viewport, and the two boxes overlapped. The
     fix has to sit on the ITEM as well as the track, not
     on the select: the select is a grandchild (`<div><label>…<select class="inp">`), so it is
     the wrapping <div> that refuses to shrink. `.inp{width:100%}` then makes the control
     follow its box down and the UA ellipsises the option text, which is what a picker should
     do with a name too long for the phone.
     ALL FOUR .g2 ROWS, not the one that was screenshotted (`fixed-one-of-N`): Confirm receipt
     carries two, Daily log one and Damage report one, and three of the four hold a job
     <select> — #job, #logJob, #dmgJob, which is every <select> in this app.

     ONE THING THIS FIX CANNOT BE PROVED AGAINST HERE, said rather than left to be discovered.
     MEASURED in Chrome at 375 with "Florkowski Paver Patio & Retaining Wall 02" selected:
     the column is 169px WITH min-width:auto and 169px WITHOUT it — Chrome lets a <select>
     shrink below its longest option, so Chrome cannot reproduce the defect from a <select> at
     all. WebKit does not; a <select>'s intrinsic width there IS its longest option, which is
     why this shows up on Wes's phone and on nothing in this harness. That is the same shape as
     the grey band, and it is handled the same way: test/receipt-form.cjs calibrates the RULE
     on the REAL row with an unbreakable child Chrome does honour (34px/288px without, equal
     with), and says out loud that the <select> half is a real-device verdict. */
  .g2>*{min-width:0}
  /* A <select> IS NOT A TEXT BOX, and WebKit does not size it like one. Wes's 2026-08-26
     screenshot of build 16 still showed the Date box running under the Job box AFTER the grid
     was fixed at both levels (minmax(0,1fr) on the track, min-width:0 on the item) and with a
     global border-box reset already in place — so the overflow was not the grid and not the
     padding. What is left is the control itself: iOS draws its own field, with its own
     intrinsic width and its own vertical metrics, over the app's box. Chrome shrinks a select
     and WebKit does not, which is exactly why every measurement taken on this machine said the
     row was fine.
     appearance:none takes that away — the select becomes a plain box this stylesheet controls,
     with our own chevron drawn as a background so the affordance survives. Now the column
     bound is the only thing that decides its width, on every engine. */
  select.inp{-webkit-appearance:none;appearance:none;max-width:100%;
    background-image:url("data:image/svg+xml;charset=utf-8,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%235c665e' stroke-width='2.4' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M7 10l5 5 5-5'/%3E%3C/svg%3E");
    background-repeat:no-repeat;background-position:right 11px center;background-size:15px;
    padding-right:34px;text-overflow:ellipsis}
  /* ...AND THE FIX ABOVE NEVER APPLIED TO THE OTHER HALF OF THAT ROW (f-7 fix round,
     2026-08-27, from Wes's build-17 phone). His screenshot shows the DATE box running
     UNDERNEATH the JOB select — the same overlap the 8-26 round was opened for, still there,
     one control over.

     WHY IT WAS MISSED, exactly. `index.html` shipped the field as a PLAIN input — `<input
     class="inp" id="rdate">`, no type — and `views-capture.js` set `d.type = 'date'` at
     RUNTIME. The rule written on 8-26 selects `select.inp`. There was no `input[type="date"]`
     rule anywhere in this file, so the control the phone was actually drawing had never been
     styled by us at all. `.g2`'s `minmax(0,1fr)` fixes the TRACK and `.g2>*{min-width:0}` fixes
     the ITEM; neither constrains a REPLACED element's own intrinsic minimum, and on iOS a
     native date field carries one and will not shrink below it. `.inp{width:100%}` is not a
     ceiling there.

     THE CORROBORATION IS IN THE SAME SCREENSHOT: the value renders CENTRED ("Aug 27, 2026")
     while Vendor, Total and Job are all left-aligned. That is iOS's own
     `::-webkit-date-and-time-value` doing its own thing — proof the control was native and
     untouched by this stylesheet, rather than a guess about why.

     THE FIX MIRRORS THE SELECT: appearance:none takes the native field away, the app draws its
     own calendar mark as a background so the affordance survives, and the column bound becomes
     the only thing deciding the width. `min-width:0` is stated on the control itself rather
     than inherited from the grid item, because that is the bound a replaced element ignores.
     The value is left-aligned to match every sibling field. TAPPING STILL OPENS THE PICKER —
     appearance:none changes how a date input is PAINTED, never whether it is a date input.

     CHROME CANNOT VERIFY THIS AND SAYING SO IS THE POINT. Chrome lets a date input shrink, so
     it reported this row as fine through the whole of f-7 while the phone showed an overlap —
     the same honest limit already written above for <select>. test/receipt-form.cjs measures the
     GENERIC invariant instead (each field's rendered right edge against its own grid cell's
     right edge, and against the next field's left edge), which is a shape that would catch ANY
     too-wide replaced control rather than a Chrome-specific width. The WebKit verdict is a
     real-device check, every time. */
  input[type="date"].inp{-webkit-appearance:none;appearance:none;max-width:100%;min-width:0;
    line-height:1.2;
    background-image:url("data:image/svg+xml;charset=utf-8,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%235c665e' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Crect x='3' y='5' width='18' height='16' rx='2'/%3E%3Cpath d='M3 10h18M8 3v4M16 3v4'/%3E%3C/svg%3E");
    background-repeat:no-repeat;background-position:right 11px center;background-size:15px;
    padding-right:34px}
  /* The value box, left-aligned like every other field on the form, and never wider than the
     box that holds it. `min-width:0` here as well: this pseudo-element is the thing carrying
     the intrinsic width in WebKit. */
  input[type="date"].inp::-webkit-date-and-time-value{text-align:left;margin:0;min-width:0}
  /* Chrome's own indicator is hidden rather than deleted — it is still there and still opens
     the picker under the mark this stylesheet draws, so the tap target is unchanged. */
  input[type="date"].inp::-webkit-calendar-picker-indicator{opacity:0;margin:0;padding:0}
  /* THE ROW STAYS ONE HEIGHT, AND IT IS THE 56px FLOOR — which now lives on `.inp` itself
     (see the base rule above). appearance:none can collapse a native date field to its bare
     text height on some iOS versions, so the guard has to be explicit; a guard set at the date
     box alone would leave Date at 56 beside a 48px Job select, which is a second number for
     one row. This row was where the floor was first written, and re-stating 56px here now
     would be two places to change one number. */
  /* THE ROW AND THE FALL-THROUGH ARE TWO BOXES, and that is the whole fix.
     .cats is the scrollport and holds ONLY the install-configured categories.
     .catsend sits beside it, outside the scrollport, and holds "Other". */
  .catsrow{display:flex;gap:7px;align-items:stretch}
  /* flex:0 1 auto, NOT 1 1 auto. Growing makes the scrollport eat the whole line on a wide
     window and strands Other 600px from the last category with nothing between them. Sizing to
     content and shrinking only when it must gives both cases: on a phone the row is narrower
     than its chips, so it scrolls; on a desktop it is exactly as wide as them and Other sits one
     gap away. min-width:0 is what lets a flex item shrink below its content at all. */
  .cats{display:flex;gap:7px;overflow-x:auto;padding-bottom:4px;flex:0 1 auto;min-width:0;
    --catfl:0px;--catfl0:0px;--catfr:0px;--catfr0:0px}
  .catsend{flex:none;display:flex;padding-bottom:4px}
  .cats::-webkit-scrollbar{display:none}
  /* THE EDGE HAS TO LOOK LIKE AN EDGE (f-7 fix round, 2026-08-27).
     The scrollbar is hidden (the line above), the row is 274px wide at 375 and its chips are
     wider than that, so the clip landed mid-word: "Disposal" sliced flat to "Disp" at 375 and
     390, and at 430 a 2px crescent of the next chip that read as a stray parenthesis. Nothing
     said "there is more" — the broken-looking chip WAS the affordance, which is the same visual
     read that killed the two previous attempts at this row.
     A MASK, NOT AN OVERLAY. A gradient pseudo-element would have to be painted in the card's
     background colour and would therefore be wrong on any surface this row is ever reused on,
     and it would sit over the chips and eat the taps at the edge. A mask fades the chips
     themselves to transparent against whatever is behind them, changes no hit-testing, and the
     56px targets underneath are untouched.
     ON THE SIDE THAT HAS MORE, AND ONLY WHEN THERE IS MORE. #cats sets data-edge from its own
     scroll position (views-capture.js catsEdge), so a row whose chips all fit — a three-category
     install — carries no attribute and no mask at all, and a row scrolled to its end has a crisp
     right edge again. Both sides, because scrolled to the right the LEFT is the clipped one. */
  .cats[data-edge]{-webkit-mask-image:linear-gradient(to right,transparent var(--catfl0),#000 var(--catfl),#000 calc(100% - var(--catfr)),transparent calc(100% - var(--catfr0)));
    mask-image:linear-gradient(to right,transparent var(--catfl0),#000 var(--catfl),#000 calc(100% - var(--catfr)),transparent calc(100% - var(--catfr0)))}
  /* UP TO 30px OF RAMP, REACHING ZERO AT THE EDGE — AND THE LENGTH IS MEASURED, NOT DECLARED
     (f-7 fix round 3). These are the FALLBACK values only: views-capture.js's catsEdge() sets
     --catfr / --catfl inline from where the chips actually are, every scroll and every resize,
     so the ramp begins at the last chip that FITS and never fades a whole one. Its comment
     carries the measurements and the reasoning.
     THE 6px DEAD STRIP IS GONE. It was a fixed number aimed at a 2px orphan crescent, and at
     430 it did the opposite of its job: the whole 7px of the clipped chip fell inside it and
     the row lost its only cue. An anchored ramp handles both ends by itself — a 2px sliver
     sits in a ramp barely wider than itself and keeps about a fifth of its ink, a 7px sliver
     keeps about half. */
  .cats[data-edge="right"],.cats[data-edge="both"]{--catfr:30px;--catfr0:0px}
  .cats[data-edge="left"],.cats[data-edge="both"]{--catfl:30px;--catfl0:0px}
  .cat{flex:none;border:1px solid var(--strong);background:var(--card);color:var(--tmut);font:inherit;font-weight:700;font-size:12.5px;padding:9px 15px;border-radius:99px;cursor:pointer}
  .cat.on{background:var(--brand);border-color:var(--brand);color:#fff}
  /* WHY "OTHER" LIVES OUTSIDE THE SCROLLER (f-7, 2026-08-27). Two shipped attempts are
     buried under this rule and both were half-right, so the reasoning is kept.

     ATTEMPT 1, position:sticky (f-6). The row is the install's categories plus one
     fall-through, it scrolls horizontally by design, and at scrollLeft 0 the last chip sat
     73-128px past the right edge at every iPhone width — off screen on arrival, with nothing
     on the form naming it. Wes asked for this chip so a receipt is never in the wrong
     category, and a fall-through nobody can find does not do that job. Sticky put it back on
     screen.
     WHY IT FAILED: sticky floats. A pinned opaque pill with another pill sliding under it is
     two overlapping ovals, and Wes said so from a build-16 screenshot. A left-edge fade was
     added to make the row read as passing behind the chip; it did not save it. Keeping a
     control on screen is worth nothing if it covers the answer beside it.

     ATTEMPT 2, a plain chip again (2ca0025). The overlap went away and the chip went back off
     screen — straight back to the `unreachable` finding sticky was built to close, now shipped
     to three installs and TestFlight 17. Caught by test/receipt-form.cjs, not by eye.

     THE FIX IS STRUCTURAL, NOT A STYLE. Sticky and static are the same wrong question — both
     ask where a chip sits INSIDE a scrollport that is too narrow for it. So the fall-through
     stops being inside it: .cats scrolls the configured categories, .catsend sits beside it
     and never scrolls. Other is wholly on screen at every width, covers nothing, and is still
     visually and in the DOM after every real answer.
     The four options the f-6 comment weighed and refused are still refused, for its reasons:
     moving Other to the FRONT (a fall-through belongs after the real answers); WRAPPING the
     row (deletes the horizontal-scroll design — a twelve-category install then owns three
     lines of form); SHRINKING chips until seven fit (only true for THIS install's six, and the
     list is config); a SECOND control below the row (two doors to one mode). This is none of
     those: one control, one door, moved out of a box it never fit in — and it is
     config-agnostic, which the shrink option was not. */
  .cat.catother{margin-left:0}
  .submit{width:100%;margin-top:18px;background:var(--lime);border:0;color:#16240b;font:inherit;font-weight:800;font-size:15px;border-radius:var(--r-card);padding:15px;cursor:pointer;box-shadow:0 6px 18px rgba(125,186,61,.35)}
  .submit:active{transform:scale(.985)}
  .subnote{text-align:center;color:var(--tfaint);font-size:10.5px;margin-top:9px}
  /* A SUBNOTE THAT ASKS A QUESTION IS NOT A SUBNOTE (2026-09-03, eval r2, `palette-drift`).
     .subnote is placeholder ink — --tfaint (#9aa2aa) is 2.59:1 on white, which the comment at
     the hours caption already calls out as below AA at any size — and it is the right ink for
     "Uses this phone's location." It is the WRONG ink for the two armed confirmations this
     round added, which are the sentences a person has to read before their second tap replaces
     a job's pin. Those wore --amber when they lived in #jbedNote and lost the tone when they
     were moved beside their controls; the words moved and the treatment did not follow.
     --copper-ink, NOT --amber (Wes, 2026-09-03: "i like the copper for warning messages").
     This app has TWO warn families and the split is by JOB, not by taste: --amber is a FILL —
     pending chips, the .locstate.warn box's tint and its border — while --copper-ink is the
     ink every warning SENTENCE in this app is already written in (.edchip, .adjsusp,
     .tmd.hole, .tmsumc.warn, and the :root comment beside the token itself). These two notes
     are sentences, so they take the sentence ink.
     THAT IS ALSO THE HONEST ANSWER ON CONTRAST: --amber measures 3.45:1 on white, short of AA
     4.5 at any size; --copper-ink is 5.9:1. An earlier version of this comment called the move
     a whole-app palette question still to be settled. It was not — the token was already in
     :root, carrying exactly this meaning, at six other sites. This rule only spends it. */
  .subnote.warn{color:var(--copper-ink);font-size:12px;font-weight:700}

  /* ---- ADDRESS AUTOCOMPLETE + THE LOCATION SENTENCE (R1, schema-v29) ------------------
     These rules go HERE and not at the end of the file: the two .nav standalone rules must
     stay last (see the note above them), because source order is what keeps the installed
     app's bottom bar fixed without breaking LAW 19 in a browser tab. */
  .acwrap{position:relative}
  /* The menu OVERLAYS what is beneath it rather than pushing the sheet down. A dropdown that
     reflows the form moves the Save button under the thumb that was reaching for it. */
  .acmenu{position:absolute;left:0;right:0;top:calc(100% + 4px);z-index:30;
    background:var(--card);border:1px solid var(--strong);border-radius:var(--r-card);
    box-shadow:0 10px 28px rgba(0,0,0,.16);max-height:244px;overflow-y:auto}
  /* 56px, NOT 44px (2026-09-03, eval r1, `touch-target`). The first cut argued a list row could
     sit at 44 because six of them at 56 fill a keyboard-up phone — and then this round put the
     MATCH-QUALITY WORD on the row, which is the thing that decides whether somebody pins a job
     to a county centroid. A control carrying that decision is a primary control, and the app's
     floor for those is 56. Six rows still fit: the menu scrolls (max-height above), which is
     what that was always for.
     COLUMN, not row: .acprec is display:block and a flex ROW parent ignores that outright —
     "Wright County, MNmatched to the county only", run together with no space, measured. The
     caveat has to be its own line or it is not a caveat, it is a suffix. */
  .acitem{padding:10px 13px;min-height:56px;display:flex;flex-direction:column;
    justify-content:center;gap:2px;
    font-size:14px;color:var(--text);border-bottom:1px solid var(--border);cursor:pointer}
  .acitem .aclabel{display:block;line-height:1.25}
  .acitem:last-child{border-bottom:none}
  .acitem:hover,.acitem:focus{background:rgba(125,186,61,.10)}

  /* THE SENTENCE THAT REPLACED THE TWO COORDINATE BOXES. Deliberately NOT green when it is
     fine: brand green is for the primary action and active state, not for every piece of good
     news (Wes's colour-restraint rule). Muted when the pin is known, amber only when the job
     genuinely has no pin -- which is the one case somebody has to do something about. */
  .locstate{font-size:12px;line-height:1.45;color:var(--tmut);
    padding:10px 12px;background:var(--scr);border:1px solid var(--border);
    border-radius:var(--r-card)}
  .locstate.warn{color:var(--amber);border-color:rgba(185,127,28,.42);background:rgba(185,127,28,.07)}

  /* costs screen */
  .jt{font-size:16px;font-weight:800}
  .jm{color:var(--tmut);font-size:11px;margin-top:2px}
  .kpis{display:grid;grid-template-columns:1fr 1fr;gap:8px;margin-top:11px}
  .kpi{border:1px solid var(--border);border-radius:var(--r-card);background:var(--card);padding:10px 12px}
  .kpi .k{color:var(--tfaint);font-size:9px;font-weight:800;letter-spacing:.06em;text-transform:uppercase}
  .kpi .v{font-size:18px;font-weight:800;margin-top:3px}
  .kpi .s{color:var(--tmut);font-size:9.5px;margin-top:1px}
  .kpi.warn .v{color:var(--amber)}
  .kpi.good .v{color:var(--brand)}
  .budget{border:1px solid var(--border);border-radius:var(--r-card);background:var(--card);padding:11px 13px;margin-top:9px}
  .budget .bt{display:flex;justify-content:space-between;font-weight:800;font-size:11.5px;margin-bottom:6px}
  .bbar{height:9px;border-radius:var(--r-ctrl);background:var(--scr);border:1px solid var(--border);overflow:hidden}
  .bbar b{display:block;height:100%;background:var(--lime)}
  .catbars{display:flex;gap:7px;margin-top:9px;overflow-x:auto;padding-bottom:2px}
  .catbars::-webkit-scrollbar{display:none}
  .cb{flex:none;min-width:120px;border:1px solid var(--border);border-radius:var(--r-card);background:var(--card);padding:8px 10px}
  .cb .ct{display:flex;justify-content:space-between;gap:8px;font-size:10.5px;font-weight:700}
  .cb .ct span:last-child{color:var(--tmut)}
  .cb .cl{height:4px;border-radius:var(--r-micro);background:var(--scr);margin-top:6px;overflow:hidden}
  .cb .cl b{display:block;height:100%}
  .exps h5{font-size:9.5px;font-weight:800;letter-spacing:.07em;text-transform:uppercase;color:var(--tfaint);margin:14px 2px 8px}
  .exp{border:1px solid var(--border);border-radius:var(--r-card);background:var(--card);padding:10px 12px;margin-bottom:7px}
  .exp .top{display:flex;align-items:center;gap:9px}
  .exp .ric{width:30px;height:30px;border-radius:var(--r-ctrl);background:var(--scr);border:1px solid var(--border);display:flex;align-items:center;justify-content:center;flex:none;color:#5b636a}
  .exp .ric svg{width:15px;height:15px}
  .exp .vn{font-weight:800;font-size:13px}
  .exp .meta{color:var(--tmut);font-size:10px;margin-top:1px}
  .exp .amt{margin-left:auto;font-weight:800;font-size:14px}
  .exp .row2{display:flex;align-items:center;gap:6px;margin-top:8px}
  .cat2{font-size:9px;font-weight:800;border-radius:var(--r-ctrl);padding:2.5px 7px;border:1px solid var(--border);background:var(--scr);color:var(--tmut)}
  .stat{font-size:9px;font-weight:800;border-radius:var(--r-ctrl);padding:3px 8px}
  .st-ok{color:#2f6a3a;background:rgba(125,186,61,.13);border:1px solid rgba(125,186,61,.45)}
  .st-pend{color:var(--amber);background:rgba(185,127,28,.10);border:1px solid rgba(185,127,28,.4)}
  .act{margin-left:auto;display:flex;gap:6px}
  /* 56, AND AT ITS OWN RULE (2026-09-04 sweep, lane 5 F7, `touch-target` — the 8th occurrence).
     Approve rendered 75x31 and Reject 61x31: the two controls in this app that BOOK OR DESTROY
     A COST, on the receipts list and on the owner's approval queue, drawn a third under the
     44px gloved-thumb floor and half the 56px this house gives anything a decision hangs on.
     Reject is the destructive one and it sat 6px from Approve. Its own rule rather than the
     shared 44px list at the foot of this file, on that list's own argument (.signout, .adjout,
     .camx all left it for this): a 44px floor under a 56px box is a second number for one
     control, and 44 is not the floor these two belong at anyway. The DRAWN width stays as the
     mockup has it — the two sit in a flex row and the row grows with them.
     Measured, never read: test/reimburse-ui.cjs walks every Approve/Reject rect on all three
     surfaces that draw them (receipts pane, approval queue, overhead queue). */
  .abtn{border:1px solid var(--strong);background:var(--scr);font:inherit;font-weight:800;font-size:11px;border-radius:var(--r-ctrl);padding:7px 13px;cursor:pointer;color:var(--text);min-height:56px}
  .abtn.ok{background:var(--lime);border-color:var(--lime);color:#16240b}

  /* bottom nav */
  .nav{position:absolute;left:0;right:0;bottom:0;background:rgba(255,255,255,.96);backdrop-filter:blur(8px);border-top:1px solid var(--border);display:flex;align-items:flex-end;justify-content:space-around;padding:6px 10px 16px}
  .nv{display:flex;flex-direction:column;align-items:center;gap:3px;color:var(--tfaint);font-size:9px;font-weight:800;background:none;border:0;cursor:pointer;width:56px;position:relative}
  /* The active tab is marked by a hairline, not by colour alone — a second channel, same rule the week chart follows. */
  .nv.on::before{content:"";position:absolute;top:-6px;left:50%;transform:translateX(-50%);width:26px;height:2px;background:var(--brand)}
  .nv.plus.on::before{display:none}
  .nv svg{width:22px;height:22px}
  .nv.on{color:var(--brand)}
  .nv.plus{width:auto}
  /* The one circle in the app, and it stays one: it is the only control that is not a rectangle of content, which is what makes it read as an action rather than a destination. */
  .plusbtn{width:60px;height:60px;border-radius:50%;background:var(--lime);display:flex;align-items:center;justify-content:center;color:#16240b;box-shadow:0 10px 24px rgba(125,186,61,.5);margin-bottom:6px;border:0;cursor:pointer}
  .plusbtn svg{width:28px;height:28px}
  .nv.plus.on .plusbtn{outline:3px solid rgba(125,186,61,.35)}
  /* THE TOAST OUTRANKS THE TAKEOVERS (2026-08-16). It shipped at z-index:20 while #onbScr is
     55 with an OPAQUE background and .sysfull/#bootScr are 60 — so every toast fired while the
     onboarding screen is up rendered underneath it and was never seen by anybody. That is not
     a cosmetic miss: those are status sentences, and several of them are the only answer a
     person gets ("Your work email — the one your office invited", "No signal — the code can't
     be sent from here", both fired from step 1 in app.js requestCode). A message the code
     believes it showed and the screen never painted is the honesty class the ledger is full
     of, so the fix belongs on the component rather than on the one caller that noticed.

     58 — ABOVE #onbScr (55), BELOW #bootScr and .sysfull (both 60). It shipped at 62, which
     cleared all three, and that was one takeover too many: .sysfull is the forced-update screen
     and the "your access changed" screen, and #bootScr is the splash. Those two are deliberately
     blocking — they are the app saying "nothing else is happening until you deal with this" —
     and a status line floating over them is a second voice on a screen that is meant to have
     one. The onboarding screen is the case the raise was written for and 58 still covers it.

     NO `bottom` HERE. It is set ONCE, in the PORT HARDENING block at the foot of this file, for
     the reason .content's clearance is: the number is derived from the tab bar's rendered height
     and it has to spend var(--sa-bottom). Two homes for one number is how it came to be 104px
     against a bar that had grown to 125. */
  .toast{position:absolute;left:50%;transform:translateX(-50%) translateY(14px);background:#1a2026;color:#fff;font-weight:700;font-size:12px;border-radius:var(--r-card);padding:10px 15px;opacity:0;pointer-events:none;transition:.25s;z-index:58;width:max-content;max-width:330px;text-align:center}
  .toast.on{opacity:1;transform:translateX(-50%) translateY(0)}

  /* THE IN-APP CAMERA (camera.js self-injects .cam-* markup into document.body).
     These rules did not exist from the R3 rewrite until 2026-08-17: the camera's styles
     lived with the retired owner.html and died with it while camera.js kept loading, so
     the viewfinder rendered as UNSTYLED divs — the video at intrinsic size squashed into
     the top-left of the page (found by Wes on real glass, day one at Luce Line; the
     receipts UI suite that would have caught it is the known unported gap).
     z-index 56: above the app and #onbScr (55), below .toast (58 — upload status must be
     readable over the viewfinder), below #bootScr/.sysfull (60 — takeovers outrank a tool).
     object-fit CONTAIN, not cover: the shutter draws the full frame (drawImage at
     videoWidth×videoHeight), so a cropped cover-preview would lie about what the photo
     captures. Letterboxing on black is the honest shape of the sensor. */
  .cam-overlay{position:fixed;inset:0;z-index:56;background:#000;display:flex;flex-direction:column}
  .cam-stage{position:relative;flex:1 1 auto;min-height:0;display:flex}
  .cam-video{width:100%;height:100%;object-fit:contain;background:#000}
  .cam-live{position:absolute;top:calc(14px + var(--sa-top));left:16px;display:flex;align-items:center;gap:7px;color:#fff;font-size:12px;font-weight:800;letter-spacing:.08em;text-transform:uppercase;background:rgba(15,18,16,.55);border-radius:999px;padding:6px 12px}
  .cam-dot{width:8px;height:8px;border-radius:50%;background:#e5484d;animation:camPulse 1.6s infinite}
  @keyframes camPulse{0%,100%{opacity:1}50%{opacity:.35}}
  .cam-msg{position:absolute;inset:auto 16px;top:40%;background:#1a2026;color:#fff;border-radius:var(--r-card);padding:16px 18px;font-size:15px;line-height:1.45;text-align:center}
  .cam-msg[hidden]{display:none}
  .cam-controls{flex:none;display:flex;align-items:center;justify-content:space-between;gap:12px;padding:18px 22px calc(18px + var(--sa-bottom));background:#000}
  .cam-controls button{-webkit-tap-highlight-color:transparent}
  .cam-lib,.cam-close{min-height:56px;min-width:84px;padding:12px 16px;border:0;border-radius:12px;background:rgba(255,255,255,.14);color:#fff;font-size:16px;font-weight:700}
  .cam-spacer{min-width:84px}
  .cam-shutter{width:76px;height:76px;border-radius:50%;border:5px solid #fff;background:transparent;padding:0;display:flex;align-items:center;justify-content:center}
  .cam-shutter span{width:56px;height:56px;border-radius:50%;background:#fff}
  .cam-shutter:active span{transform:scale(.88)}
  /* Denied/unsupported: the module disables the shutter while its message card explains.
     The house rule is absent-not-disabled — the ring leaves with the capability. */
  .cam-shutter:disabled{display:none}
  .cam-flash::after{content:"";position:fixed;inset:0;z-index:57;background:#fff;animation:camFlash .14s ease-out}
  @keyframes camFlash{from{opacity:.9}to{opacity:0}}

  /* capture hub sheet */
  .sheetovl{position:absolute;inset:0;background:rgba(15,18,16,.45);z-index:30;display:none;align-items:flex-end}
  .sheetovl.on{display:flex}
  /* A sheet opened FROM another sheet. Real build: keep a sheet stack instead of two levels.
     NOTE: `over` is also the spine's back-gesture selector — BACK_LAYERS maps
     `.sheetovl.on.over` to closeJobPicker() BY NAME (app.js §19). Only the job picker may
     wear it; anything else that needs height in the stack gets its own id rule below. */
  .sheetovl.over{z-index:31}
  /* THE JOB EDITOR, EXPLICITLY. It opens FROM the picker (its "+ New job" row) and must paint
     above it — but it must not wear `over`, or the back gesture would close the picker
     underneath and strand it (see the comment on #jobedovl in index.html). DOM order used to
     do this job by accident, which held only because the picker happens not to wear `over` on
     the one path that reaches this sheet. One id rule, above both 30 and 31, states it. */
  #jobedovl{z-index:32}
  /* THE LONG-SHIFT QUESTION, for the same reason and by the same means (f-2 W4). It can open
     on top of the Home chip's clock-catch sheet — that is the app's second clock-out door —
     and it must NOT wear `over`, because `over` is the back gesture's selector for the job
     picker BY NAME: a back tap would call closeJobPicker() and leave this sheet standing with
     a punch path parked on its answer. Without `over` the gesture falls to the last layer,
     closeAllSheets(), which releases the promise as a dismissal. An id rule, stated. */
  #lsovl{z-index:32}
  .sheet{width:100%;background:#fff;border-radius:var(--r-sheet);padding:14px 14px 26px;animation:up .18s ease-out}
  @keyframes up{from{transform:translateY(30px);opacity:.6}to{transform:translateY(0);opacity:1}}
  .grab{width:40px;height:4px;border-radius:var(--r-micro);background:#d5dade;margin:0 auto 12px}
  .sheet h6{font-size:10px;font-weight:800;letter-spacing:.08em;text-transform:uppercase;color:var(--tfaint);margin:0 4px 9px}
  /* min-height ADDED 2026-08-24 (f-5), measured not assumed: the pay-week picker's one-line rows
     rendered at 52px at 390 wide (its two-line rows clear it at 62). Under the 56px floor, on a
     sheet whose whole job is to be tapped once — and this round doubles who reaches it: the sheet
     used to open only for exportBooks holders picking a payroll week, and now every admin opens
     it to email the hours. `touch-target`'s own fifth clause is the rule being followed here —
     when a floor moves onto one control, measure every sibling wearing the old one. */
  .srow2{display:flex;align-items:center;gap:12px;border:1px solid var(--border);border-radius:var(--r-card);background:var(--card);padding:13px 13px;margin-bottom:8px;cursor:pointer;min-height:56px}
  .srow2:active{background:var(--scr)}
  .srow2 .sic{width:42px;height:42px;border-radius:var(--r-card);display:flex;align-items:center;justify-content:center;flex:none;color:#fff}
  .srow2 .sic svg{width:21px;height:21px}
  .srow2 .st1{font-weight:800;font-size:14px}
  .srow2 .st2{color:var(--tmut);font-size:10.5px;margin-top:1px;line-height:1.35}
  .srow2 .chev{margin-left:auto;color:var(--tfaint);font-size:18px}

  /* daily log + damage forms */
  textarea.inp{resize:none;min-height:88px;line-height:1.45;font-size:14px}
  .wchip{display:inline-flex;align-items:center;gap:7px;border:1px solid var(--border);background:var(--card);border-radius:99px;padding:8px 13px;font-weight:700;font-size:12.5px}
  .wchip svg{width:16px;height:16px;color:var(--amber)}
  .wchip .auto2{font-size:9px;font-weight:800;color:var(--brand);background:rgba(125,186,61,.14);border-radius:var(--r-ctrl);padding:1.5px 6px;margin-left:2px}
  .thumbs{display:flex;gap:7px;flex-wrap:wrap}
  .th{width:58px;height:58px;border-radius:var(--r-card);background:linear-gradient(180deg,#9db4c4 0 38%,#7a9a58 38% 62%,#8a7a5e 62% 100%);border:1px solid var(--strong);position:relative;overflow:hidden}
  .th::after{content:'';position:absolute;left:12%;top:46%;width:34%;height:20%;background:#5d7442;border-radius:var(--r-micro);box-shadow:26px 6px 0 -2px #6b5a44}
  .th.dmg::before{content:'';position:absolute;right:4px;top:4px;width:12px;height:12px;border-radius:99px;background:var(--red);border:2px solid #fff;z-index:2}
  /* THE REMOVE ✕ ON A STAGED PHOTO — a 44px CONTROL wearing an 18px dot (2026-09-04 sweep, lane
     4 F9, `touch-target`). It shipped 18x18, drawn and tapped, which is the smallest target in
     the app on a screen used in gloves. The button now carries the hit area and the child <span>
     carries the glyph, so nothing about the drawing changed: the same dark circle, the same
     2px inset, the same corner.
     WHY NOT A ::after EXPANSION, which is what this file does everywhere else (.sx, .camx, .sw):
     `.th` above is overflow:hidden, so anything reaching past the tile's edge is clipped out of
     hit-testing as well as out of paint — the expansion would have measured 44 and answered
     18. And why 44 rather than the 56 this ledger usually asks for: 56 inside a 58px tile is the
     ✕ swallowing the whole thumbnail, which turns every mis-tap on a photo into a deletion.
     views-capture's renderThumbs builds it; camera-roll.cjs hit-tests the ring and prints hit56
     beside it, so the number is on record the day the tile grows. */
  .thx{position:absolute;top:0;left:0;width:44px;height:44px;min-height:0;border:0;background:none;padding:0;cursor:pointer;z-index:3;display:flex;align-items:flex-start;justify-content:flex-start}
  .thx span{display:flex;align-items:center;justify-content:center;width:18px;height:18px;margin:2px;border-radius:50%;background:rgba(17,20,23,.72);color:#fff;font-size:10px;line-height:1}
  .thadd{width:58px;height:58px;border-radius:var(--r-card);border:1.5px dashed var(--strong);background:var(--scr);color:var(--tmut);display:flex;align-items:center;justify-content:center;font-size:20px;font-weight:800;cursor:pointer}
  .tags{display:flex;gap:6px;flex-wrap:wrap}
  .tag{border:1px solid var(--strong);background:var(--card);color:var(--tmut);font:inherit;font-weight:700;font-size:11.5px;padding:8px 13px;border-radius:99px;cursor:pointer}
  .tag.on{background:var(--red);border-color:var(--red);color:#fff}
  .tag.on.pre{background:#3f6fae;border-color:#3f6fae}
  .tag.on.doc{background:#69727b;border-color:#69727b}

  /* jobs seg + activity */
  .seg2{display:flex;background:var(--card);border:1px solid var(--border);border-radius:var(--r-card);padding:3px;gap:3px;margin-top:11px}
  .seg2 button{flex:1;border:0;background:transparent;color:var(--tmut);font:inherit;font-weight:800;font-size:12px;padding:8px;border-radius:var(--r-ctrl);cursor:pointer}
  .seg2 button.on{background:var(--brand);color:#fff}
  .aitem{border:1px solid var(--border);border-radius:var(--r-card);background:var(--card);padding:11px 12px;margin-bottom:7px}
  .aitem .top{display:flex;align-items:center;gap:9px}
  .aitem .aic{width:30px;height:30px;border-radius:var(--r-ctrl);display:flex;align-items:center;justify-content:center;flex:none;color:#fff}
  .aitem .aic svg{width:15px;height:15px}
  .aitem .at{font-weight:800;font-size:12.5px}
  .aitem .am{color:var(--tmut);font-size:10px;margin-top:1px}
  .aitem .body{color:var(--text);font-size:11.5px;margin-top:8px;line-height:1.45}
  /* receipt index */
  .kpi.tap{cursor:pointer}
  .kpi.tap .s{color:var(--brand);font-weight:800}
  /* THE WAYPOINT HEADER (f-5, 2026-08-24) — DIRECTION B, the owner's FINAL pick from five
     verified mockups (BACKBTN-HANDOFF.md, back-b-waypoint.html). Replaces the f-2 .rback pill
     above this comment for eleven months of one lesson learned twice: f-2 made the door look
     like a control because a green TEXT link nobody recognised was unreachable in practice;
     this round asks a harder question of the SAME door — once it looks like a control, what
     does it say? A pill answered "go back". It never answered "back to WHERE" — the label was
     already that.
     CORRECTED 2026-08-25, and the correction matters more than the feature: this comment used
     to claim the screen's NAME was new here, that "nothing on any sub-screen said so at all".
     That was false when it was written. The app bar has printed ROUTES[view].title on every
     screen since v2 (app.js), so the .ttl below was repeating a string sitting twelve pixels
     above it — byte-identical on eleven of the twelve sites. It was invisible during design
     because back-b-waypoint.html:102 drew the HOME app bar over a Bookkeeping screen; the
     mockup could not render the chrome the real screen has. The fix is not to drop the title
     but to have ONE of them: app.js blanks #scrTitle on any screen carrying a .wp, so this
     block is that screen's header outright and the bar keeps only its sub line.
     No shape at all — the pill's border, fill and radius are gone on purpose, not lost by
     accident. What is left is two lines of type doing wayfinding's job: a 10px letterspaced
     CRUMB (chevron + destination, muted) sitting as an eyebrow over a 17px screen TITLE (full
     ink) — the pair a sub-screen needs to answer both "where does back go" and "where am I",
     in one control instead of across two. The whole two-line block is ONE <button>;
     nothing about the shape below needs align-items or a flex row, because the crumb and the
     title stack in ordinary block flow.
     THE 56px FLOOR IS THE BUTTON'S OWN BOX (min-height, not a pseudo-element hit trick) — see
     the departure note this class leaves in the 44px hardening list further down this file,
     the same note .rback wrote for itself at f-2. ZERO BRAND GREEN: the crumb, its chevron and
     the title are --tmut/--text only — Direction B's own thesis is that removing the one green
     label a sub-screen used to show makes the screen's REAL primary actions the only green on
     it. Pressed: a faint ink wash, the crumb darkens to --text, the chevron slides -2px — all
     three of which the sitewide `@media(prefers-reduced-motion:reduce)` rule at the foot of
     this file already crushes to instant (transition-duration:.001ms) for the decorative slide,
     while the wash and the colour swap, carrying no transition property of their own, change on
     :active regardless of that media query — the state change was never gated on motion. */
  .wp{display:block;width:100%;text-align:left;border:0;background:none;cursor:pointer;
    padding:9px 8px 11px 2px;margin:-2px 0 8px;min-height:56px;border-radius:12px}
  .wp .crumb{display:inline-flex;align-items:center;gap:2px;font-size:10px;font-weight:800;
    letter-spacing:.09em;color:var(--tmut);text-transform:uppercase}
  .wp .crumb svg{width:13px;height:13px;margin-left:-3px;transition:transform .15s}
  .wp .ttl{display:block;font-size:17px;font-weight:800;color:var(--text);margin-top:2px;
    white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
  .wp:active{background:rgba(26,32,38,.05)}
  .wp:active .crumb{color:var(--text)}
  .wp:active .crumb svg{transform:translateX(-2px)}
  .alloc{border:1px solid var(--border);border-radius:var(--r-card);background:var(--card);padding:12px}
  .alloc .ah{font-size:9.5px;font-weight:800;letter-spacing:.07em;text-transform:uppercase;color:var(--tfaint)}
  .alloc .bar{display:flex;height:12px;border-radius:99px;overflow:hidden;margin:9px 0 10px;background:var(--scr)}
  .alloc .bar i{display:block;height:100%}
  .alloc .leg{display:flex;flex-wrap:wrap;gap:6px}
  .lchip{font-size:10px;font-weight:800;border:1px solid var(--border);border-radius:99px;padding:4px 9px;display:inline-flex;gap:6px;align-items:center;color:var(--tmut)}
  .lchip .dot{width:7px;height:7px;border-radius:99px}
  .rfilters{display:flex;gap:6px;overflow-x:auto;margin:12px 0 2px}
  .rf{border:1px solid var(--border);background:var(--card);border-radius:99px;font:inherit;font-weight:800;font-size:11px;padding:7px 12px;cursor:pointer;color:var(--tmut);white-space:nowrap}
  .rf.on{background:#2f6a3a;border-color:#2f6a3a;color:#fff}
  .rsec{margin-top:13px}
  .rsec .rh{display:flex;align-items:center;gap:7px;font-size:9.5px;font-weight:800;letter-spacing:.07em;text-transform:uppercase;color:var(--tfaint);padding:0 2px}
  .rsec .rh .dot{width:8px;height:8px;border-radius:99px}
  .rsec .rh .sum{margin-left:auto;color:var(--text);font-size:11.5px;letter-spacing:0}
  .rsec .rh .pd{color:var(--amber);font-size:9.5px;letter-spacing:0;margin-left:6px}
  .rrow{border:1px solid var(--border);background:var(--card);border-radius:var(--r-card);padding:9px 11px;margin-top:7px;cursor:pointer}
  .rrow .l1{display:flex;align-items:center;gap:10px}
  .rth{width:34px;height:44px;border-radius:var(--r-ctrl);background:#fff;border:1px solid var(--border);position:relative;overflow:hidden;flex:none}
  .rth i{position:absolute;left:5px;right:5px;height:2px;border-radius:var(--r-micro);background:#d7dbe0}
  .rth .tt{background:#8a929a;height:3px}
  .rrow .vn2{font-weight:800;font-size:12.5px}
  .rrow .m2{color:var(--tmut);font-size:10px;margin-top:1px}
  .rrow .amt2{margin-left:auto;text-align:right}
  .rrow .amt2 b{font-size:13px}
  .rrow .amt2 .st2c{display:block;font-size:8.5px;font-weight:800;margin-top:2px}
  .rxp{border-top:1px dashed var(--border);margin-top:9px;padding-top:10px;display:flex;gap:11px}
  .rxp .trail{font-size:10.5px;color:var(--tmut);line-height:1.5;min-width:0}
  .rxp .trail b{color:var(--text)}
  .rxp .tl2{display:flex;gap:7px;align-items:flex-start;margin-top:4px}
  .rxp .td2{width:6px;height:6px;border-radius:99px;background:var(--brand);margin-top:4px;flex:none}
  /* foreman hours pane */
  .hnote{color:var(--tmut);font-size:10.5px;margin-top:9px;line-height:1.45}
  .drow{display:flex;align-items:center;gap:9px;border:1px solid var(--border);background:var(--card);border-radius:var(--r-card);padding:9px 12px;margin-top:7px;font-size:12px}
  .drow .dd{font-weight:800}
  .drow .dh{margin-left:auto;font-weight:800}
  .drow .dw{color:var(--tmut);font-size:10.5px}
  /* settings */
  .pcard{display:flex;align-items:center;gap:12px;border:1px solid var(--border);background:var(--card);border-radius:var(--r-card);padding:13px 14px}
  .pcard .pav{width:44px;height:44px;border-radius:99px;background:#2f6a3a;color:#fff;font-weight:800;font-size:15px;display:flex;align-items:center;justify-content:center;flex:none}
  .pcard .pn{font-weight:800;font-size:13.5px}
  .pcard .pm{color:var(--tmut);font-size:10.5px;margin-top:1px}
  .pcard .rolechip2{margin-left:auto;font-size:9px;font-weight:800;background:rgba(47,106,58,.12);color:#2f6a3a;border-radius:var(--r-ctrl);padding:3px 8px;letter-spacing:.05em}
  .sh{font-size:9.5px;font-weight:800;letter-spacing:.07em;text-transform:uppercase;color:var(--tfaint);margin:16px 2px 8px;display:flex;align-items:center;gap:7px}
  .sh .who{margin-left:auto;font-size:8.5px;background:var(--scr);border:1px solid var(--border);border-radius:var(--r-ctrl);padding:2.5px 7px;color:var(--tmut);letter-spacing:.04em}
  /* 56, THE HOUSE FLOOR (f-14, item 9, `touch-target`). .trow measured 362x52 and it is EVERY
     door on the Office hub, every Team and Crews row, and every Settings row — the comment at
     the hazard mark below already called it "a row that answers a thumb", and it was four
     pixels short of one. min-height rather than height: the two-line rows (name over role)
     already stand taller and must keep doing so. */
  .trow{display:flex;align-items:center;gap:10px;border:1px solid var(--border);background:var(--card);border-radius:var(--r-card);padding:10px 12px;margin-top:7px;min-height:56px}
  /* AVATAR is a component, not a row detail — do not re-scope this under .trow.
     It is used in the settings rows, the roster, and the time-adjust sheet header. */
  .tav{width:30px;height:30px;border-radius:99px;background:#69727b;color:#fff;font-weight:800;font-size:10.5px;display:flex;align-items:center;justify-content:center;flex:none}
  .trow .tn{font-weight:800;font-size:12px}
  .trow .tm{color:var(--tmut);font-size:9.5px;margin-top:1px}
  .trow .rl{margin-left:auto;font-size:9px;font-weight:800;border-radius:var(--r-ctrl);padding:3px 8px;letter-spacing:.05em}
  /* THE HAZARD MARK (#timeHz / #payHz, T3, 2026-08-29) — Wes's own words for the whole review
     surface: "just having a little icon by the box, like, with a hazard sign or something".
     COPPER, NEVER RED. LAW 21 reserves red for safety, and a shift that needs review is a
     bookkeeping fact, not a hazard to a person. The "hazard sign" is carried by the SHAPE — a
     triangle with an exclamation, drawn in the markup — while the colour is the --copper /
     --copper-ink pair every other "somebody should look at this" in this app already wears
     (.edchip, .adjsusp, .tmd.hole, .tmsumc.warn). The tint is --copper at fill weight; the words
     are --copper-ink at 6.0:1 on --card.
     IT IS NOT A CONTROL. The whole .trow is the tap target and keeps its own box; this mark adds
     no height (12px glyph, 10.5px number, inside a row whose avatar is taller), which
     test/office-time.cjs measures as a CONTROL PAIR — the row's height with the mark in and out
     — rather than asserting.
     NEVER COLOUR ALONE: the number is inside it and the row's own subtitle says the same thing
     in words (views-team paintReviewMarks). margin-left:auto so it takes the free space and the
     chevron still trails it. */
  .hz{display:inline-flex;align-items:center;gap:4px;margin-left:auto;flex:none;
      background:rgba(201,131,47,.14);border:1px solid rgba(201,131,47,.42);color:var(--copper-ink);
      border-radius:99px;padding:3px 8px;font-weight:800;font-size:10.5px;font-variant-numeric:tabular-nums}
  .hz svg{width:12px;height:12px;flex:none}
  /* TWO auto MARGINS SPLIT THE FREE SPACE BETWEEN THEM, which is why the Time row's pill and the
     Payroll row's sat 29px apart on rows whose only difference is the length of the words before
     them (measured by the eval). The chevron's `margin-left:auto` is INLINE in the markup on
     every hub row, so it cannot be removed for these two without touching all of them — it is
     neutralised here instead, and only where a mark actually precedes it. The pill takes the
     slack; the chevron trails it by a fixed 8px on both rows. */
  .hz + .chev{margin-left:8px!important}
  /* The hub row's subtitle when it is carrying the review sentence rather than its own
     description. Same ink as the mark, so one fact reads as one fact. */
  .trow .tm.warn{color:var(--copper-ink);font-weight:800}
  .rl.own{background:rgba(47,106,58,.12);color:#2f6a3a}
  .rl.mgr{background:rgba(63,111,174,.13);color:#3f6fae}
  .rl.frm{background:rgba(201,131,47,.13);color:#a06322}
  /* 56 (f-14, item 9). #inviteBtn2 and #newCrewBtn are this class, they measured 44, and they
     are the only way to add a person or a crew from those two screens. */
  .invite{width:100%;margin-top:7px;border:1.5px dashed var(--strong);background:none;border-radius:var(--r-card);color:var(--tmut);font:inherit;font-weight:800;font-size:11.5px;padding:10px;cursor:pointer;min-height:56px}
  .prow{display:flex;align-items:center;gap:10px;border:1px solid var(--border);background:var(--card);border-radius:var(--r-card);padding:11px 12px;margin-top:7px}
  .prow .pt{font-weight:800;font-size:12px}
  .prow .ps{color:var(--tmut);font-size:10px;margin-top:2px;line-height:1.35}
  .sw{margin-left:auto;flex:none;width:38px;height:22px;border-radius:99px;background:#c9ced3;border:0;cursor:pointer;position:relative;transition:.18s}
  .sw::after{content:'';position:absolute;top:2px;left:2px;width:18px;height:18px;border-radius:99px;background:#fff;box-shadow:0 1px 3px rgba(0,0,0,.25);transition:.18s}
  .sw.on{background:#7DBA3D}
  .sw.on::after{left:18px}
  /* NO-LOCKS LAW: never render a padlock or a disabled/greyed control anywhere.
     A policy that cannot be changed shows a static ALWAYS chip — information, not a dead switch. */
  .always{margin-left:auto;flex:none;font-size:9.5px;font-weight:800;letter-spacing:.06em;
          background:rgba(47,106,58,.10);color:var(--brand);border:1px solid rgba(47,106,58,.22);
          border-radius:var(--r-ctrl);padding:4px 8px;white-space:nowrap}
  /* .signout IS THE APP'S DESTRUCTIVE OUTLINED BUTTON — Deactivate, Delete crew, Remove from
     the list, Archive job, and the delete-account confirm all wear it. The class keeps the red;
     what it loses at f-2 is the RAW HEX. `#c2412e` and `rgba(194,65,46,.35)` were the same
     colour as var(--red) written out twice by hand, three rules apart from the token — the
     `palette-drift` check's plainest shape ("no raw hex outside :root"), and the way a token
     and its hand-copies quietly stop agreeing. --red-line is the 35% outline, defined next to
     --red so the pair moves together.

     AND IT CARRIES ITS OWN 56px FLOOR AS OF f-2 WAVE 1.1 (eval `touch-target`, 7th occurrence).
     It used to take 44px off the shared floor list near the foot of this file — so in the
     account sheet it rendered 362x44 directly above a 94x56 #delOpen, two floors for two
     controls a thumb hits in the same sheet. That list's own note, three rules along, already
     says why that cannot stand: "a 44px floor sitting under a 56px box is a second number for
     the same control — which is how the two drift, and how 'the composer is at the floor'
     quietly becomes false the next time somebody edits the smaller of the two." .agsend and
     .rback left that list for exactly this reason; .signout is the third, and it is the one a
     gloved hand hits to end an account. The delete sheet's own 60px override (#delovl .signout,
     further down) still wins there — it is the primary action of its own screen. */
  .signout{width:100%;margin-top:14px;min-height:56px;border:1px solid var(--red-line);background:none;border-radius:var(--r-card);color:var(--red);font:inherit;font-weight:800;font-size:12px;padding:11px;cursor:pointer}
  /* "REOPEN THIS JOB" IS THE SAME BUTTON AS "CLOSE OUT THIS JOB", one state later — and it was
     39px while its twin, drawn from .signout one line up, was 56 (f-13, lane 3 item 15e;
     `touch-target`). It is built from .kc, a CHIP class, because it is not destructive; that is
     the right ink and the wrong box. As of f-13 it is also the last step of the ONLY road back
     to a closed job, which is why the floor is pinned by id rather than left to the chip.

     …AND THE RADIUS COMES WITH THE FLOOR (eval f-13 r1, `radius-scale`, 2nd occurrence of the
     f-12 `.qbgo` shape). .kc is `border-radius:99px`, which at 39px tall reads as a chip and at
     362x56 reads as a lozenge — sitting directly under a 9px "Save changes" in the same sheet.
     One control, two states, two radii. A floor was owed; a new shape was not, so the box takes
     the CARD radius its neighbour wears and the chip's ink is left alone. Measured in
     job-editor's §7e against #jbedSave's own computed radius, never against a literal.
     …AND THE LABEL HAD TO COME WITH THE BOX (eval f-13 r2, `component-drift`). Leaving the ink
     alone left `.kc`'s 11.5px CHIP type on a 362x56 rectangle sitting directly under a 15px
     "Save changes" in the same sheet — a control grown to the floor with its caption still at
     chip size. It takes the size its neighbour wears (`.submit`, 15px), which is also the size
     every other full-width 56px control in this file already carries (#hmovl .ghost, #lsovl
     .ghost, #camPerm .ghost). Colour and weight are untouched: this is a secondary control and
     the .kc ink is what says so. Measured in §7e against #jbedSave's own computed size. */
  #jbedArch{min-height:56px;border-radius:var(--r-card);font-size:15px}
  /* …AND SIGNING OUT IS NOT A DESTRUCTIVE ACT (f-2, 2026-08-21 — TASKS 19 / eval f-1.1).
     This is the one button in that sheet EVERY role taps, on the most ordinary errand there
     is, and it was drawn in the colour LAW 21 reserves for safety — the same ink as "Delete
     crew". Nothing is lost by signing out: the session ends and the next sign-in restores it.
     So it takes the app's neutral outlined treatment — --strong for the line, --text for the
     label, the family every non-destructive outlined control here already wears — and the red
     stays on the row directly below it, which really is one-way. Two controls, two meanings,
     one of them no longer shouting. */
  #signoutBtn{border-color:var(--strong);color:var(--text)}
  /* home */
  .hcard{border:1px solid var(--border);border-radius:var(--r-card);background:var(--card);padding:13px 14px;margin-top:11px}
  .hh{display:flex;align-items:center;font-size:9.5px;font-weight:800;letter-spacing:.07em;text-transform:uppercase;color:var(--tfaint);margin-bottom:9px}
  /* ---- A SECTION HEADER THAT FOLDS (Wes, 2026-08-23: "colapse the jobs section at the top off
     office the open jobs area") ---------------------------------------------------------------
     THE WHOLE HEADER IS THE BUTTON, not the chevron. A 17px glyph with a listener on it is the
     `touch-target` class this file has now paid for seven times; the header is a full-width
     <button> carrying the 56px floor, so the target is 340x56 on a 390 and the chevron is
     decoration that says which way it goes.

     IT IS THE SAME HEADER IT ALWAYS WAS. Every type property below is copied from .hh above and
     re-stated only because a <button> does not inherit font, colour or text-align from the card
     — no new size, no new weight, no new colour, and no new visual language on a screen whose
     other three cards keep the plain one. margin-bottom drops from 9px to 4px because the box
     now carries its own vertical air.

     NO TRANSITION ON THE CHEVRON, deliberately. A rotation would need a prefers-reduced-motion
     arm to be correct (see the block at the bottom of this file), and a 90-degree turn that
     nobody asked to watch is not worth a second rule that can fall out of step with it. */
  .hfold{width:100%;min-height:56px;border:0;background:none;padding:0;margin:0 0 4px;
    display:flex;align-items:center;gap:8px;cursor:pointer;text-align:left;
    font:inherit;font-size:9.5px;font-weight:800;letter-spacing:.07em;text-transform:uppercase;color:var(--tfaint)}
  .hfold .hfoldc{margin-left:auto;font-size:19px;line-height:1;letter-spacing:0;
    transform:rotate(90deg);transform-origin:50% 45%}
  .hfold[aria-expanded="false"] .hfoldc{transform:none}
  /* The one line a folded section still says. .bigsub's type, with the top air the big figure
     used to provide and no longer does. */
  .hfoldsum{margin-top:2px}
  /* .ghost IS A CONTROL, wherever it is drawn. It only ever had a rule as `.hh .ghost` — a
     descendant of a card HEADER — so the ones that are not in one shipped as raw UA buttons:
     the approval queue's Retry (views-jobs paintQueueList), errCard's Retry (views-team), the
     Settings jump rows. A button offering the way out of a failed fetch, styled like nothing
     else in the app, does not read as the way out. The look lives on .ghost; .hh only adds the
     push to the right that a header row needs. */
  .ghost{border:1px solid var(--strong);background:none;border-radius:99px;color:var(--brand);font:inherit;font-weight:800;font-size:10.5px;padding:5px 11px;cursor:pointer;letter-spacing:.02em;text-transform:none}
  .hh .ghost{margin-left:auto}
  .wx{display:flex;align-items:center;gap:13px;border:1px solid var(--border);border-radius:var(--r-card);background:linear-gradient(135deg,#fdfefd,#eef4ee);padding:13px 15px}
  .wx svg{width:38px;height:38px;color:var(--amber);flex:none}
  .wx .t{font-size:22px;font-weight:800}
  .wx .m{color:var(--tmut);font-size:11px;margin-top:1px}
  .wx .auto2{margin-left:auto;font-size:9px;font-weight:800;color:var(--brand);background:rgba(125,186,61,.14);border-radius:var(--r-ctrl);padding:2px 7px}
  /* THE PLACE A FORECAST IS FOR (R0, 2026-08-30) — Home card and the daily-log chip, one rule.
     11.5px on --tmut is the 4.89:1 pair fix round 3 landed for exactly this kind of small
     caption; --tfaint measured 2.37:1 and is not allowed to carry a sentence that matters.
     .warn is COPPER, never red: red on this app means safety (LAW 21), and a job with no pin
     is a gap somebody should close, not a hazard. It is not colour alone either — the sentence
     says the whole thing in words and the icon changes with it. */
  /* flex-START, not center: the copper sentence wraps to two lines at 375px on any real town
     name, and centred the icon floated in the gutter between them instead of marking the line
     it belongs to. */
  .wxpl{display:flex;align-items:flex-start;gap:5px;color:var(--tmut);font-size:11.5px;line-height:1.35;margin-top:3px}
  .wxpl.warn{color:var(--copper-ink);font-weight:700}
  /* color:inherit IS LOAD-BEARING, and it was found by opening the screenshots rather than by
     an assertion. `.wx svg{color:var(--amber)}` catches every svg inside the Home card, so the
     same line rendered an AMBER icon beside copper words on Home and a COPPER one in the daily
     log, where no such rule reaches it — one sentence, two inks, on the two surfaces this round
     exists to make agree (`fixed-one-of-N`). The icon takes the line's own colour now, which
     also keeps amber for the wind/rain chips above it rather than spending it twice. */
  .wxpl .mi{width:12px;height:12px;flex:none;color:inherit;margin-top:2px}
  #v-log .wxpl{margin-top:6px}
  /* THE JOB ROW IS A CONTROL — it opens the app's job picker — and it was 53px on the clock
     card while every field and chip beside it was 56 (`touch-target`, 2026-09-05, f-13 lane 2
     item 1). It reads as decoration because it looks like a caption, which is why nothing
     caught it until a box was measured. The floor lives HERE, at the base rule, rather than
     scoped to one sheet: the same class carries the Costs screen's job row (#jbJobRow) and the
     adjust sheet's (#adjJobRow), and all three of them open the same picker. Argument inherited
     from the `#csovl .ckjob` rule this replaces. */
  .ckjob{display:flex;align-items:center;gap:10px;min-height:56px;border:1px solid var(--border);background:var(--scr);border-radius:var(--r-card);padding:10px 12px;cursor:pointer}
  .ckjob .jn{font-weight:800;font-size:12.5px}
  .ckjob .ja{color:var(--tmut);font-size:10px;margin-top:1px}
  .ckjob .chg{margin-left:auto;color:var(--brand);font-weight:800;font-size:11px;white-space:nowrap}
  /* 56, at its own rule (see the floor list at the foot of this file for why it left the 44
     one). Clock in and Clock out are the two buttons this whole app exists for. */
  .ckbtn{width:100%;min-height:56px;margin-top:10px;border:0;border-radius:var(--r-card);background:var(--lime);color:#1c2b22;font:inherit;font-weight:800;font-size:14.5px;padding:14px;cursor:pointer;box-shadow:0 6px 18px rgba(125,186,61,.35)}
  .ckbtn.out{background:#26313a;color:#fff;box-shadow:none}
  /* .ckstat HAS NO USERS. The R6 mockup swap (06d115a) renamed this wrapper to .ckhead and
     moved it to the block at `.ckhead{`; the rule is left here rather than deleted because the
     DOT's rule below is the one that mattered and its story is this one. */
  .ckstat{display:flex;align-items:center;gap:8px;font-weight:800;font-size:13px}
  /* THE LIVE DOT, RETARGETED 2026-09-05 (f-13 lane 2 item 4, `invisible-but-passing`). It read
     `.ckstat .dot` and #ckDot's wrapper has been `.ckhead` since the R6 swap — so the one
     element on the card that says "the clock is running right now" rendered 0x0 and passed
     every assertion it had, because a programmatic read works perfectly on a box with no size. */
  .ckhead .dot{width:9px;height:9px;border-radius:99px;background:var(--lime);box-shadow:0 0 0 4px rgba(125,186,61,.25);animation:pu 1.6s infinite}
  @keyframes pu{50%{box-shadow:0 0 0 7px rgba(125,186,61,.12)}}
  .ckel{margin-left:auto;font-variant-numeric:tabular-nums;font-weight:800;font-size:16px}
  .gpsnote{color:var(--tfaint);font-size:9.5px;margin-top:8px;text-align:center}
  /* ---- THIS WEEK (2026-08-13) ------------------------------------------------------------
     Replaces a progress ring + seven arbitrary-height bars. The chart sits on a FIXED 0-12h
     domain at 2h = 14px, so two weeks are the same shape and comparable; a stretch-to-fit axis
     draws a 4h week and a 12h week identically. Radii come from the scale, never literals. */
  .wk2hd{display:flex;align-items:flex-end;gap:12px;margin-bottom:12px}
  .wkc{font-size:27px;font-weight:800;letter-spacing:-.02em;line-height:1;font-variant-numeric:tabular-nums}
  .wkc small{display:block;font-size:10px;color:var(--tmut);font-weight:700;margin-top:3px;letter-spacing:0}
  .wk2goal{margin-left:auto;text-align:right;flex:none}
  .wk2meter{width:104px;height:7px;border-radius:99px;background:var(--strong);overflow:hidden;display:flex}
  .wk2meter i{display:block;height:100%;background:var(--lime)}
  /* COPPER, not red (f-1). This segment is the WEEKLY overtime — the number payroll owes —
     and My-week already prints that exact figure in copper (.mwot: "Over the weekly line is COPPER,
     red"). Red in this app is reserved for safety, and the same fact must not wear two
     colours on two screens or the reader learns nothing from either. */
  .wk2meter i.ot{background:var(--copper)}
  .wk2goal small{display:block;font-size:9.5px;font-weight:700;color:var(--tmut);margin-top:5px}
  .wk2chart{position:relative;height:84px}
  .wk2gl{position:absolute;left:0;right:26px;height:1px;background:var(--border)}
  .wk2gl.k{background:var(--strong)}
  .wk2gt{position:absolute;right:0;font-size:9px;font-weight:700;color:var(--tfaint);transform:translateY(-50%)}
  .wk2gt.k{color:var(--tmut);font-weight:800}
  .wk2base{position:absolute;left:0;right:26px;bottom:0;height:1px;background:var(--strong)}
  .wk2bars{position:absolute;left:0;right:26px;bottom:0;top:0;display:flex;align-items:flex-end;justify-content:space-around}
  /* The whole column is the target — the old card carried a title attribute, which never
     fires on a touch screen, so tapping a day did nothing for every crew member. */
  .wk2dc{width:34px;height:100%;display:flex;flex-direction:column;justify-content:flex-end;align-items:center;position:relative;background:none;border:0;padding:0;font:inherit;cursor:pointer}
  .wk2stk{width:26px;display:flex;flex-direction:column;justify-content:flex-end;height:100%}
  .wk2stk i{display:block;width:100%}
  .wk2stk i.reg{background:var(--lime);border-radius:0 0 var(--r-micro) var(--r-micro)}
  .wk2stk i.reg.solo{border-radius:var(--r-micro)}
  /* `.wk2stk i.ot` — the red bar cap — is DELETED at f-1 along with the code that emitted it
     (app.js renderWeekBars). Every day bar is now one solid `reg solo`. The rule is removed
     rather than left orphaned: dead CSS carrying var(--red) on a crew surface is exactly what
     gets re-emitted by accident later, and LAW 21 reserves red for safety. The daily-overtime
     GRIDLINE (.wk2gl.k) stays — a long day is still visibly past a labelled rule, by geometry
     instead of by colour, which also survives greyscale and colour-blindness. */
  .wk2tip{position:absolute;font-size:9px;font-weight:800;color:var(--text);font-variant-numeric:tabular-nums;white-space:nowrap}
  .wk2days{display:flex;justify-content:space-around;margin-top:7px;padding-right:26px}
  .wk2dl{width:34px;text-align:center;font-size:11px;font-weight:700;color:var(--tmut);position:relative}
  .wk2dl.z{color:var(--tfaint);font-weight:600}
  .wk2dl.td{color:var(--text);font-weight:800}
  .wk2dl.td::after{content:"";position:absolute;left:50%;bottom:-5px;transform:translateX(-50%);width:4px;height:4px;border-radius:99px;background:var(--brand)}
  /* THE OVERTIME STATUS LINE. Idle it names the rule — "Overtime after 40h" — and naming a
     rule is not a warning, so it is the neutral caption treatment (--tfaint, same as
     .wk2note directly below). Accruing it goes COPPER and only copper, matching .mwot on
     My-week, which prints the identical number: one fact, one colour, both screens.
     WAS var(--red) with a red ::before swatch. The swatch keyed the red bar caps and died
     with them; the colour was wrong twice over — red is safety (LAW 21), and this line used
     to describe DAILY overtime while the meter beside it showed the WEEKLY figure in the same
     red, so a reader had no way to tell two different quantities apart. */
  .wkot{display:flex;align-items:center;margin-top:13px;font-size:10.5px;font-weight:800;color:var(--tfaint);letter-spacing:.02em}
  .wkot.on{color:var(--copper)}
  .wk2note{margin-top:10px;font-size:10px;font-weight:700;color:var(--tfaint);letter-spacing:.02em}
  /* THE DAY WITH NO CLOCK-OUT (f-2 W4 T4). Same caption, one token different: --copper, which
     is what this app already means by "somebody should look at this" and what .mwot on the
     very next card wears. Not red — LAW 21 keeps red for a punch that is LOST, and these hours
     are not lost, only uncounted until the office moves them. No new family, no raw hex. */
  .wk2note.warn{color:var(--copper)}
  .toffrow{display:flex;align-items:center;gap:10px;border:1px solid var(--border);background:var(--scr);border-radius:var(--r-card);padding:9px 12px;margin-top:7px}
  .toffrow .ti{width:30px;height:30px;border-radius:var(--r-ctrl);background:rgba(63,111,174,.14);color:#3f6fae;display:flex;align-items:center;justify-content:center;flex:none}
  .toffrow .ti svg{width:16px;height:16px}
  .toffrow .tn2{font-weight:800;font-size:12px}
  .toffrow .tm2{color:var(--tmut);font-size:10px;margin-top:1px}
  .toffrow .tst{margin-left:auto;font-size:8.5px;font-weight:800;border-radius:var(--r-ctrl);padding:3px 8px;letter-spacing:.05em}
  .tst.ok{background:rgba(125,186,61,.15);color:var(--lime-ink)}
  .tst.pend{background:rgba(224,150,60,.16);color:#a06322}
  .jrow{display:flex;align-items:center;gap:11px;border:1px solid var(--border);background:var(--card);border-radius:var(--r-card);padding:12px 13px;margin-top:8px;cursor:pointer}
  .jrow .jic{width:34px;height:34px;border-radius:var(--r-card);background:rgba(47,106,58,.1);color:#2f6a3a;display:flex;align-items:center;justify-content:center;flex:none}
  .jrow .jic svg{width:18px;height:18px}
  .jrow .onchip{margin-left:auto;font-size:8.5px;font-weight:800;background:rgba(125,186,61,.16);color:var(--lime-ink);border-radius:var(--r-ctrl);padding:3px 8px}
  .jrow mark{background:rgba(125,186,61,.34);color:inherit;border-radius:var(--r-micro);padding:0 1px}
  /* JOB SEARCH — the picker is a search field, not a list. Typing filters name AND address. */
  .srch{display:flex;align-items:center;gap:9px;border:1.5px solid var(--strong);background:var(--card);
        border-radius:var(--r-card);padding:0 11px;height:46px;margin:0 0 4px}
  .srch:focus-within{border-color:var(--brand);box-shadow:0 0 0 3px rgba(47,106,58,.1)}
  .srch .si{width:17px;height:17px;flex:none;color:var(--tfaint)}
  .srch:focus-within .si{color:var(--brand)}
  .srch input{flex:1;min-width:0;border:0;outline:0;background:none;font:inherit;font-size:15px;font-weight:600;color:var(--text)}
  .srch input::placeholder{font-weight:600;color:var(--tfaint)}
  .sx{border:0;background:var(--scr);color:var(--tmut);width:22px;height:22px;border-radius:50%;flex:none;
      cursor:pointer;font-size:13px;line-height:1;display:none;align-items:center;justify-content:center;padding:0}
  .sx.on{display:flex}
  .srchmeta{display:flex;align-items:center;gap:6px;margin:9px 4px 0;font-size:9.5px;font-weight:800;
            letter-spacing:.07em;text-transform:uppercase;color:var(--tfaint)}
  .srchmeta b{color:var(--brand);font-weight:800}
  .nohit{display:flex;flex-direction:column;align-items:center;gap:7px;padding:26px 0 20px;color:var(--tfaint)}
  .nohit svg{width:28px;height:28px;opacity:.55}
  .nohit .nt{font-size:12px;font-weight:700;color:var(--tmut)}
  /* Results scroll, the search field does NOT. A crew of 40 jobs must not push the
     input off the top of the sheet. Cap is ~4.5 rows so the cut edge reads as scrollable. */
  #jobList{max-height:330px;overflow-y:auto;-webkit-overflow-scrolling:touch;
           margin:0 -2px;padding:0 2px 2px}
  /* ---- CREW ON THE CLOCK (owner/manager) ---- */
  .hh .cnt{margin-left:6px;background:rgba(125,186,61,.18);color:var(--lime-ink);border-radius:var(--r-ctrl);padding:2px 7px;font-size:9px;letter-spacing:.03em}
  /* THE 56px FLOOR, written here rather than assumed (eval 2026-08-21, `touch-target`, EIGHTH
     occurrence of the class and the first time it was recorded anywhere at all). Measured at
     332x51 on owner Home at 390: min-height 0, padding 9/10 around a 31px avatar. This row is
     the ONLY door to the paid-time adjust sheet, and the 60-second poll (f-2 w3 R1) just made
     the card behind it a live, changing surface — the standing note on this class is that the
     small control is always the entry point to the thing that was made big.
     min-height rather than more padding: the row WRAPS its chips onto a second line (.crow .cj
     below), and padding would push an already-tall two-line row to ~70px for nothing. The floor
     is the same 56 the sheets, the door and the role editor already take. Pinned in
     team-books-people (owner crew card, 390x900) so the ninth occurrence is a red suite. */
  .crow{display:flex;align-items:center;gap:10px;border:1px solid var(--border);background:var(--card);
        border-radius:var(--r-card);padding:9px 10px;margin-top:7px;cursor:pointer;min-height:56px}
  .crow:active{background:var(--scr)}
  .crow .cav{width:31px;height:31px;border-radius:50%;flex:none;color:#fff;font-size:10.5px;font-weight:800;
             display:flex;align-items:center;justify-content:center;position:relative}
  .crow .cav i{position:absolute;right:-1px;bottom:-1px;width:9px;height:9px;border-radius:50%;
               background:var(--lime);border:2px solid var(--card)}
  .crow .cmid{flex:1;min-width:0}
  .crow .cn{font-weight:800;font-size:12.5px;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
  /* The EDITED chip lives on the job line, NOT inside .cn — an ellipsis on the name
     would eat it, and "this punch was edited" outranks the tail of a job name. */
  /* WRAPS, and the job name keeps a floor. A punch row (W1 1.5) can carry three chips at
     once — edited, closed by an admin, no GPS — and on one nowrap line flex shrank the
     JOB NAME, the one thing the row is actually about, down to a single letter to make
     room for them. Chips drop to a second line instead. */
  .crow .cj{display:flex;align-items:center;flex-wrap:wrap;gap:5px 5px;color:var(--tmut);font-size:10px;margin-top:1px}
  .crow .cjt{white-space:nowrap;overflow:hidden;text-overflow:ellipsis;flex:1 1 auto;min-width:46%}
  .crow .cr{margin-left:auto;text-align:right;flex:none;padding-left:6px}
  .crow .ch{font-weight:800;font-size:12.5px;font-variant-numeric:tabular-nums}
  .crow .cs{color:var(--tfaint);font-size:9.5px;margin-top:1px}
  .crow .cpen{flex:none;width:28px;height:28px;border-radius:var(--r-ctrl);background:var(--scr);color:var(--tmut);
              display:flex;align-items:center;justify-content:center;margin-left:8px}
  .crow .cpen svg{width:14px;height:14px}
  /* ---- HOURS VS ESTIMATE, ON HOME (f-6, 2026-08-25) --------------------------------------
     One row per open job. Almost nothing here is new furniture: the row is .crow's box and its
     56px floor, the heading is .hh with .hh .cnt, and the bar is .bbar's look with the fill
     recoloured by burn. What IS new is the two-line shape — name + figure, then the meter —
     because a job name and "18.4 / 40 hrs · 46%" cannot both survive on one 375px line.

     THE 56px FLOOR IS THE ROW'S, and the row is a DOOR: it opens that job's own drilldown — the
     Hours pane for a foreman, where the same figure is printed with the day rows under it, and
     the Costs pane for a money-holder, which is his half of the same screen. The ledger's
     `touch-target` note is the reason it is written rather than assumed — the small control is
     always the entry point to the thing that was made big.

     THE BURN COLOURS ARE THE JOB DRILLDOWN'S, to the value: --lime under 85, --amber from 85,
     and OVER the estimate is COPPER. Not red. LAW 21 reserves red for safety, and this app
     already uses copper for over-40 overtime — an owner who has learned copper means "past the
     line, look at this" should not have to learn a second colour for the same sentence.

     .hvv.nofig IS .bignum.nofig's RULE at row scale: a placeholder must not wear the figure's
     weight. An em dash at 800 reads as a redaction — a number that exists and is being withheld
     — which is the opposite of "there is nothing to measure yet". */
  .hvrow{display:block;width:100%;text-align:left;border:1px solid var(--border);background:var(--card);
         border-radius:var(--r-card);padding:9px 10px;margin-top:7px;min-height:56px;cursor:pointer;
         font:inherit;color:inherit}
  .hvrow:active{background:var(--scr)}
  .hvh{display:flex;align-items:baseline;gap:8px}
  .hvn{font-weight:800;font-size:12.5px;min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
  .hvv{margin-left:auto;flex:none;font-weight:800;font-size:12.5px;font-variant-numeric:tabular-nums}
  .hvv.nofig{font-weight:400;color:var(--tmut)}
  .hvbar{height:7px;border-radius:var(--r-ctrl);background:var(--scr);border:1px solid var(--border);
         overflow:hidden;margin-top:7px}
  .hvbar i{display:block;height:100%;background:var(--lime)}
  .hvbar.warn i{background:var(--amber)}
  .hvbar.over i{background:var(--copper)}
  .hvs{font-size:10.5px;color:var(--tmut);margin-top:5px;line-height:1.45}
  /* THE TWO LINES THAT MAKE THE FIGURE ABOVE HONEST (f-6 fix round; the first of them changed
     job at f-7, 2026-08-27). The server has always sent both — inProgressHours, unpairedShifts
     — and the first cut of this card read neither, so Home printed "8 / 40 hrs · 20%" over a job
     with three hours on the clock right now and a shift that never got a clock-out, while the
     drilldown one tap away printed both.
     THEY ARE NO LONGER THE SAME KIND OF LINE. inProgressHours is now INSIDE the figure (the
     hours burn counts live work, as the cost burn always has), so its line is a DISCLOSURE —
     "includes 2.5 hrs on the clock now". unpairedShifts is still missing from the figure and
     unrecoverable, so its line stays a CAVEAT and keeps the copper ink: it is the one an owner
     has to go and fix. */
  /* 11.5px AND --tmut, NOT 9.5px AND --tfaint (f-6 fix round, EVAL-LEDGER `palette-drift`).
     --tfaint (#9aa2aa) on the white card is 2.59:1 — below AA at any size, let alone at 9.5px —
     and the line wearing it is the one that says what the figure above it is made of: "includes
     3 hrs on the clock now". 11.5px is this stylesheet's caption floor, set 2026-08-23
     when .confchip went 9px -> 11.5px on exactly this argument (a caption carrying a payroll
     fact has to be legible); --tmut (#69727b) is 4.89:1, the same ink .hvs above it spends.
     AND IT IS DELIBERATELY NO LONGER SMALLER THAN .hvs. The old size said "footnote". It is not
     a footnote on the sentence — it is what the FIGURE at the top of the row is made of, and the
     only line on this card that changes what an owner does about a number. The hierarchy this
     row needs is figure -> prose, and both of these lines are prose. */
  .hvs2{font-size:11.5px;color:var(--tmut);margin-top:3px;line-height:1.45}
  .hvs2.warn{color:var(--copper-ink);font-weight:800}
  .hvs.over{color:var(--copper-ink);font-weight:800}
  /* OVER THE ESTIMATE, IN THE FIGURE AS WELL AS UNDER IT — found by looking at the screenshot,
     not by an assertion (§8: eyes). --amber (#b97f1c) and --copper (#c9832f) are one hue apart
     and at 7px of meter they are the same brown: on the rendered card the 90% row and the 250%
     row read alike, and the only thing separating them was the width of a fill and the weight
     of a sentence. So the NUMBER carries it too. The ink is #8a5a17, the darker copper this app
     already prints warning prose in (.hvs.over, the edchip, views-jobs' noteHtml) — not a new
     family for one row. It is a REINFORCEMENT and never the only signal: the sentence under it
     says "6 hrs over the estimate." in words, which is what a colour-blind owner reads. */
  .hvrow.over .hvv{color:var(--copper-ink)}
  /* The cap line and the two empty states. Muted, never a control — it states a fact about the
     list above it (how many open jobs did not fit, or that there are none), and a list that
     just ends at six is the silent cap this line exists to prevent. */
  .hvcap{font-size:10.5px;color:var(--tfaint);line-height:1.5;margin-top:9px}
  .hvcap:empty{display:none}
  .hvnone{font-size:11.5px;color:var(--tmut);line-height:1.5;margin:2px 2px 0}
  .edchip{display:inline-flex;align-items:center;gap:3px;background:rgba(201,131,47,.14);color:var(--copper-ink);
          border-radius:var(--r-ctrl);padding:1px 5px;font-size:8.5px;font-weight:800;letter-spacing:.04em;flex:none}
  .edchip svg{width:8px;height:8px}
  /* ---- OFFICE TIME (v-time) — component T2, 2026-08-29 -------------------------------------
     Everyone's week, by crew or by person. Its punch rows are .crow/.edchip above — this block
     is only the shell around them, because a second row style would be a second dialect for the
     same fact.
     THE TOUCH FLOOR IS TAKEN ON THIS SCREEN'S OWN CLASSES, never by widening a shared one.
     .calnav (30px, 44px ghost) already carries the person page's week arrows; moving IT to 56
     would have relaid out four screens this round never opened, which is exactly how the T1
     round's `min-height:56px` on `.inp` broke onboarding step 6. So .tmnav is its own control
     at the house floor and .calnav is untouched. Measured, not read: test/office-time.cjs. */
  /* EVERY MARGIN ON THIS SCREEN IS PAID FOR BY THE ROWS BELOW IT. At 375x667 with the three
     caveats an ordinary week produces, the first person row sat 20px clear of the tab bar —
     on screen, and not enough of it to read or tap. The block below is a density pass over
     the chrome, not over the copy (`keyboard-fold`'s own rule: fix the shape). Nothing here
     touches a control's own box: .tmnav, #tmLens button and .tmrow all keep their 56px. */
  .tmweek{display:flex;align-items:center;gap:8px;margin-top:5px}
  .tmnav{min-width:56px;min-height:56px;border:1px solid var(--border);border-radius:var(--r-card);
         background:var(--card);font:inherit;font-size:18px;font-weight:800;color:var(--tmut);cursor:pointer;flex:none}
  .tmnav:active{background:var(--scr)}
  .tmwk{flex:1;min-width:0;text-align:center}
  .tmwkl{font-weight:800;font-size:13px}
  .tmwks{font-size:11.5px;color:var(--tmut);margin-top:1px}
  /* The lens control is the job pane's .seg2, at this screen's floor and NOWHERE else — same
     reasoning as .tmnav: .seg2 is shared with Costs/Docs/Activity and does not move. */
  #tmLens{margin-top:5px}
  #tmLens button{min-height:56px}
  /* THE COMPANY'S OWN TOTAL, ONCE. Every caveat sentence lives here and not on the rows: seven
     people with a live shift each would otherwise print the same sentence seven times on one
     375px screen (`duplicate-prose`, EVAL-LEDGER 2026-08-24). The rows carry chips; the card
     makes the statement. */
  .tmsum{border:1px solid var(--border);background:var(--card);border-radius:var(--r-card);padding:8px 12px;margin-top:5px}
  .tmsumh{display:flex;align-items:baseline;gap:8px;flex-wrap:wrap}
  .tmsumv{font-weight:800;font-size:19px;font-variant-numeric:tabular-nums}
  .tmsuml{font-size:11.5px;color:var(--tmut);font-weight:700}
  .tmsumc{font-size:11.5px;color:var(--tmut);line-height:1.45;margin-top:4px}
  /* --copper-ink, the INK weight (6.0:1 on --card) — the same step every warning SENTENCE in
     this app is written in, and never --red: a shift that needs a clock-out is not a safety
     event (LAW 21). */
  .tmsumc.warn{color:var(--copper-ink);font-weight:700}
  /* THE FOLDED REMAINDER. A disclosure, not an action - so it takes the caveat block's own ink
     and size rather than a button's, and it carries .warn when anything behind it is coppered,
     because a closed control hiding a copper note must not read as neutral.
     44px and NOT this screen's 56px floor: .tmnav / #tmLens button / .tmrow are 56 because they
     are the screen's PRIMARY controls, and this one exists to BUY fold space - at 56 it would
     hand back most of what folding the notes just saved. 44 is the house tap floor and it clears
     it. */
  .tmsummore{display:block;width:100%;text-align:left;min-height:44px;
             margin-top:4px;padding:6px 0;border:0;background:none;cursor:pointer;
             font:inherit;font-size:11.5px;font-weight:700;line-height:1.45;
             color:var(--tmut);text-decoration:underline;text-underline-offset:2px}
  .tmsummore.warn{color:var(--copper-ink)}
  .tmsummore:active{opacity:.6}
  .tmrow{border:1px solid var(--border);background:var(--card);border-radius:var(--r-card);
         padding:9px 10px;margin-top:7px;cursor:pointer;min-height:56px}
  .tmrow:active{background:var(--scr)}
  .tmrow.open{border-color:var(--strong)}
  .tmtop{display:flex;align-items:center;gap:10px}
  .tmid{flex:1;min-width:0}
  .tmn{font-weight:800;font-size:12.5px;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
  .tmc{margin-top:3px;display:flex;flex-wrap:wrap;gap:4px;align-items:center}
  .tmh{font-weight:800;font-size:13px;font-variant-numeric:tabular-nums;flex:none}
  .tmh.warn{color:var(--copper-ink)}
  /* One glyph, rotated — the app's own chevron, so an open row reads as the same control
     turned rather than a second symbol nobody has met. */
  .tmchev{color:var(--tmut);font-size:15px;flex:none;width:13px;text-align:center;
          display:inline-block;transition:transform .12s ease}
  .tmrow.open .tmchev{transform:rotate(90deg)}
  /* SEVEN DAYS, IN THE INSTALL'S OWN ORDER — the strip starts on the week's own anchor date, so
     a Monday install reads MON…SUN and a Sunday one reads SUN…SAT. The labels come from the
     browser's own locale weekday names (toLocaleDateString weekday:'short', uppercased here by
     CSS the way .sh does it), never a hardcoded array — payWeekSpan has been wrong about
     exactly this shape of assumption before. */
  .tmdays{display:flex;gap:4px;margin-top:8px}
  /* --card, NOT --scr, and the reason is a measurement: --tmut on --scr is 4.48:1, a hair under
     AA, at 9.5px — the exact size and family this app has twice remediated (`palette-drift`).
     On --card the same ink is 4.89:1, the figure .hvs2 and .mycs were both moved to. The cell
     still reads as a cell because the border carries it; it sits flush with the row instead of
     inset, which is a lighter look and a legible one. */
  .tmd{flex:1;min-width:0;text-align:center;border:1px solid var(--border);border-radius:var(--r-ctrl);
       padding:5px 2px;background:var(--card)}
  .tmd i{display:block;font-style:normal;font-size:9.5px;font-weight:800;letter-spacing:.05em;
         color:var(--tmut);text-transform:uppercase}
  .tmd b{display:block;font-size:11.5px;font-weight:800;color:var(--text);margin-top:2px;
         font-variant-numeric:tabular-nums}
  .tmd.zero b{color:var(--tmut);font-weight:700}
  /* A DAY CARRYING A SHIFT WITH NO PAIR. Not the same thing as a day off, and not the same
     thing as a day somebody is still working — the screen must not print any two of those
     alike. This is where the clock-out went missing, shown on the day it happened, and it is
     copper WHATEVER the day's figure says: a day holding one broken shift and one good one
     reads as an ordinary 8h from byDay alone. --copper-ink is the ink weight that carries
     small type (6.0:1 on --card) — never --red, which LAW 21 keeps for safety.
     The rgba() tint is --copper at fill weight, written the way .edchip fifty lines above it
     writes the same tint (rgba(201,131,47,…)) and the way .hvbar/.gstrack/.gsrow do — this app
     has no tint token, and inventing one for a single new rule would be the drift `palette-drift`
     is actually about. The INK is a token, which is the half that carries words. */
  .tmd.hole{background:rgba(201,131,47,.12);border-color:rgba(201,131,47,.35)}
  .tmd.hole b{color:var(--copper-ink)}
  /* …AND A DAY SOMEBODY IS STILL ON THE CLOCK FOR. Neutral, with the app's own live dot (the
     one .crow .cav wears on the crew card). Their hours land at the clock-out, which is not a
     problem — painting it copper would put a warning on every crew member's today, every day,
     and a warning that fires daily stops being read. */
  .tmd.live{border-color:var(--strong)}
  .tmd.live i::after{content:'';display:inline-block;width:5px;height:5px;border-radius:50%;
                     background:var(--lime);margin-left:3px;vertical-align:middle}
  /* SECTION HEADINGS ON THIS SCREEN CARRY CREW NAMES, so they are content, not decoration.
     `.sh` is --tfaint app-wide — 9.5px at 2.37:1, the exact value the standing `palette-drift`
     row names — and the shared class is not moved from here (four screens this round never
     opened wear it). Lifted to --tmut for this body alone: 4.89:1, same size, same weight. */
  /* …and its margins, which `.sh` sets to 16px/8px for screens where a heading separates two
     unrelated blocks. Here it separates a crew from its own people. */
  #tmBody .sh{color:var(--tmut);margin:10px 2px 7px}
  /* AND THE RETRY BUTTON, which on a dead read is the ONLY control left on the screen. errCard's
     shared `.ghost` is 44px; every control this round invented took 56, and the one that appears
     when everything else has gone should not be the small one (`touch-target` — "the SMALL one is
     always the entry point to the thing that was made big"). Scoped, so no other errCard moves. */
  #tmBody .ghost{min-height:56px;padding-left:16px;padding-right:16px}
  /* The expanded person's punch rows — the SAME .crow the person page draws, nested under the
     row that opened them. Indented by a hair so the nesting is visible without a second style. */
  .tmexp{margin:0 0 10px 10px;padding-left:9px;border-left:2px solid var(--border)}
  /* ---- TIME-ADJUST SHEET ---- */
  .adjhd{display:flex;align-items:center;gap:11px;padding:0 2px 12px;border-bottom:1px solid var(--border);margin-bottom:4px}
  .adjhd .tav{width:38px;height:38px;font-size:12px}
  .adjhd .adjn{font-weight:800;font-size:14.5px}
  .adjhd .adjm{color:var(--tmut);font-size:10.5px;margin-top:1px}
  /* white-space:nowrap ADDED fix round 2: with a long name in the row above, "No clock-out" wrapped
     to "No clock-" / "out". flex:none so the name gives instead — the name is the part that can
     be truncated without changing what the figure says. */
  .adjhd .adjtot{margin-left:auto;flex:none;white-space:nowrap;font-weight:800;font-size:15px;color:var(--brand);font-variant-numeric:tabular-nums}
  /* T1 (2026-08-28) — "No clock-out" IS NOT GOOD NEWS AND MUST NOT WEAR THE GOOD-NEWS INK.
     This slot printed "Still open" in --brand green over a shift booking ZERO hours that nobody
     was working. --copper-ink (#8a5a17) is the INK weight of the same copper family the un-closed
     shift already wears on Home's meter rows (.hvs2.warn) and on the job pane; the week card's
     own caption uses --copper (#c9832f), the FILL weight of that family, which is a legibility
     choice at that size and not a second semantic (the two are documented as one pair at the
     :root block). Red stays for safety (LAW 21). A real total and a shift that is genuinely
     running both stay --brand. */
  .adjhd .adjtot.warn{color:var(--copper-ink)}
  .stepper{display:flex;align-items:center;gap:6px}
  /* 56px IS THE ROW'S FLOOR, AND IT IS THE WHOLE ROW'S (f-2 W4 T1). The readout became a
     control this round, and the ledger's `touch-target` check says in as many words: when a
     floor moves onto ONE class, measure every sibling wearing the old one. These four were
     42px tall beside it, so they take the same 56 — one height for one family, on a sheet a
     gloved hand uses. The WIDTH stays 44 (iOS's own floor) and stated as deliberate: five
     controls share this row, and at 375px there is not 56px of width for each of them and a
     readable time between. */
  .stp{flex:none;width:44px;height:56px;border:1px solid var(--strong);background:var(--card);border-radius:var(--r-card);
       font:inherit;font-weight:800;font-size:12px;color:var(--tmut);cursor:pointer}
  .stp:active{background:var(--scr);border-color:var(--brand);color:var(--brand)}
  /* The readout and the typed field are ONE SLOT taking turns — same flex, same box, so the
     row does not jump when it is tapped. The readout wears a border now because it is a
     button; it was a bare <div> when nothing happened if you touched it. */
  .stepval{flex:1;min-width:0;min-height:56px;padding:6px 4px;border:1px solid var(--strong);
           background:var(--card);border-radius:var(--r-card);color:var(--text);font:inherit;
           text-align:center;font-weight:800;font-size:15px;font-variant-numeric:tabular-nums;
           line-height:1.15;cursor:pointer}
  .stepval:active{border-color:var(--brand)}
  .stepval small{display:block;font-size:9px;font-weight:700;color:var(--copper);margin-top:1px;letter-spacing:.02em}
  /* appearance:none for the same reason input[type="date"].inp has it (see that rule): this is
     the OTHER native temporal control in the app, iOS draws its own field over this box, and a
     replaced element's intrinsic width ignores the flex item's min-width:0. It keeps its
     CENTRED value — it is the readout half of a stepper and lines up with .stepval, which is
     the opposite of the date field's requirement and is why the two are not one rule. */
  .stepin{flex:1;min-width:0;min-height:56px;padding:6px 4px;border:1px solid var(--brand);
          background:var(--card);border-radius:var(--r-card);color:var(--text);font:inherit;
          -webkit-appearance:none;appearance:none;max-width:100%;
          text-align:center;font-weight:800;font-size:15px;font-variant-numeric:tabular-nums}
  .stepin::-webkit-date-and-time-value{text-align:center;margin:0;min-width:0}
  /* "Says they forgot to clock out" (#adjSusp). COPPER, not red: LAW 21 keeps red for safety —
     a lost punch, a hazard — and nothing here is lost. It is the same copper this app already
     uses for "somebody should look at this", and the same treatment .brknote gets one rule
     down: a fact about the shift, stated above the controls that fix it. */
  .adjsusp{margin:8px 2px 0;padding:9px 11px;border-radius:var(--r-card);
           background:rgba(201,131,47,.12);color:var(--copper-ink);font-size:11.5px;font-weight:700;line-height:1.45}
  /* The jump to the other end of the shift (#adjPair). A ROAD, not an action: neutral ink and
     the card's own border, so the one affirmative colour on this sheet stays with Save. Same
     56px floor as everything else in the time region above it. */
  .adjpair{display:block;width:100%;min-height:56px;margin-top:8px;padding:10px 13px;
           border:1px solid var(--border);background:var(--card);border-radius:var(--r-card);
           color:var(--tmut);font:inherit;font-weight:700;font-size:12.5px;text-align:left;
           line-height:1.35;cursor:pointer}
  .adjpair:active{border-color:var(--strong)}
  .req{margin-left:6px;font-size:8.5px;font-weight:800;letter-spacing:.06em;color:var(--copper);
       background:rgba(201,131,47,.13);border-radius:var(--r-ctrl);padding:2px 6px}
  .req.met{color:var(--lime-ink);background:rgba(125,186,61,.16)}
  .whychips{display:flex;flex-wrap:wrap;gap:7px}
  .wc{display:inline-flex;align-items:center;gap:5px;border:1px solid var(--strong);background:var(--card);
      color:var(--tmut);font:inherit;font-weight:700;font-size:11px;padding:8px 11px;border-radius:99px;cursor:pointer}
  .wc svg{width:13px;height:13px}
  .wc.on{background:var(--brand);border-color:var(--brand);color:#fff}
  .adjaud{border:1px solid var(--border);background:var(--scr);border-radius:var(--r-card);padding:9px 11px;margin-top:12px}
  .adjaud .at{font-size:9px;font-weight:800;letter-spacing:.07em;text-transform:uppercase;color:var(--tfaint);margin-bottom:5px}
  .adjaud .ar{font-size:10.5px;color:var(--tmut);line-height:1.5}
  .adjaud .ar b{color:var(--text);font-weight:800}
  /* THE ADJUST SHEET IS A COLUMN (T1, 2026-08-28 — `keyboard-fold`). See the note on
     .adjscroll in index.html for the measurement that forced it. The header, Save and the
     clock-out are flex:none because they are the two things LAW 19 is about plus the row that
     says whose time this is; the middle is the region that gives. min-height:0 is load-bearing
     — without it a flex item refuses to shrink below its content and the whole point is lost. */
  /* 56px ON THE CHIPS TOO (fix round 2, 2026-08-28 — `touch-target`, and the eval's point is
     exact: this round lifted .adjout to 56 in this sheet and left EIGHT controls at 33px beside
     it, four of which are the reason chips the sheet makes MANDATORY). Three other .kc rows in
     this app already carry the same lift by id (`#navPick .kc`, `#onbNav .kc`,
     `.rateedit.wide .kindchips .kc`), so this is the fourth instance of one rule rather than a
     new number. Scoped to #adjovl so nothing outside this sheet moves on a rider.
     THE COST IS REAL AND STATED: at 375x407 with the keyboard up the two chip rows push the
     reason below the fold of .adjscroll, which is why the region carries the `.cut` cue and why
     test/team-books-people.cjs asserts the chips are SCROLLABLE INTO VIEW and self-hitting
     there. The alternative was leaving a gloved thumb a 33px target on a paid-time screen. */
  #adjovl .kindchips .kc,#adjovl .whychips .wc{min-height:56px}
  #adjovl .sheet{display:flex;flex-direction:column}
  #adjovl .adjhd,#adjovl .submit,#adjovl .adjout{flex:none}
  #adjovl .adjscroll{flex:0 1 auto;min-height:0;overflow-y:auto;overscroll-behavior:contain}
  /* ---- THREE MORE SHEETS TOOK THIS SHAPE (f-14, item 7) ---------------------------------
     COPIED, NOT INVENTED — one column shape in this app, not four. Each has exactly ONE
     shrinkable region and everything else is flex:none, which is the half of `keyboard-fold`
     that is load-bearing: a flex item with a min-height can still be out-shrunk by a sibling's
     content, so the primary action and the way out have to be told not to give.

     #invovl had ZERO rules of its own. Measured at 375x407 — an iPhone SE with the software
     keyboard up — the wage pair, the role chips and "Add to the team" all rendered between 538
     and 594: off the glass, with no cue and no way out on screen.
     #accovl is the sheet somebody reads an access code off, standing next to a new hire, and
     its only control was under the fold for the same reason.
     #plcovl AUTOFOCUSES #plName on open, so 375x407 IS its opening state, not an edge case —
     and it shipped with no cancel at all, so the only exits were a 49px strip of backdrop and
     the Android back gesture. */
  #invovl .sheet,#accovl .sheet,#plcovl .sheet{display:flex;flex-direction:column}
  #invovl .grab,#invovl h6,#invovl .submit,#invovl #invNote{flex:none}
  #invovl #invScroll{flex:0 1 auto;min-height:0;overflow-y:auto;overscroll-behavior:contain}
  #accovl .grab,#accovl h6,#accovl #accFoot{flex:none}
  #accovl #accBody{flex:0 1 auto;min-height:0;overflow-y:auto;overscroll-behavior:contain}
  #plcovl .grab,#plcovl h6,#plcovl #plFoot{flex:none}
  #plcovl .adjscroll{flex:0 1 auto;min-height:0;overflow-y:auto;overscroll-behavior:contain}
  .adjout{width:100%;margin-top:8px;border:1px solid var(--strong);background:var(--card);color:var(--red);
          font:inherit;font-weight:800;font-size:13.5px;padding:12px;border-radius:var(--r-card);cursor:pointer}
  /* ---- T3, 2026-08-29 -------------------------------------------------------------------
     "LOOKS RIGHT" (.adjconf) is a ROAD in the .adjpair family and takes its box wholesale —
     same width, same 56px floor, same left-aligned two-line copy. What it adds is the copper
     TINT that says which of the two roads on this sheet is the one about a flagged shift, and
     it is a tint rather than a new colour: rgba(--copper) at the same fill weight .edchip and
     .adjsusp already use, with --copper-ink carrying the words at 6.0:1. NOT a fifth family
     (`palette-drift`), and NOT --brand: the affirmative colour on this sheet belongs to Save. */
  .adjconf{background:rgba(201,131,47,.10);border-color:rgba(201,131,47,.38);color:var(--copper-ink)}
  .adjconf:active{border-color:var(--copper)}
  /* …and the other half: a confirmation that already stands. Neutral, never green — a shift
     somebody vouched for is a RECORD, not a success state, and one more green on this sheet is
     the `green-overload` finding waiting to be written. Same box as .adjsusp so the two facts
     about a shift read as the same kind of thing. */
  .adjrev{margin:8px 2px 0;padding:9px 11px;border-radius:var(--r-card);background:var(--scr);
          border:1px solid var(--border);color:var(--tmut);font-size:11.5px;font-weight:700;line-height:1.45}
  /* THE CREATE-SHIFT SHEET IS THE SAME COLUMN, for the same reason (`keyboard-fold`) — and it
     needs it MORE than #adjovl does: this sheet has a date field, two time fields, a job row and
     a chip row above its Save. The four .wc chips take the 56px floor the adjust sheet's already
     have (`touch-target`), scoped to this sheet so nothing outside it moves. */
  /* LAW 21 — THE WAY OUT IS NOT RED, and this rule exists because it was. #csCancel borrows
     .adjout for its box (full width, 56px, the card's own border), and .adjout draws its label in
     var(--red) because on the adjust sheet it is "Clock out now" — a control that ends somebody's
     paid day. Here it is "Not now" on a sheet that has written nothing at all, and red is reserved
     for safety: a lost punch, a hazard. Muted ink, the same weight, same box. Caught by opening
     the screenshot rather than by an assertion, which is the point of opening them. */
  #csCancel{color:var(--tmut)}
  /* THE CONTROLS THIS SHEET EXISTS FOR, AT KEYBOARD HEIGHT (`keyboard-fold`, 8th occurrence —
     and it is the clause the 7th one added: "measure the control the surface exists to OPERATE,
     not only the primary action and the way out"). MEASURED at 375x407 before this rule:
     .adjscroll top 131 / bottom 243 / clientHeight 112 against a scrollHeight of 444, with
     #csIn and #csOut at 241–297 hit-testing to #csSave and #csJobRow hit-testing to #csCancel.
     Save and the way out were both fine, which is exactly why the round's own LAW 19 check
     passed and the eval's did not.
     WHAT MOVES IS DECORATION, never a control's own box: the label margins, the header's
     padding, the note field's top gap and the gap under the span readout. The date field, both
     time fields, the job row, the chips, Save and the way out all keep their 56px.
     The scroller still says it is cut (#csovl .adjscroll is in DEL_CUT_REGIONS) — this buys
     the two time fields their place on the glass, it does not pretend the sheet fits. */
  @media (max-height:620px){
    #csovl .adjhd{padding-bottom:6px;margin-bottom:2px}
    #csovl .adjscroll label{margin:6px 2px 4px}
    #csovl .brknote{margin:-1px 0 4px}
    #csovl #csNote{margin-top:4px!important}
    #csovl .empcap{margin-top:3px;min-height:0}
    #csovl .whychips{gap:5px}
  }
  /* `#csovl .ckjob{min-height:56px}` RETIRED 2026-09-05 (f-13 lane 2 item 1): redundant now
     that .ckjob carries the floor at its base rule, where its argument moved with it. A scoped
     rule that repeats a base rule is a second number for one control. */
  #csovl .whychips .wc{min-height:56px}
  #csovl .sheet{display:flex;flex-direction:column}
  /* #csSay JOINED THIS LINE (f-14, item 7c): it sits outside .adjscroll now, beside the button
     it answers, so at 375x407 the refusal is on the glass with the primary action. */
  #csovl .adjhd,#csovl .submit,#csovl .adjout,#csovl #csSay{flex:none}
  #csovl .adjscroll{flex:0 1 auto;min-height:0;overflow-y:auto;overscroll-behavior:contain}
  /* The two-up time row. .inp already carries min-height:56px on the base class (f-7 fix round
     3 moved the floor there); these two lines are the OTHER half of that round's lesson —
     min-width:0 on the control itself, because a native time control's intrinsic width is wider
     than its track and it runs under its neighbour otherwise, which Chrome cannot show you. */
  #csovl .g2 .inp{min-width:0;width:100%;max-width:100%}
  #csovl .g2 label{margin-top:0}
  /* The refusal line. Copper, never red: nothing is lost here, somebody is being told a time
     does not fit. #csSpan carries the SAME state now (f-14, item 16): with both times typed and
     csBuild holding a refusal, the span used to print the empty string — the sheet answering a
     real question with nothing — so the length line prints that refusal and wears this colour
     while it does. Two elements, one state, one colour: #csSay is the field-adjacent sentence,
     #csSpan is the one that stands where the figure would have been. */
  #csSay,#csSpan.warn{color:var(--copper-ink);font-weight:700}
  /* The break the CREW punched, stated above chips that cannot change it (#adjBrkPunch). Muted
     and small: it is a fact about the shift, not a control — and it is the sentence that stops
     the same half hour being deducted twice. */
  .brknote{color:var(--tmut);font-size:10px;line-height:1.4;margin:-2px 0 8px;overflow-wrap:anywhere}
  .kindchips{display:flex;gap:7px}
  .kc{border:1px solid var(--strong);background:var(--card);color:var(--tmut);font:inherit;font-weight:700;font-size:11.5px;padding:8px 14px;border-radius:99px;cursor:pointer}
  /* --blue, not the raw hex twice (f-14, item 26b). The colour does not move — var(--blue) IS
     #3f6fae — but it now lives in the one place the palette lives. Whether these chips should
     be blue at all is Wes's open question (his green-is-the-active-state rule against the note
     beside .kc that chose blue for the time-off flow); resolving the token does not answer it. */
  .kc.on{background:var(--blue);border-color:var(--blue);color:#fff}
  /* THE PAYMENT CHIPS ARE AT THE FLOOR. Measured at 33px when they first painted — the same
     number the Fern chips were flagged at and raised from, and the ledger's touch-target check
     names 56-64px for anything a gloved hand taps. This control decides whether a company owes
     somebody money and it is tapped on a job site, so it does not get to be the exception.
     Scoped to #paidChips: every other .kc is still 33px, which is a class-wide question with a
     blast radius across a dozen screens and is Wes's call, not this round's. */
  #paidChips .kc{min-height:56px;display:inline-flex;align-items:center;padding:8px 16px}
  /* THE PIN BUTTON IS A PRIMARY CONTROL NOW (2026-09-03, eval r1, 'touch-target'). .kc is the
     small chip shape and this button wore it at 175x33 — under the 56px floor on a control that
     a gloved hand taps TWICE to confirm replacing a job's pin. Measured, not read: the second
     tap of a two-tap confirmation is the worst place in the app for a target somebody misses. */
  #jbedPin{min-height:56px}
  /* One sheet, one affirmative colour. The break chips sit next to a green total and a
     green Save, so they select green — the blue above belongs to the time-off flow. */
  #adjBrk .kc.on{background:var(--brand);border-color:var(--brand)}
  /* job header: address / docs / personal time */
  .addrrow{display:flex;align-items:center;gap:7px;margin-top:9px;font-size:11.5px;font-weight:700;color:var(--tmut)}
  .addrrow svg{width:15px;height:15px;color:var(--red);flex:none}
  .maps{display:flex;gap:5px} /* LEFT-aligned — myTime owns the right side */
  /* #myTime's 9px of top air was calibrated against a 34px badge beside it. With the badge at
     the 56px floor (below) that nudge reads as a row that does not line up, and .addrrow is
     already align-items:center — so the row centres itself once the nudge comes off. */
  .addrrow .mych{margin-top:0}
  /* 56, BECAUSE IT IS THE ONLY DOOR FROM THIS SCREEN TO THE NAV APP (2026-09-05, f-13 lane 3
     item 11 — `touch-target`). Measured 128x34: a driver taps it at 6am, in a glove, with the
     truck running, and it was the smallest control on the screen. Measured now in
     test/directions.cjs at 390x844 and at 375x407. */
  .dirbadge{display:inline-flex;align-items:center;gap:7px;min-height:56px;border:1px solid var(--border);background:var(--card);border-radius:var(--r-card);padding:8px 12px;font:800 11.5px 'Segoe UI',-apple-system,system-ui,sans-serif;color:var(--text);cursor:pointer}
  .dirbadge svg{width:16px;height:16px;flex:none}
  .mapchip{border:1px solid var(--strong);background:var(--card);border-radius:99px;font:inherit;font-weight:800;font-size:10px;color:var(--brand);padding:4.5px 10px;cursor:pointer}
  .mapchip.ic{width:30px;height:30px;padding:0;display:inline-flex;align-items:center;justify-content:center}
  .mapchip.ic svg{width:15px;height:15px}
  .m .mi{width:11px;height:11px;vertical-align:-1.5px;margin:0 1px 0 7px}
  .m .mi:first-child{margin-left:0}
  /* Docs tab rows */
  .drow{display:flex;align-items:center;gap:11px;border:1px solid var(--border);background:var(--card);border-radius:var(--r-card);padding:11px 12px;margin-top:7px;cursor:pointer}
  .drow .dico{width:34px;height:34px;border-radius:var(--r-ctrl);background:rgba(47,106,58,.08);color:var(--brand);display:flex;align-items:center;justify-content:center;flex:none}
  .drow .dico svg{width:17px;height:17px}
  .drow .dn{font-size:12.5px;font-weight:800}
  .drow .fvc{margin-left:auto;font-size:8.5px;font-weight:800;background:rgba(47,106,58,.1);color:var(--brand);border-radius:var(--r-ctrl);padding:3px 7px;letter-spacing:.04em;white-space:nowrap}
  .drow .chev2{color:var(--tfaint);font-size:16px;flex:none}
  /* --- IMPORTED FILES (.drow.dimp, 2026-08-18) — the other half of Fern's "+" ---------------
     TOLD APART BY WHAT THEY ARE MISSING, not by a badge. Forge-synced paper wears the FORGE·vN
     stamp and the brand-green icon tile because a version is a real thing the office published;
     an imported file has no version to stamp, and views-jobs' own note at the Forge row says a
     fake one "would make a scope PDF look like published office paper." So the import gets the
     NEUTRAL tile (no green — green is the office's mark here) and, in place of a stamp, the two
     facts a person actually needs about a document that came off somebody's phone: who put it
     there and when. That line is the distinction, and it is information rather than decoration.

     THE ROW IS A DIV, NOT AN <a>, on this branch alone: it carries a delete button, and a
     <button> inside an <a> is invalid markup that browsers un-nest in their own ways. The
     filename is the link; the button is its sibling. */
  .drow.dimp .dico{background:rgba(90,99,104,.09);color:var(--tmut)}
  .drow.dimp{align-items:center}
  .drow .dsub{font-size:10.5px;font-weight:700;color:var(--tmut);margin-top:2px}
  /* 56, AND IT IS THE ROW THE DELETE BUTTON SITS BESIDE (2026-09-05, f-13 lane 3 item 11 —
     `touch-target`). .drowdel went to 56 because it is destructive; this — the control that
     OPENS the document, which is what a person actually comes to this row to do — stayed at 46.
     Two floors in one row, with the destructive one bigger, is the shape the standing check was
     promoted for on the account sheet. An import row is already 78px because of the delete, so
     nothing moves there; a Forge row without one grows by ten pixels at most. */
  .drow .dlink{display:flex;align-items:center;gap:11px;min-height:56px;flex:1;min-width:0;color:inherit;text-decoration:none}
  .drow .dlink .dn{word-break:break-word}
  /* 56px is the house floor for a gloved thumb and this one is DESTRUCTIVE, so it is the floor
     and not a hair under it. min-width rather than width: the first tap replaces the glyph with
     the words "Tap again", and a fixed box would clip the confirmation the tap is asking for. */
  .drow .drowdel{min-width:56px;height:56px;flex:none;display:flex;align-items:center;justify-content:center;
    border:1px solid var(--border);background:none;border-radius:var(--r-card);color:var(--tmut);
    /* see the `font:inherit` note at .agsrc button -- this is one of the four sites */
    font:inherit;font-weight:800;font-size:11px;padding:0 10px;cursor:pointer}
  .drow .drowdel svg{width:17px;height:17px}
  .drow .drowdel.armed{border-color:var(--red);color:var(--red)}
  /* crew sheet rows.
     THE TWO CONTROLS ON THIS ROW WERE THE SMALLEST IN THE APP — .star 21x24 and .kick 20x26,
     measured on the real sheet at 390 (eval 2026-08-22, `touch-target`, 9th AND 10th
     occurrence, beside a 56px Done button on the same sheet). Both are now at the house floor,
     56, the same floor .drow .drowdel holds and for the same reason: min-width rather than
     width, because the kick's first tap replaces the glyph with the words "Tap again" and a
     fixed box would clip the confirmation the tap is asking for. The name column takes the
     space back with min-width:0 + ellipsis, so a long name shortens instead of pushing a
     control off the row. */
  .cwrow{display:flex;align-items:center;gap:9px;border:1px solid var(--border);background:var(--card);border-radius:var(--r-card);padding:8px 10px;margin-top:6px}
  .cwrow .tav{width:26px;height:26px;font-size:10px;flex:none}
  .cwrow .cwid{flex:1;min-width:0}
  .cwrow .cn{font-size:12px;font-weight:800;display:flex;align-items:center;min-width:0}
  .cwrow .cnn{overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
  .cwrow .ct{font-size:9.5px;color:var(--tfaint);font-weight:700;text-transform:uppercase;letter-spacing:.05em}
  .cwrow .star,.cwrow .kick{min-width:56px;height:56px;flex:none;display:flex;align-items:center;justify-content:center;
    border:0;background:none;cursor:pointer;padding:0 8px}
  .cwrow .star{font-size:17px;color:var(--strong)}
  .cwrow .star.on{color:var(--lime)}
  .cwrow .kick{color:var(--tfaint);font-size:15px;font-weight:800}
  /* ARMED — the same two-tap shape .drow .drowdel wears, in the same red, on the app's other
     irreversible row control. Red is reserved for safety in this app and this is the one thing
     on the sheet that takes a person off a crew. */
  .cwrow .kick.armed{color:var(--red);font-size:10.5px;font-weight:800;letter-spacing:.02em}
  /* --lime-ink, not a raw hex (f-14, item 17a). f-13's own token pass converted four siblings
     (.tst.ok, .jrow .onchip and two more) and skipped this one, so the app carried #4e7f1f
     against the token's #4d7a1f — one hex off, in the same family, which is exactly the kind of
     residue a `fixed-one-of-N` count is supposed to catch. The colour moves by one value; the
     point is that there is now one place it lives. */
  .leadchip{font-size:8px;font-weight:800;background:rgba(125,186,61,.16);color:var(--lime-ink);border-radius:var(--r-ctrl);padding:2px 6px;letter-spacing:.05em;margin-left:6px}
  /* onboarding */
  #onbScr{position:absolute;inset:0;z-index:55;background:var(--scr);overflow-y:auto}
  /* THE TOP INSET IS PAID HERE TOO, AND IT WAS NOT (f-6 fix round, EVAL-LEDGER
     `fixed-one-of-N`). #onbScr is a full-screen takeover at z-index 55 that COVERS .abar — so
     the app-bar fix bought exactly nothing on the seven screens a person meets FIRST: the
     sign-in door, the six-digit code, "You're in", the GPS disclosure, "Set your password",
     "Your access code" and the company picker. Measured native-stubbed with env() at 0 so the
     floor fires: steps 3/5/6 put their heading at y=60 on 390x844, 375x812 and 430x932, and the
     door's wordmark at y=44 (375x812), y=26 (375x667) and y=16 (390x520, keyboard up) — every
     one of them inside the 62px band, drawn under the iOS clock. The BOTTOM half of this pair
     has been paid since the Android-15 round (see PORT HARDENING at the foot of this file); the
     top half was never written, on the same declaration, which is this class by construction.
     padding-bottom stays OUT of this shorthand: the hardening block owns it at equal specificity
     and later in the file, so it wins regardless — writing the number twice is how the two
     drift apart. */
  #onbScr .onb{display:none;flex-direction:column;padding:calc(26px + var(--sa-top)) 22px 26px;min-height:100%}
  #onbScr .onb.on{display:flex}
  /* STEP 9 IS THE COMPANY PICKER (2026-08-23, the App Store round) and it wears the DOOR's
     treatment because it IS the door on the App Store build — the first surface anyone sees,
     before there is a company to be signed in to. One selector added, no rule copied: a second
     gradient declaration is exactly how two screens that must look identical stop looking
     identical. Same reason the .submit width rule below carries both steps. */
  #onbScr .onb[data-step="1"],
  #onbScr .onb[data-step="9"]{background:linear-gradient(160deg,#2f6a3a,#24512c);color:#fff;align-items:center;justify-content:center;text-align:center}
  .onblogo{width:120px;margin-bottom:14px}
  .onbword{display:flex;gap:8px;font-size:24px;font-weight:800;letter-spacing:.14em}
  .onbword b{color:var(--lime)}
  .onbsub{font-size:11px;font-weight:700;color:rgba(255,255,255,.6);margin:6px 0 26px}
  .onblab{align-self:flex-start;font-size:9.5px;font-weight:800;letter-spacing:.08em;text-transform:uppercase;color:rgba(255,255,255,.6);margin-bottom:5px}
  .onbinp{background:rgba(255,255,255,.12);border-color:rgba(255,255,255,.25);color:#fff;text-align:center;font-size:16px}
  .onbinp::placeholder{color:rgba(255,255,255,.4)}
  #onbScr .onb[data-step="1"] .submit,
  #onbScr .onb[data-step="9"] .submit{width:100%}
  /* the emailed code POPS UP MID-SCREEN — the whole group rides the vertical centre (Wes) */
  #onbScr .onb[data-step="2"]{justify-content:center;text-align:center}
  #onbScr .onb[data-step="2"] .onbh{margin-top:0}
  .onbh{font-size:19px;font-weight:800;margin:34px 0 18px;text-align:center}
  .onbcode{display:flex;gap:8px;justify-content:center;margin-bottom:22px}
  .onbcode span{width:44px;height:52px;border:1.5px solid var(--strong);border-radius:var(--r-card);background:var(--card);display:flex;align-items:center;justify-content:center;font-size:20px;font-weight:800}
  .onbcode span.f{border-color:var(--brand)}
  .onbghost{margin:10px auto 0;border:0;background:none;color:var(--tmut);font:700 12px 'Segoe UI',-apple-system,system-ui,sans-serif;cursor:pointer}
  .onbrole{text-align:center;font-size:11px;font-weight:800;color:var(--tmut);background:var(--card);border:1px solid var(--border);border-radius:999px;padding:8px 14px;margin:0 auto 16px;width:fit-content}
  .onbrole b{color:var(--copper)}
  .onblab2{font-size:9.5px;font-weight:800;letter-spacing:.08em;text-transform:uppercase;color:var(--tfaint);margin:12px 0 4px;display:block}
  /* onboarding step 4 — the location disclosure (R3 §2c) */
  #onbScr .onb[data-step="4"]{justify-content:center}
  .onbconsent{font-size:13px;line-height:1.6;color:var(--tmut);margin-bottom:18px}
  .onbconsent p{margin:0 0 12px}
  .onbconsent b{color:var(--text)}
  .onbconsent a{color:var(--brand);font-weight:800}

  /* ---- THE PASSWORD SURFACES (auth round, 2026-08-13) --------------------------------------
     The door's password box, "Set your password" (step 5) and Settings → Password → Change.
     These three shipped as ~14 hardcoded inline style="" strings on the elements themselves,
     plus cssText set from app.js — the code even said "styles.css is not this phase's file".
     It is this file's now, and the reason is not tidiness: the whole-app system pass restyles
     the app FROM this stylesheet, and it physically cannot reach anything written inline.
     (EVAL-LEDGER 2026-08-13, class system-drift.)

     The sizes are the brief these screens were designed to and were measured against: a gloved
     hand, direct sun, 6:15am in a gravel lot. Everything tappable is at least 56px; the copy
     that a person has to READ to get out of trouble — the refusal line, the rule ticks, the way
     past — was 10.5-13px and is not any more. */

  /* the box + its eye. The eye was 34x34, which is smaller than a fingertip. */
  .pwwrap{position:relative;width:100%}
  .pwwrap .inp{padding-right:64px}
  /* the door's box is centre-aligned, so it needs the same gutter on both sides or the text
     sits visibly off-centre behind the eye */
  #onbScr .onb[data-step="1"] .pwwrap .inp{padding-left:64px}
  .pweye{position:absolute;right:4px;top:50%;transform:translateY(-50%);width:56px;height:56px;
    display:flex;align-items:center;justify-content:center;border:0;background:none;
    color:var(--tmut);cursor:pointer;padding:0}
  .pweye svg{width:23px;height:23px}
  #onbScr .onb[data-step="1"] .pweye{color:rgba(255,255,255,.72)}

  /* THE ONE MESSAGE LINE, on all three surfaces. Absent until there is something to say — and
     that is a CLASS, never an inline display, so a refusal can never be left visible by a
     stylesheet that thinks it is hidden. */
  .pwmsg{display:none;width:100%;font-size:20px;font-weight:700;line-height:1.45;margin:14px 2px 0;color:var(--tmut)}
  .pwmsg.on{display:block}
  .pwmsg.warn{color:var(--copper)}
  #onbScr .onb[data-step="1"] .pwmsg,
  #onbScr .onb[data-step="9"] .pwmsg{text-align:center;color:rgba(255,255,255,.72)}
  /* var(--copper) is unreadable on the dark green door; this is the same warning in a copper
     that actually carries on that background */
  #onbScr .onb[data-step="1"] .pwmsg.warn,
  #onbScr .onb[data-step="9"] .pwmsg.warn{color:#f3c78a}

  /* the live rule ticks. MET IS AN ATTRIBUTE (app.js sets data-ok), so met/unmet is one rule
     here instead of a hex pair inside a string.

     AND IT IS NOT A COLOUR ANY MORE. The old pair — words in --tfaint and a 9px --strong dot,
     going green together — was state by hue and nothing else: MEASURED at 2.36:1 for the words
     and 1.35:1 for the dot on --scr, which is invisible to the ~8% of a crew with red-green
     colour deficiency and close to invisible to everyone else at 6:15am in a gravel lot. app.js
     now writes a GLYPH into the <i> (a tick, or an empty ring), so met and unmet are two shapes
     before they are two colours, and unmet moved from --tfaint to --tmut — the app's own
     secondary ink, used by every other secondary line in the product. A bespoke darker hex
     invented here instead would be the `palette-drift` class.

     ZERO VERTICAL PIXELS: 17px x 1.4 = a 23.8px line box, and the glyph's line-height is that
     same 23.8px, so the row is exactly the height it was. Only the gutter got wider. */
  .pwrules{margin-top:12px}
  .pwrule{display:flex;align-items:flex-start;gap:9px;font-size:17px;font-weight:700;line-height:1.4;margin-top:9px;color:var(--tmut)}
  .pwrule i{flex:none;width:19px;font-style:normal;font-weight:800;font-size:15px;
    line-height:23.8px;text-align:center;color:inherit}
  .pwrule[data-ok="1"]{color:var(--brand)}
  .pwnote{font-size:15px;font-weight:700;color:var(--tfaint);margin-top:11px;line-height:1.45}

  /* the current-password box and the way-out button: ABSENT until they are the thing being
     asked for, and then the layout closes up around them (LAW 1 — never greyed, never a
     padlock). A class, so nothing has to remember which display value to put back. */
  .pwreveal{display:none}
  .pwreveal.on{display:block}

  /* 56px+ on everything these screens ask a gloved hand to hit (the house minimum is 56-64;
     the primary buttons measured 50). #delovl joins every one of these lists rather than
     getting a sizing block of its own: it is the same password flow wearing a different verb,
     and a second set of numbers for it is exactly how the two drift. #hmovl (the Home chip's
     clock catch, 2026-08-18) joins them for the same reason and gets no numbers of its own —
     its primary is the same 60px box, its way out the same 56px box.
     STEP 9 JOINS THE INPUT LIST (2026-08-23 — EVAL-LEDGER `fixed-one-of-N`, and the miss was on
     the touch floor itself). The picker got step 1's gradient by adding a selector and did not
     get step 1's SIZING: #onbCoCode measured 47px next to #onbEm and #onbPw at 56px, same
     class, same 16px type, same 331px width — the one input in the onboarding family under the
     floor, on the first screen the App Store binary ever shows. Same rule, one more selector;
     no second number. */
  #onbScr .submit,#pwovl .submit,#hmovl .submit,#aiovl .submit{min-height:60px;font-size:17px}
  /* The per-step .inp floor that used to be here is gone: `.inp` carries 56px on the base
     class now, so every field on every step has it without a selector list that has to be
     extended each time a step is added — which is exactly how #onbCoCode shipped at 47px. */
  /* #plcovl joined this line at f-14 (item 7b) — its "Not now" is the sheet's only way out
     and it is tapped with the keyboard up, so it takes the same 56px floor its siblings do. */
  #onbScr .onbghost,#delovl .onbghost,#hmovl .onbghost,#aiovl .onbghost,#plcovl .onbghost{min-height:56px;font-size:15px;padding:0 12px}
  /* THE ONE-WAY DOOR (2026-08-16). Its confirm is .signout — this app's EXISTING destructive
     treatment, red-outlined, already sitting at the foot of the very sheet this flow starts
     from — grown to the same 60px/17px the password sheet's primary button carries, because
     it is the primary action of its own screen. Nothing new is invented: no class, no colour,
     no hex. .submit (lime) is deliberately NOT reused, and neither is var(--lime) anywhere in
     this block: green is the positive/primary colour in this product, and a green button that
     ends an account is the screen contradicting its own words.

     The consequence copy is 20px — the same size as .pwmsg, the app's largest body type, set
     by the same rule the auth surfaces were raised to (a gloved hand, direct sun, 6:15am). It
     is the part a person MUST read before typing anything, so it is not allowed to be the
     smallest text on the sheet. */
  #delovl .signout{min-height:60px;font-size:17px;margin-top:18px}
  /* THE TWO REGIONS THAT GIVE WAY, IN THE ORDER THEY GIVE WAY. See the block below for why
     there are two and not one, and why the shrink factors are 1000 and 1 rather than 1 and 1. */
  #delovl .delsay{font-size:20px;font-weight:700;line-height:1.45;color:var(--tmut);margin:2px 2px 0;
    flex:0 1000 auto;min-height:0;overflow-y:auto;overscroll-behavior:contain}
  #delovl .delsay p+p{margin-top:12px}
  #delovl .pwmsg{flex:0 1 auto;min-height:0;overflow-y:auto;overscroll-behavior:contain}
  /* THE COPY AND THE REFUSAL SCROLL; THE BOX AND THE TWO BUTTONS DO NOT MOVE.
     MEASURED at 375x407 — an iPhone SE with the keyboard up, which is not an edge case on this
     sheet but its ONLY state, because it cannot be used without typing: #delPw sat at y=450,
     #delGo at 524 and #delCancel at 594, i.e. the password box, the confirm and the way out
     were ALL below the fold, and the sheet's own scrollHeight was 627 against a 358 clientHeight.
     The screen showed three paragraphs of consequence and no way to act on any of them.
     Making the copy shorter would have been the wrong repair — the copy is the point of the
     screen, and the next long refusal (the sole-owner 400 is eight lines at this width) would
     fold it again at FULL height. So the SHAPE changes: the sheet is a column, and the parts of
     it that are PROSE are the parts allowed to shrink and scroll.

     THE MESSAGE IS ONE OF THOSE PARTS — THIS IS THE HALF THAT WAS MISSING, AND THE PARAGRAPH
     ABOVE USED TO CLAIM THE OPPOSITE OF THE TRUTH. It read "everything below them keeps its
     measured height, which is what puts the box, the confirm and Cancel on screen at every
     size". That sentence was false at every width anyone measured. .pwmsg was in the flex:none
     list, so the moment ANY refusal printed it took its full measured height out of a 358px
     box and pushed the actions back out: with the everyday wrong-password 401 up, #delGo ended
     at 398 and #delCancel began at 408 in a 407px viewport — the destructive button the only
     control still lit and the way out below the fold — identical at 375, 390, 414 and 430
     (sheet 441/358 at all four), and worse under the sole-owner 400 (go 454, cancel 524, sheet
     557/358). The app GUARANTEES that state: submitDeleteAccount's 401 arm calls focusPw(),
     which re-raises the keyboard itself. (EVAL-LEDGER 2026-08-17, `keyboard-fold`, 4th
     occurrence + `doc-drift` on this very comment.)

     WHY 1000 AND 1, NOT 1 AND 1. With both regions at flex-shrink:1 the browser divides the
     deficit in proportion to their content heights, so at 375x407 under the sole-owner refusal
     .delsay lands ~26px and .pwmsg ~7px — a seven-pixel sliver of the sentence that just told
     somebody why they cannot leave. The two are not equally sacrificial: the consequence copy
     is the part you have already read (you got past it to reach the box), the refusal is the
     part that just arrived and names the next tap. So the copy is spent FIRST and completely,
     and the message loses height only after there is no copy left to give. Flexbox has no
     "shrink in this order" keyword; a shrink factor two decades larger is the idiom for it,
     and it is exact rather than approximate here because the copy hits its 0 floor, freezes,
     and the remainder is redistributed to the one item still able to move. MEASURED after:
     375x407 everyday 401 → sheet 358/358, pw · go · cancel all on screen, message 33px and
     scrolling under a fade; 390x900 sole-owner → copy 233px, message untouched at its full
     232px with no fade, exactly as before.
     flex:none on the action region is load-bearing — a flex item with a min-height can still be
     out-shrunk by its siblings' content, and "the button is 60px tall" is exactly the assertion
     that must not be able to become false. */
  #delovl .sheet{display:flex;flex-direction:column}
  #delovl label,#delovl .pwwrap,#delovl .signout,#delovl .onbghost{flex:none}
  /* AND THE FOCUS RING IS NOT GREEN. .inp:focus paints rgba(125,186,61,.4) over a var(--lime)
     border, and openDeleteAccount() focuses this box on every open — so the single green thing
     on the app's one destructive surface was the field it asks you to type your password into,
     on a sheet whose own comment says --lime appears nowhere in it (EVAL-LEDGER, `palette-drift`).
     Same SHAPE as the house ring, in the house's neutral inks: the primary ink for the border,
     the hairline-strong token for the halo. No new value is invented for this one screen. */
  #delovl .inp:focus{outline:2px solid var(--strong);border-color:var(--text)}
  /* AND THE CUT HAS TO LOOK LIKE A CUT. With the copy scrolling, a sliced sentence with nothing
     under it reads as broken text, not as "scroll". A soft fade at the bottom edge is the whole
     affordance.
     GATED ON THE OVERFLOW ITSELF, NOT ON A VIEWPORT HEIGHT. This shipped inside
     `@media (max-height:560px)` on the stated argument that a short screen was "the only case
     where the region can overflow". MEASURED at 390x900 — a full-height phone — with the real
     sole-owner refusal on screen: .delsay scrollHeight 401 against clientHeight 233, 168px of
     copy below the cut, computed mask `none`. The region overflows when a long MESSAGE pushes
     it, which is the flex column doing its job, and that has nothing to do with how tall the
     phone is (EVAL-LEDGER 2026-08-17, `keyboard-fold`). So the condition is now the two numbers
     themselves: app.js delCutSync() sets `.cut` when scrollHeight exceeds clientHeight, on open,
     on every message render, and on every viewport resize while the sheet is open — the keyboard
     coming up IS a resize on this sheet, and it is the event that moves both regions the most.
     The media query is gone — a second gate would only be a second place for the two to
     disagree. The constraint the old rule was right about is kept: with nothing to scroll to
     there is no mask, so a full paragraph's last line is never faded.
     ONE SELECTOR FOR BOTH REGIONS, because as of the fix above there are two scrollable regions
     on this sheet and a refusal squeezed to 33px is sliced exactly as badly as a paragraph is —
     a second rule for the message would only be a second place for the cue to drift.
     EVERY SELECTOR IN app.js's DEL_CUT_REGIONS IS IN THIS RULE, and as of f-14 (item 21) that
     is enforced rather than remembered: #csovl .adjscroll and #stlovl .adjscroll were both
     REGISTERED in that array — with comments in app.js and above their own shape rules saying
     the cue was there — and appeared nowhere in this selector, so two sheets carried a `.cut`
     class that painted nothing. reachability.cjs now greps each selector out of app.js and
     fails if it is missing here, so the two lists cannot part again. */
#delovl .delsay.cut,#delovl .pwmsg.cut,#hmovl .pwmsg.cut,#agfovl .agflist.cut,#coovl .coshfields.cut,#aiovl .aisay.cut,#aiovl .pwmsg.cut,#bkovl .coshfields.cut,#adjovl .adjscroll.cut,#csovl .adjscroll.cut,#stlovl .adjscroll.cut,#acctovl .sheet.cut,#invovl #invScroll.cut,#accovl #accBody.cut,#plcovl .adjscroll.cut{-webkit-mask-image:linear-gradient(180deg,black calc(100% - 20px),transparent);
    mask-image:linear-gradient(180deg,black calc(100% - 20px),transparent)}
  /* --- THE AI DISCLOSURE SHEET (#aiovl, f-5, 2026-08-24) ----------------------------------
     THE COPY IS THE SCREEN, so it is the one region allowed to shrink and scroll, exactly as
     #delovl's .delsay is and for the same reason: this sheet exists to be READ before a tap, and
     shortening the words would only move the fold to the next install whose provider sentence is
     longer than Anthropic's. The shape gives, not the sentence.
     17px/700 in the PRIMARY ink, not --tmut: every line here is a fact somebody is being asked to
     act on. It is not #delovl's 20px — that size belongs to consequence copy in front of a
     one-way door, and nothing on this sheet is one-way (the Settings row reverses it in one tap).
     THE LIST IS THE PART THAT MATTERS, and it is the part that grows: one bullet per active AI
     feature, painted from whoami. Its bullets are plain discs at a normal indent — nothing
     invented, no new token, no colour of its own.
     BOTH BUTTONS AND THE MESSAGE ARE flex:none, which is load-bearing in the way #delovl's note
     spells out: a flex item with a min-height can still be out-shrunk by a sibling's content, and
     "the answer buttons are on screen" is the one assertion on a consent sheet that must not be
     able to become false. Measured at 375x407 and 390x844 — see test/ai-consent-ui.cjs. */
  #aiovl .sheet{display:flex;flex-direction:column}
  #aiovl .aisay{font-size:17px;font-weight:700;line-height:1.45;color:var(--text);margin:2px 2px 0;
    flex:0 1000 auto;min-height:0;overflow-y:auto;overscroll-behavior:contain}
  #aiovl .aisay p+p,#aiovl .aisay ul+p{margin-top:12px}
  #aiovl .aisay ul{margin:12px 0 0;padding-left:22px}
  #aiovl .aisay li{margin-top:8px}
  #aiovl .aisay li:first-child{margin-top:0}
  /* The provider's own name is the one word on this sheet a person has to be able to pick out of
     the paragraph — it is the whole of what 5.1.2(i) asks the app to say — so it wears the same
     --text-on-strong emphasis <strong> carries everywhere else in this product rather than a
     colour or a size of its own. */
  #aiovl .aisay strong{color:var(--text)}
  /* The way to the full policy is the quietest thing on the sheet and the last line of the
     scroller: it is the OTHER document, not this decision. --brand is the app's link ink. */
  #aiovl .aipriv{font-size:15px;font-weight:800}
  /* 56 (f-14, item 23, `touch-target`). This link is the ONLY road from the 5.1.2(i) disclosure
     sheet to the policy it is asking somebody to agree to, and it rendered as bare 15px text
     with no box at all. inline-flex so it takes a real height without becoming a block that
     breaks the sentence it sits in. */
  #aiovl .aipriv a{color:var(--brand);display:inline-flex;align-items:center;min-height:56px}
  #aiovl .pwmsg{flex:0 1 auto;min-height:0;overflow-y:auto;overscroll-behavior:contain}
  #aiovl .submit,#aiovl .onbghost{flex:none}
  /* --- THE HOME CHIP'S CLOCK CATCH (#hmovl, 2026-08-18) -----------------------------------
     THE SENTENCE. 17px/700 in the primary ink: this is a FACT the person is being asked to act
     on, not a caption, so it is not allowed to be the smallest thing on its own sheet. It is
     not #delovl's 20px either — that size belongs to consequence copy somebody must read before
     a one-way door, and nothing here is one-way. --tmut is deliberately not used: --tmut is the
     app's secondary ink and this line is the sheet's primary content.

     THE FOLD, SETTLED THE WAY #delovl SETTLES IT, in three lines and for the same reason: the
     three controls ARE the sheet, so they are the parts that never move. The sheet is a column,
     the one region that is prose is the only thing allowed to shrink and scroll, and it joins
     the .cut list above so a sliced sentence still reads as "scroll" rather than as broken text
     (app.js DEL_CUT_REGIONS — the list exists to be joined). flex:none on the buttons is
     load-bearing exactly as it is on the delete sheet: a flex item with a min-height can still
     be out-shrunk by a sibling's content, and "the primary button is 60px tall" is the one
     assertion that must not be able to become false.
     MEASURED at 390x844 and at 375x407 (an iPhone SE) — see the r4-seven assertions. There is
     no input on this sheet, so unlike #delovl there is no keyboard state to measure: 407 here is
     a short phone, full stop.

     "Just directions" IS THE HOUSE .ghost, not a new component — the same pill #delOpen wears —
     laid out full width at the same 56px box, because in a sheet a button that spans the column
     is the shape every other secondary action in this app already has. Only the box changes;
     the ink (--brand) and the border (--strong) are the component's own.

     THE FACT LINE DOES NOT SHRINK (2026-08-18, eval `prose-starved`, 2nd instance). It used
     to be the one region on this sheet that could — the fold rule's "one shrinkable region"
     — and it held the whole sentence, job name included. At 375x407 with a 49-character job
     name it was squeezed to 52px of 74 and the elapsed time and "· on break" went under the
     fade; at 740x360 (a 360x800 Android held sideways) it was 12px of 25 — under one line,
     entirely inside its own 20px gradient, reading as a rendering fault rather than as
     "scroll me". The sentence is now split so the part that MUST survive is flex:none and
     bounded (a fixed phrase, an elapsed, an optional "· on break" — two lines at 375px, no
     job name in it), and the job name is a separate one-line row that ellipsises. What is
     left shrinkable is #hmMsg, which only exists when there is a refusal to read.
     .hmjob IS ALLOWED TO GIVE UP ITS 19px last — flex:0 1 auto with min-height:0 — so on the
     shortest landscape phone the buttons still win. It never wraps, so it never eats the
     sheet the way the old sentence did. */
  #hmovl .hmsay{font-size:17px;font-weight:700;line-height:1.45;color:var(--text);margin:2px 2px 4px;
    flex:none}
  #hmovl .hmjob{font-size:13px;font-weight:800;line-height:1.45;color:var(--tmut);margin:0 2px 14px;
    flex:0 1 auto;min-height:0;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
  /* The refusal region — the same component and the same rules as #delovl .pwmsg (see its
     note): absent until it has something to say, allowed to shrink and scroll, and in the
     .cut family above so a sliced refusal reads as "scroll" and not as broken text. */
  #hmovl .pwmsg{font-size:15px;margin:0 2px 10px;flex:0 1 auto;min-height:0;overflow-y:auto;
    overscroll-behavior:contain}
  /* AND THIS SHEET GETS MORE OF THE SCREEN THAN THE OTHERS, scoped to itself and argued rather
     than assumed. `.sheet` caps at 88dvh so a sheet with an INPUT leaves room for the software
     keyboard and a strip of scrim to tap out on. This sheet has no input anywhere on it (the
     template says so), so the keyboard half of that reason does not apply — and it is the one
     sheet that has to hold a safety sentence beside three 56px controls on a 407px phone.
     MEASURED at 375x407: at 88dvh the message region is 37px and a two-line refusal is 44px —
     the instruction to write the time down went under the fade, which is the exact defect this
     region was added to end, moved one element over. At 94dvh it is 61px, and the two-line
     refusal has room to become three without clipping. 6% of a short screen is still 24px of
     scrim above the sheet, so the way out by tapping outside survives. */
  /* MINUS THE TOP INSET, for the reason the base .sheet rule now carries (f-6 fix round): 94% of
     a 520pt window leaves 31px, which is inside the 62px band the binary's status bar owns. On
     the web --sa-top is 0px and this is byte-identical to what it has always been. */
  #hmovl .sheet{max-height:calc(94vh - var(--sa-top));max-height:calc(94dvh - var(--sa-top))}
  /* TWO THINGS REACH THIS REGION AND THEY ARE NOT THE SAME KIND OF BAD, so they do not wear
     the same colour — and neither is told apart BY colour alone, both are sentences.
     .warn (copper, the component's own) is a server refusal: try again, nothing is lost.
     .lost is the punch that is NOWHERE — the app's safety case by definition, and LAW 21
     reserves red for exactly that. It is the same event, in the same words, as the red
     #ffSafety banner this region exists to repeat where the person can actually see it; two
     colours for one event across two surfaces would be the reader's problem, not the
     palette's. --red is #c2412e, 4.9:1 on the sheet's white. */
  #hmovl .pwmsg.lost{color:var(--red)}
  #hmovl .sheet{display:flex;flex-direction:column}
  #hmovl .submit,#hmovl .ghost,#hmovl .onbghost{flex:none}
  #hmovl .ghost{display:block;width:100%;min-height:56px;font-size:15px;border-radius:var(--r-card);
    padding:0 12px;margin-top:10px}
  /* --- THE LONG-SHIFT QUESTION (#lsovl, f-2 W4, 2026-08-22) -------------------------------
     THE SAME THREE LINES AS #hmovl, deliberately: a column, everything that is a control is
     flex:none, and the one region that is prose is the only thing allowed to shrink. It is a
     sheet with a sentence and three 56px buttons on a phone that may be 407px tall, which is
     the exact shape the fold rule was written for.
     THE ELAPSED IS THE SHEET'S PRIMARY CONTENT and takes #hmovl's 17px/700 in the primary ink
     — it is the fact the question is being asked about, not a caption. The reassurance under
     it is secondary ink, because it is context.
     NO --lime ANYWHERE ON IT. The two answers are equal choices, so they are the house .ghost
     at the same full-width 56px box #hmovl's second choice already uses; the sheet has no
     primary action because the question has no expected answer. */
  #lsovl .sheet{display:flex;flex-direction:column}
  #lsovl .lssay{font-size:17px;font-weight:700;line-height:1.45;color:var(--text);margin:2px 2px 6px;flex:none}
  #lsovl .lssub{font-size:13px;font-weight:700;line-height:1.45;color:var(--tmut);margin:0 2px 12px;
    flex:0 1 auto;min-height:0;overflow-y:auto;overscroll-behavior:contain}
  #lsovl .ghost,#lsovl .onbghost{flex:none}
  #lsovl .ghost{display:block;width:100%;min-height:56px;font-size:15px;border-radius:var(--r-card);
    padding:0 12px;margin-top:10px}
  #lsovl .onbghost{min-height:56px;font-size:15px;padding:0 12px}
  /* --- "FILE IT UNDER…" (#agfovl, 2026-08-18) — the import's destination sheet ------------
     THE SAME THREE LINES AS #hmovl ABOVE, and they are the third copy on purpose: this is the
     shape the fold check asks for (one shrinkable region, everything else flex:none), and the
     region that shrinks here is a LIST rather than a paragraph. It joins the .cut family above
     for the same reason a paragraph does — a job list sliced flat at the sheet's edge reads as
     "that's all of them", and on a company with fourteen jobs it is not.

     THE ROWS ARE .jrow, THE APP'S EXISTING JOB ROW, not a new component: this is the same list
     of jobs the clock-in picker shows, so it is the same row, and a person who has used one
     recognises the other. Only the container is new. 56px floor on the row and on Cancel —
     both are things a gloved thumb hits. */
  #agfovl .sheet{display:flex;flex-direction:column}
  #agfovl .agfname{font-size:13px;font-weight:800;color:var(--text);margin:0 2px 10px;
    word-break:break-word;flex:none}
  #agfovl .agfname span{display:block;font-size:11px;font-weight:700;color:var(--tmut);margin-top:2px}
  #agfovl .agflist{flex:0 1 auto;min-height:0;overflow-y:auto;overscroll-behavior:contain}
  #agfovl .agflist .jrow{min-height:56px}
  #agfovl .onbghost{flex:none;min-height:56px;font-size:15px;padding:0 12px}
  /* Same ink as the sheet it opens, on the same .ghost component and the same 56px box as
     #pwOpen. var(--red) is the app's existing danger token (.adjout draws its label in it on a
     bordered card exactly like this); the alternative was leaving the word "Delete" in brand
     green, which is the one colour this product reserves for the action you DO want. */
  #delOpen{color:var(--red)}
  /* AND THE DOOR TO ALL OF THAT. #pwOpen is the ONLY way into the change sheet, and it measured
     44px tall at 10.5px type while the surfaces it opens are 56-60px at 15-17px — two standards
     inside one flow, the smallest control in the flow being the one that starts it. 44px is not
     wrong in isolation: it is the gloved-thumb FLOOR measured on a Galaxy A12 by test/android.cjs
     (see the port-hardening block at the bottom of this file). A floor is not the size a password
     control gets. margin-left:auto lives here now instead of an inline style on the button. */
  /* white-space:nowrap is not a nicety here: #delOpen's row carries a two-line sub-sentence, and
     under .prow's flex the label was squeezed until the chevron wrapped onto a second line — a
     115px control reading "Delete" over "›", which is a button that looks broken on the one row
     in the app that most needs to look deliberate. CAUGHT BY OPENING THE SCREENSHOT, not by an
     assertion: every measurement passed, because a wrapped button is TALLER than the floor. */
  /* THE .ghost-IN-A-.prow FAMILY, ENUMERATED (2026-08-23 — EVAL-LEDGER `touch-target`, 6th and
     7th occurrences). #instSwitch — the gear sheet's "Change ›" for the company row — joined
     its siblings rather than getting numbers of its own; it measured 71x44 at 10.5px inside a
     68px row while #signoutBtn directly beneath it measured 56px, and it is the ONLY road off a
     wrongly-picked company, which is this class's own pattern: the small control is the entry
     point to the big thing.

     THE 7th OCCURRENCE IS THAT FIX'S OWN LESSON REPEATED. The comment here used to say "its two
     SIBLINGS". There were FOUR. #privacyLink (index.html:1779) is the same .ghost, in the same
     .prow, in the same #acctovl sheet, thirteen lines above #instRow — measured 58x44 at 10.5px
     with #pwOpen 101x56, #instSwitch 101x56 and #delOpen 94x56 around it, and it is the
     privacy-policy road App Review taps on the App Store binary. #permJump (index.html:722) is
     the fifth: the same .ghost in the same .prow on the Settings view, 60x44, wired at
     app.js:5917. So the rule now names the SHAPE it is about — a ghost button sitting at the
     right-hand end of a policy row — and every member of it is listed here, in one place, so
     the next one to be added is added to a list rather than discovered by a grader.

     #aiRowBtn IS THE SIXTH (f-5, 2026-08-24), AND IT WAS DISCOVERED RATHER THAN LISTED — which
     is the paragraph above failing on its very next use, so it is written down instead of
     quietly fixed. The AI-disclosure row shipped with the bare .ghost and measured 44px in the
     round's own browser suite, one row above #privacyLink at 56 and two above #delOpen at 56.
     It is also this class's own pattern one more time: it is the SMALL control that is the only
     entry point to the big thing, because once somebody has declined it is the ONLY road back
     to an AI feature anywhere in the app. Added to the list, not given numbers of its own.

     display:inline-flex IS LOAD-BEARING FOR #privacyLink AND NOT DECORATIVE. It is an <a>, not a
     <button>: blockified as a flex item its min-height applies, but its text sits at the TOP of
     the 56px box instead of the middle, so the control would measure right and look broken. The
     four buttons already centre their own content, so the declaration costs them nothing. */
  #pwOpen,#delOpen,#instSwitch,#privacyLink,#permJump,#aiRowBtn{min-height:56px;font-size:15px;padding:0 16px;
    margin-left:auto;white-space:nowrap;flex:none;display:inline-flex;align-items:center;justify-content:center}

  /* the second label on the door — the password box needs air under the email box */
  .onblab.pwlab{margin-top:14px}
  #onbScr .onb[data-step="1"] .onbghost{color:rgba(255,255,255,.78)}
  /* Step 9 takes the door's subnote colour too (2026-08-23 — `fixed-one-of-N`). #onbCoSub was
     measured at rgb(154,162,170) — the LIGHT theme's grey, inherited from a screen that is not
     dark green — sitting on the dark green picker. The white-on-green value is step 1's. */
  #onbScr .onb[data-step="1"] .subnote,
  #onbScr .onb[data-step="9"] .subnote{color:rgba(255,255,255,.55)}

  /* ---- THE DOOR AT KEYBOARD HEIGHT (2026-08-17, EVAL-LEDGER `keyboard-fold`) ---------------
     MEASURED at 375x407 — an iPhone SE with the software keyboard up, which is the ONLY state
     a sign-in screen is in from the moment somebody taps the email box. #onbEm was the only
     control on screen: #onbPw ended at 418, "Sign in" ran 436-496 and "Forgot password" — the
     other road, and the ONLY other road since the access button became config — ran 506-562,
     against a 407px window. #onbScr scrollHeight 611 vs clientHeight 407.

     THE FIX IS THE SHAPE, NOT THE COPY, per the standing check: the column has ONE region that
     is allowed to give way, and it is the BRANDING — a 125px logo, a wordmark and a subtitle,
     218px of the 611 spent saying whose app this is to somebody who is already holding it.
     Nothing that a thumb touches moves: every control keeps its 56/60px box and its type.
     Three steps rather than one, because a 568px-tall phone, a 407px keyboard state and a
     407px keyboard state WITH A REFUSAL ON IT are not the same problem. At 620 the logo
     shrinks; at 460 it goes altogether, along with "Invited by your company · nothing to
     create", leaving the wordmark and the company line; and at 460 WITH a message showing
     (`.saying`, below) those last two go as well, because at that point the sentence a person
     is trying to read is worth more than the sentence telling them whose app it is. Keyed on
     HEIGHT only — a short LANDSCAPE window gets the same treatment for the same reason, and a
     tall narrow one is untouched. Measured after: 611 → 407 clean, 407 with a refusal.

     WHAT IS LEFT OVER IS SCROLLED, AND SAID SO. Even trimmed, this column still exceeds 407:
     the third road is ON by default (DEFAULT_CONFIG.accessCodeEnrollment is TRUE — this line
     named "Luce Line" as the install that had it, which was true for the hours between Wes's
     two calls on 2026-08-17 and stopped being true that evening), and a two-line refusal in
     #onbSignMsg pushes it further. MEASURED on the default door at 375x407: 458 against 407,
     with the third road the part below the fold. A column that silently overflows is the
     defect whether or not it can be scrolled. .onbmore below is the cue, gated on the overflow
     itself. */
  @media (max-height:620px){
    /* THE CHROME YIELDS ON EVERY STEP, NOT JUST THE DOOR (2026-08-17, fix round 3). The two
       rules directly below are the whole of the discipline applied to the other five screens:
       26px of column padding and a 34px heading margin are decoration, and at keyboard height
       they were the difference between a primary action on screen and one clipped by 7px.
       MEASURED at 375x407, before → after: step 3 534→486 (still over — it carries two text
       inputs, a chip row and a switch, and genuinely does not fit; it scrolls and says so),
       step 4 440→392 FITS OUTRIGHT, step 6 518→470 with "Continue" fully on screen and only
       "‹ Back to sign in" below the fold behind the cue. Steps 2 and 5 fit before and after —
       they only gain slack for the message states.
       NOTHING A THUMB TOUCHES MOVES: every control keeps its 56/60px box and its type size,
       which is the same rule the step-1 block below already answers to.
       `[data-step]` IS LOAD-BEARING, not decoration: the PORT HARDENING block at the bottom of
       this file carries `#onbScr .onb{padding-bottom:calc(26px + var(--sa-bottom))}`
       for the Android 15 gesture pill, at the same specificity as a bare `#onbScr .onb` and
       LATER in the file — so a bare selector here would silently lose the bottom half of the
       trim and only the top 10px would land. The attribute puts this back above it, which is
       the specificity the step-1 rule it replaces already had.
       The heading rule stays at `#onbScr .onb .onbh` deliberately: step 2's own
       `[data-step="2"] .onbh{margin-top:0}` outranks it, so the centred code screen keeps the
       zero top margin it was designed with and only gives up the bottom 6px. */
    /* AND THE TOP INSET SURVIVES THE COMPACTION (f-6 fix round). This block is the short-window
       trim and it OVERRIDES the base rule's padding-top — so leaving a bare 16px here would put
       the status bar back over the heading on exactly the window that has least room to argue
       about it: measured at 390x520 with the keyboard up, every step drew its first pixel at
       y=16 or y=28. The compaction is of the DESIGNED trim, never of the strip iOS owns. */
    #onbScr .onb[data-step]{padding-top:calc(16px + var(--sa-top));padding-bottom:calc(16px + var(--sa-bottom))}
    #onbScr .onb .onbh{margin:12px 0 12px}
    /* STEP 6 GIVES BACK THE 10px THE TOUCH FLOOR TOOK (f-7 fix round 3), and it gives it back
       out of SPACING rather than out of the control — which is the whole rule this block's own
       header states: "nothing a thumb touches moves".
       WHAT HAPPENED: the 56px floor moved onto `.inp` itself this round, and step 6's
       #onbAcEm was one of the four fields nobody had ever measured — 46px. Ten pixels taller
       pushed "Continue" from 328-388 to 338-398 at 375x407, and the scroll cue is a 19px band
       pinned to the bottom of the scroller (388-407). So the primary action ended up under the
       wash: `access-code-ui.cjs` measures exactly that and went red on it, having passed at
       EQUALITY (band top 388, button bottom 388) before — a real assertion sitting on a
       zero-pixel margin.
       WHAT THIS BUYS: 10px from the consent paragraph and 4px from each of the two field
       labels = 18px, all of it decoration, none of it a target. Measured at 375x407 after:
       Continue 320-380 against a band at 388, which is 8px of daylight instead of none.
       Above 620px nothing here applies and step 6 is untouched. */
    #onbScr .onb[data-step="6"] .onbconsent{margin-bottom:8px}
    #onbScr .onb[data-step="6"] .onblab2{margin-top:8px}
    /* STEP 9 COMPACTS WITH STEP 1 (2026-08-23 — EVAL-LEDGER `fixed-one-of-N`). The picker is
       the SAME door on the same short window: measured at 375x320 the wordmark was still 24px
       and the margins were still the tall ones, because the whole compaction block was scoped
       to `[data-step="1"]` while the round only extended the gradient and the .submit width.
       Every line below is step 1's number, applied to step 1's twin — no new value is
       introduced, which is the whole reason this is a selector list and not a second block. */
    #onbScr .onb[data-step="1"] .onblogo,
    #onbScr .onb[data-step="9"] .onblogo{width:76px;margin-bottom:8px}
    #onbScr .onb[data-step="1"] .onbword,
    #onbScr .onb[data-step="9"] .onbword{font-size:19px}
    #onbScr .onb[data-step="1"] .onbsub,
    #onbScr .onb[data-step="9"] .onbsub{margin:4px 0 14px}
    #onbScr .onb[data-step="1"] .onblab,
    #onbScr .onb[data-step="9"] .onblab{margin-top:8px}
    #onbScr .onb[data-step="1"] .submit,
    #onbScr .onb[data-step="9"] .submit{margin-top:12px}
  }
  @media (max-height:460px){
    /* The LOGO goes and the reassurance line goes. What stays is the wordmark and
       #onbCo — "The crew app for <their company>" — because that line is the one thing on the
       column that tells somebody they are at the right company's door, and 33px is not worth
       taking it. "Invited by your company · nothing to create" is reassurance, which is the
       only thing here that is not a control, a label, a road, or an identity. */
    #onbScr .onb[data-step="1"] .onblogo,
    #onbScr .onb[data-step="9"] .onblogo{display:none}
    #onbScr .onb[data-step="1"] #onbSub1,
    /* The picker's reassurance line, on the same argument as #onbSub1: on a 407px window
       it is the only thing on the column that is not a control, a label or a road. */
    #onbScr .onb[data-step="9"] #onbCoSub{display:none}
    /* AND WHEN THE SCREEN IS REFUSING SOMEBODY, THE REST OF THE BRAND YIELDS TO THE REFUSAL.
       `.saying` is set by app.js onbSetSignMsg whenever #onbSignMsg carries text. MEASURED at
       375x407: the refusal line is 58px plus a 14px margin and the trimmed column has 16px of
       slack, so ANY message pushed "Forgot password" — the ONLY other road off this door —
       below the fold. What goes instead is a wordmark and "The crew app for <company>", to
       somebody who is holding the phone, has already typed their address into it, and is
       reading a sentence about why they are not in yet. Both come straight back the moment the
       message clears, and above 460px neither ever moves.
       Keyed on the MESSAGE, not on the measured overflow: a rule that hid a block because the
       column overflowed would shorten the column, clear its own condition, and oscillate. */
    #onbScr .onb[data-step="1"].saying .onbword,
    #onbScr .onb[data-step="9"].saying .onbword{display:none}
    #onbScr .onb[data-step="1"].saying .onbsub,
    #onbScr .onb[data-step="9"].saying .onbsub{display:none}
    /* STEP 6 MAKES THE SAME TRADE (2026-09-04 eval r2, `keyboard-fold`, inherited — the class
       was promoted for exactly this and this column was never checked with a refusal on it).
       Measured at 375x407 with one up: "Continue" at 421-481 and "Back to sign in" at 491-547,
       both under the scroll cue — the primary action AND the way out off the glass at once.
       What stands down is the paragraph explaining what an access code is, to somebody who has
       already typed one and is reading a sentence about why it did not work; every label, both
       fields, the button and the road back all stay. It comes back the instant the message
       clears. app.js's onbSetAcMsg toggles .saying on the message, never on the overflow. */
    #onbScr .onb[data-step="6"].saying .onbconsent{display:none}
    /* …AND THE REFUSAL ITSELF YIELDS TYPE, because standing the paragraph down was not enough.
       Measured: hiding .onbconsent moved "Continue" from 421-481 to 353-413 in a 407px window —
       68px, and still 6px over, with "‹ Back to sign in" at 423-479 entirely gone. The reason is
       the message: .pwmsg is 20px, the app's largest body type, and the longest refusal this
       door prints ("That code doesn't match this email · 5 tries left — check the email is the
       one your office has for you.") wraps to SIX lines at 343px of column, ~174px of the 407 it
       has. At 15px it is four lines and the whole column fits with both controls on the glass.
       THE TRADE, said plainly: the sentence gets smaller so that the button it is about stays
       reachable. Shortening the words instead would only move the fold to the next long message,
       which is what `keyboard-fold` says to stop doing — fix the shape. Above 460px the refusal
       keeps all 20px, which is every phone held normally.
       SCOPED TO STEP 6 DELIBERATELY (`fixed-one-of-N` wants the site list, so: steps 1, 3, 4, 5
       and 9 all carry a .pwmsg too). They are left because each has its own measured trim in the
       620px block above and each is asserted clean at 375x407 with a message up — this is the
       one column that runs 518px before a refusal is added to it. If another goes red, it joins
       this selector rather than growing a second number. */
    #onbScr .onb[data-step="6"].saying .pwmsg{font-size:15px;line-height:1.4;margin-top:10px}
    /* …AND THE HEADING STANDS DOWN WITH IT, which is step 1's trade exactly: step 1 drops the
       wordmark and "The crew app for <company>" — its IDENTITY block — while it is refusing, on
       the argument that a person holding the phone and reading a refusal already knows where
       they are. "Your access code" is the same kind of line, and it is the last 44px between
       "‹ Back to sign in" (395-451 after the type yield, in a 407px window) and the glass.
       WHAT NEVER YIELDS, and this is the whole ordering: both labels, both fields, the refusal,
       Continue, and the road back. Only the decoration goes, and it comes back the instant the
       message clears. */
    #onbScr .onb[data-step="6"].saying .onbh{display:none}
  }
  /* THE CUT. Same affordance and the same gate as the delete sheet's `.cut` (see #delovl
     .delsay.cut): app.js compares scrollHeight to clientHeight and toggles the class, so it is
     on exactly when there is something below the fold and off when there is not — never keyed
     on a viewport height, which is the mistake that rule already records.
     STICKY, not absolute: #onbScr is the scroller, so anything positioned inside it scrolls
     away with the content; sticky pins it to the bottom edge of the window instead. The
     negative margin means it occupies NO layout height — the column it appears over is the
     column that exists without it — and pointer-events:none keeps it from eating the tap
     meant for "Forgot password" underneath.
     NOT SCOPED TO A STEP ANY MORE (2026-08-17, fix round 3). It shipped as
     `#onbScr.cut .onb[data-step="1"] .onbmore`, and .cut is set by onbCutSync() on the SHARED
     scroller for WHICHEVER step is showing — so on steps 3 (534/407), 4 (440/407) and 6
     (518/407) the class was on, the overflow was real, and the one element that could have
     said so was inside a display:none step, computing to a 0x0 box. That is the 6th occurrence
     of `keyboard-fold`, and every one of them has been the same mistake: a correct gate wired
     to too small a scope. The gate was never wrong; only this selector was. The node is now a
     SIBLING of the six steps and last in the scroller (index.html, THE CUT), so `>` is the
     whole of it and there is exactly one of them to keep right.
     THE COLOUR FOLLOWS THE STEP UNDERNEATH. There are TWO green screens — step 1 and step 9,
     the company picker, which wears the door's gradient because on the App Store build it IS
     the door (corrected 2026-08-23; this line said "step 1 is the only green screen in the app"
     and the same commit that wrote step 9 made it false, which is exactly the drift the
     enumeration in sw.js was deleted for). #24512c is the end stop of that gradient, so on
     either of them the fade is the screen dissolving into itself — and steps 2-6 sit on
     var(--scr). A single `~` off the SHOWING step is what picks between them; it works
     because .onbmore is after all of them in the
     markup, which is the other reason it is last rather than first.

     A CUE MAY NOT LAND ON THE THING IT IS ANNOUNCING YOU CAN SCROLL PAST (2026-08-17, fix
     round 4). It shipped as one 40px band with the glyph at its foot on every step, and that
     put both halves of the cue — the WASH and the CHEVRON — wherever the fold happened to
     fall. MEASURED at 375x407, and the two steps that overflow at rest fail it in opposite
     directions:
       * STEP 6 — "Continue" runs 328-388 and the band ran 367-407, so the primary action's
         bottom 21px sat under a gradient reaching rgba(...,.94): about a 49% wash across the
         button's own bottom edge, and the composition at rest was a dimmed CTA under a
         chevron (EVAL-LEDGER `density`). Fixed by making the band 19px — the glyph's own
         height — so it begins at 388, exactly where Continue's box ends. PROVEN BY PIXELS, not
         by geometry: the step photographed with the cue on and again with it hidden differs
         only on rows 389-406, so not one pixel of the primary action is touched by the wash or
         by the chevron's ink.
       * STEP 1 — the fold lands INSIDE a control's text rather than below one: the third road
         runs 385-441 with its first line of type at 393-413, and the chevron's box (382-408
         measured over the text node; a 19px line box at 386-405) landed in the middle of a
         word ("Use the c⌄de your office gave"). A short
         band cannot fix that, because moving the glyph DOWN moves it further into the line.
         So step 1 keeps the tall fade — which is correct there, and was: what the band covers
         is a road below the fold, which is the thing being announced — and moves the GLYPH to
         the TOP of it instead (align-items:flex-start: line box 367-386, and 363-389 measured
         generously over the text node — clear of 393 either way). That lands the chevron in
         the 10px gutter between "Forgot password" (ends 375) and the peeking third road
         (starts 385), clear of both controls' type, pointing straight down at the road it is
         there to reveal.
     TWO GEOMETRIES BECAUSE THERE ARE TWO SHAPES OF FOLD, not because step 1 is special: a
     screen whose last control ends above the fold wants the band to start where that control
     ends, and a screen whose fold cuts through a control wants the glyph out of its type.
     Both are stated as numbers and both are asserted at 375x407 in test/access-code-ui.cjs,
     so a copy change that moves either control turns a suite red rather than quietly
     re-creating this. */
  .onbmore{display:none}
  #onbScr.cut > .onbmore{display:flex;align-items:flex-end;justify-content:center;
    position:sticky;bottom:0;flex:none;height:19px;margin:-19px 0 0;
    pointer-events:none;line-height:1;font-size:19px;font-weight:800;
    color:var(--tmut);
    background:linear-gradient(180deg,rgba(243,245,247,0),rgba(243,245,247,.94))}
  /* height/margin restated rather than inherited: this rule is the WHOLE of the green cue's
     geometry, so a reader who finds it does not have to hold the base rule in their head to
     know where the band is. The pair still cancels to zero layout height, which is what keeps
     .cut from oscillating — turning the cue on must never change the number onbCutSync()
     reads.
     STEP 9 IS ON THIS RULE TOO (2026-08-23 — EVAL-LEDGER `fixed-one-of-N`, and it was LATENT:
     the eval could not make the picker overflow at 375x407, 375x360 or 375x320, so this is a
     trap being closed rather than a defect being seen). Without the selector, a step 9 that
     ever DID overflow would paint the light wash rgba(243,245,247,.94) and a --tmut chevron
     across a dark-green screen — the base rule above, on the one screen it was never meant
     for. Two green screens, one cue rule. */
  #onbScr.cut > .onb[data-step="1"].on ~ .onbmore,
  #onbScr.cut > .onb[data-step="9"].on ~ .onbmore{
    height:40px;margin:-40px 0 0;align-items:flex-start;
    color:rgba(255,255,255,.78);
    background:linear-gradient(180deg,rgba(36,81,44,0),rgba(36,81,44,.94))}
  /* step 5's own copy — this is the sentence that tells a person the password is theirs alone */
  #onbScr .onb[data-step="5"] .onbconsent{font-size:17px;margin-bottom:6px}
  /* STEP 6 — the access code box. Typed once, from something read aloud over a running
     engine, so it is the largest input in the app: 26px, wide-tracked, centred, tabular
     figures so the eight characters sit in eight fixed columns and a gloved thumb can count
     them against what is being shouted at it. Uppercase is enforced here as well as in JS,
     because a lowercase keyboard must not make the code LOOK different from the one on the
     owner's screen while it is being typed — a person checking their own work is the whole
     point of the box. The placeholder carries the same tracking so the field does not resize
     the instant the first character lands. */
  .onbaccode{text-align:center;font-size:26px;font-weight:800;letter-spacing:.16em;
    text-transform:uppercase;font-variant-numeric:tabular-nums;min-height:64px;padding:0 10px}
  .onbaccode::placeholder{letter-spacing:.16em;font-weight:700;opacity:.32;text-transform:none}
  #onbScr .onb[data-step="6"] .onbconsent{font-size:15px}

  /* ---- THE HANDOFF SHEET (#accovl) — the owner's side of the same code ---------------------
     The code is read ALOUD from this screen while someone types it into theirs, so the two
     have to LOOK like the same eight characters: same uppercase alphabet, same tabular
     columns, same dash after four. The size difference is deliberate — the reader is holding
     the phone at arm's length in the sun, the typist is looking straight down at theirs. */
  .acccode{font-size:38px;font-weight:800;letter-spacing:.14em;text-align:center;
    font-variant-numeric:tabular-nums;color:var(--text);background:var(--scr);
    border:1.5px solid var(--strong);border-radius:var(--r-card);padding:18px 10px;margin:2px 0 10px;
    /* the one place in this app where selecting text is the point */
    user-select:all;-webkit-user-select:all}
  .accwho{font-size:13px;line-height:1.6;color:var(--tmut);margin:0 0 14px}
  .accwho b{color:var(--text)}
  /* The countdown. Copper, never red: an expiring code is a deadline, not a safety event,
     and red in this app is reserved for the second thing. */
  .acclife{display:flex;align-items:center;justify-content:center;gap:7px;font-size:12px;
    font-weight:800;color:var(--copper);margin:0 0 14px;font-variant-numeric:tabular-nums}
  .accsteps{font-size:13px;line-height:1.75;color:var(--tmut);margin:0 0 16px;padding:0 0 0 18px}
  .accsteps li{margin:0 0 4px}
  .accsteps b{color:var(--text)}
  .accrow{display:flex;gap:8px;margin-bottom:10px}
  .accrow>*{flex:1}
  .accerr{font-size:13px;line-height:1.6;color:var(--red);font-weight:700;margin:0 0 12px}
  /* ---- GET SET UP (#setupCard) — the owner's first morning -------------------------------
     REDESIGNED 2026-08-22 from Wes's refined mockups. What it used to be: six rows of equal
     weight, outstanding ones on top. What it is now, and why each piece exists:

     THE UNMISSABLE NEXT MOVE. Exactly ONE row wears .next — copper tint, copper edge, 15px
     title, a copper target ring with a centre dot. Every other outstanding row waits in a
     plain 13px row with a faint ring. Six equally loud rows is six decisions; this is one.

     PROGRESS IS A FACT, TWICE. .setupcount is the fact ("2 OF 6", copper, top right) and
     .gstrack is the feel (six segments filling left to right). Same numbers, two readings —
     the count answers "how much", the track answers "how far" without being read.

     DONE ROWS EARN THEIR PLACE AS A RECEIPT, NOT AS ROWS. .gsdone is half-height, 12px,
     muted, and is a DIV — nothing to tap, nothing disabled, which is LAW 1 and also the
     reason the old version's `disabled` buttons were removed. The card SHRINKS as the owner
     gets through it: at five of six it is one loud panel over a five-line receipt.

     COPPER, NEVER GREEN, ANYWHERE INSIDE THIS CARD. Brand green is the primary action and
     the active state (Wes's standing rule); the one green CTA on Home is Clock in. A
     checklist wearing green would be a second thing on that screen asking to be pressed.

     56px IS THE FLOOR ON EVERYTHING TAPPABLE (`touch-target`, five prior occurrences). The
     mockup drew 44; a gloved hand on a jobsite gets 56, and it is set on .gsrow itself so a
     one-line row cannot fall under it. */
  .setupcard{border-color:var(--strong);overflow:hidden}
  .setupcount{margin-left:auto;font-size:9.5px;font-weight:800;letter-spacing:.06em;color:var(--copper)}
  .setupcount i{font-style:normal;display:inline-block}
  .setupcount.tick i{animation:gspop .3s var(--setup-curve)}
  @keyframes gspop{40%{transform:scale(1.35)}}
  .gstrack{display:flex;gap:4px;margin:1px 0 8px}
  .gstrack i{flex:1;height:4px;border-radius:var(--r-micro);background:var(--border);
    transition:background .4s var(--setup-curve)}
  .gstrack i.fl{background:var(--copper)}
  /* One row = one step. The FIRST outstanding row wears the panel; the rest wait. */
  .gsrow{display:flex;align-items:flex-start;gap:11px;width:100%;text-align:left;background:none;
    border:1px solid transparent;border-radius:var(--r-card);padding:11px 2px;cursor:pointer;font:inherit;
    color:var(--text);position:relative;min-height:56px;
    transition:background var(--setup-rise-ms) var(--setup-curve),border-color var(--setup-rise-ms) var(--setup-curve),
      padding var(--setup-rise-ms) var(--setup-curve)}
  /* The hairline is a pseudo-element, not a border, so the panel row can drop it without the
     rows above and below re-laying-out around a changed box. */
  .gsrow::after{content:'';position:absolute;left:35px;right:2px;bottom:-1px;height:1px;background:var(--border)}
  .gsrow:last-child::after,.gsrow.next::after{display:none}
  .gsrow .stt{display:block;font-weight:800;font-size:13px;line-height:1.3;
    transition:font-size var(--setup-rise-ms) var(--setup-curve)}
  /* 11.5px, not the mockup's 10.5. The shipped caption was 12px and the eval reads type
     sizes off the rendered box; 10.5 on a phone in the sun is a caption nobody reads. */
  .gsrow .std{display:block;color:var(--tmut);font-size:11.5px;line-height:1.45;margin-top:2px}
  .gsrow .stchev{flex:none;color:var(--tfaint);font-weight:800;font-size:14px;align-self:center}
  .gsrow.next{background:var(--setup-tint);border-color:var(--setup-line);padding:13px 12px;margin:2px 0 6px}
  .gsrow.next .stt{font-size:15px}
  .gsrow.next .std{font-size:12px}
  .gsrow.next .stchev{color:var(--copper)}
  /* The step marker. A faint ring while waiting, a copper target ring on the next move, a
     filled copper disc with a check that DRAWS once the fact lands. Never red — an
     unfinished setup is a to-do, not a fault, and red in this app means safety. */
  .stumk{flex:none;width:22px;height:22px;border-radius:50%;border:2px solid var(--strong);margin-top:1px;
    position:relative;display:flex;align-items:center;justify-content:center;box-sizing:border-box;
    transition:border-color .25s,background .25s}
  .gsrow.next .stumk{border-color:var(--copper)}
  .gsrow.next .stumk::before{content:'';width:8px;height:8px;border-radius:50%;background:var(--copper)}
  .stumk svg{position:absolute;left:-2px;top:-2px;width:22px;height:22px}
  .stumk svg path{fill:none;stroke:#fff;stroke-width:3;stroke-linecap:round;stroke-linejoin:round;
    stroke-dasharray:24;stroke-dashoffset:24}
  .gsrow.cked .stumk{background:var(--copper);border-color:var(--copper)}
  .gsrow.cked .stumk::before{display:none}
  .gsrow.cked .stumk svg path{transition:stroke-dashoffset var(--setup-check-ms) var(--setup-curve);stroke-dashoffset:0}
  .gsrow.leaving{overflow:hidden;opacity:0;
    transition:max-height var(--setup-sink-ms) var(--setup-curve),opacity var(--setup-sink-ms),
      padding var(--setup-sink-ms) var(--setup-curve),margin var(--setup-sink-ms) var(--setup-curve)}
  /* The done ledger: a receipt, not a task list. Small, muted, and not controls. */
  .gsledger{border-top:1px solid var(--border);margin-top:9px;padding-top:5px}
  .gsledger:empty{display:none;border-top:0;margin-top:0;padding-top:0}
  .gsdone{display:flex;align-items:center;gap:9px;padding:5.5px 2px;color:var(--tmut);font-weight:700;font-size:12px}
  /* A FINISHED STEP IS THE QUIETEST THING ON THE CARD, not the loudest — the disc is grey and
     not brand green, which is the inversion an earlier eval called out (the passive marker was
     carrying more colour weight than the one button on Home asking to be pressed). */
  .gsdone .dchk{flex:none;width:17px;height:17px;border-radius:50%;background:var(--tfaint);color:#fff;
    display:flex;align-items:center;justify-content:center}
  .gsdone .dchk svg{width:9px;height:9px}
  .gsdone.arriving{max-height:0;opacity:0;overflow:hidden;padding-top:0;padding-bottom:0}
  .gsdone.arrived{max-height:34px;opacity:1;padding-top:5.5px;padding-bottom:5.5px;
    transition:max-height var(--setup-sink-ms) var(--setup-curve),opacity var(--setup-sink-ms),
      padding var(--setup-sink-ms) var(--setup-curve)}
  /* The send-off — plays once, on a crossing this session watched, then the card is gone. */
  .gsbye{display:none;opacity:0;transform:translateY(5px);text-align:center;padding:16px 8px 12px;
    transition:opacity var(--setup-rise-ms),transform var(--setup-rise-ms) var(--setup-curve)}
  .gsbye.show{display:block}
  .gsbye.on{opacity:1;transform:none}
  .gsbye .bchk{width:36px;height:36px;border-radius:50%;background:var(--copper);color:#fff;margin:0 auto 10px;
    display:flex;align-items:center;justify-content:center}
  .gsbye .bchk svg{width:17px;height:17px}
  .gsbye b{display:block;font-size:15px}
  .gsbye small{display:block;color:var(--tmut);font-size:11.5px;margin-top:4px;line-height:1.5}
  .setupcard.gone{overflow:hidden;opacity:0;max-height:0!important;padding-top:0;padding-bottom:0;border-width:0;
    transition:max-height var(--setup-bye-ms) var(--setup-curve),opacity var(--setup-bye-ms),
      margin var(--setup-bye-ms) var(--setup-curve),padding var(--setup-bye-ms) var(--setup-curve)}

  /* ---- THE COMPANY SHEET (#coovl) — the company's two facts, asked in one breath ----------
     Wes's own addition to the Get Set Up mockup (2026-08-22): the name, and the headcount that
     is the DENOMINATOR of the checklist's two crew steps.

     ALMOST NOTHING HERE IS NEW. The sheet is `label` + `.inp` + `.submit` + `.onbghost`, the
     app's own components, so it inherits the type scale, the one lime CTA and the ghost way
     out without inventing a second treatment for any of them. What is new is the stepper, which
     this app did not have, and the fold below it.

     56px, NOT THE MOCKUP'S 44 — the same call the checklist rows made two blocks up, and for
     the same reason (`touch-target`, six prior occurrences). A gloved hand on a jobsite taps
     the minus button, and it is the one control on this sheet somebody presses five times in a
     row.

     THE MINUS BUTTON IS NEVER DISABLED. It clamps at zero in views-team.js, and the CAPTION
     says what zero means in words rather than leaving a bare 0 to be read as a mistake. LAW 1:
     nothing in this app is disabled, anywhere.

     14px RADIUS, not the mockup's 18: --r-sheet is this app's settled sheet stop and every
     other sheet already wears it. One sheet rounder than the rest is drift, not design. */
  .emprow{display:flex;align-items:center;gap:10px}
  .stepbtn{flex:none;width:66px;min-height:56px;border:1px solid var(--strong);background:var(--scr);
    border-radius:var(--r-ctrl);color:var(--text);font-family:var(--font);font-size:22px;font-weight:800;
    line-height:1;cursor:pointer;display:flex;align-items:center;justify-content:center}
  .stepbtn:active{background:var(--border)}
  /* THE NUMBER IS A FIELD (eval 2026-08-23, `zero-one-many`). It was a read-only DIV, so the
     only road to a headcount was one tap of + per employee against a ceiling of 500. It is now
     an input between the two step buttons and therefore a CONTROL, which puts it on the same
     56px floor as everything else on this sheet — measured on the rendered box, never assumed
     from the font. -webkit-appearance:none keeps iOS from drawing its own field over the app's,
     and the placeholder is the em dash that says the question has never been answered. */
  .empn{flex:1;min-width:0;min-height:56px;text-align:center;font-size:22px;font-weight:800;
    font-variant-numeric:tabular-nums;font-family:var(--font);color:var(--text);
    background:var(--card);border:1px solid var(--strong);border-radius:var(--r-ctrl);
    padding:0 6px;-webkit-appearance:none;appearance:none}
  .empn::placeholder{color:var(--tmut);opacity:1}
  /* NOT GREEN, for the second time on this sheet and for the cause named twelve lines down: the
     house focus ring is lime, and a focused field beside the one lime button asking to be
     pressed is two greens competing (`palette-drift` / `green-overload`). */
  #coovl .empn:focus{outline:2px solid var(--strong);border-color:var(--text)}
  /* min-height so the sheet does not jump by a line when the caption changes length. */
  .empcap{margin-top:7px;text-align:center;color:var(--tmut);font-size:11.5px;line-height:1.45;min-height:17px}
  /* The name box's own 56px floor is deleted: `.inp` carries it on the base class now (a text
     field is a control a gloved hand taps, on every sheet, not only the ones somebody
     remembered). Save still takes the 60px a primary action gets on these sheets. */
  #coovl .submit,#bkovl .submit{min-height:60px;font-size:17px}
  #coovl .onbghost,#bkovl .onbghost{min-height:56px;font-size:15px;padding:0 12px}
  /* THE FOLD, the same three lines #delovl uses and for the same reason: one shrinkable region,
     everything else flex:none. MEASURED at 375x407 before it existed — Save at 60px sat on
     screen and "Not now" landed at y431 on a 407px viewport. flex:none on the two actions is
     load-bearing: a flex item with a min-height can still be out-shrunk by its siblings. */
  #coovl .sheet,#bkovl .sheet{display:flex;flex-direction:column}
  #coovl .grab,#coovl h6,#coovl .coshacts,
  #bkovl .grab,#bkovl h6,#bkovl .coshacts{flex:none}
  #coovl .coshfields,#bkovl .coshfields{flex:1 1 auto;min-height:0;overflow-y:auto;overscroll-behavior:contain}
  /* The two actions in one wrapper, a COLUMN — the layout they already had as direct children
     of the sheet, unchanged at every ordinary height, and one thing for the short-viewport rule
     below to turn. */
  #coovl .coshacts,#bkovl .coshacts{display:flex;flex-direction:column}
  /* ---- THE KEYBOARD IS UP AND THE QUESTION IS STILL ANSWERABLE (eval 2026-08-23,
     `keyboard-fold`, 7th occurrence and a NEW shape: the fold check had been run on the wrong
     controls). Save and the way out both passed at 375x407 — and the STEPPER, the one control
     this sheet exists to operate, rendered at y226-282 inside a 102-237 scroller, so
     elementFromPoint at its centre returned DIV.sheet and its caption "Just you — sole
     proprietor" was not on screen at all. The sheet asked a question, hid both the answer and
     its explanation, and put the button that commits under the slice.
     MEASURED, then fixed by giving the fields the 69px they were short:
       · the second action stops owning a row of its own      66px
       · the sheet's bottom padding, 26 -> 16                  10px
       · the field labels' 14/6 margins, -> 8/4 (and 0 on top) 24px off the content
     Nothing shrinks below the 56px touch floor and nothing is hidden — the two buttons sit side
     by side instead of stacked, which is a shape only this viewport ever sees. The shrink region
     and its fade cue are untouched and still fire on anything shorter. */
  @media (max-height:480px){
    #coovl .sheet,#bkovl .sheet{padding-bottom:16px}
    #coovl .coshfields label,#bkovl .coshfields label{margin:8px 2px 4px}
    #coovl .coshfields label:first-child,#bkovl .coshfields label:first-child{margin-top:0}
    #coovl .empcap,#bkovl .empcap{margin-top:5px}
    #coovl .coshacts,#bkovl .coshacts{flex-direction:row;align-items:stretch;gap:10px}
    #coovl .coshacts .submit,#bkovl .coshacts .submit{flex:3 1 0;min-width:0;margin-top:0;min-height:56px;padding:0 12px}
    #coovl .coshacts .onbghost,#bkovl .coshacts .onbghost{flex:2 1 0;min-width:0;margin:0;min-height:56px}
  }
  /* AND THE FOCUS RING IS NOT GREEN, for the second time and for the same cause as the first.
     .inp:focus paints rgba(125,186,61,.4) over a var(--lime) border, and this sheet FOCUSES the
     name box on every open of an unnamed company — so on the first morning the first frame
     showed two greens competing: the field, and the one button asking to be pressed
     (`palette-drift` / `green-overload`, prior instance #delovl). Same shape as the house ring
     in the house's neutral inks; no new value invented. */
  #coovl .inp:focus,#bkovl .inp:focus{outline:2px solid var(--strong);border-color:var(--text)}
  /* THE REFUSAL IS NOT THE CAPTION (f-5, 2026-08-24). #bkSay and #bkCap are the same .empcap
     component two lines apart, and in the muted ink they were the same GREY — a rejection sitting
     under an explanation, told apart only by its words. That is `state-by-colour-alone` inverted:
     no colour at all where a state change needs one. --copper is the app's existing warning ink
     (.pwmsg.warn wears it on every password surface); no new family is invented here, which is
     the whole of `palette-drift`. It is left empty when there is nothing to refuse, so an empty
     box paints nothing rather than a coloured blank. */
  #bkovl #bkSay{color:var(--copper);font-weight:700}
  /* The permanent door to this sheet, in Office · Company. A .prow lands near 53px on two
     lines of type, which is under the floor every other tappable row on this screen clears. */
  #hcRow,#bkRow,#bkNote{min-height:56px}
  /* ---- THE COMPANY OVERVIEW (Office) -----------------------------------------------------
     Ported from design/mockups/office-overview.html, which was built on this stylesheet — so
     almost nothing here is new. The overview reuses .hcard/.hh, .kpis/.kpi and .trow/.tav
     wholesale; these are the four rules the mockup actually invented.

     THE HEADLINE. One number per screen and this is it: what is left after crew and
     materials across every open job. 34px because it is the reason the page exists, tabular
     because a column of other money sits under it. */
  .bignum{font-size:34px;font-weight:800;line-height:1;font-variant-numeric:tabular-nums;
    letter-spacing:-.01em;margin:2px 0 4px}
  /* NO FIGURE, DRAWN AS NO FIGURE. See the note at ovLeft in views-team.js: the em-dash
     placeholder at 800 weight is a black slab that reads as a redaction. Same character, same
     34px slot, ordinary weight and the muted ink every other "nothing here" line in this app
     uses. */
  .bignum.nofig{font-weight:400;color:var(--tmut)}
  .bigsub{font-size:12px;color:var(--tmut);line-height:1.45}
  .bigsub b{color:var(--text)}

  /* A JOB LINE. Quoted, spent, and the bar between them. .bbar's look with a SECOND fill,
     because "where did it go" is the question the bar is being asked: green is crew, copper
     is materials, and the pale remainder is what is left. */
  .jrow2{padding:13px 0;border-top:1px solid var(--border)}
  .jrow2:first-of-type{border-top:0}
  .jr2h{display:flex;align-items:baseline;gap:8px}
  .jr2n{font-size:14.5px;font-weight:800;min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
  .jr2v{margin-left:auto;flex:none;font-size:14.5px;font-weight:800;font-variant-numeric:tabular-nums}
  .jr2s{font-size:11.5px;color:var(--tmut);margin-top:3px;font-variant-numeric:tabular-nums}
  /* ---- CLOSED JOBS (2026-09-05, f-13 lane 3 item 15) --------------------------------------
     The archive's rows, under the Office overview's "Closed jobs · N" fold. A <button>, because
     tapping one opens that job's editor — and at the 56px floor for the same reason every other
     row that OPENS something is: this is the ONLY door back to a job somebody closed out, and
     before this round there was no door at all. It borrows .jrow2's type and rhythm rather than
     inventing a component, so the archive reads as the same book as the open jobs above it. */
  /* THE TOP BORDER IS ON EVERY ROW INCLUDING THE FIRST, and that is deliberate as of the f-13
     eval: `.cjrow:first-of-type{border-top:0}` sat here and never matched anything, because
     `:first-of-type` counts ELEMENT type and the fold's own header is a <button> too — so the
     first row is the second button in the card, always. It was copied from the pattern used
     under a card whose title is a <div>. REPAIRED BY DELETION rather than by re-aiming it: on
     THIS card `.hfold` is `border:0`, so the first row's top border is the only line between the
     header and the list, and a working version of that rule would have removed it. Nothing on
     screen moves; a rule that stated something false is gone. */
  .cjrow{display:block;width:100%;min-height:56px;border:0;border-top:1px solid var(--border);
         background:none;padding:11px 0;margin:0;text-align:left;font:inherit;color:inherit;cursor:pointer}
  .cjrow .cjn{font-size:14.5px;font-weight:800}
  .cjrow .cjs{font-size:11.5px;color:var(--tmut);margin-top:3px}
  .cjrow .cjm{font-size:11.5px;color:var(--tmut);margin-top:2px;font-variant-numeric:tabular-nums}
  .split{display:flex;height:9px;border-radius:99px;overflow:hidden;margin:8px 0 0;background:var(--scr)}
  .split i{display:block;height:100%}
  .split .lab{background:var(--brand)}
  .split .mat{background:var(--copper)}
  .legend{display:flex;flex-wrap:wrap;gap:12px;margin-top:7px;font-size:10px;font-weight:700;color:var(--tmut)}
  .legend span{display:flex;align-items:center;gap:5px}
  .legend i{width:8px;height:8px;border-radius:2px;display:block;flex:none}

  /* THE HONESTY LINE. Copper, never red — an hour with no wage on it is a gap in the data,
     not a safety event, and red in this app means the second thing. Rendered only when there
     is something to say. */
  .caveat{margin-top:10px;font-size:11.5px;line-height:1.5;color:var(--copper);font-weight:700}

  /* Money right-aligned against a label, on the overhead rows. */
  .ovnum{margin-left:auto;flex:none;font-weight:800;font-variant-numeric:tabular-nums}

  /* ---- MY WEEK (v-week) — the crew's fifth tab -------------------------------------------
     The hours are the hero, because the hours are the whole reason this screen exists. 40px
     is the largest number anywhere in the crew app on purpose: it is read at arm's length,
     in a truck, in the sun, by someone who wants one fact. Tabular figures so 8.25 and 41.75
     have the same width and the day column below stays a column.
     NOT ONE DOLLAR SIGN LIVES IN HERE, and that is LAW 2, not an oversight. */
  .mwtot{display:flex;align-items:baseline;gap:9px;flex-wrap:wrap}
  .mwtot b{font-size:40px;font-weight:800;line-height:1;font-variant-numeric:tabular-nums;color:var(--text)}
  .mwtot span{font-size:12.5px;font-weight:700;color:var(--tmut)}
  /* Over the weekly line is COPPER, never red. (The number is per-install config and is never
     written down here — `overtimeWeeklyHours`, off whoami. eval 2026-08-23, `fixed-one-of-N`.)
     Red in this app means safety; overtime is a fact about a
     week, and for most of the people reading this it is a good one. */
  .mwot{margin-top:9px;font-size:13px;font-weight:800;color:var(--copper)}
  .mwleft{margin-top:9px;font-size:13px;font-weight:700;color:var(--tmut)}
  /* THE WEEK PAGER. 56px arrows, because this is the crew app and everything tappable in it
     is 56px; the label between them is the week, not a decoration, so it carries the weight. */
  .mwnav{display:flex;align-items:center;gap:6px;margin:-2px 0 10px}
  .mwarrow{flex:none;width:56px;height:56px;margin:-9px 0;border:0;background:none;color:var(--tmut);
    font:800 26px 'Segoe UI',-apple-system,system-ui,sans-serif;line-height:1;border-radius:50%;
    cursor:pointer;-webkit-tap-highlight-color:transparent;display:flex;align-items:center;justify-content:center}
  .mwarrow:active{background:var(--scr)}
  .mwnavlbl{font-size:12.5px;font-weight:800;letter-spacing:.04em;text-transform:uppercase;color:var(--tmut)}
  /* "This week" is the way BACK from wherever you paged to, so it sits at the end of the row
     and only exists once you have gone somewhere. */
  .mwnow{margin-left:auto;flex:none;min-height:40px;padding:0 14px;border:1px solid var(--border);
    background:var(--card);color:var(--brand);border-radius:999px;
    font:800 12px 'Segoe UI',-apple-system,system-ui,sans-serif;cursor:pointer}
  .mwnow:active{background:var(--scr)}
  /* 56px, THE HOUSE FLOOR, ON EVERY DAY ROW (f-2 W4, decision 8). A day carrying an edited
     punch became a control this round, and the `touch-target` standing check's own clause is
     "when a floor moves onto ONE class, measure every sibling wearing the old one" — so the
     floor goes on .mwday and not on .mwdedit. The button variant also has to unlearn a
     button's defaults (its own font, background, border and centred text), which is why the
     second rule exists at all: it is a reset, never a second look. */
  .mwday{display:flex;align-items:center;gap:10px;padding:11px 0;border-top:1px solid var(--border);
    min-height:56px;width:100%;text-align:left;font:inherit;color:inherit;background:none;border-left:0;border-right:0;border-bottom:0}
  .mwday.mwdedit{cursor:pointer}
  .mwday:first-child{border-top:0}
  .mwdn{font-size:14px;font-weight:800;color:var(--text);flex:none;width:88px;
    display:flex;align-items:center;gap:6px;flex-wrap:wrap}
  /* WHAT CHANGED ON THIS DAY (decision 8). It sits INSIDE the days card, directly under the
     row it belongs to, indented to the width of the day-name column so the eye reads it as
     that day's detail and not as an eighth day. Secondary ink throughout: this is a record,
     not an alarm — nobody is being accused of anything by their own timesheet. */
  .mwhist{padding:2px 0 12px 88px;border-top:0}
  .mwhr{font-size:11.5px;color:var(--tmut);line-height:1.5;margin-top:6px}
  .mwhr:first-child{margin-top:0}
  .mwhr b{color:var(--text);font-weight:800}
  .mwhy{color:var(--tfaint);font-size:11px;line-height:1.45;overflow-wrap:anywhere}
  /* TWO SITES IN ONE DAY HAS TO SURVIVE THE COLUMN (eval 2026-08-15). This was one nowrap
     line, so a long first job name ate the whole cell and the second site vanished with no
     sign it had ever been there — and because the hours column was auto-width, the
     truncation point moved from row to row. Now: the hours column is FIXED (so every row
     truncates in the same place, and a week of days reads as a column), and the where-cell
     wraps to at most two lines instead of dropping what does not fit. */
  .mwdw{font-size:12px;color:var(--tmut);flex:1;min-width:0;line-height:1.35;
    display:-webkit-box;-webkit-line-clamp:2;-webkit-box-orient:vertical;overflow:hidden}
  .mwdh{font-size:15px;font-weight:800;font-variant-numeric:tabular-nums;color:var(--text);
    flex:none;width:58px;text-align:right}
  /* A DAY OFF IS QUIET, NOT EMPTY. It keeps its row so the week still reads as seven days —
     a list that silently skips Saturday makes a person count on their fingers. */
  .mwday.off .mwdn{font-weight:700;color:var(--tfaint)}
  .mwday.off .mwdh{color:var(--tfaint);font-weight:700}
  /* THE OPEN DAY'S OWN WORDS (f-2 W4 T4). The where-cell says whether the day is still running
     or has no clock-out at all; --copper is the same "look at this" ink .mwot and .wk2note.warn
     carry, so the three sentences about one unfinished day on two screens are one colour. Not
     red: nothing here is lost. The row also keeps its normal (non-.off) ink, which is the half
     that stops a 13-hour night reading as a Sunday. */
  .mwday .mwdw.mwdopen{color:var(--copper);font-weight:700}
  /* contacts / directory (v-dir, R3 §2a) — rows reuse .trow/.tav; only the call affordances are new */
  .callbtn{display:inline-flex;align-items:center;gap:6px;margin-left:auto;flex:none;border:1px solid rgba(201,131,47,.45);background:rgba(201,131,47,.10);color:#a06322;border-radius:999px;padding:7px 14px;font:800 11.5px 'Segoe UI',-apple-system,system-ui,sans-serif;text-decoration:none;white-space:nowrap}
  .callbtn svg{width:13px;height:13px;flex:none}
  .callbtn:active{background:rgba(201,131,47,.2)}
  .youtag{margin-left:auto;flex:none;font-size:9px;font-weight:800;letter-spacing:.05em;color:var(--tmut);background:var(--scr);border:1px solid var(--border);border-radius:999px;padding:4px 10px}
  .nonum{margin-left:auto;flex:none;font-size:9.5px;font-weight:700;color:var(--tfaint)}
  .dirwhy{font-size:10px;color:var(--tfaint);margin:2px 2px 6px;line-height:1.5}
  .dirwhy b{color:#a06322}
  /* clock break + quick directions */
  .ckhead{display:flex;align-items:center;justify-content:center;gap:7px;font-size:11.5px;font-weight:800;color:var(--tmut);margin-top:2px}
  .ckel2{font-size:36px;font-weight:800;text-align:center;letter-spacing:.5px;margin:8px 0 10px;font-variant-numeric:tabular-nums}
  .ckbrkwrap{text-align:center}
  .ckbrkline{font-size:10.5px;font-weight:800;color:#8a5e13;background:rgba(185,127,28,.1);border-radius:var(--r-ctrl);padding:6px 12px;margin-bottom:10px;display:inline-block}
  /* 56, at its own rule (f-13 lane 2 item 1) — see the floor list at the foot of this file. */
  .brkbtn{width:100%;min-height:56px;border:1.5px solid var(--border);background:var(--card);color:var(--text);border-radius:var(--r-card);padding:12px;font:800 13px 'Segoe UI',-apple-system,system-ui,sans-serif;cursor:pointer;margin-bottom:9px}
  .brkbtn.on{border-color:var(--amber);background:rgba(185,127,28,.1);color:#8a5e13}
  .dot.brk{background:var(--amber);box-shadow:0 0 0 4px rgba(185,127,28,.15)}
  /* f-2 W5 (decision 5): full-width spread, stacked icon-over-label, no role logic here —
     the row is 3, 4 or 5 chips wide purely because JS did or didn't push one (LAW 1). Every
     chip is a flex:1 column so N chips always fill the card edge to edge, and min-height:56
     keeps the whole tile a touch target even on the one- or two-line labels ("Last job",
     "Set address") that wrap rather than truncate. */
  .qkrow{display:flex;gap:7px;padding-bottom:2px}
  .qk{display:flex;flex:1 1 0;flex-direction:column;align-items:center;justify-content:center;gap:5px;min-height:56px;min-width:0;border:1px solid var(--border);background:var(--scr);border-radius:var(--r-card);padding:9px 4px;font-size:10.5px;font-weight:800;cursor:pointer;color:var(--text)}
  .qk svg{width:18px;height:18px;flex:none;color:var(--brand)}
  .qk span{max-width:100%;text-align:center;line-height:1.15;overflow-wrap:break-word}
  .qk.set{border-style:dashed;color:var(--tmut)}
  /* places — rows inside the search sheet (the strip is retired) */
  /* min-height:56px JOINED HERE 2026-08-23 (EVAL-LEDGER `touch-target`, 7th occurrence sweep).
     Both rows measured 35px — the general .kc chip's size, which is right for a filter and
     wrong for a control that is the whole answer to a question. #navPick's three chips are the
     nav-app choice in the gear sheet and #onbNav's are the same choice on onboarding step 3;
     they are one control rendered twice, so they move together or the smaller one is the one a
     gloved thumb finds. The precedent is .rateedit.wide .kindchips .kc, which took this exact
     floor for this exact reason. Step 3 already scrolls at 375x407 and carries the cue for it
     (laws-v2 LAW 19), so the extra 21px is a longer column, never a hidden primary action. */
  #navPick .kc,#onbNav .kc{display:inline-flex;align-items:center;gap:7px;padding:9px 13px;white-space:nowrap;min-height:56px}
  #navPick .kc svg,#onbNav .kc svg{width:16px;height:16px;flex:none;display:block}
  .pcat{font-size:8px;font-weight:800;background:rgba(201,131,47,.12);color:#a06322;border-radius:var(--r-ctrl);padding:2px 6px;letter-spacing:.05em;vertical-align:1px;white-space:nowrap}
  .plgo{margin-left:auto;flex:none}
  .plgo svg{width:17px;height:17px;display:block}
  .pledit{flex:none;border:0;background:none;color:var(--tfaint);cursor:pointer;padding:2px 0 2px 6px}
  .pledit svg{width:14px;height:14px;display:block}
  .plmaps{display:flex;gap:10px;justify-content:center;margin:12px 0 4px}
  /* f-2 census kill #22: the chip carries its own words now (icon + "Directions — Google
     Maps"), so it sizes to the text instead of the old fixed 44x44 icon-only square. */
  /* 56 (f-14, item 9). #plNav measured 192x44 and it is the only road from the place editor to
     the nav app. SCOPED to .plmaps, deliberately: the bare .mapchip rule also dresses a 30x30
     .ic variant elsewhere, and widening it would grow a control this item never measured. */
  .plmaps .mapchip{display:inline-flex;align-items:center;gap:8px;min-height:56px;padding:0 16px}
  .plmaps .mapchip svg{width:20px;height:20px;flex:none}
  .pladdr{text-align:center;font-size:12px;font-weight:700;color:var(--tmut);margin-top:2px}
  .plnamero{text-align:center;font-size:15px;font-weight:800;margin-top:4px}
  /* time-off calendar */
  .cal{border:1px solid var(--border);background:var(--card);border-radius:var(--r-card);padding:10px 10px 8px;margin-top:2px}
  .calh{display:flex;align-items:center;justify-content:space-between;margin-bottom:6px}
  .calh b{font-size:12.5px;font-weight:800}
  .calnav{width:30px;height:30px;border-radius:var(--r-ctrl);border:1px solid var(--border);background:var(--scr);font-size:15px;font-weight:800;color:var(--tmut);cursor:pointer}
  .calg{display:grid;grid-template-columns:repeat(7,1fr);gap:2px}
  .calg.dow span{text-align:center;font-size:8.5px;font-weight:800;letter-spacing:.06em;color:var(--tfaint);padding:2px 0}
  .cd{height:34px;display:flex;align-items:center;justify-content:center;border-radius:var(--r-ctrl);font-size:12px;font-weight:700;color:var(--text);cursor:pointer}
  .cd.out{visibility:hidden}
  .cd.past,.cd.wknd{color:var(--tfaint);cursor:default;font-weight:600}
  .cd.wknd{background:var(--scr)}
  .cd.today{box-shadow:inset 0 0 0 1.5px var(--strong)}
  .cd.in{background:rgba(125,186,61,.18)}
  .cd.sel{background:var(--brand);color:#fff;font-weight:800}
  .calsum{margin-top:7px;text-align:center;font-size:10.5px;font-weight:800;letter-spacing:.04em;color:var(--tmut)}
  /* time-off wheels — ONE card, true Apple picker: 5 rows, edges fade to nothing,
     a soft grey pill marks the selection. NO hairlines anywhere (Wes). */
  .whlbox2{position:relative;display:flex;align-items:stretch;border:1px solid var(--border);border-radius:var(--r-card);background:var(--card);margin:8px 0 2px;overflow:hidden}
  .whlbox2:before{content:'';position:absolute;left:8px;right:8px;top:72px;height:36px;background:var(--scr);border-radius:var(--r-card)}
  .whlgap{flex:none;width:16px;display:flex;justify-content:center;color:var(--tfaint);font-weight:800;padding-top:81px;font-size:13px;position:relative}
  .whl{flex:1;height:180px;overflow-y:auto;scroll-snap-type:y mandatory;scrollbar-width:none;position:relative;
    -webkit-mask-image:linear-gradient(transparent,#000 26%,#000 74%,transparent);
    mask-image:linear-gradient(transparent,#000 26%,#000 74%,transparent)}
  .whl::-webkit-scrollbar{display:none}
  .whl>div{height:36px;display:flex;align-items:center;justify-content:center;scroll-snap-align:center;font-size:13px;font-weight:600;color:var(--tmut)}
  .whl>div.on{color:var(--text);font-weight:800;font-size:14px}
  .whl>div.pad{pointer-events:none}
  /* ---- BOOT SPLASH — the bobcat grades the lot; its finished strip IS the
     loading bar. Plays for humans on load; navigator.webdriver and #noboot skip
     it entirely so test runs and shoots never pay the 4 seconds. ---- */
  #bootScr{position:absolute;inset:0;z-index:60;overflow:hidden;color:#fff;
    background:linear-gradient(160deg,#2f6a3a,#24512c);
    display:flex;flex-direction:column;align-items:center;justify-content:center}
  #bootScr .bwrap{display:flex;flex-direction:column;align-items:center;gap:14px;transform:translateY(-46px)}
  /* Wes's REAL logo, animated: his skyline traced into strokes (geometry lifted
     from the logo file) so it still builds itself, then a crossfade into the
     actual PNG — the settled frame is his art. The logo lives ONLY on this boot
     screen, never in the app chrome (Wes's call). */
  #bootScr .blogo{position:relative;width:92px}
  #bootScr .blogo .realogo{width:100%;display:block;opacity:0;animation:btup .45s 1.5s ease-out forwards}
  #bootScr .blogo .trsky{position:absolute;inset:0;width:100%;height:100%;animation:bttrout .3s 1.55s ease forwards}
  #bootScr .trsky path{fill:none;stroke-width:16;stroke-linejoin:miter;
    transform:scaleY(0);transform-box:fill-box;transform-origin:bottom;
    animation:btrise .5s cubic-bezier(.34,1.5,.64,1) forwards}
  #bootScr .trsky .tb{stroke:#e6e8ea}
  #bootScr .trsky .tg{stroke:#7DBA3D}
  @keyframes btrise{to{transform:scaleY(1)}}
  @keyframes bttrout{to{opacity:0}}
  #bootScr .bword{display:flex;gap:9px;font-size:20px;font-weight:800;letter-spacing:.14em;opacity:0;transform:translateY(9px);animation:btup .5s 1.75s ease-out forwards}
  #bootScr .bword b{color:var(--lime);font-weight:800}
  #bootScr .bsub{font-size:9.5px;font-weight:800;letter-spacing:.34em;color:rgba(255,255,255,.55);opacity:0;animation:btup .5s 1.25s ease-out forwards}
  @keyframes btup{to{opacity:1;transform:translateY(0)}}
  /* status microcopy — JS feeds one span from the phrase pool (see BOOTSAY) and
     re-pops the fade on each swap. Loops forever on a slow load; the intro never
     replays. Only the bobcat + these words move after the first second. */
  #bootScr .bstat{position:absolute;bottom:172px;left:0;right:0;text-align:center;font-size:10.5px;font-weight:800;letter-spacing:.16em;color:rgba(255,255,255,.62);height:16px}
  #bootScr .bstat span{display:block;opacity:0;animation:btst 1.05s both}
  @keyframes btst{0%{opacity:0;transform:translateY(5px)}18%,80%{opacity:1;transform:none}100%{opacity:0;transform:none}}
  #bootScr .bsite{position:absolute;left:0;right:0;bottom:96px;height:86px}
  #bootScr .brough{position:absolute;left:26px;right:26px;bottom:18px;height:8px;opacity:.5;
    background:
      radial-gradient(4px 4px at 8px 6px, rgba(255,255,255,.8) 42%, transparent 46%),
      radial-gradient(3px 3px at 24px 3px, rgba(255,255,255,.6) 42%, transparent 46%),
      radial-gradient(4px 3px at 41px 6px, rgba(255,255,255,.7) 42%, transparent 46%);
    background-size:52px 8px;background-repeat:repeat-x}
  #bootScr .bgrade{position:absolute;left:26px;bottom:20px;height:4px;width:0;border-radius:var(--r-micro);
    background:linear-gradient(90deg,rgba(125,186,61,.55),var(--lime));
    box-shadow:0 0 10px rgba(125,186,61,.55);
    animation:btgrade 2.07s .95s linear forwards} /* synced: strip ends at the rear wheel */
  @keyframes btgrade{to{width:calc(100% - 52px)}}
  /* the drive is INFINITE — a slow load means more passes, never a replayed intro.
     Both ends of the pass are off-screen, so the loop jump is invisible. The graded
     strip fills once (forwards) and stays — finished work doesn't un-grade itself. */
  #bootScr .bcat{position:absolute;bottom:14px;left:-96px;width:96px;animation:btdrive 3.1s .3s linear infinite}
  @keyframes btdrive{to{transform:translateX(calc(398px + 118px))}}
  #bootScr .brig{animation:btbob .34s linear infinite}
  @keyframes btbob{0%,100%{transform:translateY(0)}25%{transform:translateY(-1.1px)}75%{transform:translateY(.7px)}}
  #bootScr .bcat svg{width:96px;height:70px;overflow:visible;display:block}
  #bootScr .bwhl{transform-box:fill-box;transform-origin:center;animation:btspin .5s linear infinite}
  @keyframes btspin{to{transform:rotate(360deg)}}
  #bootScr .bpuff{opacity:0;transform-box:fill-box;transform-origin:center;animation:btpuff 1s linear infinite}
  #bootScr .bp2{animation-delay:.33s}#bootScr .bp3{animation-delay:.66s}
  @keyframes btpuff{0%{opacity:0;transform:translate(0,0) scale(.5)}18%{opacity:.7}100%{opacity:0;transform:translate(-9px,-16px) scale(1.5)}}
  #bootScr .bdust{opacity:0;animation:btdust .42s linear infinite}
  #bootScr .bd2{animation-delay:.21s}
  @keyframes btdust{0%{opacity:0;transform:translate(0,0)}30%{opacity:.55}100%{opacity:0;transform:translate(7px,-7px)}}
  /* ---- Fern (agent) page ---- */
  .agctx{display:inline-flex;align-items:center;gap:7px;border:1px solid var(--border);background:var(--card);border-radius:999px;padding:7px 13px;font-size:10.5px;font-weight:800;color:var(--tmut);margin:2px 0 4px}
  .agctx i{width:7px;height:7px;border-radius:50%;background:var(--lime);box-shadow:0 0 6px rgba(125,186,61,.8);flex:none;animation:dotbreathe 4s ease-in-out infinite}
  @keyframes dotbreathe{0%,100%{opacity:.55}50%{opacity:1}}
  /* overflow-wrap: a 600-character paste with no spaces in it used to run straight out of its
     own rounded right edge onto the page background (eval fern-pro r1). #agIn now carries the
     server's 500-character cap too, so the paste is refused before it is sent -- this is the
     half that covers everything already in a thread. */
  .agb{max-width:86%;border-radius:var(--r-card);padding:10px 13px;margin-top:9px;font-size:12.5px;line-height:1.5;overflow-wrap:anywhere;word-break:break-word}
  .agb.bot{background:var(--card);border:1px solid var(--border);border-bottom-left-radius:5px}
  .agb.me{background:var(--brand);color:#fff;margin-left:auto;border-bottom-right-radius:5px}
  .agb .an{font-size:9px;font-weight:800;letter-spacing:.08em;color:var(--tfaint);margin-bottom:3px}
  .agb b{font-weight:800}
  /* MESSAGES LAND — one motion language for the greeting and every answer (fern-alive wave,
     2026-08-22). paintThread() rebuilds every bubble on each push; views-fern.js tags ONLY the
     newest one with .arrive, or every old message in the thread would re-animate on every turn. */
  .agb.arrive{animation:riseIn var(--fern-settle) var(--fern-curve)}
  @keyframes riseIn{from{opacity:0;transform:translateY(8px) scale(.98)}to{opacity:1;transform:none}}
  /* THE PRE-CAP BUDGET NOTE. It was .subnote plus an inline style override: 10.5px --tfaint,
     2.59:1 on white, and the palest text in the frame -- on the ONLY warning a company gets
     before a hard spend stop (eval fern-pro r1, `palette-drift`). Its own class now, at the
     app's contrast floor, left-aligned because it sits inside a bubble and not under a form. */
  .agnote{color:var(--tmut);font-size:11px;line-height:1.45;margin:8px 0 0;text-align:left}
  .agsrc{display:flex;gap:6px;flex-wrap:wrap;margin-top:8px}
  /* `font: <weight> <size> inherit` IS NOT A LEGAL FONT SHORTHAND -- a CSS-wide keyword
     cannot appear as one component of one -- so the whole declaration was DROPPED by every
     browser and this control rendered in the UA button default, 13.333px/400 Arial, a
     typeface that appears nowhere in this design system (eval fern-pro r1, `touch-target`,
     four sites, all four fixed in one pass). The house pattern is `.cat`'s: `font:inherit`
     ALONE is legal, then the longhands on top of it. */
  /* AND THE TARGET. This is the provenance road -- the button that takes somebody from an
     answer to the screen the answer came off -- and it measured 97x25 beside 56px siblings
     30px below it. min-height, never height: a label that wraps grows instead of clipping. */
  /* 56 (f-14, item 9). These are the citation buttons under an answer — the ONLY road from a
     sentence Fern just said to the screen that proves it — and they were the smallest control
     in the thread. */
  .agsrc button{display:inline-flex;align-items:center;gap:6px;min-height:56px;border:1px solid var(--border);background:var(--scr);border-radius:var(--r-ctrl);padding:4px 12px;font:inherit;font-weight:800;font-size:11px;color:var(--tmut);cursor:pointer}
  .agsrc svg{width:13px;height:13px;flex:none}
  .agsrc.settle button{opacity:0;transform:translateY(5px);animation:riseIn .45s var(--fern-curve) forwards}
  /* THE TYPING BUBBLE — restructured to match the ported rhythm: an outer pill that POPS in and
     hugs its dots (fit-content, no padding of its own), wrapping the dot row AND the honest
     stage line. Was a flat .agb.bot.agtyp with three <i> as direct children; the mockup's
     .agb.bot.typing > .agtyp > i / .agstage shape is what riseIn/typpop and the stage swap are
     built against, so the real markup now matches it (views-fern.js ask()). */
  .agb.typing{transform-origin:0 100%;animation:typpop .4s var(--fern-curve);width:-moz-fit-content;width:fit-content;padding:0}
  @keyframes typpop{from{opacity:0;transform:translateY(6px) scale(.92)}}
  .agtyp{display:inline-flex;gap:5px;padding:12px 14px;align-items:flex-end}
  .agtyp i{width:7px;height:7px;border-radius:50%;background:var(--tfaint);animation:agty var(--fern-typing) ease-in-out infinite}
  .agtyp i:nth-child(2){animation-delay:.16s}.agtyp i:nth-child(3){animation-delay:.32s}
  /* the wave rolls, then rests a beat — the rhythm of someone actually typing */
  @keyframes agty{0%,60%,100%{opacity:.35;transform:translateY(0) scale(1);background:var(--tfaint)}30%{opacity:1;transform:translateY(-4px) scale(1.25);background:var(--tmut)}}
  /* honest thinking: the stage line under the dots, swapped on the REAL elapsed request time
     (views-fern.js ask()) — never a fixed shimmer, never a spinner percent. */
  /* --tmut, NOT --tfaint. This is the ONLY feedback a person gets during a multi-second wait
     and it rendered at 2.59:1 on white -- the app's tertiary/placeholder token, used exactly as
     designed; the finding was which content had been given it (eval fern-pro r1,
     `palette-drift`). laws-v2 LAW 19 holds this app's text to 4.4:1 and states why; --tmut on
     white measures 4.89. */
  .agstage{font-size:10.5px;font-weight:700;color:var(--tmut);padding:0 14px 10px;letter-spacing:.02em}
  /* THE SUGGESTION CHIPS, AT THE FLOOR (TASKS 20(c); eval f-2, `touch-target`). Measured at
     33px tall on the real screen at 390 AND 375 — the composer's own controls two rows below
     them are 56, which is this app's floor for anything a gloved hand taps. min-height, not
     height: a chip whose label wraps grows instead of clipping. The row already wraps
     (flex-wrap), so the taller box costs vertical space and never horizontal overflow.

     2026-09-02 — WES ASKED FOR THESE AT "HALF TO A THIRD" OF THEIR SIZE, AND THE HEIGHT DID
     NOT MOVE. Half of 56 is 28, which is under the 44px this app has already twice been
     caught below and once fixed FROM (33px, right here). The ledger's `touch-target` check
     names 56-64 as the floor for a gloved hand, so shrinking the box would amend a standing
     check rather than tune a style. THE BLOCK SHRANK INSTEAD, AND BY MORE THAN HE ASKED:
     views-fern paints at most TWO chips now instead of six, so the strip goes from wrapping
     three or four rows (~190-250px) to one or two (~56-119px). Padding is trimmed 15->13 and
     the gap 7->6 so two chips share one row more often, which is the rest of the height.
     If the drawn box should get smaller too, that is Wes's call to make against the ledger,
     not a thing to slip in under a padding change. */
  /* OUT OF POCKET. The figure is NEUTRAL INK on purpose: red is safety only (LAW 21) and brand
     green is the primary CTA and the active state (`green-overload`). Money somebody is owed is
     neither an alarm nor an action — it is a fact, and it reads like one. */
  .oopnum{font-size:26px;font-weight:800;color:var(--text);font-variant-numeric:tabular-nums;line-height:1.15}
  .oopsub{font-size:11.5px;color:var(--tmut);margin-top:3px;line-height:1.45}

  /* ==== REIMBURSEMENT, THE OWNER'S HALF (2026-09-02) ======================================
     The Owed screen (#v-owed), the settle sheet (#stlovl), the crew's own list (#oopovl) and
     the "own money" tags every receipt surface now wears.

     NO NEW COLOUR ANYWHERE IN THIS BLOCK (`palette-drift`). The tags reuse .cat2's neutral
     chip; the two refusal boxes (.setnote/.setroad, .ownote) reuse the AMBER family this file
     already spends on "we could not reach it" — rgba(224,150,60,…) fills and borders with
     --copper-ink carrying the words, exactly as noteHtml('warn') does — never red, which LAW 21
     keeps for safety; every money figure is NEUTRAL INK, because money somebody is owed is
     neither an alarm nor an action.

     THE GREENS ON THESE TWO SURFACES, COUNTED (`green-overload`): #v-owed has ZERO in every
     state that carries data — it is a list of facts and there is no primary action on it. The
     ONE exception is the FAILURE state, where the Retry button is a real primary action and
     wears .ghost's --brand ink; that is the rule being followed, not broken, and S8 counts it
     in both states so the exception cannot quietly grow a second member. The settle sheet has
     exactly the two the rule allows: #stlSave (.submit, --lime, the primary action) and the
     ACTIVE states — the selected method chip (.wc.on) and the ticked rows, which is the same
     language .wc.on already speaks on #csovl. */

  /* A PERSON'S ROW ON THE OWED SCREEN — a door to the settle sheet, so it takes the 56px floor
     with room to spare (`touch-target`; measured, not read, in test/reimburse-ui.cjs). */
  .owrow,.oprow,.oporow{display:flex;align-items:center;gap:11px;width:100%;min-height:64px;
     padding:12px 13px;margin-top:8px;border:1px solid var(--border);border-radius:var(--r-card);
     background:var(--card);text-align:left;font:inherit;color:var(--text)}
  .owrow{cursor:pointer}
  .oprow,.oporow{flex-wrap:wrap}
  .owl,.opl{flex:1 1 auto;min-width:0;display:flex;flex-direction:column;gap:2px}
  /* overflow-wrap:anywhere, not a truncation: a 40-character name is a real name, and a row
     that hides half of it is a row an owner cannot tell from another person's. */
  .own{font-size:14px;font-weight:800;color:var(--text);line-height:1.3;overflow-wrap:anywhere}
  .ows{font-size:11px;font-weight:700;color:var(--tmut);line-height:1.45;overflow-wrap:anywhere}
  .opnote{font-size:11.5px;color:var(--tmut);line-height:1.45;margin-top:2px;overflow-wrap:anywhere}
  /* tabular-nums + nowrap so $148,392.17 and $6.00 line up and neither one wraps mid-figure. */
  .owamt{flex:none;white-space:nowrap;font-size:15px;font-weight:800;color:var(--text);
     font-variant-numeric:tabular-nums}
  .owchev{flex:none;color:var(--tfaint);font-size:16px}
  /* The chevron on Home's card — the same glyph, weight and ink every other door in this app
     wears, pushed to the end of the .hh header row (which is already display:flex). */
  .owdoor{margin-left:auto;color:var(--tfaint);font-size:16px;line-height:1}
  /* UNDO IS NOT A CTA. .ghost draws its label in --brand, which on this screen would be the
     only green and would read as the thing to do — it is the opposite: the rare correction to
     a record of money that really moved. Muted until it is armed, then copper, which is what
     the rest of this app spends on "read this before you tap again". */
  /* .ghost is a 99px pill sized for a 10.5px label; at the 56px floor with "Undo" in it that
     draws a CIRCLE, which reads as an icon button rather than as a word. Same fix the method
     chips took two blocks down: the house card radius and a real gutter, no new family. */
  .opundo{flex:none;min-height:56px;padding:8px 16px;border-radius:var(--r-card);
     color:var(--tmut);border-color:var(--strong)}
  /* …AND IT HAS TO OUT-SPECIFY `.relink,.ghost{min-height:44px}` AT THE BOTTOM OF THIS FILE.
     The rule above says 56 and lost anyway: same specificity, and that one is declared later, so
     the Undo control MEASURED 44px on a real screen while the stylesheet read as if it were 56.
     This is the `touch-target` shape the ledger names in its fifth instance — a floor stated on
     one class, silently overruled by the family the element also wears — and it is only ever
     caught by measuring the rendered box, which is what test/reimburse-ui.cjs S7 does.
     THE RETRY BUTTON IN THE FAILURE STATE IS THE SIBLING wearing the same `.ghost`, so it moves
     with it rather than being left at 44 for somebody to find next time.

     …AND IT NEEDS THE WHOLE SHAPE, NOT JUST THE HEIGHT (fixed 2026-09-03, `fixed-one-of-N`).
     The first cut gave Undo a floor, a card radius and a gutter, and gave Retry only the floor —
     so `.ghost`'s 99px radius and 5px/11px padding drew it as a 53x56 GREEN CIRCLE on the one
     screen a person reaches when the read has failed. The two controls are the same family and
     take the same box; scoping them together is what stops the next one being half-fixed. */
  #owedPaid .opundo,#owedList .ghost{min-height:56px;border-radius:var(--r-card);padding:8px 16px}
  .opundo.armed{color:var(--copper-ink);border-color:var(--copper);background:rgba(201,131,47,.10)}

  /* ---- THE SETTLE SHEET ------------------------------------------------------------------
     THE COLUMN, and it is the shape rather than the wording that keeps Save and the way out on
     the glass at 375x407 (`keyboard-fold`): one shrinkable region, everything else flex:none.
     #stlovl .adjscroll is in DEL_CUT_REGIONS, so the cue is gated on the region's OWN overflow
     and never on a viewport height. */
  #stlovl .sheet{display:flex;flex-direction:column}
  #stlovl .adjhd,#stlovl .submit,#stlovl .adjout{flex:none}
  #stlovl .adjscroll{flex:0 1 auto;min-height:0;overflow-y:auto;overscroll-behavior:contain}
  /* THE METHOD CHIPS. `.wc` is a 99px pill built for #csovl, where every chip carries an icon
     and two or three words — at 56px tall with a ONE-WORD label ("Cash") that radius draws a
     circle, which reads as a toggle dot rather than as a word you pick. The house card radius
     and a wider gutter make them read as the five buttons they are. Same family, same colours,
     same active state: a radius and a padding, not a new chip. Caught by opening the screenshot
     rather than by an assertion, which is the point of opening them. */
  #stlovl .whychips .wc{min-height:56px;border-radius:var(--r-card);padding:8px 16px;font-size:12px;
     flex:1 1 auto;justify-content:center}
  /* LAW 21 — THE WAY OUT IS NOT RED. .adjout paints its label in var(--red) because on the
     adjust sheet it ends somebody's paid day; here it is "Not now" on a sheet that has written
     nothing at all. Same box, muted ink — exactly what #csCancel had to learn. */
  #stlCancel{color:var(--tmut)}
  /* The refusal line. Copper, never red (LAW 21): nothing is lost here — somebody is being told
     the order of two steps, or which receipt moved underneath them. Without this it inherited
     .empcap's muted grey and a refusal read like a caption, which is the same rule #csSay had
     to be given on the create-shift sheet. */
  #stlSay{color:var(--copper-ink);font-weight:700}
  /* The live total. `.adjhd .adjtot` draws in --brand; this figure is money about to leave a
     bank account, which is neither an action nor an alarm, so it is neutral ink like every
     other money figure in this block.
     THE SELECTOR IS THE FIX, not the class. A bare `.stlnum{color:var(--text)}` LOSES to
     `.adjhd .adjtot` on specificity (0,1,0 against 0,2,0), so the class was there, the intent
     was there, and the rendered figure was brand green anyway — caught by counting greens by
     COMPUTED COLOUR rather than by reading the stylesheet (`green-overload`, and exactly what
     that check exists for). Scoped to #stlovl so no other sheet's total moves. */
  #stlovl .adjhd .adjtot.stlnum{color:var(--text)}
  /* THE WHOLE ROW TOGGLES — the box is decoration (`touch-target`: the small thing is always
     the entry point to the thing that was made big). */
  .stlrow{display:flex;align-items:center;gap:11px;width:100%;min-height:56px;padding:9px 11px;
     margin-top:7px;border:1px solid var(--border);border-radius:var(--r-card);background:var(--card);
     text-align:left;font:inherit;color:var(--text);cursor:pointer}
  /* The empty list — see paintSettle. Muted, same size as a row's meta line: it is a statement
     of fact under a heading, not a warning. */
  .stlnone{margin-top:7px;padding:9px 2px;color:var(--tmut);font-size:11.5px;font-weight:700}
  .stlbox{flex:none;width:26px;height:26px;border-radius:8px;border:1px solid var(--strong);
     background:var(--scr);display:flex;align-items:center;justify-content:center;color:#fff}
  .stlbox svg{width:15px;height:15px}
  /* TICKED IS AN ACTIVE STATE, which is what brand green is for — the same rule .wc.on follows
     two rows below it on this same sheet. */
  .stlrow.on{border-color:rgba(47,106,58,.45)}
  .stlrow.on .stlbox{background:var(--brand);border-color:var(--brand)}
  .stlt{flex:1 1 auto;min-width:0;display:flex;flex-direction:column;gap:2px}
  .stlv{font-size:13.5px;font-weight:800;color:var(--text);overflow-wrap:anywhere}
  .stlm{font-size:10.5px;font-weight:700;color:var(--tmut);overflow-wrap:anywhere}
  .stla{flex:none;white-space:nowrap;font-size:14px;font-weight:800;color:var(--text);
     font-variant-numeric:tabular-nums}
  #oopovl .sheet{display:flex;flex-direction:column}
  #oopovl .adjout{flex:none;color:var(--tmut)}
  #oopovl .adjscroll{flex:0 1 auto;min-height:0;overflow-y:auto;overscroll-behavior:contain}

  /* ---- WHOSE MONEY BOUGHT IT, on every surface that lists receipts -----------------------
     .cat2's box exactly — the neutral chip family this app already wears. A company row carries
     no tag at all, so "Own money" is the only thing on a row that is saying anything. */
  .ptag{font-size:9px;font-weight:800;border-radius:var(--r-ctrl);padding:2.5px 7px;
     border:1px solid var(--border);background:var(--scr);color:var(--tmut);white-space:nowrap}
  .rtags{display:flex;flex-wrap:wrap;gap:5px;margin-top:6px}
  /* The sentence a person reads before approving. A FACT, not a warning: neutral, because the
     thing it describes is ordinary and copper here would make every own-pocket receipt look
     like a problem. */
  .ownote{margin-top:8px;padding:8px 10px;border-radius:10px;background:var(--scr);
     border:1px solid var(--border);color:var(--tmut);font-size:11px;font-weight:700;line-height:1.45}
  /* ...and the refusal, which IS one. Copper, never red (LAW 21): nothing is lost, somebody is
     being told the order of two steps. The road under it is a full-width 56px control, because
     it is the whole point of the message. */
  .setnote{margin-top:9px;padding:9px 11px;border-radius:10px;background:rgba(224,150,60,.12);
     border:1px solid rgba(224,150,60,.45);color:var(--copper-ink);font-size:11px;font-weight:700;
     line-height:1.5}
  .setsay{display:block}
  .setroad{display:flex;align-items:center;justify-content:center;width:100%;min-height:56px;
     margin-top:8px;border:1px solid rgba(224,150,60,.45);border-radius:var(--r-card);
     background:var(--card);color:var(--copper-ink);font:inherit;font-weight:800;font-size:12px;
     cursor:pointer;text-align:center}
  .agchips{display:flex;gap:6px;flex-wrap:wrap;margin-top:10px}
  .agchips button{display:inline-flex;align-items:center;min-height:56px;border:1px solid var(--border);background:var(--card);border-radius:999px;padding:8px 13px;font:inherit;font-weight:700;font-size:11.5px;color:var(--text);cursor:pointer;text-align:left}
  .agchips button.settle{opacity:0;transform:translateY(6px);animation:riseIn var(--fern-settle) var(--fern-curve) forwards}
  .agbar{position:sticky;bottom:0;display:flex;gap:8px;align-items:center;background:var(--scr);padding:10px 0 4px;margin-top:14px}
  /* THE COMPOSER'S CONTROLS, AT THE HOUSE FLOOR (2026-08-18, with the import "+").
     They were 40x40 with a 44px min-height bolted on further down this file — i.e. 40x44,
     under the 56-64px this app's own ledger has been holding every gloved-hand control to
     since 2026-08-15. The "+" arrived specced at 56, and shipping it beside two 44px siblings
     would have been the exact defect that check names in its fifth instance: "when a floor
     moves onto ONE class, measure every sibling wearing the old one." So the floor moves onto
     the bar, not onto the newcomer.
     THE MIC LEFT THIS RULE at f-2 (2026-08-21) — removed from the app, not shrunk. The floor
     did not move with it: the two that remain are still 56x56 and the box between them is
     still 56 tall. What changed is the width the box gets back, and fern-import measures it
     rather than this comment asserting it. */
  .agplus,.agsend{width:56px;height:56px;border-radius:var(--r-card);border:1px solid var(--border);background:var(--card);color:var(--brand);display:flex;align-items:center;justify-content:center;cursor:pointer;flex:none}
  /* AND THE BOX BETWEEN THEM (eval 2026-08-18, `touch-target`). The paragraph above quotes the
     rule — "when a floor moves onto ONE class, measure every sibling wearing the old one" —
     and then moved three BUTTONS to 56 while the thing a gloved thumb has to hit to start
     typing stayed at .inp's 46px, a 10px mismatch on one row and the only control on the bar
     that was not re-measured. It was scoped to .agbar so the app's other inputs would keep
     .inp's 46px; .inp IS 56px now, on the base class, so the scoped copy is deleted. */
  .agplus svg,.agsend svg{width:19px;height:19px}
  .agsend{background:var(--brand);border-color:var(--brand);color:#fff}
  /* The "+" stays the OUTLINE treatment the mic wears, not the filled brand green of send.
     Green is the primary action and the active state in this app and nothing else (Wes's
     standing rule): sending the message is the bar's primary action, and a second filled
     green beside it would be two primaries on one row. */
  /* THE VOICE STYLES ARE GONE (f-2, 2026-08-21): .agmic + .agmic.rec, @micpulse, .agwave and
     its five bars, @agwv. The mic they dressed is out of the app — iOS Safari has no
     SpeechRecognition, so it was already absent on most of the crew's phones. Removed here in
     the same commit as the markup so no rule outlives the element it styled. */

  /* SEND <-> STOP (fern-alive wave). A real answer is a network round trip with no fixed
     length, so the button that started it is the honest way to end it early — the swap is a
     LABEL CHANGE on the same 56x56 control, never a second button. */
  .agsend .halt{display:none}
  .agsend.stop .go{display:none}
  .agsend.stop .halt{display:block}
  .agsend.stop{background:var(--card);border-color:var(--strong);color:var(--tmut)}

  /* ==================== FERN PRESENCE (2026-08-22, the fern-alive wave) ====================
     Ported from the Claude Design motion mockup fern-alive.html onto the real Fern tab. The
     frond is the nav mark's exact paths (index.html #navAgent), split per-leaflet so it can
     trail — #fernMark sits in a .fernrow beside #agCtx, static markup in index.html (JS never
     rebuilds it), so its animations run exactly once per page load.

     WHY SWAY/REST LIVE ON A SEPARATE WRAPPER (.fsway), NOT ON THE <svg> ITSELF — a measured
     defect in the mockup, not carried forward: the mockup put fernsway/fernrest and fernlean
     (the thinking lean) on the SAME element's `animation` property. Toggling .thinking on and
     off reassigns that property, and a browser treats a reassigned `animation` shorthand as a
     NEW animation even when the text is identical — so ending every thinking cycle restarted
     the ×2 wake sway from scratch, forever, instead of the ONE time per session PORT-SPEC.md
     calls for. Splitting sway/rest onto a plain wrapper span (no animation property lean/nod
     ever touch) makes the restart structurally impossible rather than timed around. */
  .fernrow{display:flex;align-items:center;gap:9px}
  .fernmark{width:30px;height:30px;flex:none;position:relative}
  .fernmark .aura{position:absolute;inset:-5px;border-radius:50%;background:radial-gradient(circle,var(--fern-breath),transparent 70%);opacity:0;animation:fernbreathe 4s ease-in-out infinite}
  @keyframes fernbreathe{0%,100%{opacity:0}50%{opacity:1}}
  .fernmark .fsway{display:block;width:30px;height:30px;transform-origin:50% 87.5%;
    animation:fernsway var(--fern-sway) ease-in-out 2 alternate,
              fernrest var(--fern-rest) ease-in-out calc(var(--fern-sway) * 2) infinite alternate}
  @keyframes fernsway{from{transform:rotate(-1.6deg)}to{transform:rotate(1.6deg)}}
  @keyframes fernrest{from{transform:rotate(-.45deg)}to{transform:rotate(.45deg)}}
  .fernmark svg{position:relative;width:30px;height:30px;color:var(--brand);display:block;transform-origin:12px 21px}
  .fernmark svg .lf{transform-origin:12px 21px}
  .fernmark svg .lf.l2{animation:leaftrail var(--fern-sway) ease-in-out .12s 2 alternate}
  .fernmark svg .lf.l3{animation:leaftrail var(--fern-sway) ease-in-out .24s 2 alternate}
  @keyframes leaftrail{from{transform:rotate(-.7deg)}to{transform:rotate(.7deg)}}
  /* thinking: the frond leans toward the thread and holds, for as long as #v-agent carries
     .thinking — a real network wait, not a fixed 300ms theatre. */
  #v-agent.thinking .fernmark svg{animation:fernlean .3s var(--fern-curve) forwards}
  @keyframes fernlean{to{transform:rotate(5deg) translateY(1px)}}
  /* arrival: one nod, added and removed by views-fern.js so it can replay on every answer */
  .fernmark.nod svg{animation:fernnod .38s var(--fern-curve)}
  @keyframes fernnod{40%{transform:rotate(2.5deg) translateY(1.5px)}100%{transform:rotate(0)}}
  /* tab hidden => nothing ticks (visibilitychange, views-fern.js). .agtyp i joins this list —
     the mockup missed it (its own line 166), which left the typing dots animating unwatched. */
  #v-agent.paused .fernmark .fsway,#v-agent.paused .fernmark svg,#v-agent.paused .fernmark .aura,
  #v-agent.paused .agctx i,#v-agent.paused .agtyp i{animation-play-state:paused}

  /* ---- SYSTEM STATES (Wes core set). One slim banner, three tones — amber =
     degraded, green = back & syncing. RED IS RESERVED FOR SAFETY ONLY. The bar
     pushes content down; it never covers anything. ---- */
  /* 56 (f-14, item 23, `touch-target`). #netBar measured 34px and it is the ONLY tap in the app
     that takes an update — the road a person walks after being told there is a new build. It
     PUSHES content rather than covering it, so growing it costs nothing but the strip. */
  .netbar{min-height:56px;display:none;align-items:center;gap:8px;font:800 11px 'Segoe UI',-apple-system,system-ui,sans-serif;padding:9px 14px;color:#7a4d12;background:#fdf2df;border-bottom:1px solid #eedcb2}
  .netbar.on{display:flex}
  .netbar.ok{color:#2f6a3a;background:#e9f4e4;border-color:#cfe6c6}
  .netbar svg{width:14px;height:14px;flex:none}
  .qchip{display:inline-flex;align-items:center;font:800 8.5px 'Segoe UI',-apple-system,system-ui,sans-serif;letter-spacing:.05em;color:#a06322;background:rgba(224,150,60,.14);border:1px solid rgba(224,150,60,.4);border-radius:99px;padding:2px 7px;margin-left:6px}
  /* permission ask = ONE calm card, one live button — never a dead control */
  .permcard{display:flex;flex-direction:column;align-items:center;text-align:center;gap:8px;background:var(--card);border:1px solid var(--border);border-radius:var(--r-card);padding:26px 20px;margin:40px 6px}
  .permcard .pcic{width:44px;height:44px;border-radius:var(--r-card);background:var(--scr);display:flex;align-items:center;justify-content:center;color:var(--brand)}
  .permcard .pcic svg{width:20px;height:20px}
  .permcard b{font-size:14.5px}
  .permcard>span:not(.pcic){color:var(--tmut);font-size:12px;line-height:1.5;max-width:250px}
  /* 50px as shipped: .submit's 15px padding + a 15px line came to 50, which was never measured
     because until now this card's button was an instruction and not a road. It is the primary
     way off a dead end for a person whose camera is refused, in gloves — 56 is the floor
     (`touch-target`, and its sibling below is held to the same one). */
  .permcard .submit{margin-top:8px;min-height:56px}
  /* The card's SECOND road (2026-08-18). `.ghost` on its own is a 10.5px pill built for a card
     heading; sat under a full-width primary it read as a caption, not a control. Same three
     lines as #hmovl .ghost — the established shape for "a real second choice in a stack" — so
     the settings trip is a 56px block under the import, and the one green on this card is the
     button that actually works. */
  #camPerm .ghost{display:block;width:100%;min-height:56px;font-size:15px;border-radius:var(--r-card);
    padding:0 12px;margin-top:10px}
  /* SHORT SCREEN: THE CHROME YIELDS, NO CONTROL DOES — the same discipline, and the same
     breakpoint, as the onboarding column's @media (max-height:620px) block above.
     WHY IT IS NEEDED HERE NOW. This card used to hold one button that was an INSTRUCTION ("open
     phone settings"), so nothing about it was time-critical and nobody measured it. It now holds
     the only working road off a dead end, and at 375x407 that road was UNDER THE FIXED NAV BAR:
     MEASURED, `Choose from your library` bottom 377 against a nav top of 296 — reachable only by
     scrolling a screen that gives no sign there is anything below it.
     WHAT MOVES: 32px of stage margin, 12px of card padding, 10px of icon, 12px across the four
     gaps, 8px of view padding — and at 460 and under, the camera glyph goes entirely, which is
     the same call the onboarding block makes about the logo one screen over ("what stays is the
     wordmark… because that line is the one thing that tells somebody they are at the right
     company's door"). Here what stays is the sentence and the two roads; a picture of a camera
     on a card whose whole subject is that there is no camera is the most expendable thing on it.
     MEASURED at 375x407, `Choose from your library` bottom: 377 → 306 with the trim → 267 with
     the glyph gone, against a nav top of 296. WHAT DOES NOT MOVE: both buttons keep their 56px
     box and their 15px type, and the heading and the sentence keep their size — the same rule
     the onboarding block answers to. The SECOND road still falls below the fold and is reached
     by scrolling, which is the trade that block already made for "‹ Back to sign in". */
  @media (max-height:620px){
    .permcard{margin:8px 6px;padding:14px 16px;gap:5px}
    .permcard .pcic{width:34px;height:34px}
    .permcard .pcic svg{width:16px;height:16px}
    #camPerm .ghost{margin-top:6px}
    #v-cam.content{padding-top:6px}
  }
  @media (max-height:460px){
    .permcard .pcic{display:none}
  }
  .ntfoff{border-color:rgba(224,150,60,.5)!important;background:#fdf6ea!important}
  /* the ONLY two full screens in the app: forced update + access changed */
  .sysfull{position:absolute;inset:0;z-index:60;display:none;flex-direction:column;align-items:center;justify-content:center;text-align:center;padding:34px;background:linear-gradient(160deg,#2f6a3a,#24512c);color:#fff}
  .sysfull.on{display:flex}
  .sysfull .sfmk{color:var(--lime)}
  .sysfull .sfmk svg{width:46px;height:46px}
  .sysfull h6{font-size:20px;font-weight:800;margin:14px 0 0}
  .sysfull .sfsub{color:rgba(255,255,255,.65);font-size:12.5px;line-height:1.55;max-width:260px;margin:10px 0 24px}
  .sysfull .submit{width:min(260px,100%)}
  /* payroll rows */
  .ratebox{display:inline-flex;align-items:center;gap:2px;border:1px solid var(--strong);background:var(--card);border-radius:var(--r-card);padding:7px 9px;font-size:12.5px;font-weight:800;color:var(--tmut)}
  .ratebox input{border:0;outline:0;width:52px;font:800 12.5px 'Segoe UI',-apple-system,system-ui,sans-serif;color:var(--text);background:transparent}
  /* THE FLOOR IS ON THE BASE CLASS (2026-08-24, EVAL-LEDGER `touch-target`, 8th occurrence).
     .ratebtn.signbtn already carried 56 and the strip below already learned that a floor put
     on a PARENT is a floor a lone child does not get — and the same commit that wrote both of
     those sentences then rendered a bare .ratebtn inside the new payroll row (views-team.js
     ovRow / the .prow.payrow surface) at 81x34, two controls in one 58px row, 34px and 58px.
     A per-site floor is a list that has to be extended every time the class is reused, which
     is exactly the shape that produced eight occurrences of one finding. So the number lives
     on .ratebtn itself: every use of this class starts at the house floor for a gloved hand,
     and the two overrides below can only ADD to it. The padding is unchanged — a button
     centres its own label, so the box grows around the words rather than moving them. */
  .ratebtn{margin-left:auto;min-height:56px;border:1px solid var(--border);background:var(--scr);border-radius:var(--r-card);padding:8px 11px;font:800 12.5px 'Segoe UI',-apple-system,system-ui,sans-serif;color:var(--text);cursor:pointer}
  .ratebtn small{font-size:9px;color:var(--tfaint);font-weight:800;margin-left:2px}
  .rated{margin-left:auto;font:800 12.5px 'Segoe UI',-apple-system,system-ui,sans-serif;color:var(--text);padding:8px 2px}
  .rated small{font-size:9px;color:var(--tfaint);font-weight:800;margin-left:2px}
  /* ---- A PAYROLL ROW IS A DOOR (Wes, 2026-08-23: "see and edit a specific persons hours
     worked from the payroll section in office") ------------------------------------------------
     The row opens that person's punches for the pay week the screen is already showing. Two
     rules and no new furniture: the app's existing value-then-chevron shape (the Employees row
     on Office reads "Not answered yet ›"), and the pressed state every other tappable row in
     this app has — .crow, .trow and .srow2 all answer a thumb, and a row that does not read as
     dead under a glove. THE ROW IS ALREADY PAST THE FLOOR at 78px tall (measured at 390 on the
     seeded roster), so there is no min-height here: a second number for a height that is
     already right is how the smaller one quietly wins later. */
  .prow.payrow{cursor:pointer}
  .prow.payrow:active{background:var(--scr)}
  .payschev{margin-left:8px;flex:none;color:var(--tfaint);font-size:17px;line-height:1}
  /* home address: enter → CONFIRM → text + pencil, the time-edit manners */
  .homeline{display:flex;align-items:center;gap:8px;flex-basis:100%;margin:8px 0 2px}
  .homeline .inp{flex:1;margin:0}
  #homeTxt{flex:1;min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;font-size:13px;font-weight:700}
  /* 56 (f-14, item 23, `touch-target`). Measured 36x32 on EVERY role's gear sheet — the pencil
     that opens the home address. .homeline already took the floor beside it, so this was the
     small control next to the big one, which the ledger names as the recurring shape. The
     .coline variant below keeps its own smaller box: it sits inline in a text row, not on the
     row a thumb aims at. */
  .hpen{border:1px solid var(--border);background:var(--scr);border-radius:var(--r-card);width:56px;height:56px;display:flex;align-items:center;justify-content:center;color:var(--tmut);cursor:pointer;flex:none}
  .hpen svg{width:14px;height:14px}
  .hok{border:none;background:var(--brand);color:#fff;border-radius:var(--r-card);padding:0 14px;height:34px;font:800 12.5px 'Segoe UI',-apple-system,system-ui,sans-serif;cursor:pointer;flex:none}
  /* …AND THE ROW IT SITS IN TAKES THE 56px FLOOR TOGETHER (2026-08-23 — EVAL-LEDGER
     `touch-target`, 7th occurrence sweep). .hok was on the shared 44px list at the bottom of
     this file and measured 78x44 beside a 46px input; both of its two instances — #homeOk (the
     home-address confirm in the gear sheet) and #coOk (the company-name confirm on Office) —
     are the commit half of an enter-then-confirm pair, which is the shape where a missed tap
     costs the typing. THE INPUT MOVES WITH IT rather than the button alone: a 56px button beside
     a 46px box is the two-floors-in-one-row shape the .signout note above this file's 44px list
     spells out. min-height, not height, so .hok's own 34px stays the drawn size everywhere it
     is not in a .homeline — which today is nowhere, and tomorrow is somebody else's problem
     solved in advance. */
  .homeline .inp,.homeline .hok{min-height:56px}
  /* company name keeps ONE thin line when set — name + a smaller pencil at the far
     right, no second row (Wes: "reduce the vertical space it takes up") */
  .coline{display:flex;align-items:center;gap:7px;margin-left:auto;flex:1 1 auto;min-width:0;justify-content:flex-end}
  /* THE VALUE TAKES THE ROOM THE ROW HAS (f-14, item 17b). max-width:150px is a number from a
     mockup, not from a layout: a 40-character company name ellipsed at 150px on a 390 screen
     while ~90px of the row sat empty beside it. The flex line grows and the span stops capping
     itself; the ellipsis stays, so a name longer than the ROW still truncates rather than
     pushing the pencil off. */
  .coline span{font-size:12.5px;font-weight:700;max-width:none;min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
  .coline .hpen{width:30px;height:28px;border-radius:var(--r-ctrl)}
  .coline .hpen svg{width:12px;height:12px}
  .rateedit{display:inline-flex;gap:6px;align-items:center;margin-left:auto}
  /* 56px, THE HOUSE FLOOR FOR A GLOVED HAND — the same number #pwOpen, #delOpen, .peracts and
     the auth boxes were all raised to. These two are the ✓ and the ✕ that COMMIT or cancel a
     wage change and a role change, i.e. the buttons at the end of the only in-app road a
     one-owner company has to deleting its account, and they measured 34x34 — the identical
     number the touch-target class was first logged for, on the show-password eye
     (EVAL-LEDGER 2026-08-17, 4th occurrence). The room was measured and the door was not.
     Sized as a flex box rather than a bare width/height so the glyph stays centred at 56. */
  .rc{width:56px;height:56px;display:inline-flex;align-items:center;justify-content:center;
    border-radius:var(--r-card);border:1px solid var(--border);background:var(--card);font-size:16px;font-weight:800;cursor:pointer;color:var(--tmut);flex:none}
  .rc.ok{background:var(--lime);border-color:var(--lime);color:#153313}
  /* The box they sit beside grows with them: a 33px input next to a 56px button is the row
     admitting the two were sized by different rules. min-height only — the padding and the
     type are unchanged. */
  .rateedit .ratebox{min-height:56px}
  /* THE PERSON EDITOR (W1 1.4) — the SAME enter-then-CONFIRM furniture the wage editor
     uses, widened. A wage is 5 characters and fits beside its row; an email address is 30
     and cannot, so .wide drops the editor onto its own full-width line inside the same
     wrapping .prow rather than squeezing the field to nothing. Nothing new is invented:
     .ratebox / .rc / .rc.ok are the payroll editor's own classes. */
  /* flex-wrap + row-gap so the CHIP BLOCK below can claim a whole line and put the ✓/✕ under it.
     row-gap rather than a margin because it only pays out BETWEEN lines: the .ratebox editors
     that share this class stay one line and are untouched by it. */
  .rateedit.wide{display:flex;flex-wrap:wrap;row-gap:9px;flex-basis:100%;margin-left:0;margin-top:9px}
  .ratebox.wide{flex:1;min-width:0}
  .ratebox.wide input{width:100%;flex:1;min-width:0}
  /* THE CHIPS WRAP, AND THEY WRAP ACROSS THE WHOLE CARD. This comment claimed the wrap for
     months and the rule did not implement it — .kindchips is display:flex with no flex-wrap, so
     a chip row too wide for its box simply overflowed. Three chips fitted, so nobody found out.
     MEASURED the moment the fourth (Owner) arrived at 390px: Manager · Foreman · Laborer · Owner
     pushed the ✕ clean off the right edge, i.e. adding a role took away the way OUT of the
     editor. flex-wrap fixed the overflow — and left a second shape behind it, which is what
     flex-basis:100% closes now.
     WHY THE CHIPS GET THE WHOLE LINE. Sharing the line with the two 56px buttons leaves the
     block ~212px of a 390px screen, so at SEVEN chips — the pay-week anchor picker, which is the
     widest set this editor ever draws — the days came out two-wide over FOUR rows, 245px tall,
     with a large empty gutter beside rows 1, 3 and 4 and the ✓/✕ stranded halfway down beside
     row 2 (EVAL-LEDGER 2026-08-17, `touch-target` NIT; shots R3-16 / R3-17). The chips are the
     content of this editor; the confirm pair is its footer. So the chips take the full width and
     the buttons drop underneath, which is the same order the sentence under them already reads
     in. The four-chip role picker gets the same treatment and the same benefit — one row instead
     of two — and every 56px floor is untouched, because nothing here changes a size. */
  .rateedit.wide .kindchips{flex:1;flex-basis:100%;min-width:0;margin:0;flex-wrap:wrap;row-gap:7px}
  /* AND THE CHIPS ARE THE SAME SIZE AS THE BUTTONS THAT COMMIT THEM. Measured at 390px they
     were 66-78 wide by 33 TALL, next to a ✓ and a ✕ now at 56 — a role picker whose targets
     were half the house floor, on the road the sole-owner refusal sends a one-owner company
     down. Scoped to THIS editor: .kc is the app's general chip and also carries the break
     picker, the nav picker and the crew picker, which are laid out around their own sizes; the
     class this closes is about the controls on the person page. min-height (not height) so a
     label that wraps in another install's copy grows the chip instead of spilling out of it,
     and the flex box keeps the label centred either way. */
  .rateedit.wide .kindchips .kc{min-height:56px;display:inline-flex;align-items:center;
    justify-content:center;padding:0 18px;font-size:13px}
  .perval{color:var(--tmut);font-size:10px;margin-top:2px;line-height:1.35;overflow-wrap:anywhere}
  .perval a{color:var(--copper);text-decoration:none;font-weight:800}
  /* "no email — can't sign in" and "invited, hasn't signed in yet". Amber, never red: it is
     a state to fix, not a destructive action, and red is reserved for safety in this app. */
  .perval.warn{color:#7a4d12;font-weight:800}
  /* Two actions on a sign-in row go BELOW the sentence, full width, instead of squeezing it
     into a ~170px column where it wraps to six lines. flex-basis:100% makes the parent .prow
     (which already wraps) break the line for us. */
  .peracts{display:flex;gap:8px;flex-basis:100%;margin-top:10px}
  .peracts .ratebtn{margin-left:0;flex:1}
  /* THE FLOOR IS ON THE BUTTON, NOT ON THE STRIP (2026-08-17). It lived on the strip
     (`.peracts .ratebtn{min-height:44px}`) — and views-team.js hands a LONE remediation
     button back to the row instead of the strip, so the same control measured 164x44 beside a
     sibling and 107x34 by itself. The knob that decides how many there are is
     accessCodeEnrollment, i.e. the default install ships the small one, on the ONE road a new
     hire has into the app (EVAL-LEDGER 2026-08-17, `touch-target`, 5th occurrence — and its
     own standing note is "the room was measured and the door was not").
     Sized where a thumb actually lands, so no parent, no sibling count and no config value
     can take it away. 56 is the house floor for a gloved hand — the same number .rc, #pwOpen,
     #delOpen and the auth boxes carry — and the padding grows with it so the label stays
     centred rather than floating in a tall box. Every one of these buttons is built by
     views-team.js signBtn(), which is what makes the class impossible to forget. */
  .ratebtn.signbtn{min-height:56px;padding:10px 14px}
  #bootScr.done{animation:btlift .55s ease-in forwards}
  @keyframes btlift{to{opacity:0;transform:translateY(-16px) scale(1.02);visibility:hidden}}
  @media (prefers-reduced-motion: reduce){
    #bootScr *{animation:none!important}
    #bootScr .trsky{display:none}#bootScr .blogo .realogo{opacity:1}
    #bootScr .bword{opacity:1;transform:none}
    #bootScr .bstat,#bootScr .bsite{display:none}
  }
  /* WRAPS, so the live-work disclosure can sit under the figure it qualifies (f-7 fix round).
     A laborer has no hours pane, so this chip is the only place her screen can say that the
     number includes time she is still working. min-width:0 lets it shrink instead of shoving
     the directions badge off a 375px row; text-align:right keeps both lines on the edge the
     chip is anchored to. */
  .mych{display:flex;flex-wrap:wrap;justify-content:flex-end;text-align:right;align-items:center;gap:2px 7px;margin-top:9px;min-width:0;font-size:11.5px;font-weight:700;color:var(--tmut)}
  .mych b{color:var(--text)}
  .mych svg{width:14px;height:14px;color:var(--brand)}
  /* THE CAPTION FLOOR IS 11.5px AND THE MUTED ARM IS --tmut, HERE TOO (f-7 fix round 3,
     `palette-drift`, the eval's r2 finding). This shipped at 9.5px in --tfaint: measured
     rgb(154,162,170) at 9.5px is 2.37:1 on the card, well under AA's 4.5:1 for small text,
     on the ONE screen a laborer reads outdoors in gloves and the ONE line that explains why
     her number is not the number on her own Home clock.
     THE SITE LIST for "the sentence that makes the figure honest", written here because
     `fixed-one-of-N` says the fix is not done until every site is closed or named:
       * .hvs2  (Home's card)      — closed 2026-08-25 at f-6: 11.5px / --tmut, and its warn
                                     arm takes --copper-ink. test/home-hours.cjs asserts both.
       * .mycs  (the laborer's chip) — this, closed here, on the SAME rule and the same tokens
                                     rather than a second dialect one screen over.
     --tmut #69727b on this card is 4.89:1 — AA. The warn arm is COPPER, never red: LAW 21
     keeps red for safety, and a punch nobody closed is neither damage nor a refusal. It is
     the same ink .hvs2.warn wears for the same sentence. */
  .mycs{flex-basis:100%;font-size:11.5px;font-weight:700;color:var(--tmut);line-height:1.35}
  .mycs.warn{color:var(--copper-ink);font-weight:800}
  /* exports */
  /* .qbchip DELETED 2026-09-04 (sweep, lane 8 F7) with the CONNECTED chip it drew. */
  .xrow{display:flex;gap:8px;margin-top:7px}
  .xbtn{flex:1;border:1px solid var(--strong);background:var(--card);border-radius:var(--r-card);font:inherit;font-weight:800;font-size:11.5px;color:var(--text);padding:11px 8px;cursor:pointer;line-height:1.3}
  .xbtn small{display:block;font-weight:700;font-size:9px;color:var(--tmut);margin-top:2px}
  /* THE EXPORT REFUSAL LIVES IN THE CAPTION (f-14, item 1c). The same copper the rest of this
     app spends on "a human has to look at this" (.subnote.warn, .tmsumc.warn) — not a new
     family for one sentence — and a size a person can actually read it at, because it is an
     instruction rather than a label. */
  .xbtn small.warn{color:var(--copper-ink);font-size:10.5px;font-weight:800}
  /* THE QUICKBOOKS DOOR (2026-09-04 sweep, lane 8 F4, `touch-target`). The Export button
     views-team.js builds into the QuickBooks row is the house .ghost, which takes 44px off the
     shared list at the foot of this file — so it rendered 66x42 beside four .xbtn exports at
     56, and it is the ONLY door to the QBO file. Exactly the shape the standing check names:
     "the SMALL one is always the entry point to the thing that was made big". Scoped to this
     screen and written the same way #tmBody .ghost is, so no other errCard or hh ghost moves.
     Measured in test/team-books-people.cjs. */
  #v-book .ghost{min-height:56px;padding-left:16px;padding-right:16px;border-radius:var(--r-card)}
  /* THE SAME SHAPE ON THE TWO OTHER SCREENS THAT DRAW ONE (f-14, item 9). errCard's Retry is a
     .ghost and measured 53x44 on Team and on Crews — the only control on a screen that has
     just failed to load. */
  #v-emp .ghost,#v-crews .ghost{min-height:56px;padding-left:16px;padding-right:16px;border-radius:var(--r-card)}
  /* THE SAME FLOOR ON HOME (f-13 lane 2 item 1). #toffBtn — "＋ Request", the only door a crew
     member has to asking for a day off — is the house .ghost and rendered 81x44 in a card whose
     neighbours all just went to 56. Scoped by view, exactly like the QuickBooks door above.
     THE FLOOR AND NOTHING ELSE, unlike #v-book's rule, and that is a measured correction rather
     than a style choice: Home carries a SECOND .ghost — the approval queue's Retry, the only way
     out of a failed backlog fetch — and the first cut of this rule also rewrote the radius,
     which turned that pill into a 9px rectangle. job-editor's own "the Retry is drawn as a
     control — real border, PILL radius" assertion is what caught it. A floor is what this round
     is owed; a shape is not.

     …AND THAT CORRECTION WAS HALF RIGHT (eval f-13 r1, `radius-scale`). Leaving the radius on
     the SHARED selector was right; leaving #toffBtn AT the pill radius was not — a 44px pill is
     a chip and an 81x56 pill is an egg, on a card of 9px rectangles. So the radius lands BY ID,
     on the one control the floor moved, and the Retry pill beside it — matched only by
     `#v-home .ghost` — is untouched by construction rather than by care. Both halves measured in
     r4-seven §13 at 390x844 and 375x407.
     …AND THE THIRD HALF, THE LABEL (eval f-13 r2, `component-drift`). The floor took the box to
     56px and left `.ghost`'s 10.5px chip type on it — a full-height control captioned like a
     tag. 14px rather than the 15px the full-width controls wear: this one is an 81px button
     sitting inline beside a 9.5px card caption, and it has to stay on that row. Scoped by id for
     the same reason the radius is — the approval queue's Retry is a small pill on purpose and
     keeps its own type. Measured in r4-seven §13 at both viewports. */
  #v-home .ghost{min-height:56px}
  #toffBtn{border-radius:var(--r-card);font-size:14px}
  .aitem .pchip{margin-left:auto;font-size:9.5px;font-weight:800;border:1px solid var(--border);background:var(--scr);color:var(--tmut);border-radius:var(--r-ctrl);padding:3px 7px;white-space:nowrap}

/* ================= INTEGRATIONS (v-intg) — NOT from the mockup =============================
   The mockup has no Integrations screen; this page is W1 1.2 and its rules are written
   against the existing token set (--card/--border/--brand/--tmut), never new colours.

   .istat is THE honest-status element and its three tones are the point: .on = configured,
   .off = configured-and-empty, .bad = THE LOAD FAILED. The third exists because "couldn't
   reach the server" must never be painted as "not configured" — one is a network problem,
   the other is a statement about this install's security posture, and rendering the first as
   the second tells the owner his connector is off when it may be wide open.

   .ival wraps and breaks anywhere: these are 64 hex characters on a 390px phone, and a value
   that overflows its card is a value that can't be read back to check what was pasted. */
.icard{border:1px solid var(--border);background:var(--card);border-radius:var(--r-card);padding:13px 14px;margin-top:7px}
.icard .pt{font-weight:800;font-size:12.5px}
.icard .ps{color:var(--tmut);font-size:10.5px;margin-top:3px;line-height:1.45}
.icard textarea.inp{margin-top:10px;font-size:13px}

/* The match-quality word on an address suggestion (.acitem). Quiet and on its own line: it is
   a caveat on the row above it, not a second thing to read — and "matched to the county only"
   has to be legible BEFORE the tap, which is the whole reason it is on the row at all. */
.acitem .acprec{display:block;color:var(--tmut);font-size:11px;font-weight:700;line-height:1.2}
.istat{margin-top:10px;font-weight:800;font-size:11px;line-height:1.4;border-radius:var(--r-ctrl);padding:8px 10px;
       background:var(--scr);border:1px solid var(--border);color:var(--tmut)}
.istat.on{background:rgba(47,106,58,.10);border-color:rgba(47,106,58,.24);color:var(--brand)}
.istat.off{background:var(--scr);border-color:var(--border);color:var(--tmut)}
.istat.bad{background:#fdf2df;border-color:#eedcb2;color:#7a4d12}
.istat.wait{color:var(--tfaint)}
.istat:empty{display:none}
.ibtns{display:flex;flex-wrap:wrap;gap:8px;margin-top:10px}
/* 56 (f-14, item 24). Seven buttons on Integrations plus #cfgAdvBtn, all at a WRITTEN 44 while
   every sheet they open is at 56. The two-tier rule is stated at `.relink,.ghost` near the foot
   of this file; these are commits, not ghosts. */
.ibtn{border:1px solid var(--strong);background:var(--card);color:var(--text);border-radius:var(--r-card);
      font:800 11.5px 'Segoe UI',-apple-system,system-ui,sans-serif;padding:10px 13px;cursor:pointer;min-height:56px}
.ibtn.go{background:var(--lime);border-color:var(--lime);color:#16240b}
.ibtn.rv{color:var(--red);border-color:rgba(194,65,46,.4)} /* the .4 outline is NOT --red-line's .35; left as the only hand-written red alpha rather than invented into a second token or quietly repainted */
.ibtn.cp{margin-top:9px;width:100%}
/* the SHOW-ONCE block. Amber, not green: this is a "act now, it will not come back" state,
   and red is reserved for safety in this app. */
.irev{margin-top:10px;border:1px solid #eedcb2;background:#fdf2df;border-radius:var(--r-card);padding:11px 12px;color:#7a4d12}
.irev>b{display:block;font-size:11.5px;margin-bottom:8px}
.ikv{display:flex;gap:8px;align-items:flex-start;margin-top:6px;font-size:10.5px}
.ikv>span{flex:none;width:52px;font-weight:800;color:#a06322;text-transform:uppercase;letter-spacing:.05em;font-size:9px;padding-top:2px}
.ikv code{font-family:ui-monospace,SFMono-Regular,Menlo,Consolas,monospace;font-size:11px;color:#5c3a0d;
          word-break:break-all;overflow-wrap:anywhere;min-width:0}
.ihint{margin-top:9px;font-size:10px;line-height:1.5;color:var(--copper-ink)}
.ihint code{font-family:ui-monospace,SFMono-Regular,Menlo,Consolas,monospace;font-size:9.5px}
.ijrnl{margin-top:10px;max-height:260px;overflow:auto;white-space:pre-wrap;word-break:break-word;
       background:var(--scr);border:1px solid var(--border);border-radius:var(--r-card);padding:10px 11px;
       font-family:ui-monospace,SFMono-Regular,Menlo,Consolas,monospace;font-size:10.5px;color:var(--text);
       overscroll-behavior:contain}

/* ================= BUSINESS RULES (v-rules) — W1 1.6 ======================================
   ONE new class, and it earns its place: .cfgsay is the sentence a change prints BEFORE it is
   committed, on the two keys that are payroll money (weekStartDay, overtimeMultiplier). It is
   NOT .istat.bad wearing a different name — .istat's three tones are claims about what IS, and
   this is a claim about what WOULD BE if the next tap lands. Amber for the same reason .irev is
   amber: this app reserves red for safety, and amber is "read this before you continue".
   flex-basis:100% so it drops onto its own line inside the wrapping .prow rather than fighting
   the confirm buttons for the same row on a 390px phone. */
.cfgsay{flex-basis:100%;margin-top:9px;border-radius:var(--r-ctrl);padding:8px 10px;
        background:#fdf2df;border:1px solid #eedcb2;color:#7a4d12;
        font-weight:800;font-size:10.5px;line-height:1.45;overflow-wrap:anywhere}
/* The current value, stated plainly next to the control that changes it. Same size and colour
   as .perval (the person editor's value line) because it is the same idea — what is on file
   right now, in words, so nobody has to infer it from a switch position. */
.cfgnow{color:var(--tmut);font-size:10px;margin-top:2px;line-height:1.35;overflow-wrap:anywhere}
.cfgnow b{color:var(--text);font-weight:800}
/* THE ROAD OFF A ROW (f-2 fix round, eval `false-success`). A row whose .cfgnow has to name a
   missing half offers the trip that supplies it, right there. Sized at the house floor (56px)
   like every other control a gloved hand taps, and it is a SECONDARY road — outlined, never a
   second brand-green primary competing with the switch beside it (`green-overload`). */
.cfgroad{display:inline-flex;align-items:center;min-height:56px;margin-top:9px;padding:0 14px;
         border:1px solid var(--border);background:var(--card);border-radius:var(--r-card);
         color:var(--text);font:inherit;font-weight:800;font-size:11px;cursor:pointer;text-align:left}

/* ---- THE PAY-WEEK ASK (#payWeekConfirm) — the one setup step a server cannot detect --------
   Five of the six steps prove themselves the moment the data exists. This one is an
   ACKNOWLEDGEMENT, and it is asked here, on the screen where the fact lives, beside its own
   neighbours (Overtime, Payroll export) — not in a wizard and not on Home. Money math gets a
   human yes.

   THE GREEN IS LEGAL AND IT IS COUNTED. "Yes, that's right" is this SCREEN'S primary action,
   and Wes's rule is that brand green is the primary action and the active state — the
   one-green law is a law about HOME, where the only green thing is Clock in. The switches on
   this page are active states, which is the other half of the same rule. `.cfgroad.pri` is the
   only new green on the screen, and it wears the SAME 56px road shape as the neutral button
   beside it so the pair reads as one choice with a recommended half. */
.askcard{flex-direction:column;align-items:stretch;
         border-color:var(--setup-line);background:var(--setup-tint)}
.askq{font-weight:800;font-size:15px;line-height:1.3}
/* The copper tie back to the row on Home that sent them here. Same accent, same card. */
.gsdot{display:inline-block;width:8px;height:8px;border-radius:50%;background:var(--copper);
       margin-right:7px;vertical-align:1px}
.askfacts{font-size:11.5px;margin-top:4px}
.askbtns{display:flex;gap:8px;flex-wrap:wrap}
.cfgroad.pri{background:var(--lime);border-color:var(--lime);color:#16240b}
.askwrap{overflow:hidden}
.askwrap.folding{opacity:0;
  transition:max-height var(--setup-sink-ms) var(--setup-curve),opacity var(--setup-sink-ms)}
.confrow{display:none;align-items:flex-start;gap:10px}
.confrow.show{display:flex}
.confrow.show.fresh{animation:confin var(--setup-rise-ms) var(--setup-curve)}
@keyframes confin{from{opacity:0;transform:translateY(4px)}}
/* MUTED, NOT GREEN — see payWeekConfirmed's note in views-team.js. A marker with nothing to
   ask of you must not carry more colour weight than the controls that do. */
/* 11.5px, NOT 9 (eval 2026-08-23, `palette-drift`). This chip is the only record a company
   keeps that a human confirmed how its overtime week is anchored, and it was the smallest type
   in the app on a payroll fact. 11.5px is this stylesheet's caption floor — the same size
   .empcap and .gsbye small already sit on — so the chip is now legible at the size every other
   caption is, and the tick beside it grows with it. */
.confchip{flex:none;display:inline-flex;align-items:center;gap:5px;font-size:11.5px;font-weight:800;
  border-radius:var(--r-ctrl);padding:3px 8px;color:var(--tmut);background:var(--scr);
  border:1px solid var(--border);letter-spacing:.03em;white-space:nowrap}
.confchip svg{width:11px;height:11px}
/* THE ARRIVAL FLASH. A ring, not a fill: the card keeps its own copper tint underneath, and a
   box-shadow costs no layout, so nothing on the screen moves while it plays. */
.arrive{animation:gsflash 1.4s var(--setup-curve)}
@keyframes gsflash{0%{box-shadow:0 0 0 0 rgba(201,131,47,0)}
                   25%{box-shadow:0 0 0 4px var(--setup-line)}
                   100%{box-shadow:0 0 0 0 rgba(201,131,47,0)}}

/* ================= PORT HARDENING — carried forward from the old public/styles.css =========
   Every rule below is a fix that cost real debugging on a real Galaxy A12. None is cosmetic. */

/* --- [hidden] must always win. The UA default is unlayered and loses to ANY author display
   rule (.ghost, .hh button, a flexed row) — which is how a permission-gated control ships
   hidden in markup and still paints. The old styles.css carried this for exactly that
   reason; R1's port missed it, and the time-off Request button was the proof (finding #7:
   visible and TAPPABLE for the whole whoami round trip on a reduced-motion phone).
   Enforced here once so it can't recur element by element. --- */
[hidden]{display:none !important}

/* --- safe areas. viewport-fit=cover in index-v2.html is what makes env() resolve to anything
   other than 0px. Without the .nav rule the bottom tab bar sits UNDER Android 15's gesture
   pill: swiping up to hit "Jobs" fires the system home gesture instead, and the app's primary
   navigation is unreachable. --- */

/* ONE NAME PER INSET, AND EVERY RULE BELOW SPENDS THE NAME (2026-08-25). The two tokens ARE
   env() — on the web they resolve to exactly the values these rules have always read, so a
   browser measures byte-identically to before this line existed. What the indirection buys is a
   place for the native floor to land: index.html's head script overrides --sa-top / --sa-bottom
   on <html> when, and only when, the app is running as the iOS binary AND env() came back 0px.
   Read the long note there for why that state exists (short version: contentInset "never" hands
   the safe area to the page and stops telling it how big the area is) and for where the numbers
   come from. THE POINT OF THE TOKEN is that the floor reaches every rule at once: the app-bar
   collision Wes screenshotted and the tab bar sitting on the home indicator are the same defect
   at two ends of the same screen, and a fix applied to only the screenshotted one is this
   ledger's `fixed-one-of-N` by construction. */
:root{
  --sa-top:env(safe-area-inset-top,0px);
  --sa-bottom:env(safe-area-inset-bottom,0px);
}
/* THE APP BAR IS ON EVERY SCREEN — one element, `.abar` in index.html, whose #scrTitle and
   #scrSub the router rewrites per view (app.js ROUTES). So this single rule is the whole of
   Wes's "it is on every screen": Home, Confirm receipt, Fern, Jobs, Office, My week and the
   rest all wear this box. test/safe-area.cjs walks them and measures the TOP-MOST DRAWN CHILD
   of the bar against the inset on each one — not the title, because on the screens that carry
   a waypoint header #scrTitle is deliberately empty (ce74b66) and a 0x0 box clears everything. */
.abar{padding-top:calc(10px + var(--sa-top))}
/* MAX, NOT PLUS — 2026-08-24, off Wes's screenshot of the shipped binary. `calc(20px + inset)`
   paid BOTH the browser's comfort padding and the phone's home-indicator inset: 20 + 34 = 54pt
   of empty white under the labels, which reads as a white bar sitting below the app rather than
   as a tab bar. iOS's own tab bars clear the indicator and nothing more. max() keeps the 20px
   where there is no inset (desktop, and a browser tab with its own toolbar) and spends exactly
   the inset where there is one, so the bar is the same height iOS would draw. .content's
   clearance below is unchanged and stays a ceiling: shrinking the bar only widens the margin
   the setup-card suite asserts. */
.nav{padding-bottom:max(20px, var(--sa-bottom))}
/* --- nav clearance --- THE ONLY PLACE .content's bottom padding is set. It clears the
   bottom bar, which is why it is not 14px like the other three sides, and it is measured
   against the bar's RENDERED height, never against a number in the source: .nav has no
   height of its own, it is its content plus its own padding-bottom (20px + inset), and it
   comes out at 111px in a 390px-wide tab. 124px leaves 13px of air under the last element
   of a fully scrolled view. At 96px it did not clear the bar at all -- #aboutRow and the
   closing .subnote of #v-rules both hit-tested to `plusbtn` at 390x900, 375x667 and
   430x932, with correct innerText and a non-zero rect the whole time (EVAL-LEDGER
   `unreachable`, 2026-08-21). test/setup-card.cjs scrolls #v-set and #v-rules to the end
   and elementFromPoint's the bottom-most element at all three viewports; it also asserts
   this padding is >= the bar's measured height, so shrinking either one goes red. */
.content{padding-bottom:calc(124px + var(--sa-bottom))}
/* --- THE PRIMARY ACTION ON THE THREE CAPTURE PANES IS DOCKED (2026-09-04 sweep, lane 4 F1, a
   MAJOR regression from 42f8922) ---------------------------------------------------------------
   .nav is 96%-opaque white with a blur behind it, and it is 91px + max(20px, inset) tall (the
   111px this file's comment above already quotes). Adding the paid-with row to #v-add pushed
   "Add to job costs" down to y726 at 390x844 — with the bar's top at 733 on a zero inset, and at
   719 on a real iPhone, where the 34px home-indicator inset RAISES the bar. So the first thing a
   person taps on the confirm screen was UNDER the bar the moment the screen opened, and
   elementFromPoint at its centre returned the ＋: a real tap opened the Capture sheet instead of
   booking the receipt. #logSubmit and #dmgSubmit are the same shape at 375x407.
   The padding above is a ceiling for a FULLY SCROLLED pane and it always was; it can do nothing
   about where the button comes to rest at scrollTop 0, which is where the screen opens and where
   nobody had measured. Nothing caught it because every suite scrolled to the bottom before
   tapping (camera-roll's tapSubmit) and a person does not — that pre-scroll is deleted.
   THE SHAPE IS .agbar's, not a new one: sticky, so the dock sits in normal flow at the end of
   a short form and lifts itself clear on a long one, with no fixed-position layer to keep in
   sync. It is a WRAPPER, not the button — `.dock` holds the button and, on #v-add, the subnote
   that says what the tap will do. Docking the button alone (the first cut of this round) closed
   the finding for the control and left it open for the sentence, which then sat under the tab
   bar at rest and under the ＋ with the Other box open (`fixed-one-of-N`). The wrapper carries
   an opaque `var(--scr)` background, which is also what stops content scrolling visibly through
   the gap above the button and past its rounded corners.

   THE OFFSET IS 0, AND THAT IS THE WHOLE TRICK — MEASURED, NOT REASONED. The obvious value is
   the bar's height, and it is wrong, because a sticky element is clamped to its containing
   block and a block's padding-bottom is NOT part of it. So `.content`'s existing 124px already
   holds the button off the bottom of the scrollport, and any `bottom` offset ADDS to it. The
   resting position came out as exactly `733 − padding` at every value tried:
     bottom:111 + 124  -> the button rests at y553-609, 124px of dead air under it, ON TOP of
                          #logThumbs (532-590) — the dock covering the photo strip and the ＋
                          that opens the camera.
     bottom:111 + 220  -> y457-513 at 390x844, and y92-148 at 375x407: a "docked" primary action
                          floating near the TOP of the screen.
     bottom:0   + 124  -> y664-720 against a bar at 733. Thirteen pixels of air, which is the
                          same 13px `.content`'s own comment above was calibrated for.
   So the offset is 0 and the padding is the one that already ships. Nothing was added to it. */
#v-add > .dock,
#v-log > .dock,
#v-dmg > .dock{
  position:sticky;
  /* THE INSET IS NEGATIVE, AND THAT IS THE POINT (2026-09-04 eval r3). `bottom:0` is not the
     bottom of the SCREEN — a sticky rectangle is the scroller's CONTENT box, and .content's
     `padding-bottom:calc(124px + var(--sa-bottom))` has already contracted it. So a dock at 0
     came to rest 13px above the tab bar at a zero inset and 33px above it on an iPhone, with the
     form sliding through the band between the two: a docked bar with daylight under it, which
     reads as a floating button rather than a floor.
     This gives back exactly what the padding took and adds the bar's own height, so the dock's
     bottom edge lands ON the bar's top edge at every inset:
       0 inset : 91 + 20 - 124 - 0  = -13
       34 inset: 91 + 34 - 124 - 34 = -33
     gapToNav is then 0, and it is MEASURED on all three panes at both insets in camera-roll's
     at-rest section rather than asserted here. A negative inset is legal, and it only applies
     while the dock is STUCK — on a pane too short to reach the threshold the dock sits in normal
     flow and none of this touches it. */
  bottom:calc(91px + max(20px, var(--sa-bottom)) - 124px - var(--sa-bottom));
  z-index:5;
  background:var(--scr);
  padding:10px 0 4px;
  margin-top:8px;
}
/* The button loses .submit's own 18px top margin inside the dock — the wrapper's padding is the
   spacing now, and two of them stacked put the sentence and the button too far apart to read as
   one thing. On #v-add the subnote sits directly above it and keeps its own small gap. */
#v-add > .dock > .submit,
#v-log > .dock > .submit,
#v-dmg > .dock > .submit{margin-top:0}
#v-add > .dock > .subnote{margin:0 0 8px}
/* AND NOTHING GETS SCROLLED INTO VIEW UNDERNEATH IT. A docked action necessarily overlays the
   tail of a scrolling form — that is what docking is — so the one thing that must never happen
   is the BROWSER parking a control under it when the keyboard opens and it scrolls the focused
   field into view.
   THE FIRST ATTEMPT PUT scroll-margin-bottom ON EACH INPUT AND IT WAS THE WRONG PROPERTY
   (2026-09-04 eval r2, `keyboard-fold`). A scroll margin is measured against the SCROLLPORT's
   bottom edge — which here is behind the tab bar — so it bought 84px of clearance from a line
   that is itself ~111px too low. Measured at 375x407: with a 34px inset the focus scroll landed
   #catOther, #note and #dmgNote 27px UNDER the dock; at a zero inset #vendor and #dmgWhat landed
   under the dock AND the nav; #logDone never scrolled at all and sat wholly under the tab bar,
   hit-testing to plusbtn. Every one of those is a man typing into a box he cannot see.
   scroll-padding-bottom is the scroller's OWN property and the right one: it shrinks the region
   the browser will scroll something into, for every scroll it performs, on every element inside
   — nothing to enumerate and nothing to keep in sync with the markup. The number is the whole
   obstruction: the nav (124px, .content's own clearance) plus THAT PANE'S DOCK, and it spends
   the safe-area token because the nav does.
   THE DOCK IS NOT ONE HEIGHT, which is why there are two rules and not one (2026-09-04 eval r3).
   #v-log and #v-dmg dock a bare button: 56px plus the wrapper's 10+4 padding = 74. #v-add docks
   the SUBNOTE as well — the sentence saying what the tap will do — and that is 18px more of type
   and margin: 92. Billing #v-add at 74 leaves its boxes 18px short, which is the same defect as
   the scroll-margin one this rule replaced, just smaller. Each pane pays for the dock it has.
   PROVED BY FOCUS, NOT BY A PROGRAMMATIC SCROLL — receipt-form.cjs §5b drives page.focus() on
   all six text boxes across the three screens at BOTH insets and asserts each box's bottom edge
   clears the dock and that a tap on its last line lands on the box. §5's older assertion used
   scrollIntoView({block:'center'}) at a zero inset, which is not what a phone does and is why
   this passed while every one of those six was broken. */
#v-log,#v-dmg{scroll-padding-bottom:calc(124px + var(--sa-bottom) + 74px)}
#v-add{scroll-padding-bottom:calc(124px + var(--sa-bottom) + 92px)}
/* …AND THE ONE BOX THE SCROLL CANNOT SAVE GIVES UP HEIGHT INSTEAD. scroll-padding only works
   while there is somewhere left to scroll TO. "What got done today" is an 88px textarea, and at
   375x407 with a 34px inset the pane runs out of scroll with its bottom edge still 4px under the
   dock (measured) — the fold is not a scroll problem there, it is a height problem. So the
   shrinkable region shrinks, which is what `keyboard-fold` asks for: fix the SHAPE, not the
   copy. 56px is the app's touch floor, so it is still a comfortable target and still the tallest
   field on the screen; above 560px it keeps all 88. Nothing a thumb touches gets smaller than
   the floor — the same trade the onboarding steps make on a short window. */
@media (max-height:560px){
  #v-log textarea.inp{min-height:56px}
}
/* NO EXTRA BOTTOM PADDING ON THESE THREE PANES — see the measurements in the rule above.
   `.content`'s 124px is what positions the dock, and adding to it pushes the primary action UP
   the screen and over the controls above it (the photo strip, the ＋ that opens the camera).
   scroll-padding is a different property from padding and does not move the dock at all. */
.sheet{padding-bottom:calc(26px + var(--sa-bottom))}
#onbScr .onb{padding-bottom:calc(26px + var(--sa-bottom))}
/* the two full-screen takeovers must clear both insets — their button is the only way out */
.sysfull{padding-top:calc(34px + var(--sa-top));padding-bottom:calc(34px + var(--sa-bottom))}
/* THE TOAST IS THE LAST BOTTOM-ANCHORED NUMBER IN THE APP, and until the f-6 fix round it was
   the only one that did not spend the token (EVAL-LEDGER `fixed-one-of-N`, LOW). 104px was
   calibrated against a 111px .nav; the bar is now `20px + var(--sa-bottom)` tall, so with a real
   34px inset the toast printed 21px OVER the tab labels — and even at zero inset it overlapped
   by 7px, which was there before the token existed and is fixed in the same breath rather than
   left as "pre-existing". 119 = the bar's own rendered 111px at zero inset (the number .content's
   124 is derived from, twelve lines up) + 8px of air. pointer-events:none means the tabs were
   always tappable underneath; what was lost was the app's only status channel being legible for
   the 2.5 seconds it is on screen. */
.toast{bottom:calc(119px + var(--sa-bottom))}
/* …AND ON THE THREE SCREENS THAT NOW HAVE A DOCK, IT CLEARS THAT TOO (2026-09-04 eval r3).
   119px was calibrated for a bar and a button that scrolled away with the form. The button does
   not scroll away any more, so the refusal LANDED ON THE CONTROL IT WAS REFUSING: tap "Add to
   job costs" with a bad total and the sentence telling you why prints across the button, at both
   insets. That is the one message a person is looking for at that instant, over the one thing
   they just touched.
   Lifted by the dock's own height, which is the same pair of numbers scroll-padding-bottom uses
   above — 74 for a bare button, 92 where the subnote rides along — so the two cannot drift apart.
   SCOPED BY data-view, set in app.js's show(): the toast is outside every pane, so there is
   nothing to descend from, and every other screen keeps the 119px it was measured at.
   (Printing refusals into #subnote instead is Wes's recommendation and is deliberately NOT done
   here — this round only gets the sentence off the button.) */
:root[data-view="log"] .toast,
:root[data-view="dmg"] .toast{bottom:calc(119px + var(--sa-bottom) + 74px)}
:root[data-view="add"] .toast{bottom:calc(119px + var(--sa-bottom) + 92px)}

/* --- overscroll containment on every nested scroller. A scroller that reaches its end and
   chains the gesture to the document is a pull-to-refresh waiting to happen. --- */
.content{overscroll-behavior:contain}
.sheetovl{overscroll-behavior:contain}
/* ...AND A TALL SHEET STOPS AT THE STATUS BAR (f-6 fix round, the takeover sweep). A bottom
   sheet is anchored to the bottom, so it is not a "top-anchored" surface — but 88dvh of a short
   window reaches into the top inset anyway: at 390x520 with the keyboard up, 88% leaves 62px,
   exactly the band. Subtracting the token costs a browser nothing (--sa-top is 0px off the
   binary, so this is byte-identical to `88dvh` on the web) and on the phone it stops the sheet's
   rounded lip and grabber being drawn under the clock. #hmovl's 94dvh gets the same subtraction
   at its own specificity, in the rule that sets it. */
.sheet{max-height:calc(88vh - var(--sa-top));max-height:calc(88dvh - var(--sa-top));overflow-y:auto;overscroll-behavior:contain}
#jobList{overscroll-behavior:contain}
.whl{overscroll-behavior:contain}
#onbScr{overscroll-behavior:contain}
#agThread{overscroll-behavior:contain}
/* horizontal scrollers: contain the x-axis so a sideways flick on a chip row can't rubber-band
   the page or trigger Chrome's back-navigation gesture. */
.cats,.catbars,.rfilters,.qkrow,.kindchips,.tags,.thumbs{overscroll-behavior-x:contain}

/* --- 44px is the gloved-thumb floor, measured on a Galaxy A12 by test/android.cjs.
   In-flow controls simply grow. Icon buttons keep their DRAWN size and grow only their HIT
   AREA via a ::before overlay, so the layout the mockup specifies is preserved exactly. --- */
/* .agsend LEFT THIS LIST 2026-08-18: it is 56x56 at its own rule now (see the composer block
   above), and a 44px floor sitting under a 56px box is a second number for the same control —
   which is how the two drift, and how "the composer is at the floor" quietly becomes false the
   next time somebody edits the smaller of the two. .agplus was never added here for the same
   reason, and .agmic left the file entirely at f-2.
   .signout LEFT IT AT f-2 WAVE 1.1, on this rule's own argument: it rendered 362x44 in the
   account sheet directly above a 94x56 #delOpen — the two-floors-in-one-sheet shape spelled out
   above, measured by the eval. Its 56px floor is at its own rule now (search `.signout{`). */
/* .rf JOINED THIS LIST 2026-09-04 (sweep, lane 5 F7). The receipts category filters and the
   approval queue's two tabs both wear it, and both rendered 31px tall — the road to a whole
   pane of money, under the thumb floor, on the same screens whose Approve/Reject just went to
   56. It belongs on the LIST and not at its own rule: 44 IS its floor (it is a chip, not a
   decision), and the list is exactly where "44 is the house tap floor" is written down. */
/* .ckbtn AND .brkbtn LEFT THIS LIST 2026-09-05 (f-13, sweep lane 2 item 1), on the same
   argument .signout and .adjout won on. They are the TIME CLOCK — the one card every single
   person in the company opens this app to use, in a glove, in a gravel lot — and they rendered
   48px and 44px directly under a job row at 53 while every sheet one tap away had gone to 56.
   Two floors in one card, and the small ones were the two buttons that start and end somebody's
   paid day. Their 56px floors are at their own rules now (search `.ckbtn{` and `.brkbtn{`), so
   there is one number per control instead of a 44 sitting under a 56. */
/* ---- THE FLOOR IS TWO-TIER, AND HERE IS THE RULE (f-14, item 24's R4 rider) --------------
   56 ON ANYTHING A CREW THUMB COMMITS WITH — a punch, a save, a send, a delete, the one road
   off a screen — measured on the rendered box, in a glove, in a gravel lot.
   44 IS THE GHOST DEFAULT: a secondary that sits beside a 56 and is tapped by an office thumb
   on a desk. Every exception above is a control that moved from the second tier to the first,
   and each carries the argument it moved on beside its own rule.
   The rule was real and unwritten until now, which is how .ckbtn and .brkbtn sat at 44/48 under
   a 56 for three rounds. */
/* .invite LEFT THIS LIST 2026-09-05 (f-14, item 9), the same move .ckbtn and .brkbtn made
   above: it is #inviteBtn2 and #newCrewBtn — the only way to add a person or a crew — and this
   line sits AFTER its own rule, so a 56 written up there would have been silently beaten by the
   44 down here. Its floor is at `.invite{` now, one number per control. */
.nv,.tag,.hok,.thadd,.rf,
.relink,.ghost{min-height:44px}
/* .adjout LEFT THAT LIST 2026-08-28 (T1), and it is the same argument .signout won on: it
   rendered 362x44 in the adjust sheet directly under a 56px .adjpair and a .submit at 15px/15px
   padding, which is the two-floors-in-one-sheet shape the `touch-target` standing check was
   promoted for — and this is the control that ENDS SOMEBODY'S PAID DAY. The floor moves onto the
   class, which has exactly one user (#adjOut).
   THE SIBLINGS LEFT BEHIND, NAMED RATHER THAN FORGOTTEN (`fixed-one-of-N`): .relink and .ghost
   stay on 44 here. Neither is a paid-time control and neither sits in this sheet — .ghost is the
   app-wide secondary button (its one in-sheet appearance, #delOpen, is already lifted to 56 by
   its own rule) and .relink is the offline re-link row on the job header. They move when a round
   measures the surfaces they actually live on, not as a rider on this one. */
.adjout{min-height:56px}
/* .cat LEFT THAT LIST ON 2026-08-25, by the same rule .xbtn, .submit, .agsend and .signout
   each left it: this round adds a SEVENTH chip to that row (Other), and a new control at 56
   beside six siblings at 44 is the two-floors-in-one-row shape the `touch-target` standing
   check was promoted for. So the floor moves onto the CLASS and all seven move together —
   `.cat` has exactly one user in the app, views-capture's renderCats(), so this rule reaches
   the whole family and nothing else. Measured on the real Confirm-receipt screen at 390 and
   375 in test/receipt-form.cjs. */
.cat{min-height:56px}
/* The free-text box for Other. It wears the .inp box every other field on this form wears —
   deliberately NOT 56, because #vendor, #total, #rdate, #job and #note are all the same
   height and lifting one of them alone is the same anti-pattern the .cat note above avoids at
   the other end. `.inp`'s own height is a pre-existing app-wide number and a whole-form
   reflow; it is recorded rather than half-done here. */
#catOther{margin-top:8px}
/* .xbtn LEFT THAT LIST ON 2026-08-24 (f-5), the same move .submit made just below and on the
   same argument. THE TRIGGER was the `touch-target` standing check's own fifth clause — "when a
   floor moves onto ONE class, measure every sibling wearing the old one": this round adds #xMail
   to the Bookkeeping screen, and giving the new button 56 while the four beside it stayed at 44
   is exactly the two-floors-on-one-screen shape that check was promoted for. MEASURED before
   moving it: 11px padding + a 15px label + a 12px sub-line rendered at 50, so the list's 44
   never bound on any of them and nobody had looked since it was written. All five export buttons
   are one control class doing one job, on a screen an owner uses with a phone in one hand. */
.xbtn{min-height:56px}
/* THE ONE PRIMARY EXPORT, AND ONLY WHERE THERE IS A REAL CHOICE TO SIGNAL (f-5, 2026-08-24).
   Worn by #xMail alone, and only on a device where a file download cannot be confirmed — iOS and
   the wrapper (app.js canConfirmDownload). ONE green element on that screen, which is the whole
   of Wes's colour rule: brand green is the primary action and the active state and nothing else.
   On desktop no button wears it at all, because there the download genuinely works and the two
   roads are equals. A BORDER, not a fill: the export row is a row of peers, and a filled button
   among outlined ones reads as a different KIND of control rather than as the recommended one. */
.xbtn.xprime{border-color:var(--brand)}
/* .submit LEFT THIS LIST ON 2026-08-24, the same move .signout and .rback made and for the
   same reason: 15px padding + a 15px line came to 50, the list's 44 never bound, and nobody
   had measured it since it was written. It is the PRIMARY control on every sheet in this app
   — #ptDone, #adjSave, the receipt save, the job save — so it is the last one that should sit
   under the house floor while a ghost button beside it clears it. The four places that need
   MORE than 56 already say so at their own rules and are unaffected (#onbScr/#pwovl/#hmovl and
   #coovl at 60, .permcard and #coovl .coshacts already at 56); this only lifts the ones nobody
   had named. */
.submit{min-height:56px}
.nv{min-width:44px}
.cat,.tag{display:inline-flex;align-items:center}
/* .rback LEFT THIS LIST AT f-2, for the same reason .agsend did: it carried min-height:56px
   at its own rule (the composer note above spells out why two numbers for one control is how
   the smaller one quietly wins). .rback is GONE at f-5 — Direction B, the waypoint header,
   replaced the pill outright — and .wp is what took its place in this list's history, not by
   joining the shared 44px floor but by the same move: its own min-height:56px rule (search
   `.wp{` near the old .rback comment) rather than a borrowed number. Where .rback's
   `align-items:center` override "went with it" at f-2, .wp never needed one at all — the crumb
   and the title stack in normal block flow, not a flex row, so there is no alignment rule to
   carry forward. */

.sx,.hpen,.calnav,.privx,.camx,.sw,.flashbtn,.libbtn{position:relative}
.sx::before,.hpen::before,.calnav::before,.privx::before,.camx::before,
.sw::before,.flashbtn::before,.libbtn::before{
  content:'';position:absolute;top:50%;left:50%;transform:translate(-50%,-50%);
  width:max(100%,44px);height:max(100%,44px);
}

/* --- the mockup's cursor:pointer implies a mouse. Kill the tap-highlight flash and
   double-tap zoom on controls, which read as lag in gloves. --- */
button,.nv,.cat,.tag,.sw,.trow,.prow{-webkit-tap-highlight-color:transparent;touch-action:manipulation}

/* --- the mockup animates the boot splash unconditionally. Respect the OS switch. --- */
@media (prefers-reduced-motion:reduce){
  *{animation-duration:.001ms!important;animation-iteration-count:1!important;transition-duration:.001ms!important}
}

/* ---------- THE iOS ZOOM TRAP — MUST STAY AFTER EVERY FORM-CONTROL FONT RULE ----------
   Wes, 2026-09-01: "sometimes when i get out of pages like the edit job details page I get like
   zoomed in and cant get zoomed out or recentered within the app." iOS auto-zooms the viewport
   whenever a FOCUSED text control computes under 16px, and inside a WKWebView a pinch does not
   reliably restore the scale. iOS 10+ deliberately ignores maximum-scale and user-scalable for
   accessibility, so the font size is the only lever that exists.

   SCOPED TO iOS by the -webkit-touch-callout probe, so the 15px type scale this app was drawn
   at survives everywhere else. Relaying out every form on every platform to fix a bug that
   exists on one is a worse trade than it sounds.

   WHY IT IS DOWN HERE AND WHY IT NAMES NINE SELECTORS. The first cut lived at the top of the
   file and said `.inp, input, select, textarea` — (0,1,0) and (0,0,1) — and FIVE later rules
   beat it, four of them on specificity and one on order alone:
     textarea.inp        (0,1,1)  :652   the job editor's Scope box — the field Wes named
     .srch input         (0,1,1)  :996   the Home job picker's search
     .stepin             (0,1,0)  :1285  the adjust-shift time fields (later, so it won)
     .ratebox input      (0,1,1)  :2956  the Team screen's wage box
     .icard textarea.inp (0,2,1)  :3137  the Integrations cards, and the daily log's notes
   Every one of them is named below at EQUAL specificity, and the block sits after all five, so
   it wins on source order without an !important — which would have hidden the next such rule
   instead of losing to it visibly.

   IF YOU ADD A FORM CONTROL UNDER 16px ANYWHERE IN THIS FILE, ADD ITS SELECTOR HERE. That is
   not a request: test/safe-area.cjs §9 sweeps every rule in this stylesheet whose selector
   names input/select/textarea/.inp/.stepin, and fails if one of them sets a font size below
   16px without appearing in this block. It also asserts the EFFECT — computed font-size on
   every visible control on six real screens — because the text-level guard is exactly what
   stayed green while the cascade was lost. */
@supports (-webkit-touch-callout: none) {
  .inp, input, select, textarea,
  textarea.inp, .srch input, .stepin, .ratebox input, .icard textarea.inp { font-size: 16px; }
}


/* ---------- THE GREY BAND, second half — MUST STAY LAST IN THIS FILE ----------
   SOURCE ORDER IS LOAD-BEARING. `.nav{position:absolute;...}` is declared at :267 (search
   `.nav{position:absolute` — the line number has been wrong once already: it read :244 for a
   week after an edit above it moved the rule, in a comment whose whole subject is WHERE a
   rule sits), inside the
   ported mockup block. A standalone override placed at the TOP of this file loses to it on
   source order at equal specificity — measured, not reasoned: in a real Chrome app window
   (--app=), html and body correctly took min-height:100% while .nav silently kept
   position:absolute. Half a fix reads exactly like a whole one until you measure the element
   itself, which is why test/standalone-geometry.cjs reads getComputedStyle(nav).position and
   never "is there a rule". So these rules live at the END, after every other .nav rule, and
   anything appended below them breaks the fix.

   fixed, not absolute: pinned to the WINDOW rather than to .screen, so the bar reaches the
   glass whatever the shell's height resolves to under WebKit 301994.

   TWO ROADS, same reason as the html/body pair at :88-89. The media query is kept because it
   works where it works (desktop Chrome's app window matches it — measured). The class is added
   because NOTHING PROVES A REAL IPHONE MATCHES IT, and on 2026-08-19 a fix that depended on
   that unproven assumption shipped green and put the band back on Wes's phone within the hour.
   html.is-installed comes from navigator.standalone, set in index.html's <head> before first
   paint.

   NEITHER ROAD OPENS IN A BROWSER TAB, and that is what keeps LAW 19 alive: unscoped, a fixed
   bottom bar sits above the on-screen keyboard and covers the sign-in controls underneath it —
   ten assertions at 375x407, every one a control the crew could not reach. The tab that
   test/laws-v2.cjs drives must keep the absolute nav, and standalone-geometry asserts exactly
   that, on the element, in a plain tab. */
@media all and (display-mode: standalone){
  .nav{position:fixed;z-index:20}
}
html.is-installed .nav{position:fixed;z-index:20}
