/* ============================================================
   TechBillingApp — field-tech billing applet
   Aesthetic: industrial job ticket. Warm paper, ink, safety
   orange. Condensed display type, mono numbers, cut-corner
   cards, stamp-style status chips. Portrait-first, sunlight-
   readable, glove-sized touch targets.
   ============================================================ */

:root {
    --ink: #16232d;
    --ink-soft: #3c4c58;
    --paper: #f3efe6;
    --card: #fffdf7;
    --line: #d9d2c0;
    --line-hard: #b9b09a;
    --accent: #e8590c;
    --accent-deep: #b23e00;
    --accent-soft: #fdeee3;
    --steel: #4e6e81;
    --ok: #2b8a3e;
    --ok-soft: #edf4e7;
    --danger: #c92a2a;
    --font-body: 'Barlow', system-ui, sans-serif;
    --font-display: 'Barlow Condensed', 'Arial Narrow', sans-serif;
    --font-mono: 'IBM Plex Mono', Consolas, monospace;
    --corner: 14px;
    --tap: 56px;
}

* {
    box-sizing: border-box;
}

/* Phones paint their own gray flash on tap - the design has its own
   :active states, so kill the double feedback */
button,
a,
select,
input {
    -webkit-tap-highlight-color: transparent;
}

/* Makes the BACK TO TOP anchor glide rather than teleport. Respects a user who
   has asked their OS for less motion - see the reduced-motion block below. */

html {
    scroll-behavior: smooth;
}

@media (prefers-reduced-motion: reduce) {
    html {
        scroll-behavior: auto;
    }
}

html,
body {
    margin: 0;
    padding: 0;
    background: var(--paper);
    color: var(--ink);
    font-family: var(--font-body);
    font-size: 16px;
    line-height: 1.45;
    -webkit-text-size-adjust: 100%;
}

/* Subtle blueprint-grid atmosphere on the paper */
body {
    background-image:
        linear-gradient(var(--paper) 0 0),
        repeating-linear-gradient(0deg, transparent 0 31px, rgba(78, 110, 129, 0.07) 31px 32px),
        repeating-linear-gradient(90deg, transparent 0 31px, rgba(78, 110, 129, 0.07) 31px 32px);
    background-blend-mode: normal, multiply, multiply;

    /* ⚠️ THIS ONE LINE IS WHAT MAKES .tb-crest VISIBLE AT ALL. Without it the
       ghosted badge was painted and then buried under the very background above,
       and shipped invisible.

       WHY: `html` carries `background: var(--paper)` too (see the rule above), so
       html's background is the canvas and body's background does NOT propagate
       to it - body paints its own box instead. Body is static with z-index auto,
       so it is NOT a stacking context, so the crest's `z-index: -1` resolves in
       the ROOT context, where the order is: canvas, then negative-z-index
       descendants (the crest), then in-flow descendants' BACKGROUNDS (body's
       cream and grid) - which lands on top of it.

       `isolation: isolate` makes body a stacking context, and inside one the
       order is body's own background FIRST, then negative-z-index descendants,
       then content. So the crest sits exactly between the paper and the words.

       ⚠️ isolation, NOT `position: relative; z-index: 0`. Both create the
       stacking context, but position would also make body the containing block
       for absolutely-positioned descendants - a second, unrelated change to
       every such element in the app. Isolation does the one thing wanted. It
       cannot trap overlays the way the earlier `.tb-main { z-index: 1 }` did,
       because everything in the app is inside body already, so no relative
       ordering between the bar, the scrims and the viewer changes. */
    isolation: isolate;
}

#app {
    min-height: 100vh;  /* fallback for older phone WebViews */
    min-height: 100dvh;
    display: flex;
    flex-direction: column;
}

/* ------------------------------------------------------------
   Splash (pre-Blazor boot)
   ------------------------------------------------------------ */

.tb-splash {
    min-height: 100vh;  /* fallback for older phone WebViews */
    min-height: 100dvh;
    display: flex;
    flex-direction: column;
    align-items: center;
    justify-content: center;
    gap: 14px;
    background: var(--ink);
}

.tb-splash__mark {
    font-family: var(--font-display);
    font-weight: 700;
    font-size: 40px;
    color: var(--accent);
    border: 3px solid var(--accent);
    padding: 4px 14px;
    transform: rotate(-4deg);
}

.tb-splash__title {
    font-family: var(--font-display);
    font-weight: 600;
    font-size: 22px;
    letter-spacing: 0.35em;
    color: var(--paper);
}

.tb-splash__bar {
    width: 180px;
    height: 4px;
    background: rgba(243, 239, 230, 0.2);
    overflow: hidden;
}

.tb-splash__bar span {
    display: block;
    width: 40%;
    height: 100%;
    background: var(--accent);
    animation: tb-scan 1.1s ease-in-out infinite alternate;
}

@keyframes tb-scan {
    from { transform: translateX(-30%); }
    to { transform: translateX(280%); }
}

/* ------------------------------------------------------------
   App bar
   ------------------------------------------------------------ */

.tb-bar {
    background: var(--ink);
    color: var(--paper);
    border-bottom: 3px solid var(--accent);
    position: sticky;
    top: 0;
    z-index: 10;
}

/* Same 520px column as .tb-main - the brand and the chips must sit directly
   over the content, not out at the window edges on a wide monitor */

.tb-bar__inner {
    width: 100%;
    max-width: 520px;
    margin: 0 auto;
    padding: 10px 16px calc(10px + 2px);
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: 12px;
}

.tb-bar__brand {
    display: flex;
    flex-direction: column;
    line-height: 1.05;
    min-width: 0;
}

/* The brand became a BUTTON so it can go home - but it must not LOOK like
   one. A browser gives a button its own background, border, padding, centred
   text and inherited-nothing font; every one of those has to be undone or the
   header grows a grey box where the logo was. */

.tb-bar__brand--btn {
    padding: 0;
    text-align: left;
    color: inherit;
    background: transparent;
    border: none;
    cursor: pointer;
    touch-action: manipulation;
    /* The tap target is the logo itself, which is already ~34px tall - a
       thumb finds it without the header having to grow for it. */

    /* ⚠️ NO PRESSED STATE, DELIBERATELY (Kirk: "subtle. No visual changes - no
       highlights."). The header is chrome; a logo that lights up under a thumb
       reads as a control the tech has to think about. This one is a shortcut
       for whoever reaches for it and invisible to everybody else. */
    -webkit-tap-highlight-color: transparent;
}

.tb-bar__co {
    font-family: var(--font-display);
    font-weight: 600;
    font-size: 12px;
    letter-spacing: 0.28em;
    color: rgba(243, 239, 230, 0.65);
    white-space: nowrap;
}

.tb-bar__app {
    font-family: var(--font-display);
    font-weight: 700;
    font-size: 22px;
    /* 0.03em, not 0.06em. Measured at 375px with CHAT + On Call in the bar, the
       wordmark needed 87px and had 86 - it FIT, by one pixel, which is not a
       fit, it is a coin toss against whatever font a given handset resolves.
       Halving the tracking on eight characters buys ~5px of real headroom and
       is invisible at a glance. */
    letter-spacing: 0.03em;
    color: var(--paper);
    white-space: nowrap;
}

.tb-bar__who {
    display: flex;
    flex-direction: column;
    align-items: flex-end;
    gap: 3px;
    min-width: 0;
}

/* The background-sync heartbeat: spins in the header while any quiet
   reload (catalog / stock / warehouses / dispatch revalidate) is in flight */

.tb-bar__sync {
    width: 14px;
    height: 14px;
    flex-shrink: 0;
    border: 2px solid rgba(243, 239, 230, 0.25);
    border-top-color: var(--accent);
    border-radius: 50%;
    animation: tb-spin 0.8s linear infinite;
}

.tb-chip {
    font-family: var(--font-mono);
    font-weight: 600;
    font-size: 12px;
    background: rgba(243, 239, 230, 0.12);
    border: 1px solid rgba(243, 239, 230, 0.3);
    color: var(--paper);
    /* Kirk: "suck a px or two off the sides of each of those chips." 6px, not
       8px - four chips ride this bar, so two px a side is sixteen px back to
       the wordmark for something nobody will notice is gone. Vertical padding
       is untouched: the tap target must not get shorter. */
    padding: 2px 6px;
    white-space: nowrap;
    max-width: 46vw;
    overflow: hidden;
    text-overflow: ellipsis;
}

/* THE TRUCK CHIP, SHORTER. It was already ellipsizing - at 46vw, which on a
   375px phone is about 24 characters, and 24 characters of truck is enough to
   crowd the brand off its own header (Kirk: "Let's shorten the name of the
   truck a bit").

   ⚠️ `ch` BECAUSE THE CHIP IS MONOSPACE, so 15ch is literally fifteen
   characters - the cap says what it means instead of hiding a character count
   inside a pixel value that stops being true the day the font changes.

   Fifteen is measured, not guessed. All 34 of Charlie's real trucks were
   pulled from SERVICE: the longest legitimate label is "0035 - Delivery" at
   15, the median is "0089 - Butch" at 12, and the only two that overflow are
   "0034 - Josh B - Test" and a row whose name is fourteen asterisks. So in
   production this almost never truncates - the 31-character monster in the
   screenshot is DevMode's invented "TRUCK 07 - SPRINTER 2500", which no real
   Charlie's truck resembles.

   min() keeps the old viewport guard for a genuinely narrow phone, where 15
   characters would still be too many. */
.tb-chip--truck {
    max-width: min(15ch, 46vw);
}

.tb-chip--accent {
    background: var(--accent);
    border-color: var(--accent);
    color: #fff;
}

/* A chip that DOES something - sign out, change truck. Same look as a static
   chip (the bar stays calm) but it presses like a button under a thumb. */

.tb-chip--btn {
    display: block;
    font: inherit;
    font-family: var(--font-mono);
    font-weight: 600;
    font-size: 12px;
    text-align: right;
    cursor: pointer;
    touch-action: manipulation;
}

.tb-chip--btn:active {
    transform: translateY(1px);
    filter: brightness(1.15);
}

/* ------------------------------------------------------------
   Section pivot - the HEADING is the switcher
   Windows Phone's pivot: the live section is the big bold
   headline, the others sit beside it softer and smaller, and a
   tap moves you. A separate nav strip cost a whole row of
   vertical space; this costs none, because the h1 was always
   going to be there.
   ------------------------------------------------------------ */

.tb-pivot {
    display: flex;
    align-items: baseline;
    gap: 14px;
    margin: 2px 0 0;
    /* Never wrap - slide sideways if a future section makes it long */
    white-space: nowrap;
    overflow-x: auto;
    scrollbar-width: none;
}

.tb-pivot::-webkit-scrollbar {
    display: none;
}

.tb-pivot__item {
    flex-shrink: 0;
    font-family: var(--font-display);
    font-weight: 700;
    font-size: 21px;
    line-height: 1.1;
    letter-spacing: 0.05em;
    text-transform: uppercase;
    /* Soft, but still clearly a headline - these are peers, not chrome */
    color: var(--line-hard);
    background: none;
    border: none;
    padding: 0 0 4px;
    cursor: pointer;
    touch-action: manipulation;
}

.tb-pivot__item:active {
    color: var(--accent-deep);
}

.tb-pivot__item--on {
    font-size: 34px;
    color: var(--ink);
    cursor: default;
}

/* The orange rule follows the live section rather than sitting under the
   whole row - it is what marks WHERE you are */

.tb-pivot__item--on::after {
    content: "";
    display: block;
    width: 56px;
    height: 5px;
    margin-top: 6px;
    background: var(--accent);
}

/* ── THE "+" SECTION ────────────────────────────────────────────────────────
   A fourth pivot section whose label is a literal "+" rather than a word. It
   has to GROW on selection like the other three, but every number in the rules
   above was measured against CAPITAL LETTERS, and a "+" is not one. Four
   separate things break, all of them fixed here and NONE of them on .tb-pivot
   itself - touching the container would reflow the three sections that are
   already right. */

.tb-pivot__item--plus {
    /* SAME GLYPH PROBLEM AT REST. A "+" fills far less of its em box than a
       capital does, so 21px next to 21px words reads small. Scaled by the same
       46/34 ratio the --on rule below uses (21 x 1.35), so the plus keeps step
       with the words whether it is live or not. */
    font-size: 28px;

    /* THE TAP TARGET, NOT DECORATION. .tb-pivot__item is padding: 0 0 4px - no
       horizontal padding at all - and a "+" text box is only ~14-20px wide, so
       without this the plus is barely half the 40px floor every other small
       option button in the app respects. 12px a side takes a ~16px glyph to
       ~40px. The container's gap is 14px, so the padded box still leaves a
       clear 14px gutter to the neighbouring section - the padding is spent on
       air that was already there, not on crowding. */
    padding-left: 12px;
    padding-right: 12px;

    /* .tb-pivot is align-items: baseline, and a "+" sits on its horizontal
       stroke rather than its optical centre, so baseline-aligned it hangs
       visibly LOW beside the words. line-height:1 stops the oversized glyph's
       leading from making the whole pivot row taller, and the 2px lift puts the
       bar of the plus back on the words' optical middle. Small on purpose -
       this is a nudge, not a re-alignment. */
    line-height: 1;
    transform: translateY(-2px);
}

/* ⚠️ THIS COMPOUND IS LOAD-BEARING, NOT TIDINESS. .tb-pivot__item--plus and
   .tb-pivot__item--on are both (0,1,0) and --plus is written LATER, so a bare
   --plus font-size would beat the --on font-size and a SELECTED plus would
   shrink back to its resting 28px. Same trap the file already documents at
   .tb-xfer__opt--done.--on and .tb-sect__find--on:active; same answer.

   46px, not the 34px the words take: at 34px a "+" reads SMALLER than the
   capitals beside it, because the glyph occupies about three-quarters of the
   height a cap does. 46px puts the two at the same apparent size. */
.tb-pivot__item--plus.tb-pivot__item--on {
    font-size: 46px;
}

/* The orange rule is a hardcoded 56px, sized for a WORD. Under a plus it would
   overhang by ~35px and visually attach itself to the section next door - the
   one thing the bar exists to prevent. width:auto lets the block fill its
   content box, which is exactly the glyph's advance width, so the bar tracks
   the plus at any font-size instead of needing a second magic number. The 12px
   padding stays outside it: tap area, not underline. */
.tb-pivot__item--plus.tb-pivot__item--on::after {
    width: auto;
}

/* ------------------------------------------------------------
   Tabs inside a section - CURRENT | UPCOMING | COMPLETE
   ------------------------------------------------------------ */

.tb-tabs {
    display: flex;
    gap: 6px;
    margin: 0 0 16px;
}

.tb-tab {
    flex: 1;
    display: flex;
    align-items: center;
    justify-content: center;
    gap: 6px;
    min-height: 40px;
    padding: 4px 6px;
    font-family: var(--font-display);
    font-weight: 700;
    font-size: 13px;
    letter-spacing: 0.1em;
    color: var(--ink-soft);
    background: transparent;
    border: 2px solid var(--line-hard);
    cursor: pointer;
    touch-action: manipulation;
}

.tb-tab--on {
    color: var(--paper);
    background: var(--ink);
    border-color: var(--ink);
}

.tb-tab:active {
    transform: translateY(1px);
}

.tb-tab__count {
    font-family: var(--font-mono);
    font-size: 12px;
    font-weight: 600;
    padding: 0 5px;
    background: var(--accent);
    color: #fff;
}

.tb-tab--on .tb-tab__count {
    background: var(--accent);
}

/* ------------------------------------------------------------
   Page scaffold
   ------------------------------------------------------------ */

.tb-main {
    flex: 1;
    width: 100%;
    max-width: 520px;
    margin: 0 auto;
    padding: 20px 16px calc(28px + env(safe-area-inset-bottom, 0px));

    /* ⚠️ DO NOT GIVE THIS A z-index. It had `position: relative; z-index: 1`
       for about an hour to lift content above the crest, and that ONE
       declaration broke every overlay in the app.

       A z-index here makes .tb-main a STACKING CONTEXT, and every positioned
       descendant is then confined inside it - so the photo viewer (z-index
       1000), the scrims (40), the pickers (41) and the confirms (61) all
       collapsed into the single z-index-1 layer, which sits BELOW the sticky app
       bar at 10. Live symptom: the full-screen photo viewer lost its top
       controls, because the app bar was painting over them.

       The crest goes to z-index -1 instead - see .tb-crest. A background
       decoration should be pushed behind the content, never the whole app
       lifted above it. */
}

/* ══ THE GHOSTED HOUSE MARK ═══════════════════════════════════════════════
   See Components/BrandCrest.razor for what it is and why it is opt-in.

   ONE KNOB. --crest-strength is the only number worth touching; everything
   else about this is geometry. Tuned low on purpose - it should read as
   something the paper was printed on, not as an image on top of it. */
:root {
    --crest-strength: 0.10;

    /* How solid a card is on a crest page. 1 = opaque (the crest stops dead at
       the card edge, which is what it did first); lower lets the mark whisper
       through. Kirk, seeing it: "Now we make what are you looking for still see
       it slightly ... need to see it in daylight, so we can't do art over
       functionality, but we can make it work."

       ⚠️ 0.55 IS MEASURED, AND 0.84 WAS WRONG. The first attempt reasoned that
       0.84 would be "faint but present"; on screen the crest was invisible
       behind a card, because the crest is only 10% ink and 16% of 10% is
       nothing. So the real text colours were read off the rendered cards -
       only --ink (22,35,45) and --ink-soft (60,76,88), no steel - and the
       contrast recomputed at every candidate:

           veil   crest step   worst contrast
           0.84       3            8.32
           0.62       8            7.79
           0.55       9            7.62   <- here
           0.42      12            7.32

       Every one of those clears WCAG AAA for body text, which needs 7.0 - so
       0.55 buys three times the visible art for a contrast cost of 0.7, and
       "can't do art over functionality" is satisfied by arithmetic rather than
       by hoping. 0.42 is the floor where AAA still holds; below it, stop. */
    --card-veil: 0.55;

    /* --card as its own channels, so the veil above cannot drift out of sync
       with the solid colour it is a translucent version of. */
    --card-rgb: 255, 253, 247;
}

.tb-crest {
    position: fixed;
    left: 50%;
    /* Slightly above centre: the eye reads the top two-thirds of a phone, and
       a mark centred dead-middle sits under the busiest part of every list. */
    top: 44%;
    transform: translate(-50%, -50%);

    /* Tracks the app's own 520px column so the crest belongs to the layout
       rather than to the window - on a wide desktop a viewport-centred mark
       would drift away from the content it is meant to sit behind. */
    width: min(84vw, 430px);

    /* ⚠️ NEGATIVE, AND THAT IS THE WHOLE POINT. Painting order puts negative
       z-index children BELOW in-flow content but ABOVE the canvas - which is
       where the body's cream and ledger grid live, since body's background
       propagates to the canvas. So the crest lands exactly between the paper
       and the words, with nothing else touched.

       At z-index 0 it painted ABOVE the static content instead (a positioned
       element beats non-positioned siblings), which is why the first version
       lifted .tb-main to z-index 1 to get out of its way - and THAT turned
       .tb-main into a stacking context that trapped every overlay in the app
       below the header. One character here replaces that whole mistake. */
    z-index: -1;

    /* ⚠️ BOTH OF THESE MATTER. A decorative image that can take a tap, or be
       long-pressed into a save-image sheet, is a bug on a phone - and this one
       covers most of the screen. */
    pointer-events: none;
    user-select: none;
    -webkit-user-select: none;
    -webkit-touch-callout: none;
}

.tb-crest__img {
    display: block;
    width: 100%;
    height: auto;
    opacity: var(--crest-strength);

    /* ⚠️ SEPIA, NOT mix-blend-mode: multiply - which is what this reached for
       first and is a trap here. `opacity` establishes a stacking context, and a
       blend mode only blends within its own context, so opacity + multiply on
       the same element (or split across parent and child) is exactly the
       arrangement where browsers disagree about whether anything blends at all.
       A watermark that renders one way in Chrome and another on the iPhones
       every tech carries is worse than no watermark.

       Sepia gets the thing that was actually wanted - WARMTH. Without it the
       distressed black outlines ghost to a cool grey and the mark reads like a
       smudge; with it they land in the same brown family as the banner, which
       is the register the badge was drawn in. Fully deterministic, no stacking
       rules involved. */
    filter: sepia(0.45) saturate(1.15);
}

/* ── LETTING THE MARK THROUGH THE CARDS ───────────────────────────────────
   The crest showed beautifully on bare paper and stopped dead at every card,
   because a card is opaque. Kirk, looking at /lookup: "love how it overlaps
   dispatch and customer field and notes and tasks. Now we make what are you
   looking for still see it slightly."

   ⚠️ SCOPED WITH :has(), so ONLY a page that opted into a crest gets softened
   cards. The job detail, the invoice and the parts picker keep their solid
   cards - they were never going to have a crest behind them, and a translucent
   card there would cost contrast for no picture at all. This says the intent
   in one line ("cards on a page that has a crest") rather than a chain of
   sibling combinators that would break the day somebody reorders the markup.

   The blur is Kirk's "Light blur perhaps? artsy?" and it earns its place
   functionally: the badge is DISTRESSED line art, and unblurred its hard edges
   sit under body text and fight it. Two pixels is enough to turn it into a
   tone. ⚠️ It is also the first thing to remove if any tech reports scroll
   jank - a list can hold twenty of these, and backdrop-filter is the one
   property here with a real GPU cost on an older handset. Nothing else depends
   on it; the veil alpha does the actual work. */
/* ⚠️ NAMED IN, NOT EXCEPTED OUT - AND THAT IS THE WHOLE LESSON HERE.

   This first said "every .tb-card on a crest page" and then reset the overlays
   that reuse .tb-card back to solid. That list was .tb-confirm, .tb-taskpick and
   .tb-notes - and it MISSED .tb-contacts, so WHO TO CALL shipped see-through on
   a phone with the billing list bleeding straight through six phone numbers.
   Kirk: "our dialog modal need a solid background It's a mess on mobile."

   The two directions fail very differently:
     opt-out - a forgotten overlay goes TRANSPARENT. Unreadable, on a handset,
               in daylight, over exactly the data somebody is trying to dial.
     opt-in  - a forgotten flow card stays OPAQUE. The crest does not show
               through one card. Nobody is harmed and I may never hear about it.

   So this names the three FLOW card kinds, and none of them is used by any
   overlay: --pad is every empty-state and info card, .tb-job and .tb-ticket are
   the list rows, and --veil is for the odd wrapper that has no other class. A
   dialog added next year cannot match these by accident. */
.tb-main:has(.tb-crest) .tb-card--pad,
.tb-main:has(.tb-crest) .tb-job,
.tb-main:has(.tb-crest) .tb-ticket,
.tb-main:has(.tb-crest) .tb-card--veil {
    background: rgba(var(--card-rgb), var(--card-veil));
    -webkit-backdrop-filter: blur(2px);
    backdrop-filter: blur(2px);
}

.tb-h1 {
    font-family: var(--font-display);
    font-weight: 700;
    font-size: 34px;
    letter-spacing: 0.04em;
    text-transform: uppercase;
    margin: 4px 0 2px;
}

.tb-h1::after {
    content: "";
    display: block;
    width: 56px;
    height: 5px;
    margin-top: 6px;
    background: var(--accent);
}

/* Tightened 2026-08-17 - every row of vertical space on a phone is a row of
   work the tech can see without scrolling */

.tb-sub {
    color: var(--ink-soft);
    margin: 6px 0 14px;
    font-size: 14px;
}

/* ------------------------------------------------------------
   Cards — job-ticket look: cut corner + hard shadow
   ------------------------------------------------------------ */

.tb-card {
    background: var(--card);
    border: 1.5px solid var(--line-hard);
    clip-path: polygon(0 0, calc(100% - var(--corner)) 0, 100% var(--corner), 100% 100%, 0 100%);
    box-shadow: 4px 4px 0 rgba(22, 35, 45, 0.12);
    margin-bottom: 14px;
}

.tb-card--pad {
    padding: 18px 16px;
}

.tb-perf {
    border: none;
    border-top: 2px dashed var(--line);
    margin: 14px 0;
}

/* ------------------------------------------------------------
   Forms
   ------------------------------------------------------------ */

.tb-label {
    display: block;
    font-family: var(--font-display);
    font-weight: 600;
    font-size: 15px;
    letter-spacing: 0.12em;
    text-transform: uppercase;
    color: var(--ink-soft);
    margin: 0 0 6px;
}

.tb-input {
    display: block;
    width: 100%;
    min-height: var(--tap);
    padding: 12px 14px;
    font-family: var(--font-mono);
    font-weight: 500;
    font-size: 20px;
    color: var(--ink);
    background: #fff;
    border: 2px solid var(--line-hard);
    border-radius: 0;
    margin-bottom: 16px;
}

.tb-input:focus-visible {
    outline: 3px solid var(--accent);
    outline-offset: 1px;
    border-color: var(--accent);
}

.tb-input::placeholder {
    color: #b3ac9b;
    font-weight: 500;
}

/* ------------------------------------------------------------
   Buttons
   ------------------------------------------------------------ */

.tb-btn {
    display: flex;
    align-items: center;
    justify-content: center;
    gap: 10px;
    width: 100%;
    min-height: var(--tap);
    padding: 12px 18px;
    font-family: var(--font-display);
    font-weight: 700;
    font-size: 20px;
    letter-spacing: 0.1em;
    text-transform: uppercase;
    color: #fff;
    background: var(--accent);
    border: none;
    border-bottom: 4px solid var(--accent-deep);
    cursor: pointer;
    touch-action: manipulation;
}

.tb-btn:active {
    transform: translateY(2px);
    border-bottom-width: 2px;
}

.tb-btn:focus-visible {
    outline: 3px solid var(--ink);
    outline-offset: 2px;
}

.tb-btn:disabled {
    background: #a9a293;
    border-bottom-color: #8d8677;
    cursor: not-allowed;
}

/* The ADD action, sitting up top. Ink rather than accent on purpose - the
   accent is spoken for by the TODAY day header right below it, and two orange
   blocks stacked read as one shape. Dark keeps it the loudest thing on the
   page without fighting the day blocks. */

/* FINISH THIS JOB, on the invoice. Sits above SWITCH TRUCK with air around it -
   it is the one button on this page that ends something, and it should not read
   as a sibling of the ghost button under it. */
.tb-inv__finish {
    margin-bottom: 6px;
}

.tb-inv__finishnote {
    margin: 0 0 14px;
    font-size: 12px;
    font-weight: 500;
    text-align: center;
    color: var(--steel);
}

/* PARTS & LABOR, under the clock step on a job. Ghost, so the clock move stays
   the loudest thing on the screen it belongs to. */
.tb-jd__parts {
    margin-top: 8px;
}

.tb-btn--dark {
    background: var(--ink);
    border-bottom-color: #000;
    color: var(--paper);
}

/* Breathing room before the first day block */

.tb-btn--add {
    margin-bottom: 16px;
}

.tb-btn--ghost {
    background: transparent;
    color: var(--ink);
    border: 2px solid var(--ink);
    border-bottom-width: 2px;
    font-size: 17px;
}

.tb-btn--ghost:active {
    transform: none;
    background: rgba(22, 35, 45, 0.06);
}

/* ------------------------------------------------------------
   Tap rows (trucks, dispatches)
   ------------------------------------------------------------ */

.tb-row {
    display: flex;
    align-items: center;
    gap: 12px;
    width: 100%;
    min-height: var(--tap);
    padding: 14px 14px;
    background: transparent;
    border: none;
    border-radius: 0;
    font: inherit;
    color: inherit;
    text-align: left;
    cursor: pointer;
    touch-action: manipulation;
}

.tb-row + .tb-row {
    border-top: 2px dashed var(--line);
}

.tb-row:active {
    background: var(--accent-soft);
}

.tb-row:focus-visible {
    outline: 3px solid var(--accent);
    outline-offset: -3px;
}

.tb-row__code {
    font-family: var(--font-mono);
    font-weight: 600;
    font-size: 15px;
    background: var(--ink);
    color: var(--paper);
    padding: 4px 8px;
    white-space: nowrap;
}

.tb-row__body {
    flex: 1;
    min-width: 0;
}

.tb-row__title {
    font-weight: 600;
    font-size: 17px;
}

.tb-row__meta {
    color: var(--ink-soft);
    font-size: 14px;
}

.tb-row__chev {
    font-family: var(--font-display);
    font-weight: 700;
    font-size: 22px;
    color: var(--accent);
}

/* ------------------------------------------------------------
   Dispatch tickets
   ------------------------------------------------------------ */

.tb-ticket {
    padding: 14px 16px 16px;
}

.tb-ticket__top {
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: 10px;
    margin-bottom: 8px;
}

.tb-ticket__wo {
    font-family: var(--font-mono);
    font-weight: 600;
    font-size: 16px;
    letter-spacing: 0.02em;
}

.tb-stamp {
    display: inline-block;
    font-family: var(--font-display);
    font-weight: 700;
    font-size: 14px;
    letter-spacing: 0.14em;
    text-transform: uppercase;
    color: var(--accent-deep);
    border: 2px solid var(--accent-deep);
    padding: 1px 8px;
    transform: rotate(-3deg);
    white-space: nowrap;
}

.tb-stamp--steel {
    color: var(--steel);
    border-color: var(--steel);
}

.tb-ticket__cust {
    font-family: var(--font-display);
    font-weight: 700;
    font-size: 24px;
    letter-spacing: 0.02em;
    line-height: 1.1;
}

.tb-ticket__addr {
    color: var(--ink-soft);
    font-size: 15px;
    margin-top: 2px;
}

/* WHAT THE JOB WAS FOR, on the billing card.

   DELIBERATELY IDENTICAL TO .tb-job__tasks - same 14px, same weight, same --ink,
   same two-line clamp. Kirk on the dispatch card's version: "nice and subtle".
   The two cards show the SAME job, so a line that changed size or weight
   between them would read as a different KIND of information rather than the
   same fact in a second place.

   It does NOT take the weight-500 treatment the notes got. That was for prose a
   tech stands and READS in the sun; this is a short label they GLANCE at, and
   making it heavier would pull the eye off the dispatch number and the address -
   which are what this card is actually for. */
.tb-ticket__what {
    margin-top: 8px;
    font-size: 14px;
    line-height: 1.35;
    color: var(--ink);
    display: -webkit-box;
    -webkit-line-clamp: 2;
    -webkit-box-orient: vertical;
    overflow: hidden;
}

.tb-ticket__desc {
    font-size: 15px;
    margin-top: 10px;
}

.tb-ticket__tags {
    display: flex;
    flex-wrap: wrap;
    align-items: center;
    gap: 6px;
    margin-top: 10px;
}

/* ------------------------------------------------------------
   Job notes - the card button + the panel
   ------------------------------------------------------------ */

/* Sits first in the tag row and reads as a BUTTON, not a chip */

.tb-notesbtn {
    display: inline-flex;
    align-items: center;
    gap: 5px;
    min-height: 30px;
    padding: 2px 10px;
    font-family: var(--font-display);
    font-weight: 700;
    font-size: 13px;
    letter-spacing: 0.1em;
    color: #fff;
    background: var(--steel);
    border: none;
    border-bottom: 3px solid #3a5464;
    cursor: pointer;
    touch-action: manipulation;
}

.tb-notesbtn:active {
    transform: translateY(1px);
    border-bottom-width: 2px;
}

/* CALL is the same chip as NOTES because it is the same KIND of thing - a
   door to a panel - but it opens PEOPLE rather than prose, so it reads in ink
   rather than steel. Deliberately NOT the accent: the PO tag owns that on this
   row and two orange things would fight for the same eye. */

.tb-notesbtn--call {
    background: var(--ink);
    border-bottom-color: #0b1319;
}

/* Shared by every modal - notes panel, sign-out confirm */

.tb-scrim {
    position: fixed;
    inset: 0;
    background: rgba(22, 35, 45, 0.5);
    z-index: 60;
}

.tb-notes {
    position: fixed;
    left: 50%;
    top: 6dvh;
    transform: translateX(-50%);
    /* NOT vw - that includes the scrollbar, which on a scrolled list left
       the panel flush against both edges. % on a fixed element measures the
       viewport WITHOUT the scrollbar, so the gutter is honest. */
    width: min(calc(100% - 24px), 720px);
    max-height: 88dvh;
    display: flex;
    flex-direction: column;
    border-color: var(--ink);
    z-index: 61;
}

.tb-notes__head {
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: 10px;
    padding: 10px 12px;
    background: var(--ink);
    color: var(--paper);
    flex-shrink: 0;
}

.tb-notes__title {
    font-family: var(--font-display);
    font-weight: 700;
    font-size: 17px;
    letter-spacing: 0.12em;
}

.tb-notes__close {
    width: 40px;
    height: 40px;
    flex-shrink: 0;
    font-size: 17px;
    font-weight: 700;
    color: var(--paper);
    background: transparent;
    border: 2px solid rgba(243, 239, 230, 0.3);
    cursor: pointer;
    touch-action: manipulation;
}

.tb-notes__body {
    overflow-y: auto;
    -webkit-overflow-scrolling: touch;
    /* Stop the flick at the end of the notes - without this the scroll chains
       into the dispatch list behind the scrim and the job you were reading
       moves out from under you */
    overscroll-behavior: contain;
    padding: 14px 16px;
}

.tb-notes__label {
    font-family: var(--font-display);
    font-weight: 700;
    font-size: 14px;
    letter-spacing: 0.14em;
    text-transform: uppercase;
    color: var(--accent-deep);
    margin: 16px 0 6px;
    padding-top: 12px;
    border-top: 2px dashed var(--line);
}

.tb-notes__label:first-child {
    margin-top: 0;
    padding-top: 0;
    border-top: none;
}

/* The office's line breaks arrive as <br> from TextToHtml - do NOT add
   pre-wrap on top or every break renders twice */

/* Same sunlight reasoning as .tb-reader__text - see the block there. */

.tb-notes__text {
    overflow-wrap: anywhere;
    font-size: 16px;
    font-weight: 500;
    line-height: 1.55;
    color: var(--ink);
}

/* Phone / email / link inside a note: a real tap target, obviously live */

.tb-notes__text a {
    color: var(--accent-deep);
    font-weight: 700;
    text-decoration: underline;
    text-underline-offset: 2px;
    padding: 2px 0;
}

.tb-notes__text a:active {
    background: var(--accent);
    color: var(--paper);
}

/* ------------------------------------------------------------
   Empty state - a dead end with a way out
   ------------------------------------------------------------ */

.tb-empty__lead {
    margin: 0 0 6px;
    font-size: 16px;
}

.tb-empty__hint {
    margin: 0;
    font-size: 14px;
    line-height: 1.45;
    color: var(--ink-soft);
}

.tb-empty__row {
    margin-top: 14px;
}

/* ------------------------------------------------------------
   Dispatch job card
   Billing asks "what am I charging?". Dispatch asks "where am I
   going, when, and what am I walking into?" - so WHEN leads,
   WHERE is the biggest target, and the money is a small tag.
   ------------------------------------------------------------ */

.tb-job {
    padding: 12px 14px 10px;
    cursor: pointer;
    border-left: 5px solid var(--line-hard);
}

/* The edge carries the state at a glance, down the whole card */

.tb-job--live {
    border-left-color: var(--ok);
}

.tb-job--late {
    border-left-color: var(--danger);
}

.tb-job--done {
    border-left-color: var(--line);
    opacity: 0.85;
}

.tb-job__when {
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: 10px;
}

.tb-job__rel {
    font-family: var(--font-display);
    font-weight: 700;
    font-size: 20px;
    letter-spacing: 0.06em;
    color: var(--ink);

    /* Pushes the priority and the status stamp together on the right so the
       WHEN stays hard left. Without it, space-between spreads three children
       evenly and the priority floats mid-row, attached to neither. */
    margin-right: auto;
}

.tb-job__rel--late {
    color: var(--danger);
}

/* PRIORITY, beside the status stamp (Kirk, 2026-08-21): "as long as it's not
   NORMAL show the priority up there to the left of the status, so it's obvious
   what the priority is." Only drawn when HasPriority() is true - which excludes
   NORMAL and 0 - so it is always an exception worth the top-line room.

   Deliberately QUIETER than the stamp beside it: the stamp is what the tech is
   doing about this job and changes under their thumb; the priority is a fact
   about it. Outline, not fill. */
.tb-job__prio {
    flex-shrink: 0;
    padding: 3px 7px;
    font-family: var(--font-display);
    font-weight: 700;
    font-size: 12px;
    letter-spacing: 0.08em;
    color: var(--steel);
    border: 1px solid var(--line);
    border-radius: 3px;
    white-space: nowrap;
}

/* URGENT / EMERGENCY / HIGH - the one case where a priority should shout. It
   borrows the danger colour rather than inventing one, so it matches every other
   "this one is different" on the card. */
.tb-job__prio--hot {
    color: var(--danger);
    border-color: var(--danger);
}

/* ESC PICKED THE COLOUR (Kirk, 2026-08-21) - dbo.DispatchPriority.Color, so the
   card flags a job the same colour the office sees it.

   No colours here on purpose: background, border and text all arrive as an
   INLINE style, because they are data and not design. This class exists only to
   carry the shape difference a filled chip needs - our own outline version can
   sit tighter than one with a background behind the text.

   ⚠️ FILLED, with the ink computed by ESCColors.ReadableInkOn. Outlining in
   ESC's colour looked tidier and failed the daylight rule: the office can pick a
   pale yellow, which is invisible as a hairline on cream paper but perfectly
   legible as a fill with dark text on it. */
.tb-job__prio--esc {
    padding: 3px 8px;
    border-radius: 3px;
}

.tb-job__site {
    font-family: var(--font-display);
    font-weight: 700;
    font-size: 21px;
    line-height: 1.15;
    margin-top: 4px;
}

/* ------------------------------------------------------------
   THE IDENTITY ROW — two numbers, two audiences
   ------------------------------------------------------------ */

/* Kirk: "when we talk to our customer, we talk about PONumber. customers don't
   know our dispatch#. when we talk to our staff inside, we use dispatch#."

   So these are PEERS, not a hierarchy — same size, side by side, each labelled
   with WHO uses it, so nobody reads the wrong number to the wrong person. Both
   were buried before: the dispatch # was a 13px tag beside NOTES and PLAT, and
   the PO was not on the wire at all.

   Wraps rather than shrinks — a 20-character PO takes a second line before
   either number gets smaller. */

.tb-job__ident {
    display: flex;
    flex-wrap: wrap;
    gap: 8px 10px;
    margin: 10px 0 6px;
}

.tb-job__idblock {
    display: flex;
    flex-direction: column;
    gap: 1px;
    min-width: 0;
    padding: 4px 10px 4px 9px;
    background: var(--paper);
    border-left: 4px solid var(--steel);
}

/* The customer-facing one wears the accent — it is the number a tech says out
   loud to somebody who is standing in front of them. */

.tb-job__idblock--po {
    background: var(--accent-soft);
    border-left-color: var(--accent);
}

.tb-job__idlabel {
    font-family: var(--font-display);
    font-weight: 700;
    font-size: 10px;
    letter-spacing: 0.16em;
    color: var(--ink-soft);
}

.tb-job__idblock--po .tb-job__idlabel {
    color: var(--accent-deep);
}

/* 21px mono against 13px as a tag — Kirk: "LARGER and CLEAR - it's really
   important." Mono so digits stay unmistakable at arm's length in a plant
   room, and equal for both because neither audience outranks the other.
   overflow-wrap because a PO is FREE TEXT up to 20 characters: half a PO
   number is worse than useless, so it wraps rather than ellipsizing. */

.tb-job__idnum {
    min-width: 0;
    font-family: var(--font-mono);
    font-weight: 700;
    font-size: 21px;
    line-height: 1.15;
    letter-spacing: 0.01em;
    color: var(--ink);
    overflow-wrap: anywhere;
}

.tb-job__cust {
    font-size: 14px;
    color: var(--ink-soft);
}

/* The customer NUMBER trailing the customer name - quieter than the name it
   follows, which is --ink-soft.

   A token rather than a literal: measured on the card background --steel is
   5.33:1 where dimgrey was 5.40:1, so it is the same weight to the eye and
   follows the theme instead of pinning one grey. Every colour in this sheet is a
   token for that reason. */
.tb-job__custno {
    font-size: 13px;
    color: var(--steel);
}

/* WHERE - a full-width slab, because on a phone in a truck this is the
   thing being aimed at */

.tb-job__where {
    display: flex;
    align-items: center;
    gap: 10px;
    width: 100%;
    margin: 10px 0 0;
    padding: 9px 10px;
    background: var(--accent-soft);
    border-left: 3px solid var(--accent);
    text-decoration: none;
    color: var(--ink);
    text-align: left;
    touch-action: manipulation;
}

/* ⚠️ THE ROW NO LONGER LIGHTS UP ON PRESS, because the row is no longer the
   link (Kirk, 2026-08-21: "We are tapping maps too much by accident"). Feeding
   back a press on the whole strip would advertise a tap target that is now only
   the MAP corner. The .tb-job__go:active rule below owns the feedback.

   Kept as a comment rather than deleted - if the row ever becomes tappable
   again (opening the job from the detail page, say) this is the treatment.
.tb-job__where:active {
    background: var(--accent);
    color: #fff;
}

.tb-job__where:active .tb-job__city,
.tb-job__where:active .tb-job__go,
.tb-job__where:active .tb-job__pin {
    color: #fff;
}
*/

.tb-job__pin {
    font-size: 18px;
    color: var(--accent-deep);
    flex-shrink: 0;
}

.tb-job__addr {
    display: flex;
    flex-direction: column;
    min-width: 0;
    flex: 1;
}

.tb-job__street {
    font-weight: 600;
    font-size: 15px;
    line-height: 1.25;
}

.tb-job__city {
    font-size: 13px;
    color: var(--ink-soft);
}

/* THE ONLY TAP TARGET IN THE ADDRESS ROW (Kirk, 2026-08-21) - "the MAPS word
   and a little bit surrounding it".

   The padding IS the feature: the word alone is a 34px-wide target, which is
   under every touch guideline going, so it gets 10px of air each side and a
   44px minimum height. That lands it at roughly 54x44 - comfortably tappable,
   and still a small fraction of the row it used to occupy.

   The negative vertical margin keeps the taller target from growing the row:
   the box is bigger than the text without pushing the address down. */
.tb-job__go {
    display: inline-flex;
    align-items: center;
    flex-shrink: 0;
    min-height: 44px;
    margin: -9px 0;
    padding: 0 10px;
    font-family: var(--font-display);
    font-weight: 700;
    font-size: 12px;
    letter-spacing: 0.1em;
    color: var(--accent-deep);
    text-decoration: none;
    border-radius: 3px;
    touch-action: manipulation;
}

/* Feedback now lives on the link itself, since the row no longer presses. */
.tb-job__go:active {
    color: #fff;
    background: var(--accent);
}

.tb-job__tasks {
    margin-top: 8px;
    font-size: 14px;
    line-height: 1.35;
    color: var(--ink);
}

/* ⚠️ TASK CODES GOT LONGER WHEN THEY BECAME WORDS. "TLT,LKO,FCTI,QC" is 15
   characters; the same job now reads "Toilet Problems, Outside Leak, Faucet
   Inside, Dispatch Quality Check" - 68, and the measured worst case is around
   126 (four codes, longest description 30 chars).

   On the LIST that has to be capped or the tasks push the job itself off a
   phone screen - the thing a tech is scanning for. Two lines is enough to
   recognise a job and the full text is one tap away in NOTES.

   Deliberately a MODIFIER, not the base class: the detail page uses
   .tb-job__tasks too, and there the whole point is to show everything. */
.tb-job__tasks--clamp {
    display: -webkit-box;
    -webkit-line-clamp: 2;
    -webkit-box-orient: vertical;
    overflow: hidden;
}

/* THE TASK LINE AS THE LEAD (Kirk, 2026-08-21): "move the task line up above
   the dispatch # and PO ... And make it bolder?"

   It used to sit under the address, below both ID blocks - so the first thing a
   tech read was a dispatch number and the actual work was fourth. Now it is
   directly under the site name, and heavier, because this is the line that says
   whether you brought the right van.

   ⚠️ MUST COME AFTER --clamp in the file. The list card carries both classes and
   --clamp sets `display: -webkit-box`; whichever is written later wins on equal
   specificity, so this rule deliberately does NOT set display and cannot undo
   the clamp. */
.tb-job__tasks--lead {
    margin-top: 6px;
    font-weight: 600;
    color: var(--ink);
}

.tb-job__tags {
    display: flex;
    flex-wrap: wrap;
    align-items: center;
    gap: 6px;
    margin-top: 10px;
}

.tb-tag--wo {
    font-weight: 700;
    background: var(--ink);
    color: var(--paper);
    border-color: var(--ink);
}

.tb-tag--nte {
    border-color: var(--ok);
    color: var(--ok);
}

.tb-job__foot {
    display: flex;
    align-items: center;
    justify-content: space-between;
    margin-top: 10px;
    padding-top: 8px;
    border-top: 2px dashed var(--line);
}

/* WHEN THE JOB IS DUE - the last line a tech reads before tapping in.
   Kirk, 2026-08-18: "a little small/light for how important it is - we have
   room." It was 12px --ink-soft at weight 400, quieter than the address above
   it, for the one fact that decides whether they go now.

   16px / 600 / --ink, matching .tb-ticket__time on the billing card exactly.
   The two cards show the SAME job, so the time must not change weight between
   them. The relative line ("1 DAY LATE") still leads - this is the absolute a
   tech checks against their own watch. */
.tb-job__abs {
    font-family: var(--font-mono);
    font-weight: 600;
    font-size: 16px;
    color: var(--ink);
}

/* CALL TAKEN sits in the same footer on the DETAIL page and is context, not the
   appointment. Without this it would inherit the promotion above and two
   different facts would shout equally. */
.tb-job__abs--soft {
    font-weight: 400;
    font-size: 12px;
    color: var(--ink-soft);
}

/* ------------------------------------------------------------
   Invoice day blocks - NOW vs everything billed before
   ------------------------------------------------------------ */

.tb-dayhead {
    display: flex;
    align-items: baseline;
    gap: 8px;
    margin: 18px 0 6px;
    padding: 3px 10px;
    background: var(--steel);
    color: var(--paper);
}

.tb-dayhead:first-of-type {
    margin-top: 0;
}

.tb-dayhead__label {
    font-family: var(--font-display);
    font-weight: 700;
    font-size: 13px;
    letter-spacing: 0.14em;
}

/* Who billed on this day - only rendered when it was more than one of them */

.tb-dayhead__crew {
    font-family: var(--font-mono);
    font-size: 11px;
    letter-spacing: 0.04em;
    opacity: 0.75;
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
}

.tb-dayhead__sum {
    margin-left: auto;
    font-family: var(--font-mono);
    font-weight: 600;
    font-size: 13px;
    flex-shrink: 0;
}

/* TODAY wears the accent - the work in front of you, not the history */

.tb-dayhead--now {
    background: var(--accent);
}

.tb-card--now {
    border-color: var(--accent);
    box-shadow: -4px 0 0 0 var(--accent);
}

/* ------------------------------------------------------------
   Job detail — one dispatch, in full.
   Section bands borrow the day-header treatment so the two
   screens read as one app. The clock is the centrepiece: a
   rail a tech can scan in the dark of a truck cab.
   ------------------------------------------------------------ */

.tb-sect {
    display: flex;
    align-items: baseline;
    gap: 8px;
    margin: 18px 0 8px;
    padding: 3px 10px;
    background: var(--steel);
    color: var(--paper);
    font-family: var(--font-display);
    font-weight: 700;
    font-size: 14px;
    letter-spacing: 0.14em;
    text-transform: uppercase;
}

.tb-sect__note {
    margin-left: auto;
    font-family: var(--font-mono);
    font-weight: 500;
    font-size: 11px;
    letter-spacing: 0.04em;
    text-transform: none;
    opacity: 0.75;
}

/* ADD, riding in the section header. Accent on a steel band so it reads as the
   one thing in that strip you can press - the band itself is a label. The
   negative margins let a 40px tap target live inside a 3px-padded header
   without making the header taller. */

/* FIND + ADD as one right-hand cluster.

   ⚠️ THE WRAPPER IS LOAD-BEARING. Both .tb-sect__note and .tb-sect__add carry
   margin-left:auto, and flex SPLITS free space between multiple auto margins -
   so a third one would have pushed FIND into the middle of the bar instead of
   next to ADD. Inside the wrapper there is no free space, so .tb-sect__add's
   own auto is inert and the two sit together. */
.tb-sect__tools {
    margin-left: auto;
    display: inline-flex;
    align-items: center;
    gap: 8px;
}

/* Outline against the steel bar, so it reads as secondary to the accent ADD -
   finding is the commoner act but adding is the consequential one. Fills in
   when a filter is on, the same on/off language the Lookup TASKS button uses. */
.tb-sect__find {
    display: inline-flex;
    align-items: center;
    gap: 5px;
    min-height: 30px;
    margin-top: -4px;
    margin-bottom: -4px;
    padding: 3px 10px;
    font-family: var(--font-display);
    font-weight: 700;
    font-size: 14px;
    letter-spacing: 0.12em;
    color: var(--paper);
    background: transparent;
    border: 2px solid rgba(243, 239, 230, 0.45);
    cursor: pointer;
    touch-action: manipulation;
}

.tb-sect__find:active {
    background: rgba(243, 239, 230, 0.16);
}

/* ⚠️ THE ON STATE NEEDS ITS OWN PRESS. `.tb-sect__find:active` is (0,2,0) and
   `.tb-sect__find--on` is (0,1,0), so without this the active rule WINS and a
   press on a lit chip flashes it pale - the one moment the button looks like the
   bug that was just fixed. Lifting toward steel reads as a press against ink. */
.tb-sect__find--on:active {
    background: var(--steel);
    border-color: var(--steel);
}

/* ⚠️ INK, NOT PAPER. This was `background: var(--paper)` and it read as a HOLE
   PUNCHED IN THE HEADER rather than a button on it - because --paper IS the page
   colour, and the chip is 30px tall on a 28px bar, so its fill spilled 1px above
   and below the steel band in exactly the shade of the page behind it. Kirk:
   "touch up the find when it's on state showing with still some background to
   clear."

   + ADD survives the identical overhang because orange is unmistakably its own
   colour, so the spill reads as a raised tab. Ink does the same job and, unlike
   the light fill invented here, it is what the REST OF THE APP already means by
   "on": .tb-tab--on, .tb-catpick__opt--on and .tb-taskpick__opt--on are all
   ink-on-paper, and .tb-lkdeep__opt--on and .tb-up__pick--on are steel - which
   would vanish on a steel bar, which is why this one takes the ink. */
.tb-sect__find--on {
    color: var(--paper);
    background: var(--ink);
    border-color: var(--ink);
}

.tb-sect__add {
    margin-left: auto;
    display: inline-flex;
    align-items: center;
    gap: 5px;
    min-height: 30px;
    margin-top: -4px;
    margin-bottom: -4px;
    margin-right: -6px;
    padding: 3px 10px;
    font-family: var(--font-display);
    font-weight: 700;
    font-size: 14px;
    letter-spacing: 0.12em;
    color: #fff;
    background: var(--accent);
    border: none;
    border-bottom: 3px solid var(--accent-deep);
    cursor: pointer;
    touch-action: manipulation;
}

.tb-sect__add:active {
    transform: translateY(1px);
    border-bottom-width: 2px;
}

.tb-sect__add:focus-visible {
    outline: 3px solid var(--paper);
    outline-offset: 1px;
}

.tb-sect__addplus {
    font-size: 18px;
    line-height: 1;
}

/* Call / email the SITE, sitting under the address because on arrival
   "where is it" and "who do I ask for" are the same question */

.tb-jd__reach {
    display: flex;
    flex-wrap: wrap;
    gap: 6px;
    margin-top: 8px;
}

.tb-jd__call {
    display: inline-flex;
    align-items: center;
    gap: 6px;
    min-height: 38px;
    padding: 4px 12px;
    font-family: var(--font-mono);
    font-size: 14px;
    color: var(--ink);
    text-decoration: none;
    background: #fff;
    border: 2px solid var(--line-hard);
    touch-action: manipulation;
}

.tb-jd__call:active {
    background: var(--ink);
    color: var(--paper);
    border-color: var(--ink);
}

.tb-jd__callicon {
    color: var(--steel);
    font-size: 15px;
}

.tb-jd__call:active .tb-jd__callicon {
    color: var(--paper);
}

/* The primary contact now carries a NAME, because the proc knows one. The
   name reads as a label (small, steel, tracked) and the number stays mono and
   full size - the number is the thing being tapped. */

.tb-jd__callwho {
    font-family: var(--font-display);
    font-weight: 700;
    font-size: 11px;
    letter-spacing: 0.1em;
    text-transform: uppercase;
    color: var(--steel);
    /* A long site contact ("SEND INVOICES TO") must not shove the number off
       the chip - the name yields, the number never does */
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
    max-width: 11ch;
}

.tb-jd__call:active .tb-jd__callwho {
    color: var(--paper);
}

.tb-jd__callnum {
    flex-shrink: 0;
}

/* The door to the rest. Deliberately the same chip shape as the call button
   beside it - it is the same kind of thing, one tap from a person. */

.tb-jd__more {
    font-family: var(--font-display);
    font-weight: 700;
    font-size: 13px;
    letter-spacing: 0.1em;
    cursor: pointer;
}

.tb-jd__morecount {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    min-width: 20px;
    height: 20px;
    padding: 0 5px;
    font-family: var(--font-mono);
    font-size: 12px;
    line-height: 1;
    color: var(--paper);
    background: var(--steel);
}

.tb-jd__more:active .tb-jd__morecount {
    color: var(--ink);
    background: var(--paper);
}

/* ------------------------------------------------------------
   THE MEANDER — stepping outward from a job

   Three shapes: a HOP (the door to somewhere else), a HIST row (one past
   visit at this site) and a SITE card (one of a customer's buildings).
   ------------------------------------------------------------ */

/* The door. Reads as a signpost rather than a button - it is a place you go,
   not an action you take, so it borrows the card's paper and gets an arrow
   instead of a label. */

.tb-hop {
    display: flex;
    align-items: center;
    gap: 12px;
    width: 100%;
    padding: 12px 14px;
    text-align: left;
    cursor: pointer;
    touch-action: manipulation;
}

.tb-hop:active {
    background: var(--ink);
    color: var(--paper);
    border-color: var(--ink);
}

.tb-hop__body {
    flex: 1;
    min-width: 0;
}

.tb-hop__label {
    display: block;
    font-family: var(--font-display);
    font-weight: 700;
    font-size: 11px;
    letter-spacing: 0.14em;
    text-transform: uppercase;
    color: var(--steel);
}

.tb-hop:active .tb-hop__label {
    color: var(--paper);
}

.tb-hop__name {
    display: block;
    margin-top: 2px;
    font-family: var(--font-display);
    font-weight: 700;
    font-size: 16px;
    color: var(--ink);
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
}

.tb-hop:active .tb-hop__name {
    color: var(--paper);
}

.tb-hop__go {
    flex-shrink: 0;
    font-size: 22px;
    color: var(--steel);
}

.tb-hop:active .tb-hop__go {
    color: var(--paper);
}

/* One past visit. COMPACT on purpose - sixty of these is a history you can
   scan; sixty full job cards is a wall you scroll past. */

.tb-hist {
    padding: 10px 14px 10px 16px;
    border-bottom: 2px dashed var(--line);
    border-left: 4px solid transparent;
    cursor: pointer;
    touch-action: manipulation;
}

.tb-hist:last-child {
    border-bottom: none;
}

.tb-hist:active {
    background: var(--accent-soft);
}

/* The same edge vocabulary the job cards use, so a finished job looks
   finished wherever it appears */

.tb-hist.tb-job--done {
    border-left-color: var(--line-hard);
}

.tb-hist.tb-job--live {
    border-left-color: var(--accent);
}

.tb-hist.tb-job--late {
    border-left-color: var(--accent-deep);
}

.tb-hist__top {
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: 10px;
}

.tb-hist__when {
    font-family: var(--font-mono);
    font-weight: 600;
    font-size: 13px;
    color: var(--ink-soft);
}

/* Same clamp reasoning as .tb-job__tasks--clamp: these are history ROWS being
   scanned, and expanded task words would push the date and tech off the row. */
.tb-hist__what {
    margin-top: 4px;
    font-family: var(--font-display);
    font-weight: 700;
    font-size: 15px;
    line-height: 1.25;
    color: var(--ink);
    display: -webkit-box;
    -webkit-line-clamp: 2;
    -webkit-box-orient: vertical;
    overflow: hidden;
}

.tb-hist__foot {
    display: flex;
    align-items: center;
    gap: 10px;
    margin-top: 6px;
}

.tb-hist__no {
    font-family: var(--font-mono);
    font-weight: 700;
    font-size: 14px;
    color: var(--ink);
}

.tb-hist__who {
    flex: 1;
    min-width: 0;
    font-size: 13px;
    color: var(--steel);
    text-transform: uppercase;
    letter-spacing: 0.04em;
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
}

.tb-hist__you {
    display: inline-block;
    margin-left: 6px;
    padding: 1px 5px;
    font-family: var(--font-display);
    font-weight: 700;
    font-size: 10px;
    letter-spacing: 0.1em;
    color: var(--paper);
    background: var(--accent-deep);
}

/* One of a customer's buildings. */

.tb-site {
    padding: 12px 14px;
    cursor: pointer;
    touch-action: manipulation;
}

.tb-site:active {
    background: var(--accent-soft);
}

.tb-site__top {
    display: flex;
    align-items: baseline;
    justify-content: space-between;
    gap: 10px;
}

.tb-site__name {
    flex: 1;
    min-width: 0;
    font-family: var(--font-display);
    font-weight: 700;
    font-size: 17px;
    color: var(--ink);
}

.tb-site__jobs {
    flex-shrink: 0;
    font-family: var(--font-mono);
    font-size: 12px;
    color: var(--steel);
}

.tb-site__addr {
    margin-top: 3px;
    font-size: 14px;
    color: var(--ink-soft);
}

.tb-site__foot {
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: 10px;
    margin-top: 8px;
    padding-top: 8px;
    border-top: 2px dashed var(--line);
}

.tb-site__last {
    font-family: var(--font-display);
    font-weight: 700;
    font-size: 12px;
    letter-spacing: 0.1em;
    color: var(--steel);
}

/* ------------------------------------------------------------
   LOOKUP — the deliberate search
   ------------------------------------------------------------ */

.tb-lkexact {
    display: flex;
    gap: 8px;
    margin: 8px 0 4px;
}

.tb-lkexact__box {
    flex: 1;
    min-width: 0;
    font-family: var(--font-mono);
    font-size: 16px; /* 16px floor or iOS zooms the page on focus */
    text-align: center;
}

/* ------------------------------------------------------------
   How hard should this search look? - the tech's two dials
   ------------------------------------------------------------ */

.tb-lkdeep {
    display: flex;
    flex-wrap: wrap;
    align-items: center;
    gap: 8px;
    margin: 0 0 10px;
}

/* Reads as a checkbox rather than a button, because it is a MODE that stays
   on - not an action. A tech should be able to tell at a glance why their
   last search took three seconds. */
.tb-lkdeep__opt {
    display: inline-flex;
    align-items: center;
    gap: 6px;
    min-height: 40px;
    padding: 6px 12px;
    font-family: var(--font-display);
    font-weight: 700;
    font-size: 13px;
    letter-spacing: 0.1em;
    color: var(--steel);
    background: transparent;
    border: 2px solid var(--line-hard);
    cursor: pointer;
    touch-action: manipulation;
}

.tb-lkdeep__opt--on {
    color: var(--paper);
    background: var(--steel);
    border-color: var(--steel);
}

.tb-lkdeep__since {
    display: inline-flex;
    gap: 6px;
}

.tb-lkdeep__yr {
    min-height: 40px;
    padding: 6px 10px;
    font-family: var(--font-mono);
    font-weight: 600;
    font-size: 13px;
    color: var(--steel);
    background: transparent;
    border: 2px solid var(--line);
    cursor: pointer;
    touch-action: manipulation;
}

.tb-lkdeep__yr--on {
    color: var(--paper);
    background: var(--accent);
    border-color: var(--accent);
}

.tb-lkexact__clear {
    flex-shrink: 0;
    width: auto;
    padding: 0 14px;
}

/* A result row is a HISTORY row that has to say WHERE as well as when - a
   search crosses customers, so the site name cannot be assumed context.

   ⚠️ THE ROW MUST BE FLEX or the SITE button stacks UNDER the body instead of
   sitting at its right edge - which is exactly what it did, rendering as a
   stray underlined link with an orphaned border. .tb-hist is a plain block
   because the site page's rows have no second control; this modifier is only
   for rows that do. */

.tb-hist--split {
    display: flex;
    align-items: stretch;
    padding-right: 0;
}

.tb-hist__body {
    flex: 1;
    min-width: 0;
    cursor: pointer;
    touch-action: manipulation;
}

.tb-lk__site {
    margin-top: 3px;
    font-family: var(--font-display);
    font-weight: 700;
    font-size: 17px;
    line-height: 1.15;
    color: var(--ink);
}

.tb-lk__cust {
    font-size: 13px;
    color: var(--steel);
    text-transform: uppercase;
    letter-spacing: 0.04em;
}

.tb-lk__addr {
    margin-top: 2px;
    font-size: 13px;
    color: var(--ink-soft);
}

/* Its own target, so a thumb aiming for the job never lands on the site.

   ⚠️ NO VERTICAL DIVIDER. It had a dashed border-left, and it read as an
   artifact: the dashes run ACROSS the list to separate rows, so one running
   up and down fights them - and because row heights vary it never lined up
   with anything. The gap and the pressed state carry it on their own. */

.tb-lk__site-btn {
    flex-shrink: 0;
    align-self: stretch;
    padding: 0 12px;
    font-family: var(--font-display);
    font-weight: 700;
    font-size: 11px;
    letter-spacing: 0.1em;
    color: var(--steel);
    background: transparent;
    border: none;
    cursor: pointer;
    touch-action: manipulation;
}

.tb-lk__site-btn:active {
    background: var(--ink);
    color: var(--paper);
}

/* ------------------------------------------------------------
   FIELD NOTES — the crew's own record on a job

   Yours are tappable and wear the accent edge; everyone else's are there to
   be read. That is not decoration: the proc refuses a write to somebody
   else's note, and a control whose whole job is to fail is worse than none.
   ------------------------------------------------------------ */

.tb-fn {
    display: flex;
    align-items: stretch;
    gap: 0;
    border-bottom: 2px dashed var(--line);
    border-left: 4px solid transparent;
}

.tb-fn:last-child {
    border-bottom: none;
}

.tb-fn--mine {
    border-left-color: var(--accent);
    background: var(--accent-soft);
}

.tb-fn__body {
    flex: 1;
    min-width: 0;
    padding: 10px 14px;
}

.tb-fn--mine .tb-fn__body {
    cursor: pointer;
    touch-action: manipulation;
}

.tb-fn--mine .tb-fn__body:active {
    background: rgba(232, 89, 12, 0.14);
}

.tb-fn__top {
    display: flex;
    align-items: center;
    gap: 8px;
    flex-wrap: wrap;
}

.tb-fn__who {
    font-family: var(--font-display);
    font-weight: 700;
    font-size: 14px;
    letter-spacing: 0.06em;
    text-transform: uppercase;
    color: var(--ink);
}

.tb-fn__you {
    padding: 1px 5px;
    font-family: var(--font-display);
    font-weight: 700;
    font-size: 10px;
    letter-spacing: 0.1em;
    color: var(--paper);
    background: var(--accent-deep);
}

.tb-fn__when {
    margin-left: auto;
    font-family: var(--font-mono);
    font-size: 12px;
    color: var(--steel);
}

/* pre-wrap: a tech's line breaks are meaning, not formatting */
/* The crew's own notes. Same sunlight reasoning as .tb-reader__text. */

.tb-fn__text {
    margin-top: 4px;
    font-size: 16px;
    font-weight: 500;
    line-height: 1.5;
    color: var(--ink);
    white-space: pre-wrap;
    overflow-wrap: anywhere;
}

.tb-fn__hint {
    margin-top: 6px;
    font-family: var(--font-display);
    font-weight: 700;
    font-size: 10px;
    letter-spacing: 0.14em;
    color: var(--steel);
}

/* WHERE THE JOB WENT NEXT - a stamp, not a sentence. Reads as a rubber stamp
   on the ticket rather than as prose, because it is a record of a state
   transition and it cannot be edited. Ruled off on the left instead of boxed:
   a filled chip here would compete with the YOU badge two lines up. */

.tb-fn__ho {
    display: flex;
    flex-wrap: wrap;
    align-items: baseline;
    gap: 6px;
    margin-top: 5px;
    padding-left: 8px;
    border-left: 3px solid var(--accent-deep);
}

.tb-fn__holbl {
    font-family: var(--font-display);
    font-weight: 700;
    font-size: 10px;
    letter-spacing: 0.14em;
    color: var(--steel);
}

.tb-fn__hoval {
    font-family: var(--font-display);
    font-weight: 700;
    font-size: 14px;
    letter-spacing: 0.06em;
    text-transform: uppercase;
    color: var(--accent-deep);
    overflow-wrap: anywhere;
}

.tb-fn__bin {
    flex-shrink: 0;
    width: 48px;
    font-size: 16px;
    color: var(--steel);
    background: transparent;
    border: none;
    border-left: 2px dashed var(--line);
    cursor: pointer;
    touch-action: manipulation;
}

.tb-fn__bin:active {
    background: var(--danger);
    color: #fff;
}

/* ---- the editor ---- */

.tb-fned {
    position: fixed;
    left: 50%;
    top: 10dvh;
    transform: translateX(-50%);
    /* % not vw - vw includes the scrollbar (see .tb-notes) */
    width: min(calc(100% - 32px), 460px);
    border-color: var(--ink);
    z-index: 61;
}

.tb-fned__head {
    display: flex;
    align-items: center;
    justify-content: space-between;
    padding: 8px 8px 8px 14px;
    font-family: var(--font-display);
    font-weight: 700;
    font-size: 15px;
    letter-spacing: 0.12em;
    background: var(--ink);
    color: var(--paper);
}

.tb-fned__x {
    width: 36px;
    height: 36px;
    font-size: 18px;
    line-height: 1;
    color: var(--paper);
    background: transparent;
    border: 2px solid rgba(243, 239, 230, 0.25);
    cursor: pointer;
    touch-action: manipulation;
}

/* 16px floor: anything smaller and iOS zooms the page on focus, which on a
   modal leaves the tech looking at the middle of a text box */
.tb-fned__box {
    display: block;
    width: calc(100% - 28px);
    margin: 14px;
    padding: 10px;
    font-family: var(--font-body);
    font-size: 16px;
    line-height: 1.45;
    color: var(--ink);
    background: #fff;
    border: 2px solid var(--line-hard);
    resize: vertical;
}

.tb-fned__box:focus {
    outline: none;
    border-color: var(--accent);
}

.tb-fned__meta {
    display: flex;
    align-items: center;
    justify-content: space-between;
    padding: 0 14px;
    font-family: var(--font-mono);
    font-size: 12px;
    color: var(--steel);
}

.tb-fned__count--low {
    color: var(--accent-deep);
    font-weight: 700;
}

.tb-fned__actions {
    display: flex;
    gap: 10px;
    padding: 12px 14px 14px;
}

.tb-fned__actions .tb-btn {
    flex: 1;
}

/* ------------------------------------------------------------
   Contact sheet — everyone on file, one tap from each of them
   ------------------------------------------------------------ */

.tb-contacts {
    position: fixed;
    left: 50%;
    top: 8dvh;
    transform: translateX(-50%);
    /* % not vw - vw includes the scrollbar (see .tb-notes) */
    width: min(calc(100% - 32px), 420px);
    max-height: 84dvh;
    display: flex;
    flex-direction: column;
    border-color: var(--ink);
    outline: none;
    z-index: 61;
}

.tb-contacts__head {
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: 10px;
    flex-shrink: 0;
    padding: 8px 8px 8px 14px;
    background: var(--ink);
    color: var(--paper);
}

.tb-contacts__headline {
    font-family: var(--font-display);
    font-weight: 700;
    font-size: 15px;
    letter-spacing: 0.12em;
}

.tb-contacts__x {
    width: 36px;
    height: 36px;
    flex-shrink: 0;
    font-size: 18px;
    line-height: 1;
    color: var(--paper);
    background: transparent;
    border: 2px solid rgba(243, 239, 230, 0.25);
    cursor: pointer;
    touch-action: manipulation;
}

.tb-contacts__site {
    flex-shrink: 0;
    padding: 10px 14px;
    border-bottom: 2px dashed var(--line);
}

.tb-contacts__sitename {
    display: block;
    font-family: var(--font-display);
    font-weight: 700;
    font-size: 15px;
    color: var(--ink);
}

.tb-contacts__sitesub {
    display: block;
    margin-top: 2px;
    font-size: 13px;
    color: var(--ink-soft);
}

/* overscroll-behavior: contain — a flick past the last contact must not drag
   the job page along behind the sheet */

.tb-contacts__list {
    flex: 1;
    min-height: 0;
    overflow-y: auto;
    overscroll-behavior: contain;
    -webkit-overflow-scrolling: touch;
}

/* FAT. A tech reads this in the truck and taps it with a work glove on, so
   the WHOLE ROW is the target - 60px of it - not the number inside it. */

.tb-contacts__row {
    display: flex;
    align-items: center;
    gap: 12px;
    width: 100%;
    min-height: 60px;
    padding: 10px 14px;
    color: var(--ink);
    text-decoration: none;
    border-bottom: 2px dashed var(--line);
    touch-action: manipulation;
}

.tb-contacts__row:last-child {
    border-bottom: none;
}

.tb-contacts__row:active {
    background: var(--ink);
    color: var(--paper);
}

.tb-contacts__icon {
    flex-shrink: 0;
    font-size: 18px;
    color: var(--steel);
}

.tb-contacts__row:active .tb-contacts__icon {
    color: var(--paper);
}

.tb-contacts__body {
    /* min-width: 0 or the ellipsis below never fires - a flex item defaults to
       min-width: auto and refuses to shrink under its own content */
    flex: 1;
    min-width: 0;
}

.tb-contacts__who {
    display: block;
    font-family: var(--font-display);
    font-weight: 700;
    font-size: 12px;
    letter-spacing: 0.1em;
    text-transform: uppercase;
    color: var(--steel);
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
}

.tb-contacts__row:active .tb-contacts__who {
    color: var(--paper);
}

.tb-contacts__val {
    display: block;
    margin-top: 1px;
    font-family: var(--font-mono);
    font-size: 17px;
    font-weight: 600;
}

/* An address is longer than a number and less scannable - smaller, and it
   WRAPS rather than ellipsizing, because half an email is no use at all */

.tb-contacts__val--mail {
    font-size: 14px;
    word-break: break-all;
}

.tb-contacts__go {
    flex-shrink: 0;
    font-family: var(--font-display);
    font-weight: 700;
    font-size: 11px;
    letter-spacing: 0.1em;
    color: var(--steel);
}

.tb-contacts__row:active .tb-contacts__go {
    color: var(--paper);
}

.tb-contacts__foot {
    flex-shrink: 0;
    padding: 10px 14px;
    border-top: 2px solid var(--line-hard);
    background: var(--card);
}

/* SET CLOCK is real but not wired yet - say so under the button rather
   than leaving a dead control to be discovered by tapping it */

/* ⚠️ THIS TEXT STANDS IN FOR A BUTTON, so it has to carry a button's weight.
   Kirk, 2026-09-10: "make this warning text BOLDER and a little larger." It is
   the only thing on screen where a tech expected to find ON MY WAY, and 13px of
   soft grey read as fine print next to the dark controls around it - which is
   how somebody taps twice and then rings the office. */
.tb-jd__soon {
    margin: 8px 0 0;
    font-size: 15px;
    font-weight: 700;
    color: var(--ink);
    text-align: center;
    line-height: 1.35;
}

/* ---- the clock rail ---- */

.tb-clock {
    padding: 14px 16px 12px;
}

.tb-clock__rail {
    position: relative;
    padding-left: 28px;
}

/* The spine. Inset top and bottom so it starts and ends INSIDE the first
   and last markers rather than sticking out past them. */

.tb-clock__rail::before {
    content: '';
    position: absolute;
    left: 6px;
    top: 10px;
    bottom: 10px;
    width: 2px;
    background: var(--line-hard);
}

.tb-clock__row {
    position: relative;
    display: flex;
    align-items: baseline;
    gap: 10px;
    min-height: 24px;
}

.tb-clock__mark {
    position: absolute;
    left: -28px;
    top: 4px;
    width: 14px;
    height: 14px;
    background: var(--card);
    border: 3px solid var(--line-hard);
}

/* Reached. Filled orange, and the label goes to full ink - the two together
   are readable at arm's length in sunlight, which colour alone is not. */

.tb-clock__row--on .tb-clock__mark {
    background: var(--accent);
    border-color: var(--accent-deep);
}

.tb-clock__name {
    font-family: var(--font-display);
    font-weight: 700;
    font-size: 15px;
    letter-spacing: 0.12em;
    color: #a9a293;
}

.tb-clock__row--on .tb-clock__name {
    color: var(--ink);
}

.tb-clock__time {
    margin-left: auto;
    font-family: var(--font-mono);
    font-size: 13px;
    color: var(--ink);
}

.tb-clock__time--none {
    color: #c3bcab;
}

/* The duration BETWEEN two stamps, riding the spine */

.tb-clock__gap {
    padding: 3px 0 5px;
    font-family: var(--font-mono);
    font-size: 11px;
    letter-spacing: 0.06em;
    color: var(--steel);
}

.tb-clock__total {
    display: flex;
    align-items: baseline;
    justify-content: space-between;
    margin-top: 12px;
    padding-top: 8px;
    border-top: 2px dashed var(--line);
    font-family: var(--font-display);
    font-weight: 700;
    font-size: 14px;
    letter-spacing: 0.14em;
    color: var(--ink-soft);
}

.tb-clock__totalval {
    font-family: var(--font-mono);
    font-weight: 600;
    font-size: 15px;
    letter-spacing: 0;
    color: var(--ink);
}

/* ---- every trip anyone made ---- */

.tb-visit {
    padding: 10px 14px;
    border-bottom: 1px solid var(--line);
}

.tb-visit:last-child {
    border-bottom: none;
}

/* Yours reads apart from the crew's - the same green edge the list uses
   for a job that is underway */

.tb-visit--mine {
    border-left: 4px solid var(--ok);
    background: var(--ok-soft);
}

.tb-visit__top {
    display: flex;
    align-items: center;
    gap: 8px;
}

.tb-visit__who {
    font-family: var(--font-display);
    font-weight: 700;
    font-size: 17px;
    letter-spacing: 0.02em;
    min-width: 0;
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
}

.tb-visit__you {
    flex-shrink: 0;
    font-family: var(--font-display);
    font-weight: 700;
    font-size: 11px;
    letter-spacing: 0.14em;
    color: #fff;
    background: var(--ok);
    padding: 1px 6px;
}

.tb-visit__top .tb-stamp {
    margin-left: auto;
    flex-shrink: 0;
    font-size: 12px;
}

.tb-visit__times {
    margin-top: 3px;
    font-family: var(--font-mono);
    font-size: 12px;
    color: var(--ink-soft);
    line-height: 1.5;
}

/* ---- office prose, clamped ---- */

.tb-jd__notes .tb-note + .tb-note {
    margin-top: 14px;
    padding-top: 12px;
    border-top: 2px dashed var(--line);
}

.tb-note__label {
    font-family: var(--font-display);
    font-weight: 700;
    font-size: 13px;
    letter-spacing: 0.14em;
    text-transform: uppercase;
    color: var(--steel);
    margin-bottom: 4px;
}

/* TextToHtml already emits <br> and paragraph gaps - pre-wrap here would
   double every one of them */

/* Same sunlight reasoning as .tb-reader__text - see the block there. This is
   the note preview on the job detail page, read in the same parking lot. */

.tb-note__text {
    font-size: 16px;
    font-weight: 500;
    line-height: 1.55;
    color: var(--ink);
    overflow-wrap: anywhere;
}

.tb-note__text--clamp {
    display: -webkit-box;
    -webkit-line-clamp: 6;
    -webkit-box-orient: vertical;
    overflow: hidden;
}

.tb-note__text a {
    color: var(--accent-deep);
    font-weight: 600;
}

.tb-note__more {
    display: inline-block;
    margin-top: 6px;
    padding: 4px 10px;
    font-family: var(--font-display);
    font-weight: 700;
    font-size: 12px;
    letter-spacing: 0.12em;
    color: var(--steel);
    background: transparent;
    border: 2px solid var(--line-hard);
    cursor: pointer;
    touch-action: manipulation;
}

.tb-note__more:active {
    background: var(--steel);
    color: var(--paper);
}

/* ------------------------------------------------------------
   ESC documents — the site's photo + file history
   ------------------------------------------------------------ */

/* A year band. Quieter than a section header (which is steel) because
   several of them stack down one page. */

/* The year band IS the switch. A busy site carries 265 files across several
   years, so everything but the newest starts closed. It keeps the rule the
   design already had - a dashed/solid divider with the year and the count - and
   just becomes pressable, with a tap target tall enough for a thumb. */

.tb-docyear {
    display: flex;
    align-items: baseline;
    gap: 8px;
    width: 100%;
    min-height: 40px;
    margin: 14px 0 6px;
    padding: 0 0 3px;
    background: transparent;
    border: none;
    border-bottom: 2px solid var(--line-hard);
    text-align: left;
    cursor: pointer;
    touch-action: manipulation;
}

.tb-docyear:active {
    background: rgba(78, 110, 129, 0.06);
}

.tb-docyear:focus-visible {
    outline: 3px solid var(--accent);
    outline-offset: 2px;
}

/* Points right when closed, down when open. A rotation rather than two glyphs
   so it animates and cannot get out of step with the state. */

.tb-docyear__chev {
    font-size: 13px;
    line-height: 1;
    color: var(--steel);
    transition: transform 120ms ease;
    transform-origin: 45% 55%;
}

.tb-docyear--open .tb-docyear__chev {
    transform: rotate(90deg);
}

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

.tb-docyear__label {
    font-family: var(--font-display);
    font-weight: 700;
    font-size: 17px;
    letter-spacing: 0.18em;
    color: var(--ink-soft);
}

.tb-docyear__count {
    margin-left: auto;
    font-family: var(--font-mono);
    font-size: 11px;
    color: var(--ink-soft);
}

.tb-docs {
    margin-bottom: 6px;
}

/* The row splits into OPEN (most of it) and SHARE (its own target). Two
   buttons side by side rather than nested — a button inside a button is
   invalid HTML and browsers resolve it however they like. */

.tb-docrow {
    display: flex;
    align-items: stretch;
    border-bottom: 1px solid var(--line);
}

.tb-docrow:last-child {
    border-bottom: none;
}

/* Shot on THIS job — the ones that matter most, so they get the accent edge */

.tb-docrow--job {
    border-left: 4px solid var(--accent);
}

.tb-doc {
    display: flex;
    align-items: center;
    gap: 10px;
    flex: 1;
    min-width: 0;
    min-height: 54px;
    padding: 9px 4px 9px 12px;
    background: transparent;
    border: none;
    text-align: left;
    cursor: pointer;
    touch-action: manipulation;
    font-family: var(--font-body);
    color: var(--ink);
}

.tb-doc:active {
    background: var(--accent-soft);
}

/* Inline SVG, currentColor - the row tints its own icon and nothing is
   fetched over the network */

.tb-doc__glyph {
    display: flex;
    align-items: center;
    justify-content: center;
    flex-shrink: 0;
    width: 30px;
    color: var(--steel);
}

.tb-ic {
    width: 22px;
    height: 22px;
}

/* Type-coloured, so a list of twenty files is scannable by shape AND hue */

.tb-doc__glyph--image {
    color: var(--steel);
}

.tb-doc__glyph--pdf {
    color: var(--danger);
}

.tb-doc__glyph--video {
    color: var(--accent-deep);
}

.tb-doc__glyph--audio {
    color: var(--ok);
}

.tb-doc__glyph--text,
.tb-doc__glyph--file {
    color: #8d8677;
}

.tb-docrow--job .tb-doc__glyph--image {
    color: var(--accent);
}

/* ".JPG" - said out loud on the row */

.tb-doc__ext {
    font-weight: 700;
    color: var(--ink-soft);
    letter-spacing: 0.04em;
}

/* A full-height 52px target with its own hairline, so a thumb reaching for
   SHARE can never open the viewer by mistake */

.tb-doc__share {
    display: flex;
    align-items: center;
    justify-content: center;
    flex-shrink: 0;
    width: 52px;
    padding: 0;
    background: transparent;
    border: none;
    border-left: 1px solid var(--line);
    cursor: pointer;
    touch-action: manipulation;
}

/* THE JUMP TO THE DEVICE. Same 52px column as share and the bin so the row's
   right edge stays a regular grid however many controls a given file earns -
   only some have a device, and only today's have a bin.

   Carries the SAME ▫ glyph as the equipment card's photo chip, deliberately:
   that mark means "this unit" in both directions, so a tech who has tapped it
   once on a card already knows what it does here. */
.tb-doc__equip {
    display: flex;
    align-items: center;
    justify-content: center;
    flex-shrink: 0;
    width: 52px;
    padding: 0;
    background: transparent;
    border: none;
    border-left: 1px solid var(--line);
    cursor: pointer;
    touch-action: manipulation;
}

.tb-doc__equip:active {
    background: var(--accent-soft);
}

.tb-doc__equipicon {
    font-size: 17px;
    line-height: 1;
    color: var(--steel);
}

/* THE JOB THIS PHOTO IS FROM - the same control, one row over, for an
   attachment belonging to a DIFFERENT dispatch (Kirk, 2026-08-21). Identical
   geometry to the equipment box on purpose: it is the same gesture, and the two
   can never appear on one row (equipment needs a counter on this site's device
   list, this needs a dispatch number that is not the one we are on).

   The icon takes the ACCENT rather than steel - the one visual difference,
   because this one leaves the page and the equipment box does not. */
.tb-doc__job {
    display: flex;
    align-items: center;
    justify-content: center;
    flex-shrink: 0;
    width: 52px;
    padding: 0;
    background: transparent;
    border: none;
    border-left: 1px solid var(--line);
    cursor: pointer;
    touch-action: manipulation;
}

.tb-doc__job:active {
    background: var(--accent-soft);
}

.tb-doc__jobicon {
    font-size: 17px;
    line-height: 1;
    color: var(--accent);
}

.tb-doc__share:active {
    background: var(--steel);
}

.tb-doc__shareicon {
    font-size: 20px;
    line-height: 1;
    color: var(--steel);
}

.tb-doc__share:active .tb-doc__shareicon {
    color: var(--paper);
}

/* SENT / COPIED — the confirmation, sized to fit the same 52px slot */

.tb-doc__shared {
    font-family: var(--font-display);
    font-weight: 700;
    font-size: 10px;
    letter-spacing: 0.08em;
    color: var(--ok);
    text-align: center;
}

.tb-doc__body {
    display: flex;
    flex-direction: column;
    min-width: 0;
    flex: 1;
}

.tb-doc__name {
    font-weight: 600;
    font-size: 15px;
    line-height: 1.25;
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
}

.tb-doc__meta {
    display: flex;
    gap: 8px;
    font-family: var(--font-mono);
    font-size: 11px;
    color: var(--ink-soft);
    /* Lets the row shrink inside .tb-doc__body instead of forcing it wide */
    min-width: 0;
    /* ⚠️ WRAP, don't just shrink. With min-width:0 and the ext/time holding
       their size, the NAME collapsed to clientWidth 0 on a 375px screen - the
       ellipsis "worked" by hiding it completely, which is worse than the
       overflow it was fixing. Wrapping keeps it on one line when it fits and
       gives it a full line when it doesn't. */
    flex-wrap: wrap;
    row-gap: 2px;
}

/* The extension and the timestamp are short and fixed - they hold their size so
   the NAME is what gives way. */

.tb-doc__ext,
.tb-doc__when {
    flex-shrink: 0;
}

/* WHO attached it. ⚠️ min-width: 0 IS LOAD-BEARING: a flex item defaults to
   min-width: auto, which refuses to shrink below its content, so
   text-overflow: ellipsis never fires and a long name pushes the row wide
   instead of truncating. ESC's legacy values are 10 characters, but our own
   uploads write the full Employee name - "Catera Cervantes-Ramirez" is 24. */

.tb-doc__by {
    min-width: 0;
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
}

.tb-doc__tag {
    flex-shrink: 0;
    font-family: var(--font-display);
    font-weight: 700;
    font-size: 10px;
    letter-spacing: 0.1em;
    color: var(--steel);
    border: 1.5px solid var(--line-hard);
    padding: 1px 5px;
}

.tb-doc__tag--job {
    color: #fff;
    background: var(--accent);
    border-color: var(--accent-deep);
}

.tb-doc__view {
    flex-shrink: 0;
    font-family: var(--font-display);
    font-weight: 700;
    font-size: 11px;
    letter-spacing: 0.08em;
    color: var(--accent-deep);
}

/* ------------------------------------------------------------
   Notes — a preview on the page, the whole thing in a reader
   ------------------------------------------------------------ */

.tb-notecard {
    display: block;
    width: 100%;
    padding: 14px 16px;
    text-align: left;
    font-family: var(--font-body);
    color: var(--ink);
    cursor: pointer;
    touch-action: manipulation;
}

.tb-notecard:active {
    background: var(--accent-soft);
}

.tb-notecard__block {
    display: block;
    padding-left: 10px;
    border-left: 3px solid var(--accent);
}

.tb-notecard__block + .tb-notecard__block {
    margin-top: 12px;
    padding-top: 10px;
    border-top: 2px dashed var(--line);
}

.tb-notecard__label {
    display: block;
    font-family: var(--font-display);
    font-weight: 700;
    font-size: 12px;
    letter-spacing: 0.14em;
    text-transform: uppercase;
    color: var(--steel);
    margin-bottom: 2px;
}

/* Clamped to five lines. The text is already whitespace-collapsed in C#, so
   these are five lines of CONTENT rather than five lines of template labels
   separated by blanks. */

.tb-notecard__prev {
    display: -webkit-box;
    -webkit-line-clamp: 5;
    -webkit-box-orient: vertical;
    overflow: hidden;
    font-size: 15px;
    line-height: 1.45;
    overflow-wrap: anywhere;
}

.tb-notecard__more {
    display: block;
    margin-top: 10px;
    font-family: var(--font-display);
    font-weight: 700;
    font-size: 13px;
    letter-spacing: 0.1em;
    color: var(--accent-deep);
    text-align: right;
}

/* The reader. Paper, not the viewer's dark — this is prose, and office notes
   read better on the same stock as the rest of the ticket. */

.tb-reader {
    position: fixed;
    inset: 0;
    z-index: 200;
    display: flex;
    flex-direction: column;
    background: var(--paper);
    outline: none;
}

.tb-reader__bar {
    display: flex;
    align-items: center;
    gap: 10px;
    flex-shrink: 0;
    padding: 8px 10px;
    background: var(--ink);
    color: var(--paper);
    border-bottom: 2px solid var(--accent);
}

.tb-reader__x {
    width: 40px;
    height: 40px;
    flex-shrink: 0;
    font-size: 20px;
    line-height: 1;
    color: var(--paper);
    background: transparent;
    border: 2px solid rgba(243, 239, 230, 0.25);
    cursor: pointer;
    touch-action: manipulation;
}

.tb-reader__title {
    font-family: var(--font-display);
    font-weight: 700;
    font-size: 17px;
    letter-spacing: 0.1em;
}

/* overscroll-behavior: contain — without it a flick past the end of a 7k-char
   note drags the job page along behind the reader */

/* ⚠️ SUNLIGHT. This is the screen a tech reads standing in a parking lot in
   Houston, on a phone that has thermally dimmed itself, wearing sunglasses.
   Three things were measured before changing anything:

   - CONTRAST WAS NEVER THE PROBLEM. Ink #16232d on card #fffdf7 measures
     15.73:1. Pure black on pure white is 21:1, so there was no meaningful
     colour left to win - and pure white is WORSE outdoors, because a brighter
     white reflects more of the sun straight back at the eye.
   - WHAT FAILS IS STROKE MASS. Barlow 400 has narrow stems; anti-aliasing
     renders them as partial-coverage pixels, and glare eats those first. Going
     to 500 (a real cut - index.html loads 400/500/600, so nothing is
     synthesised) puts more actual ink on the glass, which is the thing that
     survives.
   - THE READER WAS THE DIMMEST SURFACE IN THE APP. It sat on --paper
     (#f3efe6) while the popup sat on --card (#fffdf7). The screen built for
     careful reading had LESS luminance than the one built for a glance. */

.tb-reader__body {
    flex: 1;
    min-height: 0;
    overflow-y: auto;
    overscroll-behavior: contain;
    -webkit-overflow-scrolling: touch;
    padding: 18px 16px calc(18px + env(safe-area-inset-bottom));
    background: var(--card);
}

.tb-reader__label {
    font-family: var(--font-display);
    font-weight: 700;
    font-size: 13px;
    letter-spacing: 0.14em;
    text-transform: uppercase;
    color: #fff;
    background: var(--steel);
    padding: 3px 10px;
    margin: 20px 0 10px;
}

.tb-reader__label:first-child {
    margin-top: 0;
}

/* TextToHtml already emits <br> and paragraph gaps — pre-wrap here would
   double every one of them */

/* 18px/500 with generous leading. The extra line-height matters BECAUSE the
   weight went up - heavier text at tight leading clots into grey bands at
   arm's length, which undoes the whole point. Colour is stated explicitly
   rather than inherited, so a future parent change cannot quietly soften the
   one screen that must never be soft. */

.tb-reader__text {
    font-size: 18px;
    font-weight: 500;
    line-height: 1.62;
    color: var(--ink);
    overflow-wrap: anywhere;
}

/* Long office notes run to 6.8k characters. On a tablet an unconstrained
   measure gives 90+ character lines, which is where the eye loses its place
   returning to the left edge. Phones are unaffected - 375px is already well
   under the cap. */

.tb-reader__text,
.tb-reader__label {
    max-width: 62ch;
}

.tb-reader__text a {
    color: var(--accent-deep);
    font-weight: 600;
}

.tb-reader__foot {
    flex-shrink: 0;
    padding: 10px 16px calc(10px + env(safe-area-inset-bottom));
    border-top: 2px solid var(--line-hard);
    background: var(--card);
}

/* ------------------------------------------------------------
   Document viewer — full-screen, dark, out of the way
   ------------------------------------------------------------ */

.tb-dv {
    position: fixed;
    inset: 0;
    z-index: 200;
    display: flex;
    flex-direction: column;
    background: #0d1419;
    outline: none;
}

.tb-dv__bar {
    display: flex;
    align-items: center;
    gap: 8px;
    flex-shrink: 0;
    padding: 8px 10px;
    background: var(--ink);
    border-bottom: 2px solid var(--accent);
}

.tb-dv__spacer {
    flex: 1;
}

.tb-dv__x {
    width: 40px;
    height: 40px;
    flex-shrink: 0;
    font-size: 20px;
    line-height: 1;
    color: var(--paper);
    background: transparent;
    border: 2px solid rgba(243, 239, 230, 0.25);
    cursor: pointer;
    touch-action: manipulation;
}

.tb-dv__count {
    font-family: var(--font-mono);
    font-size: 13px;
    color: rgba(243, 239, 230, 0.7);
}

.tb-dv__act {
    display: inline-flex;
    align-items: center;
    min-height: 36px;
    padding: 4px 12px;
    font-family: var(--font-display);
    font-weight: 700;
    font-size: 13px;
    letter-spacing: 0.1em;
    color: var(--paper);
    text-decoration: none;
    background: transparent;
    border: 2px solid rgba(243, 239, 230, 0.35);
    cursor: pointer;
    touch-action: manipulation;
}

.tb-dv__act:active {
    background: var(--accent);
    border-color: var(--accent);
}

/* SHARE is the point of the bar, so it reads as the primary action there */

.tb-dv__act--share {
    border-color: var(--accent);
    color: var(--accent);
}

/* The stage fills what's left. position:relative so the caption can sit over
   the media rather than stealing height from it. */

.tb-dv__stage {
    position: relative;
    flex: 1;
    display: flex;
    align-items: center;
    justify-content: center;
    min-height: 0;
    overflow: auto;
}

.tb-dv__img {
    max-width: 100%;
    max-height: 100%;
    object-fit: contain;
}

.tb-dv__vid {
    max-width: 100%;
    max-height: 100%;
}

.tb-dv__audio {
    display: flex;
    flex-direction: column;
    align-items: center;
    gap: 14px;
    padding: 24px;
    width: 100%;
    max-width: 520px;
}

.tb-dv__audioname {
    font-family: var(--font-display);
    font-weight: 700;
    font-size: 18px;
    color: var(--paper);
    text-align: center;
}

.tb-dv__audio audio {
    width: 100%;
}

.tb-dv__pdf {
    align-self: flex-start;
    width: 100%;
    height: 100%;
    overflow: auto;
    background: #1a252c;
}

.tb-pdf-page {
    display: block;
    width: 100%;
    height: auto;
    margin: 0 auto 10px;
    background: #fff;
}

.tb-dv__text {
    align-self: flex-start;
    width: 100%;
    height: 100%;
    margin: 0;
    padding: 14px;
    overflow: auto;
    font-family: var(--font-mono);
    font-size: 13px;
    line-height: 1.5;
    color: var(--paper);
    white-space: pre-wrap;
    word-break: break-word;
}

.tb-dv__msg {
    display: flex;
    flex-direction: column;
    align-items: center;
    gap: 12px;
    padding: 24px;
    max-width: 320px;
    font-size: 15px;
    color: rgba(243, 239, 230, 0.8);
    text-align: center;
}

/* An error over a PDF/image host has to float, or it lands below the fold */

.tb-dv__msg--bad {
    position: absolute;
    inset: 0;
    justify-content: center;
    margin: auto;
    background: rgba(13, 20, 25, 0.94);
}

.tb-dv__glyph {
    color: var(--steel);
}

.tb-dv__glyph .tb-ic {
    width: 44px;
    height: 44px;
}

.tb-dv__glyph--big .tb-ic {
    width: 72px;
    height: 72px;
}

.tb-dv__spin {
    display: inline-block;
    width: 16px;
    height: 16px;
    border: 2px solid rgba(243, 239, 230, 0.25);
    border-top-color: var(--accent);
    border-radius: 50%;
    animation: tb-dv-spin 0.8s linear infinite;
}

@keyframes tb-dv-spin {
    to { transform: rotate(360deg); }
}

@media (prefers-reduced-motion: reduce) {
    .tb-dv__spin {
        animation: none;
    }
}

/* Caption rides OVER the media, bottom-anchored. Tapping the stage hides it
   so a photo can actually be looked at. */

.tb-dv__cap {
    position: absolute;
    left: 0;
    right: 0;
    bottom: 0;
    padding: 12px 14px calc(12px + env(safe-area-inset-bottom));
    background: linear-gradient(to top, rgba(13, 20, 25, 0.94), rgba(13, 20, 25, 0));
    pointer-events: none;
}

.tb-dv__capname {
    font-family: var(--font-display);
    font-weight: 700;
    font-size: 19px;
    line-height: 1.2;
    color: var(--paper);
}

.tb-dv__capmeta {
    display: flex;
    flex-wrap: wrap;
    gap: 10px;
    margin-top: 3px;
    font-family: var(--font-mono);
    font-size: 12px;
    color: rgba(243, 239, 230, 0.75);
}

/* The modified stamp is the point of the caption - Kirk's "big deal" */

.tb-dv__capwhen {
    color: var(--accent);
    font-weight: 600;
}

.tb-dv__capjob {
    display: inline-block;
    margin-top: 6px;
    padding: 1px 7px;
    font-family: var(--font-display);
    font-weight: 700;
    font-size: 11px;
    letter-spacing: 0.12em;
    color: var(--paper);
    border: 1.5px solid rgba(243, 239, 230, 0.4);
}

/* Big edge targets - a gloved thumb, one-handed */

.tb-dv__nav {
    position: absolute;
    top: 50%;
    transform: translateY(-50%);
    width: 48px;
    height: 76px;
    font-family: var(--font-display);
    font-size: 34px;
    font-weight: 700;
    line-height: 1;
    color: var(--paper);
    background: rgba(13, 20, 25, 0.5);
    border: none;
    cursor: pointer;
    touch-action: manipulation;
}

.tb-dv__nav:active {
    background: var(--accent);
}

.tb-dv__nav:disabled {
    opacity: 0.2;
    cursor: default;
}

.tb-dv__nav--prev {
    left: 0;
}

.tb-dv__nav--next {
    right: 0;
}

/* ------------------------------------------------------------
   Attach a photo or a file — the upload sheet
   ------------------------------------------------------------ */

/* Bottom-anchored, not centred. This is a THUMB surface: the doors and the
   ATTACH button want to be in the bottom third of a phone, and a sheet that
   rises from the edge it was summoned from reads as an action rather than an
   interruption. Rides the same .tb-scrim as the notes panel and the sign-out
   confirm, one z-index above it. */

.tb-up {
    position: fixed;
    left: 50%;
    bottom: 0;
    transform: translateX(-50%);
    /* % not vw — vw includes the scrollbar (see .tb-notes) */
    width: min(100%, 520px);
    max-height: 92dvh;
    display: flex;
    flex-direction: column;
    background: var(--card);
    border: 2px solid var(--ink);
    border-bottom: none;
    padding-bottom: env(safe-area-inset-bottom);
    z-index: 61;
    overflow-y: auto;
    overscroll-behavior: contain;
}

.tb-up__head {
    display: flex;
    align-items: center;
    gap: 10px;
    flex-shrink: 0;
    padding: 10px 12px;
    background: var(--ink);
    color: var(--paper);
    font-family: var(--font-display);
    font-weight: 700;
    font-size: 17px;
    letter-spacing: 0.12em;
    text-transform: uppercase;
}

.tb-up__x {
    margin-left: auto;
    width: 38px;
    height: 38px;
    flex-shrink: 0;
    font-size: 18px;
    line-height: 1;
    color: var(--paper);
    background: transparent;
    border: 2px solid rgba(243, 239, 230, 0.25);
    cursor: pointer;
    touch-action: manipulation;
}

.tb-up__doors {
    padding: 12px;
    display: flex;
    flex-direction: column;
    gap: 10px;
}

/* ⚠️ THE DOORS GO QUIET WHILE WE READ THE FILE, AND THAT IS A BUG FIX.

   Reading a picked photo — decode, resize, re-encode, and for a HEIC a WASM
   conversion on top — is seconds on a mid-range Android. This branch of the
   sheet used to render nothing for that, so the doors sat looking untouched
   and a tech reported "I select the photo, then I have to hit back and
   attach". The inputs carry `disabled` too, so a second tap cannot open a
   second picker; this is the part that SAYS so. */

.tb-up__doors--busy {
    opacity: 0.45;
    pointer-events: none;
}

.tb-up__working {
    display: flex;
    align-items: center;
    gap: 10px;
    margin: 0 12px 12px;
    padding: 10px 12px;
    background: var(--accent-soft);
    border-left: 3px solid var(--accent);
}

.tb-up__workingtext {
    display: flex;
    flex-direction: column;
    gap: 2px;
    font-family: var(--font-mono);
    font-size: 12px;
    letter-spacing: 0.06em;
    color: var(--ink);
}

.tb-up__workinghint {
    font-size: 11px;
    letter-spacing: 0.02em;
    text-transform: none;
    color: var(--ink-soft);
}

/* Its own spinner rather than tb-dv__spin: that one's ring is paper-coloured
   for the dark full-screen viewer and would all but disappear on this light
   sheet, leaving a lone accent arc chasing itself. Same motion, this palette,
   and the same reduced-motion respect the viewer's has. */

.tb-up__spin {
    flex: 0 0 auto;
    display: inline-block;
    width: 16px;
    height: 16px;
    border: 2px solid var(--line-hard);
    border-top-color: var(--accent);
    border-radius: 50%;
    animation: tb-dv-spin 0.8s linear infinite;
}

@media (prefers-reduced-motion: reduce) {
    .tb-up__spin {
        animation-duration: 2.4s;
    }
}

/* Each door is a LABEL wrapping its own file input, so the tap reaches the
   input natively. A JS .click() would work too, but it spends a hop of the
   user gesture — the same activation rule navigator.share lives under. */

.tb-up__door {
    display: flex;
    align-items: center;
    gap: 14px;
    min-height: 64px;
    padding: 12px 14px;
    background: var(--paper);
    border: 2px solid var(--line-hard);
    cursor: pointer;
    touch-action: manipulation;
}

.tb-up__door:active {
    background: var(--accent-soft);
    border-color: var(--accent);
}

/* ⚠️ THE CAMERA DOOR CARRIES NO ACCENT. THIS IS THE SECOND ATTEMPT AT THIS.

   It used to be accent-bordered to say "the one you want 90% of the time".
   The first fix swapped its ◉ glyph for a square and added a › to every door,
   on the theory that the glyph was what made it read as a chosen radio. It
   was not enough. Kirk, tapping FROM THE GALLERY: "take the orange 'default
   selected' around the add ... so we dont have to care it looks like wrong
   one is selected when i tap it."

   The reason is :active - a tapped door goes accent-bordered too, so for the
   moment a finger is down there are TWO orange doors on screen and the eye
   cannot tell the permanent emphasis from the live feedback. Any persistent
   colour on one of three peers loses that fight.

   Position is emphasis enough: the camera is first, and first is where a
   thumb goes. The accent now means exactly one thing on this sheet - "you are
   touching this right now". */

/* Every door goes somewhere - the same chevron the job cards and site rows
   use, so a tech already knows what it means. */

.tb-up__go {
    margin-left: auto;
    flex-shrink: 0;
    font-size: 22px;
    color: var(--steel);
}

/* Visually gone, but still focusable and clickable. display:none makes some
   browsers refuse to open the picker at all. */

.tb-up__file {
    position: absolute;
    width: 1px;
    height: 1px;
    opacity: 0;
    overflow: hidden;
}

.tb-up__glyph {
    font-size: 26px;
    line-height: 1;
    color: var(--steel);
    flex-shrink: 0;
    width: 30px;
    text-align: center;
}

/* The glyph follows the same rule - see the note above .tb-up__door--camera.
   All three doors are peers until one is touched. */

.tb-up__body {
    display: flex;
    flex-direction: column;
    gap: 2px;
    min-width: 0;
}

.tb-up__label {
    font-family: var(--font-display);
    font-weight: 700;
    font-size: 18px;
    letter-spacing: 0.08em;
    color: var(--ink);
}

.tb-up__hint {
    font-size: 13px;
    color: var(--ink-soft);
}

/* ---- the naming stage ---- */

.tb-up__meta {
    display: flex;
    align-items: center;
    gap: 12px;
    padding: 12px;
    border-bottom: 2px dashed var(--line);
}

/* A data: URL, so there is no object URL to revoke — the preview goes with the
   DOM node. See FilePrep.Picked.PreviewUrl. */

.tb-up__thumb {
    width: 84px;
    height: 84px;
    flex-shrink: 0;
    object-fit: cover;
    background: var(--ink);
    border: 2px solid var(--line-hard);
}

.tb-up__bigglyph {
    width: 84px;
    height: 84px;
    flex-shrink: 0;
    display: flex;
    align-items: center;
    justify-content: center;
    background: var(--paper);
    border: 2px solid var(--line-hard);
    font-size: 34px;
}

.tb-up__facts {
    display: flex;
    flex-direction: column;
    gap: 4px;
    min-width: 0;
}

.tb-up__ext {
    font-family: var(--font-display);
    font-weight: 700;
    font-size: 20px;
    letter-spacing: 0.1em;
    color: var(--ink);
}

.tb-up__size {
    display: flex;
    flex-direction: column;
    gap: 3px;
    font-family: var(--font-mono);
    font-size: 13px;
    color: var(--ink-soft);
}

/* "RESIZED FROM 4.1 MB · 1280×960". A tech who took a big photo and sees a
   small number should be told what happened to it, not left guessing. */

.tb-up__shrunk {
    font-size: 11px;
    letter-spacing: 0.04em;
    color: var(--ok);
}

.tb-up .tb-label {
    padding: 12px 12px 0;
    margin-bottom: 4px;
}

.tb-up .tb-input {
    width: calc(100% - 24px);
    margin: 0 12px 12px;
}

.tb-up__where {
    display: flex;
    gap: 10px;
    padding: 0 12px 12px;
}

/* THREE targets fit a 375px phone only just: at 8px gaps that is about 108px
   per button, and "EQUIPMENT" is the longest word any of them carries. Tightened
   here rather than everywhere, so the two-button case keeps its roomier setting.
   min-width:0 lets them actually share - a flex item defaults to min-width:auto
   and would otherwise refuse to go narrower than its own text. */
.tb-up__where--three {
    gap: 8px;
}

.tb-up__where--three .tb-up__pick {
    min-width: 0;
    padding: 8px 2px;
    font-size: 13px;
    letter-spacing: 0.04em;
}

.tb-up__pick {
    flex: 1;
    display: flex;
    flex-direction: column;
    align-items: center;
    gap: 2px;
    min-height: var(--tap);
    padding: 8px 6px;
    font-family: var(--font-display);
    font-weight: 700;
    font-size: 15px;
    letter-spacing: 0.08em;
    color: var(--ink);
    background: var(--paper);
    border: 2px solid var(--line-hard);
    cursor: pointer;
    touch-action: manipulation;
}

.tb-up__pick--on {
    color: #fff;
    background: var(--steel);
    border-color: var(--steel);
}

.tb-up__picksub {
    font-family: var(--font-mono);
    font-weight: 500;
    font-size: 10px;
    letter-spacing: 0.04em;
    opacity: 0.8;
}

.tb-up__err {
    margin: 0 12px 12px;
    padding: 10px 12px;
    font-size: 14px;
    line-height: 1.45;
    color: #fff;
    background: var(--danger);
}

.tb-up__foot {
    display: flex;
    gap: 10px;
    flex-shrink: 0;
    padding: 0 12px 12px;
}

.tb-up__foot .tb-btn {
    flex: 1;
}

/* The row a tech just added. Six seconds of accent so the eye finds it in a
   list banded by year that may have scrolled — "did that work" is the only
   question they have at that moment. */

.tb-docrow--fresh {
    background: var(--accent-soft);
    box-shadow: inset 4px 0 0 var(--accent);
}

/* Remove a file. Sits beside SHARE as its own control - never nested inside
   the row's button, because a button inside a button is invalid HTML and
   browsers resolve it unpredictably. Muted until pressed: it is the rarest
   action on the row and should not compete with tapping the photo. */

.tb-doc__bin {
    flex-shrink: 0;
    width: 42px;
    align-self: stretch;
    background: transparent;
    border: none;
    border-left: 2px dashed var(--line);
    cursor: pointer;
    touch-action: manipulation;
}

.tb-doc__bin:active {
    background: rgba(201, 42, 42, 0.1);
}

.tb-doc__bin:disabled {
    opacity: 0.4;
    cursor: default;
}

.tb-doc__binicon {
    font-size: 15px;
    line-height: 1;
    opacity: 0.55;
}

.tb-doc__bin:active .tb-doc__binicon {
    opacity: 1;
}

/* BACK TO TOP. The photo list is the last block on the page and a busy site
   runs to dozens of rows across several years, so the bottom can be a long way
   from the top. A plain anchor - the browser already knows how to do this. */

.tb-totop {
    display: flex;
    align-items: center;
    justify-content: center;
    min-height: var(--tap);
    margin-top: 10px;
    font-family: var(--font-display);
    font-weight: 700;
    font-size: 16px;
    letter-spacing: 0.12em;
    text-transform: uppercase;
    color: var(--steel);
    text-decoration: none;
    border: 2px dashed var(--line-hard);
}

.tb-totop:active {
    background: rgba(78, 110, 129, 0.08);
}

/* ------------------------------------------------------------
   Sign-out confirm
   ------------------------------------------------------------ */

.tb-confirm {
    position: fixed;
    left: 50%;
    top: 22dvh;
    transform: translateX(-50%);
    /* % not vw - vw includes the scrollbar (see .tb-notes) */
    width: min(calc(100% - 32px), 380px);
    border-color: var(--ink);
    z-index: 61;
}

.tb-confirm__head {
    font-family: var(--font-display);
    font-weight: 700;
    font-size: 17px;
    letter-spacing: 0.12em;
    padding: 10px 14px;
    background: var(--ink);
    color: var(--paper);
}

.tb-confirm__body {
    padding: 14px;
    font-size: 15px;
    line-height: 1.45;
    color: var(--ink);
}

.tb-confirm__actions {
    display: flex;
    gap: 10px;
    padding: 0 14px 14px;
}

/* The FIND box. 16px is not cosmetic - anything smaller and iOS Safari zooms
   the page on focus and never zooms back, the same rule the equipment sheet's
   inputs live under. */
.tb-eqfind__box {
    width: 100%;
    font-size: 16px;
}

.tb-eqfind__hint {
    margin: 8px 0 0;
    font-size: 12px;
    font-weight: 500;
    line-height: 1.35;
    color: var(--steel);
}

.tb-confirm__actions .tb-btn {
    flex: 1;
}

/* ------------------------------------------------------------
   The clock - cancel link, error line, and the confirm sheet
   ------------------------------------------------------------ */

/* Cancelling a visit is real but rare. It reads as a sentence rather than a
   second button, so it can never compete with the ordinary forward step for a
   gloved thumb - but it is still a full-width 44px tap target. */
.tb-clock__cancel {
    display: block;
    width: 100%;
    margin-top: 10px;
    padding: 12px 8px;
    min-height: 44px;
    background: none;
    border: none;
    font-family: var(--font-body);
    font-size: 13px;
    color: var(--steel);
    text-decoration: underline;
    text-underline-offset: 3px;
    cursor: pointer;
}

.tb-clock__cancel:active {
    color: var(--ink);
}

.tb-clock__err {
    margin: 10px 0 0;
    padding: 10px 12px;
    border-left: 3px solid var(--danger);
    background: rgba(201, 42, 42, 0.06);
    font-size: 14px;
    line-height: 1.4;
    color: var(--ink);
}

/* The confirm sheet grows a note box, so it needs room the sign-out one does
   not - and it must stay inside a short phone screen with the keyboard up. */
.tb-clockc {
    top: 12dvh;
    max-height: 76dvh;
    overflow-y: auto;
    overscroll-behavior: contain;
}

.tb-clockc__warn {
    margin: 10px 0 0;
    font-weight: 700;
    color: var(--danger);
}

.tb-clockc__lbl {
    display: block;
    margin: 14px 0 6px;
    font-family: var(--font-display);
    font-size: 11px;
    letter-spacing: 0.12em;
    color: var(--steel);
}

/* ══ TRANSFER FOR ═════════════════════════════════════════════════════════
   Seven words, asked once, at the only moment the tech knows the answer.

   A wrapping grid rather than fixed columns: the labels run from "PARTS" to
   "SUBCONTRACTOR" and forcing those into equal cells would either clip the long
   one or leave the short ones swimming. auto-fit lets each chip be its own
   width and the row break where it needs to.

   ⚠️ THE SHEET ALREADY SCROLLS (.tb-clockc is max-height 76dvh, overflow auto),
   which is what makes it safe to add a block this tall to a dialog that also
   holds a textarea and two buttons - on a short phone the tech scrolls to
   FINISH rather than losing it off the bottom. */
.tb-xfer {
    display: flex;
    flex-wrap: wrap;
    gap: 6px;
}

.tb-xfer__opt {
    min-height: 40px;
    padding: 7px 11px;
    font-family: var(--font-display);
    font-weight: 700;
    font-size: 13px;
    letter-spacing: 0.06em;
    color: var(--ink);
    background: var(--paper);
    border: 2px solid var(--line-hard);
    cursor: pointer;
    touch-action: manipulation;
}

.tb-xfer__opt:active {
    background: var(--accent-soft);
}

/* Chosen. Ink fill + paper text, the same "on" language as every other picker
   in the app (.tb-tab--on, .tb-catpick__opt--on, .tb-taskpick__opt--on). */
.tb-xfer__opt--on {
    color: var(--paper);
    background: var(--ink);
    border-color: var(--ink);
}

/* BILLING/DONE - "the best one" (Kirk). Green because it is the only one of the
   seven that means nothing further is owed; the other six are all hand-offs to
   somebody else. Marked in its RESTING state, so it reads as the good outcome
   before it is picked rather than only after. */
.tb-xfer__opt--done {
    color: var(--ok);
    background: var(--ok-soft);
    border-color: var(--ok);
}

/* ⚠️ Chosen-AND-done needs its own rule: --on and --done have equal specificity
   (0,1,0) and --done is written later, so without this the green would win and a
   picked BILLING/DONE would look identical to an unpicked one. Green fill,
   paper text - still recognisably the happy option, unmistakably selected. */
.tb-xfer__opt--done.tb-xfer__opt--on {
    color: var(--paper);
    background: var(--ok);
    border-color: var(--ok);
}

/* NONE - "for tech helpers who dont want to transfer to anything" (Kirk,
   2026-08-21), and the only option that writes NO note. Red border because it is
   the one pick that SUPPRESSES something: a tech should see that it is not just
   another hand-off. Border only, not a fill - it is a legitimate everyday answer
   for a helper, not a warning. */
.tb-xfer__opt--none {
    color: var(--danger);
    border-color: var(--danger);
}

/* ⚠️ Same specificity trap as --done.--on above: --none is written later than
   --on, so without this rule a PICKED None would keep its resting look and the
   tech would have no idea it had taken. Filled red, paper text. */
.tb-xfer__opt--none.tb-xfer__opt--on {
    color: var(--paper);
    background: var(--danger);
    border-color: var(--danger);
}

/* ── THE JUMP BAR ────────────────────────────────────────────────────────────
   A row of section jumps under the job card (Kirk, 2026-08-21). The job page
   runs long - notes, clock, visits, site history, equipment, up to 40
   attachments - and everything below the fold was thumb-scroll only.

   Wraps rather than scrolls sideways: four short words fit two-up on the
   narrowest handset, and a horizontally-scrolling strip hides its own contents. */
.tb-jump {
    display: flex;
    flex-wrap: wrap;
    gap: 6px;
    margin: 10px 0 4px;
}

.tb-jump__go {
    flex: 1 1 auto;
    min-height: 34px;
    padding: 6px 10px;
    font-family: var(--font-display);
    font-weight: 700;
    font-size: 12px;
    letter-spacing: 0.1em;
    color: var(--steel);
    background: var(--paper);
    border: 1px solid var(--line);
    border-radius: 3px;
    cursor: pointer;
    touch-action: manipulation;
}

.tb-jump__go:active {
    color: var(--ink);
    background: var(--accent-soft);
    border-color: var(--accent);
}

.tb-clockc__note {
    display: block;
    width: 100%;
    padding: 10px;
    border: 2px solid var(--line);
    background: var(--paper);
    font-family: var(--font-body);
    /* 16px or iOS Safari zooms the whole page on focus and never zooms back */
    font-size: 16px;
    line-height: 1.4;
    color: var(--ink);
    resize: vertical;
}

.tb-clockc__note:focus {
    outline: none;
    border-color: var(--accent);
}

.tb-notes__foot {
    flex-shrink: 0;
    padding: 10px 12px;
    border-top: 2px dashed var(--line);
    background: var(--card);
}

.tb-ticket__flag {
    display: inline-block;
    font-family: var(--font-display);
    font-weight: 700;
    font-size: 13px;
    letter-spacing: 0.06em;
    color: var(--steel);
    margin-left: 8px;
    transform: rotate(-3deg);
}

.tb-ticket__flag--hot {
    color: var(--accent-deep);
}

.tb-ticket__foot {
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: 10px;
    margin-top: 12px;
    padding-top: 10px;
    border-top: 2px dashed var(--line);
}

/* WHEN THE JOB WAS, and who took it (Kirk: "a little bit bigger", 2026-08-18).
   14 -> 16px. It is the last thing a tech reads before tapping in, and at 14px
   in a mono face it sat below the address it is meant to partner. Also lifted
   from --ink-soft to --ink: the sunlight lesson from the notes applies to any
   line somebody actually has to READ rather than glance past. */
.tb-ticket__time {
    font-family: var(--font-mono);
    font-weight: 600;
    font-size: 16px;
    color: var(--ink);
}

.tb-ticket__tech {
    color: var(--steel);
    text-transform: uppercase;
    margin-left: 4px;
}

/* ------------------------------------------------------------
   Notices
   ------------------------------------------------------------ */

.tb-error {
    background: #fdecec;
    border: 2px solid var(--danger);
    color: #7d1a1a;
    padding: 12px 14px;
    font-weight: 500;
    font-size: 15px;
    margin-bottom: 16px;
}

.tb-note {
    background: var(--accent-soft);
    border-left: 5px solid var(--accent);
    color: var(--ink);
    padding: 12px 14px;
    font-size: 14px;
    margin: 14px 0 0;
}

/* THE OFFICE SAYS DO NOT DISPATCH THIS CUSTOMER.

   Shares .tb-error's palette on purpose - a tech has seen that red mean "this
   did not work" and the recognition is worth more than a novel colour - but it
   is its OWN class, because .tb-error is a failure report and this is a
   standing fact about the customer. Two different things wearing one class is
   how a "fixed" style breaks the other caller. */

.tb-stop {
    background: #fdecec;
    border: 2px solid var(--danger);
    color: #7d1a1a;
    padding: 12px 14px;
    font-weight: 600;
    font-size: 15px;
    margin: 14px 0 0;
}

/* THE ACKNOWLEDGEMENT - a real button, not a checkbox.

   ⚠️ A NATIVE CHECKBOX WOULD BE AN 18px TARGET ON A 375px SCREEN in gloves, in
   the dark, and its label would not be part of the hit area. This is the whole
   row: 44px minimum, the box is decoration, and aria-pressed carries the state
   to a screen reader that a styled span never would. */

.tb-ack {
    display: flex;
    align-items: center;
    gap: 12px;
    width: 100%;
    min-height: 44px;
    margin: 12px 0 0;
    padding: 10px 12px;
    background: var(--paper);
    border: 2px solid var(--steel);
    text-align: left;
    cursor: pointer;
}

.tb-ack__box {
    flex: 0 0 auto;
    display: flex;
    align-items: center;
    justify-content: center;
    width: 26px;
    height: 26px;
    border: 2px solid var(--steel);
    background: transparent;
    color: transparent;
    font-weight: 700;
    line-height: 1;
}

.tb-ack__box--on {
    border-color: var(--danger);
    background: var(--danger);
    color: #fff;
}

.tb-ack__text {
    font-size: 14px;
    color: var(--ink);
}

.tb-loading {
    display: flex;
    align-items: center;
    gap: 12px;
    color: var(--ink-soft);
    font-family: var(--font-display);
    font-weight: 600;
    font-size: 18px;
    letter-spacing: 0.14em;
    text-transform: uppercase;
    padding: 26px 4px;
    justify-content: center;
}

.tb-loading::before {
    content: "";
    width: 18px;
    height: 18px;
    border: 3px solid var(--line-hard);
    border-top-color: var(--accent);
    border-radius: 50%;
    animation: tb-spin 0.8s linear infinite;
}

@keyframes tb-spin {
    to { transform: rotate(360deg); }
}

@media (prefers-reduced-motion: reduce) {
    .tb-splash__bar span,
    .tb-loading::before {
        animation-duration: 2.5s;
    }
}

/* ------------------------------------------------------------
   Blazor error bar (kept ugly on purpose - it means a crash)
   ------------------------------------------------------------ */

#blazor-error-ui {
    display: none;
    position: fixed;
    bottom: 0;
    left: 0;
    right: 0;
    background: var(--danger);
    color: #fff;
    padding: 12px 44px 12px 16px;
    font-weight: 600;
    z-index: 1000;
}

#blazor-error-ui .reload {
    color: #fff;
    margin-left: 10px;
}

#blazor-error-ui .dismiss {
    position: absolute;
    right: 14px;
    top: 12px;
    cursor: pointer;
}

/* ------------------------------------------------------------
   Selects, search, chips
   ------------------------------------------------------------ */

.tb-select {
    appearance: none;
    background-image: linear-gradient(45deg, transparent 50%, var(--ink) 50%),
        linear-gradient(135deg, var(--ink) 50%, transparent 50%);
    background-position: calc(100% - 20px) 50%, calc(100% - 14px) 50%;
    background-size: 6px 6px;
    background-repeat: no-repeat;
    padding-right: 40px;
}

.tb-label__opt {
    text-transform: none;
    letter-spacing: 0;
    color: #98917f;
    font-weight: 500;
}

.tb-searchrow {
    display: flex;
    gap: 8px;
    margin-bottom: 12px;
}

.tb-search {
    margin-bottom: 0;
    font-size: 16px;
}

.tb-btn--sq {
    width: auto;
    min-width: 72px;
    flex-shrink: 0;
}

.tb-vstoggle {
    flex-shrink: 0;
    min-height: var(--tap);
    padding: 6px 12px;
    font-family: var(--font-display);
    font-weight: 600;
    font-size: 15px;
    letter-spacing: 0.08em;
    color: var(--ink-soft);
    background: var(--card);
    border: 2px solid var(--line-hard);
    cursor: pointer;
    touch-action: manipulation;
}

.tb-vstoggle--on {
    background: var(--accent);
    border-color: var(--accent-deep);
    color: #fff;
}

.tb-chiprow {
    display: flex;
    gap: 8px;
    overflow-x: auto;
    padding: 2px 2px 10px;
    -webkit-overflow-scrolling: touch;
    scrollbar-width: none;
}

.tb-chiprow::-webkit-scrollbar {
    display: none;
}

.tb-chip-f {
    flex-shrink: 0;
    min-height: 40px;
    padding: 6px 14px;
    font-family: var(--font-display);
    font-weight: 600;
    font-size: 15px;
    letter-spacing: 0.08em;
    color: var(--ink);
    background: var(--card);
    border: 2px solid var(--line-hard);
    cursor: pointer;
    touch-action: manipulation;
}

.tb-chip-f--on {
    background: var(--ink);
    border-color: var(--ink);
    color: var(--paper);
}

/* The CATEGORIES chip - orange when a category filter is live, and the
   label swaps to the small CAT / SUBCAT readout so it fits */

.tb-chip-f--catOn {
    background: var(--accent);
    border-color: var(--accent-deep);
    color: #fff;
}

.tb-chip-f__sel {
    display: inline-block;
    font-size: 12px;
    letter-spacing: 0.05em;
    text-transform: uppercase;
    max-width: 44vw;
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
}

/* ------------------------------------------------------------
   Category picker (popup sheet over the part list)
   ------------------------------------------------------------ */

/* ══ FILTER BY TASK ═══════════════════════════════════════════════════════
   The parts CATEGORY picker's shape, which a tech already knows, plus the two
   things this job needs that one does not: MULTI-select, and a footer to
   commit it.

   Deliberately its own block rather than a variant of .tb-catpick. They look
   alike today and they are not the same control - that one picks ONE thing and
   closes on the tap; this one collects a set and waits for GO. Sharing the
   class would make the next change to either of them a coin toss. */

.tb-taskpick__scrim {
    position: fixed;
    inset: 0;
    background: rgba(22, 35, 45, 0.45);
    z-index: 40;
}

.tb-taskpick {
    position: fixed;
    left: 50%;
    top: 8dvh;
    transform: translateX(-50%);
    width: min(92vw, 480px);
    /* dvh, not vh - on iOS Safari the address bar makes vh taller than what is
       actually on screen, which would put the footer (and GO) under it. */
    max-height: 84dvh;
    display: flex;
    flex-direction: column;
    border-color: var(--ink);
    z-index: 41;
}

.tb-taskpick__head {
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: 10px;
    padding: 10px 12px;
    background: var(--ink);
    color: var(--paper);
    flex-shrink: 0;
}

.tb-taskpick__title {
    font-family: var(--font-display);
    font-weight: 700;
    font-size: 16px;
    letter-spacing: 0.14em;
}

.tb-taskpick__close {
    width: 40px;
    height: 40px;
    flex-shrink: 0;
    font-size: 17px;
    font-weight: 700;
    color: var(--paper);
    background: transparent;
    border: 2px solid rgba(243, 239, 230, 0.3);
    cursor: pointer;
    touch-action: manipulation;
}

/* The only part that scrolls, so the footer stays reachable with 69 codes
   above it. */
.tb-taskpick__body {
    overflow-y: auto;
    padding: 12px;
    flex: 1 1 auto;
}

/* TWO COLUMNS, not the category picker's free wrap. Every chip here carries a
   second line, and ragged two-line chips are far harder to scan than a grid. */
.tb-taskpick__grid {
    display: grid;
    grid-template-columns: 1fr 1fr;
    gap: 8px;
}

.tb-taskpick__opt {
    display: flex;
    flex-direction: column;
    align-items: flex-start;
    gap: 1px;
    /* min-width:0 lets a grid item be narrower than its own text, which is what
       makes the description's ellipsis possible at all. */
    min-width: 0;
    min-height: 52px;
    padding: 7px 9px;
    text-align: left;
    color: var(--ink);
    background: var(--card);
    border: 2px solid var(--line-hard);
    cursor: pointer;
    touch-action: manipulation;
}

.tb-taskpick__opt:active {
    background: var(--accent-soft);
}

.tb-taskpick__opt--on {
    background: var(--ink);
    border-color: var(--ink);
    color: var(--paper);
}

.tb-taskpick__code {
    font-family: var(--font-display);
    font-weight: 700;
    font-size: 15px;
    letter-spacing: 0.06em;
    text-transform: uppercase;
}

/* The description is the half a tech actually reads - "HSBL" means nothing,
   "Hose Bibb Leaking" means everything. Ellipsized on one line: they run to 30
   characters and a chip is about half a phone wide. */
.tb-taskpick__desc {
    max-width: 100%;
    font-size: 11px;
    font-weight: 500;
    line-height: 1.25;
    opacity: 0.78;
    white-space: nowrap;
    overflow: hidden;
    text-overflow: ellipsis;
}

.tb-taskpick__foot {
    display: flex;
    gap: 10px;
    padding: 10px 12px;
    border-top: 2px solid var(--line);
    background: var(--card);
    flex-shrink: 0;
}

.tb-taskpick__foot .tb-btn {
    flex: 1;
}

.tb-catpick__scrim {
    position: fixed;
    inset: 0;
    background: rgba(22, 35, 45, 0.45);
    z-index: 40;
}

.tb-catpick {
    position: fixed;
    left: 50%;
    top: 8dvh;
    transform: translateX(-50%);
    width: min(92vw, 480px);
    max-height: 80dvh;
    display: flex;
    flex-direction: column;
    border-color: var(--ink);
    z-index: 41;
}

.tb-catpick__head {
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: 10px;
    padding: 10px 12px;
    background: var(--ink);
    color: var(--paper);
    flex-shrink: 0;
}

.tb-catpick__title {
    font-family: var(--font-display);
    font-weight: 700;
    font-size: 16px;
    letter-spacing: 0.14em;
}

.tb-catpick__close {
    width: 40px;
    height: 40px;
    flex-shrink: 0;
    font-size: 17px;
    font-weight: 700;
    color: var(--paper);
    background: transparent;
    border: 2px solid rgba(243, 239, 230, 0.3);
    cursor: pointer;
    touch-action: manipulation;
}

.tb-catpick__body {
    overflow-y: auto;
    padding: 12px;
}

.tb-catpick__grid {
    display: flex;
    flex-wrap: wrap;
    gap: 8px;
}

.tb-catpick__opt {
    min-height: 44px;
    padding: 8px 14px;
    font-family: var(--font-display);
    font-weight: 600;
    font-size: 15px;
    letter-spacing: 0.06em;
    text-transform: uppercase;
    color: var(--ink);
    background: var(--card);
    border: 2px solid var(--line-hard);
    cursor: pointer;
    touch-action: manipulation;
}

.tb-catpick__opt:active {
    background: var(--accent-soft);
}

.tb-catpick__opt--on {
    background: var(--ink);
    border-color: var(--ink);
    color: var(--paper);
}

.tb-catpick__opt--all {
    border-color: var(--accent);
    color: var(--accent-deep);
    font-weight: 700;
}

.tb-catpick__subhead {
    font-family: var(--font-display);
    font-weight: 700;
    font-size: 14px;
    letter-spacing: 0.12em;
    color: var(--ink-soft);
    margin: 16px 0 8px;
    padding-top: 12px;
    border-top: 2px dashed var(--line);
    outline: none;
}

/* The picked category's subcats render FIRST - no divider above the top block */

.tb-catpick__subhead--first {
    margin-top: 0;
    padding-top: 0;
    border-top: none;
    color: var(--accent-deep);
}

.tb-tag {
    display: inline-block;
    font-family: var(--font-display);
    font-weight: 600;
    font-size: 12px;
    letter-spacing: 0.1em;
    padding: 1px 7px;
    border: 1.5px solid var(--steel);
    color: var(--steel);
    white-space: nowrap;
}

.tb-tag--warn {
    border-color: var(--accent-deep);
    color: var(--accent-deep);
}

/* The customer's master PO on the BILLING card. Kirk: "just not huge" — so it
   stays a tag, but a FILLED one, because among PRICE / terms / CR it is the
   only value the customer's AP department will actually match an invoice
   against.

   ⚠️ white-space is deliberately overridden. .tb-tag sets nowrap, and PONum is
   free text up to 20 characters — a tag that refuses to wrap would push the
   card sideways on a 375px screen. */

.tb-tag--po {
    font-family: var(--font-mono);
    font-weight: 700;
    letter-spacing: 0.02em;
    color: #fff;
    background: var(--accent);
    border-color: var(--accent);
    white-space: normal;
    overflow-wrap: anywhere;
}

/* The same number in the INVOICE bar, riding the dark chrome. Ink-on-paper
   would be invisible there, so it borrows the bar's own paper tone and just
   comes up to full strength. */

.tb-jobbar__po {
    color: var(--paper);
    font-weight: 700;
}

.tb-tag--ok {
    border-color: var(--ok);
    color: var(--ok);
}

/* ESC's credit rating on a customer the office has RESTRICTED - HELL NO,
   HUPAID, Bad Deb. 234 reachable customers carry one.

   ⚠️ FILLED, WHERE .tb-tag--warn IS AN OUTLINE, AND THAT IS THE WHOLE POINT.
   The softer ratings (EXPIRED 1,219 · COD 138 · PO REQ! 55 · LIENWV 12) block
   nothing and vastly outnumber these; if every rating shouted, the one that
   means "do not create this job" would be invisible in the noise. */

/* ＋ SEND SOMEONE ELSE TOO - the crew-line adder.

   Deliberately NOT a .tb-btn: it belongs to the sentence, not to the actions at
   the bottom of the page, and a full-width dark button here would read as "this
   is the thing to press next" when the thing to press next is REVIEW IT. */

.tb-madlib__add {
    display: inline-flex;
    align-items: center;
    gap: 6px;
    min-height: 44px;
    margin: 2px 0 6px;
    padding: 8px 14px;
    background: transparent;
    border: 2px dashed var(--steel);
    color: var(--steel);
    font-family: var(--font-display);
    font-weight: 600;
    font-size: 13px;
    letter-spacing: 0.08em;
    cursor: pointer;
}

.tb-madlib__add:hover {
    border-color: var(--accent);
    color: var(--accent);
}

/* Take one crew line off again. A 44px target, because it sits between two
   blanks a thumb is also aiming at. */

.tb-madlib__drop {
    min-width: 44px;
    min-height: 44px;
    margin-left: 4px;
    padding: 0 8px;
    background: transparent;
    border: none;
    color: var(--steel);
    font-size: 17px;
    line-height: 1;
    cursor: pointer;
}

.tb-madlib__drop:hover {
    color: var(--danger);
}

/* A hint that is a WARNING - a duplicate crew line, and nothing else so far.
   Same size and rhythm as .tb-madlib__hint so the sentence does not jump; only
   the colour changes, because the text is what carries the meaning. */

.tb-madlib__hint--warn {
    color: var(--danger);
    font-weight: 600;
}

.tb-tag--stop {
    color: #fff;
    background: var(--danger);
    border-color: var(--danger);
    margin-left: 6px;
}

/* The customer's labor / helper rate box on dispatch cards */

.tb-tag--rate {
    border-color: var(--ink);
    color: var(--ink);
    font-family: var(--font-mono);
    font-weight: 600;
    letter-spacing: 0.02em;
}

/* Truck / company stock split inside the badge */

.tb-stockbadge__truck {
    color: var(--ok);
    font-weight: 700;
}

.tb-stockbadge__truck--none {
    color: var(--steel);
    font-weight: 600;
}

.tb-stockbadge__co {
    color: var(--accent-deep);
    font-weight: 600;
}

.tb-ticket__loc {
    font-weight: 600;
    font-size: 15px;
    color: var(--ink-soft);
}

/* ------------------------------------------------------------
   Job context bar (invoice + parts pages)
   ------------------------------------------------------------ */

.tb-jobbar {
    display: flex;
    align-items: center;
    gap: 10px;
    background: var(--ink);
    color: var(--paper);
    margin: -20px -16px 18px;
    padding: 10px 12px;
}

/* THE BACK ARROW (Kirk: "even bigger/fatter and still centered", 2026-08-18).
   38px glyph in the same 44px tap target - the box is already the glove-sized
   minimum and the bar has no height to spare, so the GLYPH grows to fill it
   rather than the button.

   ⚠️ FLEX CENTRING, NOT line-height. It was relying on button text metrics,
   and '‹' in Barlow Condensed carries a high baseline offset - so it sat
   noticeably above centre in a box that looked symmetrical. display:flex
   centres the glyph's actual BOX, whatever its metrics, and padding:0 stops the
   button's UA padding nudging it off again.

   ⚠️ -webkit-text-stroke IS WHAT MAKES IT FATTER. Barlow Condensed only loads
   600 and 700, so weight was already maxed out and '‹' is a thin glyph at any
   weight. Stroking it in currentColor thickens the existing shape rather than
   swapping in a heavier lookalike like '❮', which renders as a different
   character on some Android builds. */
.tb-back {
    display: flex;
    align-items: center;
    justify-content: center;
    width: 44px;
    height: 44px;
    padding: 0;
    flex-shrink: 0;
    font-family: var(--font-display);
    font-size: 38px;
    font-weight: 700;
    line-height: 1;
    -webkit-text-stroke: 1.5px currentColor;
    color: var(--accent);
    background: transparent;
    border: 2px solid rgba(243, 239, 230, 0.25);
    cursor: pointer;
    touch-action: manipulation;
}

.tb-jobbar__body {
    min-width: 0;
}

.tb-jobbar__cust {
    font-family: var(--font-display);
    font-weight: 700;
    font-size: 18px;
    letter-spacing: 0.03em;
    white-space: nowrap;
    overflow: hidden;
    text-overflow: ellipsis;
}

.tb-jobbar__meta {
    font-family: var(--font-mono);
    font-weight: 500;
    font-size: 12px;
    color: rgba(243, 239, 230, 0.7);
}

/* The job's assigned tech, right-aligned. First name only now, so it no
   longer needs the ellipsis it used to live on. */

.tb-jobbar__tech {
    margin-left: auto;
    flex-shrink: 0;
    max-width: 38%;
    font-family: var(--font-display);
    font-weight: 600;
    font-size: 14px;
    letter-spacing: 0.04em;
    text-transform: uppercase;
    text-align: right;
    color: rgba(243, 239, 230, 0.75);
    white-space: nowrap;
    overflow: hidden;
    text-overflow: ellipsis;
}

/* ------------------------------------------------------------
   Invoice items
   ------------------------------------------------------------ */

.tb-item + .tb-item {
    border-top: 2px dashed var(--line);
}

.tb-item__main {
    display: flex;
    align-items: center;
    gap: 10px;
    width: 100%;
    min-height: var(--tap);
    padding: 12px 14px;
    background: transparent;
    border: none;
    font: inherit;
    color: inherit;
    text-align: left;
    cursor: pointer;
    touch-action: manipulation;
}

.tb-item--open {
    background: var(--accent-soft);
}

.tb-item--vs {
    box-shadow: inset 3px 0 0 var(--ok);
}

.tb-item__codes {
    display: flex;
    flex-direction: column;
    align-items: flex-start;
    gap: 3px;
    flex-shrink: 0;
}

.tb-item__vsflag {
    font-family: var(--font-display);
    font-weight: 700;
    font-size: 11px;
    letter-spacing: 0.12em;
    background: var(--ok);
    color: #fff;
    padding: 1px 7px;
}

.tb-item__part {
    font-family: var(--font-mono);
    font-weight: 600;
    font-size: 12px;
    background: var(--ink);
    color: var(--paper);
    padding: 3px 6px;
    white-space: nowrap;
    max-width: 110px;
    overflow: hidden;
    text-overflow: ellipsis;
}

.tb-item__body {
    flex: 1;
    min-width: 0;
    display: flex;
    flex-direction: column;
}

.tb-item__desc {
    font-weight: 800;
    font-size: 15px;
    white-space: nowrap;
    overflow: hidden;
    text-overflow: ellipsis;
}

.tb-item__math {
    font-family: var(--font-mono);
    font-size: 13px;
    color: var(--ink-soft);
}

.tb-item__wh {
    color: var(--steel);
    font-weight: 600;
}

/* The line's header: who owns it and when - "* MIKEY * 8/2 1:23p".
   Small, tight, above the description so ownership reads first. */

.tb-item__stamp {
    display: block;
    font-family: var(--font-mono);
    font-weight: 600;
    font-size: 11px;
    letter-spacing: 0.02em;
    text-transform: uppercase;
    color: var(--accent-deep);
    white-space: nowrap;
    overflow: hidden;
    text-overflow: ellipsis;
    margin-bottom: 1px;
}

/* Same fact, spelled out, when a line is open for editing */

/* The full item name, wrapping over as many lines as it needs - the row above
   it can only ever show one line of it */

.tb-edit__title {
    font-weight: 600;
    font-size: 15px;
    line-height: 1.35;
    color: var(--ink);
    padding-top: 10px;
}

.tb-edit__owner {
    font-size: 13px;
    color: var(--ink-soft);
    padding-top: 10px;
}

/* The attribution belongs to the title right above it - keep them together */

.tb-edit__title + .tb-edit__owner {
    padding-top: 3px;
}

.tb-edit__owner strong {
    color: var(--accent-deep);
    text-transform: uppercase;
}

.tb-item__notes {
    font-size: 13px;
    font-style: italic;
    color: var(--steel);
    white-space: nowrap;
    overflow: hidden;
    text-overflow: ellipsis;
}

.tb-item__amt {
    font-family: var(--font-mono);
    font-weight: 600;
    font-size: 16px;
    white-space: nowrap;
}

.tb-edit {
    padding: 4px 14px 14px;
    border-top: 2px dashed var(--line);
    background: var(--accent-soft);
}

/* How to reach whoever billed the line: ONE quiet line of text, not a stack
   of boxes. Reference material, so it stays small - but the numbers are still
   real tap targets, padded out to a thumb without pushing the line apart. */

.tb-edit__reach {
    margin-top: 3px;
    font-size: 12px;
    line-height: 2.1;   /* the padding below eats into this, so keep it roomy */
    color: var(--ink-soft);
    /* ONE line. Four numbers on a narrow phone slide sideways rather than
       stacking into a list down the panel. */
    white-space: nowrap;
    overflow-x: auto;
    -webkit-overflow-scrolling: touch;
    scrollbar-width: none;
}

.tb-edit__reach::-webkit-scrollbar {
    display: none;
}

.tb-reach__pair {
    white-space: nowrap;
}

.tb-reach {
    font-family: var(--font-mono);
    font-weight: 600;
    color: var(--accent-deep);
    text-decoration: underline;
    text-underline-offset: 2px;
    /* Vertical padding a thumb can find, without adding line height */
    padding: 8px 2px;
    margin: -8px 0;
    touch-action: manipulation;
}

.tb-reach:active {
    background: var(--accent);
    color: var(--paper);
    text-decoration: none;
}

.tb-reach__label {
    font-family: var(--font-display);
    font-weight: 700;
    font-size: 11px;
    letter-spacing: 0.08em;
    text-transform: uppercase;
}

.tb-reach__sep {
    margin: 0 5px;
    color: var(--line-hard);
}

/* Someone else's line - the banner says whose, and everything below it is
   frozen. Deliberately calm: this is a boundary, not an error. */

.tb-locked {
    display: flex;
    align-items: flex-start;
    gap: 8px;
    margin-top: 10px;
    padding: 8px 10px;
    background: var(--card);
    border-left: 4px solid var(--steel);
    font-size: 13px;
    line-height: 1.4;
    color: var(--ink-soft);
}

.tb-locked__mark {
    flex-shrink: 0;
    font-size: 14px;
    line-height: 1.3;
}

/* A frozen control must LOOK frozen - grey it and kill the press feedback */

.tb-edit input:disabled,
.tb-edit .tb-stepper button:disabled,
.tb-edit .tb-btn:disabled {
    opacity: 0.45;
    cursor: not-allowed;
    background: var(--paper);
    color: var(--ink-soft);
}

.tb-edit .tb-btn:disabled {
    border-color: var(--line-hard);
}

.tb-edit .tb-btn:disabled:active {
    transform: none;
}

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

.tb-edit__row .tb-label {
    margin: 0;
}

.tb-edit__actions {
    display: flex;
    gap: 10px;
    margin-top: 14px;
}

/* Prose inputs (descriptions, notes) - body font, more fits. 16px floor:
   iOS Safari auto-zooms the page when a focused input is smaller */
.tb-input--text {
    font-family: var(--font-body);
    font-weight: 500;
    font-size: 16px;
}

.tb-input--slim {
    margin-bottom: 0;
    max-width: 160px;
    min-height: 48px;
    font-size: 18px;
    text-align: right;
}

.tb-stepper {
    display: flex;
    align-items: stretch;
    border: 2px solid var(--line-hard);
    background: #fff;
}

.tb-stepper button {
    width: 48px;
    min-height: 48px;
    font-family: var(--font-display);
    font-size: 24px;
    font-weight: 700;
    color: var(--accent-deep);
    background: transparent;
    border: none;
    cursor: pointer;
    touch-action: manipulation;
}

.tb-stepper button:active {
    background: var(--accent-soft);
}

.tb-stepper input {
    width: 72px;
    border: none;
    border-left: 2px solid var(--line);
    border-right: 2px solid var(--line);
    font-family: var(--font-mono);
    font-weight: 600;
    font-size: 18px;
    text-align: center;
}

.tb-stepper input:focus-visible {
    outline: 3px solid var(--accent);
    outline-offset: -3px;
}

.tb-btn--sm {
    min-height: 48px;
    font-size: 16px;
    flex: 1;
}

.tb-btn--danger {
    background: transparent;
    color: var(--danger);
    border: 2px solid var(--danger);
    border-bottom-width: 2px;
}

.tb-btn--danger:active {
    transform: none;
    background: #fdecec;
}

.tb-totals {
    display: flex;
    align-items: center;
    justify-content: space-between;
    background: var(--ink);
    color: var(--paper);
    padding: 12px 16px;
    margin-bottom: 14px;
    font-family: var(--font-display);
    font-weight: 600;
    font-size: 16px;
    letter-spacing: 0.08em;
    text-transform: uppercase;
}

.tb-totals__sum {
    font-family: var(--font-mono);
    font-weight: 600;
    font-size: 22px;
    color: var(--accent);
}

/* ------------------------------------------------------------
   Part picker
   ------------------------------------------------------------ */

.tb-partrow {
    align-items: center;
}

/* Van-stock rows carry a green edge + a soft green wash; in van-stock
   mode everything else dims */

.tb-partrow--vs {
    box-shadow: inset 3px 0 0 var(--ok);
    background: var(--ok-soft);
}

.tb-partrow--dim {
    background: rgba(22, 35, 45, 0.05);
}

.tb-partrow--dim .tb-partrow__desc,
.tb-partrow--dim .tb-partrow__price {
    color: var(--ink-soft);
}

.tb-partrow--dim .tb-partrow__no {
    color: #8d9aa3;
}

.tb-partrow__no {
    display: block;
    font-family: var(--font-mono);
    font-weight: 600;
    font-size: 13px;
    color: var(--steel);
}

.tb-partrow__desc {
    display: block;
    font-weight: 600;
    font-size: 15px;
    line-height: 1.25;
}

.tb-partrow__meta {
    display: flex;
    align-items: center;
    gap: 8px;
    margin-top: 3px;
}

.tb-partrow__cat {
    font-size: 12px;
    color: var(--ink-soft);
    text-transform: uppercase;
    letter-spacing: 0.06em;
}

.tb-partrow__price {
    font-family: var(--font-mono);
    font-weight: 600;
    font-size: 16px;
    white-space: nowrap;
}

/* "No match for 'Relegator' - showing regulator" */

.tb-didyoumean {
    background: var(--accent-soft);
    border-left: 5px solid var(--accent);
    color: var(--ink);
    padding: 8px 12px;
    font-size: 14px;
    margin: 0 0 10px;
}

.tb-didyoumean strong {
    font-family: var(--font-mono);
    font-weight: 600;
}

.tb-morehint {
    text-align: center;
    color: var(--ink-soft);
    font-size: 14px;
    margin: 4px 0 16px;
}

.tb-stockline {
    display: flex;
    align-items: center;
    gap: 8px;
    min-height: 22px;
    margin: -6px 0 14px;
}

/* (the "checking stock..." state is gone - the numbers come from cache now) */

.tb-addsheet__part {
    font-family: var(--font-mono);
    font-weight: 600;
    font-size: 15px;
    background: var(--ink);
    color: var(--paper);
    display: inline-block;
    padding: 4px 10px;
    margin-bottom: 14px;
}

/* Van-stock treatment on the add sheet */

.tb-addsheet--vs {
    box-shadow: inset 0 3px 0 var(--ok), 4px 4px 0 rgba(22, 35, 45, 0.12);
}

.tb-addsheet__toprow {
    display: flex;
    align-items: center;
    gap: 8px;
    margin-bottom: 14px;
}

.tb-addsheet__toprow .tb-addsheet__part {
    margin-bottom: 0;
}

.tb-addsheet__vsbadge {
    font-family: var(--font-display);
    font-weight: 700;
    font-size: 13px;
    letter-spacing: 0.12em;
    background: var(--ok);
    color: #fff;
    padding: 3px 10px;
}

.tb-actions {
    display: flex;
    flex-direction: column;
    gap: 10px;
}

/* ------------------------------------------------------------
   One-time code (direct-web sign-in)
   ------------------------------------------------------------ */

.tb-otp__head {
    font-family: var(--font-display);
    font-weight: 700;
    font-size: 22px;
    letter-spacing: 0.1em;
    margin-bottom: 6px;
}

.tb-otp__hint {
    color: var(--ink-soft);
    font-size: 15px;
    margin: 0 0 16px;
}

.tb-otp__channels {
    display: flex;
    flex-direction: column;
    gap: 10px;
    margin-bottom: 16px;
}

.tb-otp__channel {
    justify-content: space-between;
    text-align: left;
    gap: 10px;
    min-width: 0;
}

.tb-otp__channel > span:first-child {
    flex-shrink: 0;
    white-space: nowrap;
}

/* The base .tb-btn:disabled paints solid tan - on the channel buttons that
   made BOTH look selected while the send was in flight. Disabled channels
   just dim; only the tapped one carries the selected accent. */

.tb-otp__channel:disabled {
    background: transparent;
    color: var(--ink-soft);
    border-color: var(--line-hard);
    opacity: 0.6;
}

.tb-otp__channel--on,
.tb-otp__channel--on:disabled {
    background: var(--accent-soft);
    border-color: var(--accent);
    color: var(--accent-deep);
    opacity: 1;
}

.tb-otp__channel--on .tb-otp__masked {
    color: var(--accent-deep);
}

.tb-otp__masked {
    font-family: var(--font-mono);
    font-weight: 500;
    font-size: 12px;
    letter-spacing: 0;
    text-transform: none;
    color: var(--steel);
    min-width: 0;
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
}

.tb-otp__code {
    font-size: 28px;
    letter-spacing: 0.35em;
    text-align: center;
}

.tb-check {
    display: flex;
    align-items: center;
    gap: 10px;
    font-size: 15px;
    color: var(--ink-soft);
    margin: 0 0 16px;
    cursor: pointer;
}

.tb-check input {
    width: 22px;
    height: 22px;
    accent-color: var(--accent);
    flex-shrink: 0;
}

/* Input + trailing picker button (sign-in warehouse row) */

.tb-inputrow {
    display: flex;
    gap: 8px;
    margin-bottom: 16px;
}

.tb-inputrow .tb-input {
    margin-bottom: 0;
    flex: 1;
    min-width: 0;
}

.tb-btn--pick {
    width: 56px;
    min-height: var(--tap);
    flex-shrink: 0;
    font-family: var(--font-display);
    font-weight: 700;
    font-size: 20px;
    color: var(--paper);
    background: var(--ink);
    border: none;
    border-bottom: 4px solid #000;
    cursor: pointer;
    touch-action: manipulation;
}

.tb-btn--pick:active {
    transform: translateY(2px);
    border-bottom-width: 2px;
}

.tb-btn--pick:disabled {
    background: #a9a293;
    border-bottom-color: #8d8677;
}

/* ══ EQUIPMENT IN THIS BUILDING ═══════════════════════════════════════════
   What is installed at the site, on the job page.

   Charlie's is a backflow shop - RP / PVB / DC / SVB is 75% of everything on
   file - so the card leads with the TYPE at display weight and hangs the size
   or location beside it. Make, model and serial sit underneath in body text,
   because they are what you check once you have found the thing.

   ⚠️ EVERY FIELD CAN BE BLANK. Measured over all 2,631 rows: Serial is empty on
   525, Model on 299, EquipLoc on 1,608, ID on 2,551. So nothing here reserves
   space for a field that may not come, and no rule or gap is drawn between
   lines that might both be missing.

   Weights follow the sunlight lesson from the notes work: on a phone dimmed in
   Houston sun, STROKE MASS reads before contrast does. Nothing below 500. */

.tb-eqlist {
    display: flex;
    flex-direction: column;
    gap: 2px;
}

.tb-eq {
    display: flex;
    flex-direction: column;
    gap: 1px;
    /* The anchor for the photo chip - see .tb-eq__cam, which is taken OUT of
       this column so it cannot make the card taller. */
    position: relative;
    padding: 8px 10px;
    text-align: left;
    width: 100%;
    border: 2px solid var(--line);
    background: var(--card);
    cursor: pointer;
}

/* Only a card that HAS a chip gives up the width for it, and only on the lines
   that could actually run under it - the identity row at the top is nowhere
   near it. Without this the chip would sit on top of a long serial or the
   second line of a note. */
.tb-eq--haspic > :not(.tb-eq__top):not(.tb-eq__cam) {
    padding-right: 56px;
}

.tb-eq:active {
    background: var(--paper);
}

/* The device just added or edited. Same reasoning as the fresh-document mark:
   the list is not sorted by recency, so "did that actually work" is the only
   question a tech has at that moment and the answer has to be visible. */
.tb-eq--fresh {
    border-color: var(--accent);
}

/* Type and size read TOGETHER, left to right - "PVB 1"".

   This was space-between with the photo chip as a third child in the middle,
   which is what made the chip's position wander: it was pushed around by
   whatever the two words either side happened to be, so it landed somewhere
   different on every card and read as a mistake rather than a control. The
   chip now owns the bottom-right corner and this row is purely the identity. */
.tb-eq__top {
    display: flex;
    align-items: baseline;
    gap: 10px;
    min-width: 0;
}

.tb-eq__type {
    font-family: var(--font-display);
    font-size: 19px;
    font-weight: 600;
    letter-spacing: 0.02em;
    color: var(--ink);
    text-transform: uppercase;
}

/* The size-or-location. Sits directly beside the type, in steel, so it reads as
   a qualifier rather than a second heading - "PVB 1"" is also how the trade says
   it out loud. It used to be right-aligned at the far edge, which made it look
   like a separate field and left the chip stranded between the two.

   Deliberately carries NO label: it holds pipe size on most of Charlie's rows
   and a real place on others, so any word would be wrong about half the time.

   No ellipsis and no truncation here: ESC already cut these at 10 characters
   ("Mens and W" is Mens and Women), and adding our own "..." would claim we did
   it. Show exactly what is stored. */
.tb-eq__where {
    flex: 0 0 auto;
    font-family: var(--font-display);
    font-size: 15px;
    font-weight: 600;
    letter-spacing: 0.04em;
    color: var(--steel);
    white-space: nowrap;
}

.tb-eq__make {
    font-size: 15px;
    font-weight: 500;
    line-height: 1.35;
    color: var(--ink);
}

.tb-eq__ser {
    font-size: 14px;
    font-weight: 500;
    letter-spacing: 0.02em;
    color: var(--steel);
}

/* Filled on 3% of rows, and when it is filled it is a municipal cross-connection
   or hazard number - the thing an inspector asks for. Accent-coloured because on
   the rare card that has one it is the most consequential line on it. */
.tb-eq__id {
    font-family: var(--font-display);
    font-size: 14px;
    font-weight: 600;
    letter-spacing: 0.06em;
    color: var(--accent);
    text-transform: uppercase;
}

/* ── INSTALLED / WARRANTY ────────────────────────────────────────────────
   The card's last line, and the one that fills the empty lower half a short
   device card used to end on (Kirk: "cut the gap here and use the extra
   whitespace on the control").

   WRAPS rather than scrolls or truncates: both dates together run about 34
   characters, which fits one line on a phone, and on the rare narrow case the
   second chip drops to its own row instead of being cut. Nothing here is
   ellipsized - a half-shown date is worse than no date.

   Steel and small, because this is reference material a tech checks, not the
   thing they identify the device by. The LABEL is smaller still and letter-
   spaced, so "INSTALLED 3/14/2026" reads as one fact rather than two words. */
.tb-eq__dates {
    display: flex;
    flex-wrap: wrap;
    gap: 4px 14px;
    margin-top: 2px;
}

.tb-eq__date {
    font-family: var(--font-display);
    font-size: 13px;
    font-weight: 600;
    letter-spacing: 0.02em;
    color: var(--steel);
    white-space: nowrap;
}

.tb-eq__datelbl {
    font-size: 11px;
    font-weight: 600;
    letter-spacing: 0.09em;
    opacity: 0.72;
    margin-right: 3px;
}

/* THE NOTE SNIP - two lines, then the browser's own ellipsis.

   -webkit-line-clamp is the only way to ellipsize a MULTI-line block; the
   single-line text-overflow trick cannot do it. All four properties below are
   required together, and the -webkit- prefixes are not droppable - this legacy
   combination is what Safari actually implements, and the iPhones this runs on
   are the browsers that matter.

   ⚠️ COMPUTED `display` READS BACK AS `flow-root`, NOT `-webkit-box`, AND THAT
   IS FINE. Chrome 148 normalises the legacy value onto its standard line-clamp
   implementation. It looks exactly like a flex item being BLOCKIFIED, which is
   what it was mistaken for here - and the fix that mistake suggests is a
   wrapper element that does nothing. Verified by building both shapes live: a
   -webkit-box inside a plain block parent and one as a direct flex item report
   the SAME `flow-root` and clamp IDENTICALLY. Blockification only applies to
   inline-level displays anyway, and -webkit-box is block-level; the inline one
   is -webkit-inline-box.

   Italic, because it is somebody's prose sitting under structured fields and
   the eye needs to tell them apart without a label eating a line. */
.tb-eq__note {
    display: -webkit-box;
    -webkit-line-clamp: 2;
    -webkit-box-orient: vertical;
    overflow: hidden;
    margin-top: 2px;
    font-size: 13px;
    font-weight: 500;
    font-style: italic;
    line-height: 1.35;
    color: var(--steel);
}

/* A FIND HIT. Warm paper rather than the usual card white, with an accent edge -
   Kirk: "we highlight them with a different background color on the card".

   The list is already narrowed to hits, so this is not telling a tech WHICH ones
   matched; it is telling them the list they are looking at is not the whole
   list. That is worth saying loudly on a screen whose header says 33. */
.tb-eq--hit {
    background: var(--accent-soft);
    border-left: 6px solid var(--accent);
}

/* A row with nothing but a key. 43 rows company-wide have no type, make or
   model - the office created a placeholder and never came back. Saying so turns
   a card that looks broken into an invitation. */
.tb-eq__bare {
    font-size: 13px;
    font-weight: 500;
    font-style: italic;
    color: var(--steel);
}

/* ══ ADD / EDIT A DEVICE ══════════════════════════════════════════════════
   Six fields, each capped at ESC's real column width so a tech watches
   themselves run out of room rather than finding out later that the database
   ate half of what they typed. */

/* THE SHEET MUST FIT THE VIEWPORT THAT ACTUALLY EXISTS.

   Six fields make this the tallest sheet in the app, and on a phone it pushed
   CANCEL / SAVE underneath the browser URL bar (Kirk, on an iPhone).

   Two causes, and the second was the real one:

   1. It inherited the shared `top: 22dvh`, which suits the short clock sheet but
      spends the top fifth of the screen on a form this tall. It starts higher now.

   2. THE BODY WAS SIZED IN vh, NOT dvh. On mobile Safari `vh` resolves against
      the LARGEST viewport - the one with the URL bar hidden - so while the bar is
      showing, 62vh is taller than 62% of what the tech can actually see. Add the
      top offset, the header and the action row and the buttons land off screen.
      Every other viewport measurement in this file already uses dvh; this one
      was the exception, and it was mine.

   Rather than tune numbers that drift the moment somebody adds a seventh field,
   the sheet is now a flex column capped at 80dvh: the body takes whatever is
   left and scrolls, and the actions are pinned inside that cap. SAVE stays
   reachable however tall the form grows and whatever the URL bar does. */
.tb-eqf {
    top: 10dvh;
    max-height: 80dvh;
    display: flex;
    flex-direction: column;
}

.tb-eqf__body {
    padding: 4px 18px 8px;
    /* The flex parent owns the height now - a max-height here would fight it */
    flex: 1 1 auto;
    /* REQUIRED. A flex child defaults to min-height:auto and refuses to shrink
       below its own content, so without this the form overflows the cap and the
       buttons go straight back under the URL bar. */
    min-height: 0;
    overflow-y: auto;
}

/* Pinned, and clear of the home indicator on a notched phone. */
.tb-eqf .tb-confirm__actions {
    flex: 0 0 auto;
    padding-bottom: calc(14px + env(safe-area-inset-bottom, 0px));
}

.tb-eqf__row {
    margin-bottom: 12px;
}

.tb-eqf__lbl {
    display: block;
    margin-bottom: 5px;
    font-family: var(--font-display);
    font-size: 11px;
    font-weight: 600;
    letter-spacing: 0.12em;
    color: var(--steel);
    text-transform: uppercase;
}

.tb-eqf__in {
    display: block;
    width: 100%;
    padding: 10px;
    border: 2px solid var(--line);
    background: var(--paper);
    font-family: var(--font-body);
    /* 16px or iOS Safari zooms the whole page on focus and never zooms back.
       Same rule as the note textarea - it is a viewport bug, not a taste call. */
    font-size: 16px;
    font-weight: 500;
    color: var(--ink);
}

.tb-eqf__in:focus {
    outline: none;
    border-color: var(--steel);
}

.tb-eqf__hint {
    display: block;
    margin-top: 4px;
    font-size: 12px;
    font-weight: 500;
    line-height: 1.3;
    color: var(--steel);
}

/* ── THE TWO DATES, SIDE BY SIDE ─────────────────────────────────────────
   They pair naturally and neither needs a full row, so putting them on one
   line keeps the sheet's SAVE button reachable without a scroll on a small
   phone - the same reason the photo strip scrolls sideways rather than
   wrapping.

   min-width:0 on the halves is load-bearing: a flex item defaults to
   min-width:auto, and a date input's intrinsic width is wider than half a
   narrow phone, so without it the pair overflows the sheet instead of
   sharing it. */
.tb-eqf__row--pair {
    display: flex;
    gap: 10px;
}

.tb-eqf__half {
    flex: 1 1 0;
    min-width: 0;
}

/* iOS renders an empty date input as blank and Android as a stub, so the
   control needs its own height floor to match the text fields beside it -
   otherwise the row looks broken before anything is typed. */
.tb-eqf__date {
    min-height: 44px;
    /* Safari right-aligns the native picker glyph and lets the value drift
       under it without this. */
    -webkit-appearance: none;
    appearance: none;
}

.tb-eqf__hint--dates {
    margin-top: -4px;
    margin-bottom: 12px;
}

.tb-eqf__note {
    margin: 0 0 10px;
    font-size: 14px;
    font-weight: 500;
    color: var(--steel);
}

/* Reading a device from a history door. Steel and quiet, not an error - nothing
   has gone wrong, the tech is simply somewhere that does not write. */
.tb-eqf__ro {
    margin: 0 0 10px;
    padding: 8px 10px;
    border-left: 3px solid var(--steel);
    background: var(--paper);
    font-size: 14px;
    font-weight: 500;
    line-height: 1.4;
    color: var(--steel);
}

.tb-eqf__err {
    margin: 0 0 10px;
    padding: 8px 10px;
    border-left: 3px solid var(--danger);
    background: var(--paper);
    font-size: 14px;
    font-weight: 500;
    line-height: 1.35;
    color: var(--ink);
}

/* ══ NO GEO — the header chip ═════════════════════════════════════════════
   Sits in the sync spinner's slot and appears ONLY when a tap can fix
   something: never asked, or refused. Once permission is granted it vanishes.
   A permanent badge saying everything is fine is a badge nobody reads.

   Accent-bordered rather than filled: it is a standing invitation, not an
   alarm. The office not seeing a truck is worth fixing, and is not an emergency
   in the middle of somebody's day. */

.tb-bar__geo {
    margin-left: 10px;
    padding: 3px 6px;
    border: 2px solid var(--accent);
    background: transparent;
    font-family: var(--font-display);
    font-size: 11px;
    font-weight: 600;
    letter-spacing: 0.1em;
    color: var(--accent);
    white-space: nowrap;
    cursor: pointer;
}

.tb-bar__geo:active {
    background: var(--accent);
    color: var(--paper);
}

/* ══ THE LOCATION DIALOG ══════════════════════════════════════════════════
   Same viewport discipline as the equipment sheet: a flex column capped in
   dvh, body scrolls, actions pinned clear of the home indicator. The denied
   face carries a numbered list and runs longer than the ask, so this one
   genuinely needs it. */

.tb-geo {
    top: 12dvh;
    max-height: 76dvh;
    display: flex;
    flex-direction: column;
}

.tb-geo__body {
    padding: 14px 18px 8px;
    flex: 1 1 auto;
    min-height: 0;
    overflow-y: auto;
}

.tb-geo__lead {
    margin: 0 0 12px;
    font-size: 16px;
    font-weight: 500;
    line-height: 1.45;
    color: var(--ink);
}

.tb-geo__why {
    margin: 0 0 4px;
    padding-left: 20px;
}

.tb-geo__why li {
    margin-bottom: 7px;
    font-size: 14px;
    font-weight: 500;
    line-height: 1.4;
    color: var(--ink);
}

.tb-geo__stepslead {
    margin: 0 0 8px;
    font-family: var(--font-display);
    font-size: 12px;
    font-weight: 600;
    letter-spacing: 0.1em;
    color: var(--steel);
    text-transform: uppercase;
}

/* Numbered because these are steps on somebody else's settings screen and the
   tech is going to look away from this page to follow them. */
.tb-geo__steps {
    margin: 0 0 10px;
    padding-left: 22px;
}

.tb-geo__steps li {
    margin-bottom: 9px;
    font-size: 15px;
    font-weight: 500;
    line-height: 1.4;
    color: var(--ink);
}

.tb-geo__alt,
.tb-geo__note {
    margin: 10px 0 0;
    padding: 8px 10px;
    border-left: 3px solid var(--steel);
    background: var(--paper);
    font-size: 14px;
    font-weight: 500;
    line-height: 1.4;
    color: var(--ink);
}

/* ══ EQUIPMENT PHOTOS ═════════════════════════════════════════════════════
   The camera chip on a device card, the thumbnails in the sheet, and the
   two-part add.

   The chip appears ONLY when photos exist. One on every card would say
   nothing; one that appears means somebody documented this unit - which on a
   backflow device is the difference between "we think it is a Wilkins 375" and
   a photograph of the dataplate. */

/* ⚠️ THE HIT AREA IS THE POINT, NOT THE LOOK. This chip sits INSIDE a card
   that is itself a button, so every pixel it misses opens the edit sheet
   instead of the photos - a 2px-padded chip made "just look at the photo" a
   coin toss (Kirk).

   36px tall with wide flanks gets close to the 44pt Apple asks for, and the
   NEGATIVE vertical margin means the bigger target costs the card no height:
   the box grows into space the row already had. Same trick as .tb-sect__add.

   align-self centres it against a baseline-aligned row - without that the
   taller box would drag the device type off its own baseline. */
.tb-eq__cam {
    display: inline-flex;
    align-items: center;
    /* ⚠️ ABSOLUTE, SO IT COSTS THE CARD NO HEIGHT. In flow - even as the last
       child aligned right - a 36px chip adds a 36px row, and Kirk saw exactly
       that: "the 1 or 2 - indicating photo - is making card taller. It needs to
       be aligned with last line of text. Not extending it down."

       Taking it out of the flex column means the card is sized by its WORDS and
       the chip rides the last line of them. The width it would have overlapped
       is given back by .tb-eq--haspic above, so nothing is ever underneath it.

       Offsets match the card's own padding (8px 10px) so it sits in the corner
       rather than near it. */
    position: absolute;
    right: 10px;
    bottom: 8px;
    justify-content: center;
    gap: 5px;
    min-width: 46px;
    min-height: 36px;
    padding: 0 10px;
    border: 2px solid var(--steel);
    font-family: var(--font-display);
    font-size: 14px;
    font-weight: 600;
    letter-spacing: 0.04em;
    color: var(--steel);
    white-space: nowrap;
    cursor: pointer;
    /* A stray long-press on a chip inside a button offers to save the image on
       iOS, which is not what the tap meant. */
    -webkit-touch-callout: none;
}

.tb-eq__cam:active {
    background: var(--steel);
    color: var(--paper);
}

.tb-eq__camicon {
    font-size: 13px;
    line-height: 1;
}

/* ── the sheet's photo strip ────────────────────────────────────────────
   Scrolls sideways rather than wrapping into rows: a device with eight shots
   should not push SAVE off the bottom of a phone. */

.tb-eqf__thumbs {
    display: flex;
    gap: 6px;
    margin-bottom: 8px;
    padding-bottom: 4px;
    overflow-x: auto;
    -webkit-overflow-scrolling: touch;
}

/* ⚠️ FIXED SIZE, AND THAT IS LOAD-BEARING ON CELLULAR. The box is reserved
   before the bytes arrive, so a slow photo cannot reflow the form under a
   thumb that is already reaching for SAVE. Kirk: "get them to the form for
   sure and save, even if photo not loaded yet."

   object-fit: cover because a dataplate shot is portrait and a unit shot is
   landscape; letterboxing them would make a strip of grey bars. */
.tb-eqf__thumb {
    flex: 0 0 auto;
    width: 68px;
    height: 68px;
    object-fit: cover;
    border: 2px solid var(--line);
    background: var(--paper);
    cursor: pointer;
}

.tb-eqf__thumb:active {
    border-color: var(--accent);
}

.tb-eqf__shots {
    display: flex;
    gap: 8px;
}

.tb-eqf__shot {
    flex: 1;
    /* Two words on a narrow phone - let them wrap rather than clip */
    white-space: normal;
    line-height: 1.2;
}

/* ── step two: the device is saved, the camera is offered ────────────── */

.tb-eqf__saved {
    margin: 0 0 6px;
    font-size: 16px;
    font-weight: 500;
    line-height: 1.4;
    color: var(--ink);
}

/* A quieter second line - the offer, not the news. */
.tb-eqf__note {
    margin: 0 0 14px;
    font-size: 14px;
    font-weight: 500;
    line-height: 1.35;
    color: var(--steel);
}

.tb-eqf__area {
    /* 16px is inherited from .tb-eqf__in and is NOT cosmetic - anything
       smaller makes iOS Safari zoom the page on focus and never zoom back. */
    resize: vertical;
    min-height: 68px;
    line-height: 1.4;
}

/* "- everyone sees these". The notes on a device are shared property, unlike
   a field note which belongs to whoever wrote it, and the label is the only
   place that difference is ever stated. */
.tb-eqf__lblsoft {
    font-weight: 500;
    letter-spacing: 0.02em;
    text-transform: none;
    opacity: 0.75;
}

/* ══ THE READER'S COLLAPSIBLE LOCATION NOTES ══════════════════════════════
   Kirk: location notes "can be a novella in here sometimes, and we need a way
   to get the tech to the dispatch notes they need."

   Measured on SERVICE: 12,562 sites carry location notes, average 361
   characters - but 322 run past 2,000 and the longest is 5,138. They are the
   site's STANDING instructions, the same prose every visit, and they sit
   directly above the notes about TODAY.

   ⚠️ NOT A CSS HEIGHT TRANSITION, and that is deliberate. The cheap
   max-height trick needs a guessed ceiling: too small clips a long note, too
   large makes the animation crawl through empty space at a speed nobody
   chose. On content that ranges from one line to five thousand characters
   there is no ceiling that is right, so this toggles instantly - which on a
   phone reads as responsive rather than cheap. */

.tb-reader__sec--tap {
    cursor: pointer;
    /* The whole open section is the tap target. A stray double-tap while
       reading should not select a paragraph of somebody else's prose. */
    -webkit-user-select: none;
    user-select: none;
    -webkit-tap-highlight-color: transparent;
}

/* The label was a plain bar; it now carries the SHOW / HIDE affordance on its
   right, so the control sits ON the thing it controls. */
.tb-reader__sec .tb-reader__label {
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: 10px;
}

/* The first section's label sits flush - the old :first-child rule matched the
   label against the body, and the label is now nested one level deeper. */
.tb-reader__sec:first-child .tb-reader__label {
    margin-top: 0;
}

.tb-reader__chev {
    flex: 0 0 auto;
    font-size: 11px;
    letter-spacing: 0.1em;
    opacity: 0.85;
}

/* What is hidden, in a unit a person can picture. Italic and quiet: it is a
   placeholder for content, not content. */
.tb-reader__shut {
    padding: 10px 2px 2px;
    font-size: 15px;
    font-weight: 500;
    font-style: italic;
    line-height: 1.4;
    color: var(--steel);
}

.tb-reader__sec--tap:active .tb-reader__label {
    background: var(--ink);
}

/* ══ THE SHARED NOTES PANEL'S SECTIONS ════════════════════════════════════
   <NotesPanel> wraps each block in a section so the location notes can carry
   their own tap target. The label keeps every bit of its old styling - the
   dashed rule, the accent, the spacing - and only becomes a flex row so the
   SHOW / HIDE affordance can sit on the right of the thing it controls.

   Same instant toggle as the job page's reader, for the same reason: a CSS
   max-height transition needs a guessed ceiling, and this content runs from
   one line to 5,138 characters. No ceiling is right. */

.tb-notes__sec .tb-notes__label {
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: 10px;
}

/* The old rule matched the label against the body; labels now sit one level
   deeper, so the first section is what has to lose its top rule and spacing. */
.tb-notes__sec:first-child .tb-notes__label {
    margin-top: 0;
    padding-top: 0;
    border-top: 0;
}

.tb-notes__sec--tap {
    cursor: pointer;
    /* A stray double-tap while reading must not select a paragraph of somebody
       else's prose. */
    -webkit-user-select: none;
    user-select: none;
    -webkit-tap-highlight-color: transparent;
}

.tb-notes__sec--tap:active .tb-notes__label {
    color: var(--ink);
}

.tb-notes__chev {
    flex: 0 0 auto;
    font-size: 11px;
    letter-spacing: 0.1em;
    opacity: 0.8;
}

/* What is hidden, in a unit a person can picture. Italic and quiet - it stands
   in for content rather than being content. */
.tb-notes__shut {
    padding: 2px 0 4px;
    font-size: 15px;
    font-weight: 500;
    font-style: italic;
    line-height: 1.4;
    color: var(--steel);
}

/* ── THE CHECKLISTS ────────────────────────────────────────────────────────
   ServiceProof workflows, wearing this app's clothes. A tech should never feel
   they left the building to run one - so these are the same job-ticket cards as
   everything else: cream stock, a hard rule, the accent reserved for the one
   thing you can act on.

   The play button is deliberately a WORD, not an icon. "START" and "RESUME" say
   different things to somebody halfway through a checklist in a plant room, and
   a triangle says neither. */

.tb-sect__count {
    margin-left: auto;
    font-family: var(--font-mono);
    font-style: normal;
    font-weight: 500;
    font-size: 11px;
    letter-spacing: 0.08em;
    opacity: 0.85;
}

.tb-wf {
    display: flex;
    align-items: stretch;
    gap: 10px;
    margin: 0 0 8px;
    padding: 12px 12px 12px 14px;
    background: var(--card);
    border: 1px solid var(--line);
    border-left: 4px solid var(--accent);
    border-radius: var(--corner);
}

/* Gated by an earlier stage: still listed, because knowing what is coming is
   half of knowing where you are - but visibly not yours to start yet. */
.tb-wf--waiting {
    border-left-color: var(--line-hard);
    opacity: 0.72;
}

/* Done. The green edge is the same "settled" signal van-stock rows use. */
.tb-wf--done {
    border-left-color: var(--ok);
}

.tb-wf__body {
    flex: 1 1 auto;
    min-width: 0;
}

.tb-wf__name {
    font-family: var(--font-display);
    font-weight: 700;
    font-size: 17px;
    line-height: 1.15;
    letter-spacing: 0.01em;
    color: var(--ink);
    overflow-wrap: anywhere;
}

.tb-wf__sub {
    margin-top: 3px;
    font-family: var(--font-mono);
    font-size: 11px;
    letter-spacing: 0.04em;
    color: var(--ink-soft);
}

.tb-wf__sub--live {
    color: var(--accent-deep);
    font-weight: 600;
}

/* A real tap target (--tap), because this gets pressed with a glove on. */
.tb-wf__go {
    flex: 0 0 auto;
    min-width: 92px;
    min-height: var(--tap);
    padding: 0 14px;
    background: var(--accent);
    color: var(--paper);
    border: none;
    border-radius: 10px;
    font-family: var(--font-display);
    font-weight: 700;
    font-size: 15px;
    letter-spacing: 0.12em;
    cursor: pointer;
}

.tb-wf__go:active { background: var(--accent-deep); }
.tb-wf__go:disabled { opacity: 0.55; cursor: default; }

.tb-wf__peek {
    flex: 0 0 auto;
    min-width: 72px;
    min-height: 44px;
    align-self: center;
    padding: 0 12px;
    background: transparent;
    color: var(--steel);
    border: 1px solid var(--line-hard);
    border-radius: 10px;
    font-family: var(--font-display);
    font-weight: 700;
    font-size: 13px;
    letter-spacing: 0.1em;
    cursor: pointer;
}

.tb-wf__add {
    display: block;
    width: 100%;
    min-height: 48px;
    margin: 2px 0 4px;
    background: transparent;
    color: var(--accent-deep);
    border: 1px dashed var(--line-hard);
    border-radius: var(--corner);
    font-family: var(--font-display);
    font-weight: 700;
    font-size: 14px;
    letter-spacing: 0.12em;
    cursor: pointer;
}

.tb-wf__add:active { background: var(--accent-soft); }

/* "Can't do this one" - deliberately the quietest control on the row: plain
   text, muted, under the name. Retiring work is real but rare, and it must
   never read as a peer of START. */
.tb-wf__cant {
    display: inline-block;
    margin-top: 4px;
    padding: 4px 0;
    background: transparent;
    border: none;
    color: var(--ink-soft);
    font-family: var(--font-body);
    font-size: 12px;
    text-decoration: underline;
    text-underline-offset: 2px;
    cursor: pointer;
}

.tb-wf__cant:active { color: var(--danger); }

/* The decline confirm - sits directly under its row, carries the consequences
   in words, and makes the destructive choice the red one. */
.tb-wf__confirm {
    margin: -4px 0 8px;
    padding: 10px 12px;
    background: var(--paper);
    border: 1px solid var(--line-hard);
    border-left: 4px solid var(--danger);
    border-radius: var(--corner);
}

.tb-wf__confirm-text {
    font-family: var(--font-body);
    font-size: 13px;
    line-height: 1.35;
    color: var(--ink);
    margin-bottom: 8px;
}

.tb-wf__confirm-note {
    width: 100%;
    min-height: 44px;
    padding: 8px 10px;
    margin-bottom: 8px;
    font-family: var(--font-body);
    font-size: 16px; /* the 16px floor - anything smaller makes iOS zoom on focus */
    color: var(--ink);
    background: var(--card);
    border: 1px solid var(--line);
    border-radius: calc(var(--corner) / 2);
}

.tb-wf__confirm-actions {
    display: flex;
    gap: 8px;
}

.tb-wf__confirm-yes,
.tb-wf__confirm-no {
    flex: 1 1 0;
    min-height: 48px;
    border-radius: var(--corner);
    font-family: var(--font-display);
    font-weight: 700;
    font-size: 14px;
    letter-spacing: 0.1em;
    cursor: pointer;
}

.tb-wf__confirm-yes {
    background: var(--danger);
    color: #fff;
    border: 1px solid var(--danger);
}

.tb-wf__confirm-no {
    background: var(--card);
    color: var(--ink-soft);
    border: 1px solid var(--line-hard);
}

.tb-wf__confirm-yes:disabled,
.tb-wf__confirm-no:disabled {
    opacity: 0.55;
    cursor: default;
}

/* ------------------------------------------------------------
   THE HOME PAGE - the base of the app (Kirk, 2026-08-25). Pure
   navigation: the crest as the actual logo, dispatch as the hero
   in the same ink-band voice as the clock CTA, billing under it,
   the personal-workflow tiles 2-up, utilities as quiet dashed
   ghosts. Nothing here fetches before it paints.
   ------------------------------------------------------------ */
.tb-home {
    display: flex;
    flex-direction: column;
    gap: 8px;
    padding-top: 2px;
}

/* Small on purpose: the WHOLE home page must fit one phone viewport with all
   four workflow tiles present - the base page never scrolls (Kirk). The crest
   is the greeting, not the content. */
.tb-home__logo {
    display: block;
    width: min(26vw, 104px);
    height: auto;
    margin: 0 auto 2px;
}

.tb-home__hero {
    display: flex;
    flex-direction: column;
    align-items: center;
    justify-content: center;
    gap: 3px;
    width: 100%;
    min-height: 80px;
    background: var(--ink);
    color: var(--paper);
    border: none;
    border-bottom: 4px solid var(--accent);
    border-radius: var(--corner);
    cursor: pointer;
}

.tb-home__hero:active { opacity: 0.92; }

.tb-home__hero-name {
    font-family: var(--font-display);
    font-weight: 700;
    font-size: 30px;
    line-height: 1;
    letter-spacing: 0.14em;
}

.tb-home__hero-sub {
    font-family: var(--font-mono);
    font-size: 11px;
    letter-spacing: 0.08em;
    color: var(--line);
}

.tb-home__billing {
    display: flex;
    flex-direction: column;
    align-items: center;
    justify-content: center;
    gap: 2px;
    width: 100%;
    min-height: 60px;
    background: var(--card);
    color: var(--accent-deep);
    border: 1px solid var(--line-hard);
    border-left: 4px solid var(--accent);
    border-radius: var(--corner);
    cursor: pointer;
}

.tb-home__billing:active { background: var(--accent-soft); }

.tb-home__billing-name {
    font-family: var(--font-display);
    font-weight: 700;
    font-size: 21px;
    line-height: 1;
    letter-spacing: 0.14em;
}

.tb-home__billing-sub {
    font-family: var(--font-mono);
    font-size: 11px;
    color: var(--ink-soft);
}

.tb-home__tiles {
    display: grid;
    grid-template-columns: 1fr 1fr;
    gap: 8px;
}

.tb-home__tile {
    display: flex;
    flex-direction: column;
    align-items: center;
    justify-content: center;
    gap: 3px;
    min-height: 72px;
    background: var(--card);
    color: var(--ink);
    border: 1px solid var(--line);
    border-radius: var(--corner);
    cursor: pointer;
}

.tb-home__tile:active { background: var(--accent-soft); }

.tb-home__tile:disabled {
    opacity: 0.6;
    cursor: default;
}

.tb-home__tile-glyph {
    font-size: 20px;
    line-height: 1;
    color: var(--accent-deep);
}

.tb-home__tile-name {
    font-family: var(--font-display);
    font-weight: 700;
    font-size: 13px;
    line-height: 1.1;
    letter-spacing: 0.1em;
    text-align: center;
    padding: 0 6px;
}

.tb-home__tile-sub {
    font-family: var(--font-mono);
    font-size: 10px;
    color: var(--ink-soft);
}

.tb-home__pair {
    display: grid;
    grid-template-columns: 1fr 1fr;
    gap: 8px;
    margin-bottom: 8px;
}

.tb-home__util {
    display: flex;
    align-items: center;
    justify-content: center;
    gap: 8px;
    min-height: 46px;
    background: transparent;
    color: var(--ink-soft);
    border: 1px dashed var(--line-hard);
    border-radius: var(--corner);
    font-family: var(--font-display);
    font-weight: 700;
    font-size: 13px;
    letter-spacing: 0.12em;
    cursor: pointer;
}

.tb-home__util:active { background: var(--accent-soft); }

/* ⚠️ SIZE LEFT ALONE ON PURPOSE. Kirk: "the two rows do NOT need to be taller
   ... just change glyph and that may be enough." An emoji renders visually
   larger than the dingbat it replaced at the identical font-size, so the swap
   in Home.razor was the whole fix. Don't grow this to 20px - it grows the row. */
.tb-home__util-glyph {
    font-size: 16px;
    line-height: 1;
}

.tb-wf__empty,
.tb-wf__flash {
    margin: 0 0 8px;
    padding: 10px 12px;
    border-radius: var(--corner);
    font-family: var(--font-mono);
    font-size: 12px;
    color: var(--ink-soft);
    background: var(--paper);
    border: 1px solid var(--line);
}

/* The hand-back note - the tech has just come from the runner, so this is the
   first thing they should see confirming it landed. */
.tb-wf__flash {
    color: var(--ok);
    background: var(--ok-soft);
    border-color: var(--ok);
    font-weight: 600;
}

/* ── The answers, inline ── */
.tb-wf__answers {
    margin: -4px 0 10px;
    padding: 10px 12px;
    background: var(--paper);
    border: 1px solid var(--line);
    border-top: none;
    border-radius: 0 0 var(--corner) var(--corner);
}

.tb-wf__ans + .tb-wf__ans {
    margin-top: 10px;
    padding-top: 10px;
    border-top: 1px solid var(--line);
}

.tb-wf__ansq {
    font-family: var(--font-mono);
    font-size: 10px;
    letter-spacing: 0.1em;
    text-transform: uppercase;
    color: var(--ink-soft);
}

.tb-wf__ansv {
    margin-top: 2px;
    font-family: var(--font-body);
    font-size: 15px;
    color: var(--ink);
    overflow-wrap: anywhere;
}

/* Captured in the field, resolved + signed by ServiceProof. Capped so a photo
   is a glance rather than a scroll; tapping opens the full thing. */
.tb-wf__photo {
    display: block;
    margin-top: 6px;
    max-width: 100%;
    max-height: 220px;
    border-radius: 10px;
    border: 1px solid var(--line-hard);
}

/* ── THE PURCHASE ORDER NUMBER ──
   Kirk: "present this PO number to the tech LARGE and prominently". It is the
   whole deliverable of that sentence - what gets read out at a trade counter -
   and unlike a dispatch there is no second screen carrying it.

   ⚠️ MONOSPACE AND WIDE-TRACKED ON PURPOSE. It is fifteen digits with a long
   run of leading zeros, and a proportional face turns 000000000060509 into a
   smear nobody can read back over a phone. The mono face is already loaded for
   the card sub-lines.

   ⚠️ AND user-select IS EXPLICIT. The rest of the app leans on the OS default,
   but this is the one string a tech may want to long-press and copy by hand
   when the clipboard button fails - which it does on an insecure context and in
   some in-app browsers. */
.tb-po__label {
    margin: 4px 0 2px;
    font-family: var(--font-mono);
    font-size: 12px;
    letter-spacing: 0.18em;
    color: var(--ink-soft);
}

.tb-po__number {
    margin: 0 0 14px;
    font-family: var(--font-mono);
    font-size: 30px;
    font-weight: 700;
    line-height: 1.1;
    letter-spacing: 0.02em;
    color: var(--ink);
    word-break: break-all;
    -webkit-user-select: all;
    user-select: all;
}

/* ── The picker sheet ──
   ⚠️ ANCHORED AT THE TOP, NOT THE BOTTOM, AND THAT IS A BUG FIX RATHER THAN A
   STYLE PREFERENCE (Kirk, 2026-09-04).

   It used to be `bottom: 0` with its height coming from its content. A sheet
   whose content SHRINKS therefore moved its own TOP edge DOWNWARD - and the task
   picker shrinks on every keystroke, because typing filters the list. The
   keyboard owns the bottom of a phone screen, so the search box the tech was
   typing into slid down behind it: "on an iPhone that makes the dialog 100%
   inaccessible."

   Pinning the TOP inverts it. The header and the search box sit at a fixed place
   on screen and never move; a shrinking list raises the BOTTOM edge instead,
   into empty space nobody is looking at.

   ⚠️ 10dvh RATHER THAN 24. A full-height sheet used to begin at 24dvh
   (bottom 0 + 76dvh max), so 24 would be the literal "leave it where it is".
   Higher is better: with the keyboard up roughly the top half of the screen is
   all a tech can see, and 10dvh puts the header and the box near the top of that
   band with room for several rows under them.

   ⚠️ dvh DOES NOT SHRINK FOR THE KEYBOARD on iOS - it tracks the browser chrome,
   not the input accessory. That is precisely why a fixed `top` is stable where a
   fixed `bottom` was not: nothing this is anchored to moves when the keyboard
   opens, so the sheet cannot be shoved anywhere.

   ⚠️ AND IT NO LONGER MEETS THE SCREEN EDGE, hence the full border and all four
   corners. The old rule dropped the bottom border and squared the bottom corners
   because it merged with the bottom of the screen; a panel that floats has to be
   closed on all four sides or a short one reads as torn off. The 16px inside
   max-height is what keeps it clear of the edge - and of the home indicator,
   which is why the foot no longer carries a safe-area inset. */
.tb-sheet {
    position: fixed;
    left: 50%;
    top: 10dvh;
    bottom: auto;
    transform: translateX(-50%);
    width: min(520px, 100%);
    max-height: calc(90dvh - 16px);
    display: flex;
    flex-direction: column;
    background: var(--card);
    border: 1px solid var(--line-hard);
    border-radius: var(--corner);
    z-index: 61;
}

.tb-sheet__head {
    padding: 12px 14px;
    background: var(--steel);
    color: var(--paper);
    font-family: var(--font-display);
    font-weight: 700;
    font-size: 15px;
    letter-spacing: 0.14em;
    text-transform: uppercase;
    border-radius: var(--corner) var(--corner) 0 0;
}

/* ⚠️ A MODIFIER, NOT A CHANGE TO .tb-sheet__head. That base class is shared with
   the Job and Work sheets, so the flex layout that right-aligns the tools lives
   here and nothing else in the app moves. Kirk: "do the button on the header
   like we do in jobdetail so it looks sexy and right-align it." */
.tb-sheet__head--tools {
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: 10px;
}

/* The title yields first - a long team name must shorten rather than shove the
   tools off the edge, which is the whole reason the tools are pinned right. */
.tb-sheet__title {
    min-width: 0;
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
}

.tb-sheet__tools {
    display: flex;
    align-items: center;
    gap: 8px;
    flex-shrink: 0;
}

.tb-sheet__body {
    padding: 12px 14px;
    overflow-y: auto;
    /* A flick past the end must not drag the job page behind the scrim. */
    overscroll-behavior: contain;
}

.tb-sheet__foot {
    /* No safe-area inset any more - the sheet floats clear of the bottom edge
       (see .tb-sheet), so there is no home indicator to dodge, and keeping it
       would open a 34px gap under CLOSE on a notched phone. */
    padding: 10px 14px;
    border-top: 1px solid var(--line);
    border-radius: 0 0 var(--corner) var(--corner);
}

.tb-wf__pick {
    display: block;
    width: 100%;
    min-height: 52px;
    margin-bottom: 8px;
    padding: 10px 12px;
    text-align: left;
    background: var(--paper);
    color: var(--ink);
    border: 1px solid var(--line);
    border-left: 4px solid var(--accent);
    border-radius: 10px;
    font-family: var(--font-display);
    font-weight: 700;
    font-size: 16px;
    letter-spacing: 0.02em;
    cursor: pointer;
}

.tb-wf__pick:active { background: var(--accent-soft); }

/* ── THE WORKFLOW PAGE (/job/{d}/workflow/{jwi}) ─────────────────────────────
   The applet becomes the workflow: a fixed full-viewport surface whose ONLY
   chrome is the suspend bar - the framed runner's own back/forward/review/
   complete are the rest of the navigation. The iframe is the ONLY scroller
   (100dvh minus the bar): a frame inside a scrolling page is exactly the
   double-scroll jank that makes an embed feel like an embed on iOS. */

.tb-run {
    position: fixed;
    inset: 0;
    display: flex;
    flex-direction: column;
    background: #fff;
    z-index: 50;
}

.tb-run__bar {
    display: flex;
    align-items: center;
    gap: 0.75rem;
    padding: 0.5rem 0.75rem;
    padding-top: calc(0.5rem + env(safe-area-inset-top));
    background: #16202a;
    color: #fff;
    flex: 0 0 auto;
}

.tb-run__suspend {
    display: inline-flex;
    align-items: center;
    gap: 0.25rem;
    border: 1px solid rgba(255, 255, 255, 0.35);
    background: transparent;
    color: #fff;
    border-radius: 6px;
    padding: 0.3rem 0.7rem 0.3rem 0.45rem;
    font-size: 0.85rem;
    font-weight: 600;
}

.tb-run__chev {
    font-size: 1.1rem;
    line-height: 1;
}

.tb-run__title {
    flex: 1;
    font-weight: 600;
    font-size: 0.95rem;
    white-space: nowrap;
    overflow: hidden;
    text-overflow: ellipsis;
}

.tb-run__job {
    font-size: 0.8rem;
    opacity: 0.7;
    font-variant-numeric: tabular-nums;
}

.tb-run__frame {
    flex: 1;
    width: 100%;
    border: 0;
    display: block;
}

.tb-run__loading,
.tb-run__error {
    flex: 1;
    display: grid;
    place-items: center;
    color: #555;
    padding: 2rem;
    text-align: center;
}

/* ═══════════════════════════════════════════════════════════════════════════
   TEAM DIRECTORY (/directory) — the rolodex. Same ticket voice as the rest of
   the app: paper ground, card rows, condensed display type, orange accents.
   The rail rides fixed at the right edge OF THE 520px COLUMN, not the window,
   so a desktop test looks like the phone will.
   ═══════════════════════════════════════════════════════════════════════════ */

/* The home page's full-width door — solid where the utilities are dashed:
   the directory is a destination, not a chore. */
.tb-home__util--wide {
    width: 100%;
    background: var(--card);
    border-style: solid;
    border-color: var(--line);
    color: var(--ink);
    min-height: 52px;
}

.tb-dir {
    padding-right: 20px; /* air for the letter rail */
    padding-bottom: 32px;
}

.tb-dir__head {
    display: flex;
    align-items: baseline;
    gap: 10px;
    margin: 4px 0 10px;
}

.tb-dir__headline {
    font-family: var(--font-display);
    font-weight: 700;
    font-size: 22px;
    letter-spacing: 0.06em;
    color: var(--ink);
}

.tb-dir__count {
    font-family: var(--font-mono);
    font-size: 11px;
    color: var(--steel);
}

.tb-dir__search {
    position: relative;
    margin-bottom: 8px;
}

.tb-dir__search-box {
    width: 100%;
    min-height: 48px;
    padding: 0 44px 0 14px;
    background: var(--card);
    border: 1px solid var(--line-hard);
    border-radius: var(--corner);
    font-family: var(--font-body);
    font-size: 16px; /* the iOS zoom-on-focus floor */
    color: var(--ink);
}

.tb-dir__search-box:focus {
    outline: 2px solid var(--accent);
    outline-offset: -1px;
}

/* ⚠️ KILL THE BROWSER'S OWN CLEAR BUTTON - THERE WERE TWO Xs IN THE BOX.
   Kirk, 2026-08-31: "Can you get rid of the darker X in the type field - there
   are two. The darker one only shows when a mouse hovers."

   The input is type="search", so WebKit draws its own cancel button - and
   because this box reserves `padding-right: 44px` for .tb-dir__search-x, the
   native one lands 44px in from the right edge, sitting to the LEFT of ours
   rather than underneath it. Hence two, at different weights, one of them
   appearing only on hover.

   ⚠️ IT WAS A DESKTOP-ONLY BUG, AND THIS IS A DESKTOP-ONLY FIX. Kirk, on the
   handsets: "Mobile only ever showed the one x." The native button is
   hover-gated and a touch device has no hover, so iOS Safari and Android Chrome
   never drew it - the phones only ever had the app's own X. So these two lines
   change nothing on the surface that matters most, which is also why the double
   X survived unnoticed until somebody opened the directory on a desktop.

   Hiding the native one rather than dropping type="search": the type is what it
   honestly is, and `inputmode="search"` already carries the keyboard hint
   independently, so there is nothing to gain by lying about the element.

   ⚠️ EVERY SEARCH BOX IN THIS APP DRAWS ITS OWN X (.tb-search__x on Lookup,
   .tb-dir__search-x here) - they are the only ones styled to match the ticket
   voice. So any future type="search" needs these two lines too, or it gets the
   same double-X. */
.tb-dir__search-box::-webkit-search-cancel-button,
.tb-dir__search-box::-webkit-search-decoration {
    -webkit-appearance: none;
    appearance: none;
}

.tb-dir__search-x {
    position: absolute;
    right: 6px;
    top: 50%;
    transform: translateY(-50%);
    width: 36px;
    height: 36px;
    border: none;
    background: transparent;
    color: var(--ink-soft);
    font-size: 14px;
    cursor: pointer;
}

.tb-dir__bar {
    display: flex;
    align-items: center;
    gap: 8px;
    margin-bottom: 12px;
}

.tb-dir__pill {
    display: flex;
    flex: none;
    border: 1px solid var(--line-hard);
    border-radius: 999px;
    overflow: hidden;
    background: var(--card);
}

.tb-dir__pill-btn {
    border: none;
    background: transparent;
    padding: 7px 12px;
    font-family: var(--font-display);
    font-weight: 700;
    font-size: 12px;
    letter-spacing: 0.1em;
    color: var(--ink-soft);
    cursor: pointer;
}

.tb-dir__pill-btn.active {
    background: var(--ink);
    color: var(--paper);
}

.tb-dir__chips {
    display: flex;
    gap: 6px;
    /* This div is a FLEX ITEM (of .tb-dir__rail since 2026-08-31, of
       .tb-dir__bar before that), and a flex item's min-width defaults to its
       content size - so without these two lines it GROWS to fit every chip and
       overflow-x:auto never engages (the chips just get cut at the column edge,
       unscrollable). Same family as the app's ellipsis-on-flex lesson: the fix
       is min-width:0, not more overflow. */
    flex: 1;
    min-width: 0;
    overflow-x: auto;
    scrollbar-width: none;
    -webkit-overflow-scrolling: touch;
}

.tb-dir__chips::-webkit-scrollbar { display: none; }

.tb-dir__chip {
    flex: none;
    padding: 7px 10px;
    border: 1px solid var(--line);
    border-radius: 999px;
    background: var(--card);
    font-family: var(--font-display);
    font-weight: 600;
    font-size: 11px;
    letter-spacing: 0.08em;
    color: var(--ink-soft);
    cursor: pointer;
}

.tb-dir__chip.active {
    background: var(--accent);
    border-color: var(--accent-deep);
    color: #fff;
}

.tb-dir__hint,
.tb-dir__empty {
    margin: 6px 0 10px;
    padding: 10px 12px;
    border-radius: var(--corner);
    border: 1px solid var(--line);
    background: var(--paper);
    font-family: var(--font-mono);
    font-size: 12px;
    color: var(--ink-soft);
}

/* The letter tabs of the rolodex — a thumb index, not a heading system. */
.tb-dir__letter {
    display: flex;
    align-items: center;
    gap: 8px;
    margin: 14px 0 6px;
    font-family: var(--font-display);
    font-weight: 700;
    font-size: 14px;
    letter-spacing: 0.1em;
    color: var(--accent-deep);
}

.tb-dir__letter::after {
    content: "";
    flex: 1;
    height: 1px;
    background: var(--line-hard);
}

.tb-dir__card {
    background: var(--card);
    border: 1px solid var(--line);
    border-radius: var(--corner);
    margin-bottom: 6px;
    overflow: hidden;
}

.tb-dir__card.open {
    border-color: var(--accent);
    box-shadow: inset 3px 0 0 var(--accent);
}

.tb-dir__row {
    display: flex;
    align-items: center;
    gap: 10px;
    width: 100%;
    min-height: var(--tap);
    padding: 8px 10px;
    border: none;
    background: transparent;
    text-align: left;
    cursor: pointer;
}

.tb-dir__row:active { background: var(--accent-soft); }

.tb-dir__avatar {
    flex: none;
    display: grid;
    place-items: center;
    width: 40px;
    height: 40px;
    border-radius: 50%;
    background: var(--accent-soft);
    color: var(--accent-deep);
    font-family: var(--font-display);
    font-weight: 700;
    font-size: 15px;
    letter-spacing: 0.02em;
}

.tb-dir__who {
    flex: 1;
    min-width: 0;
    display: flex;
    flex-direction: column;
    gap: 2px;
}

.tb-dir__name {
    font-family: var(--font-body);
    font-weight: 600;
    font-size: 16px;
    color: var(--ink);
    line-height: 1.2;
}

.tb-dir__name-last { font-weight: 700; }

.tb-dir__meta {
    display: flex;
    flex-wrap: wrap; /* ellipsis-on-flex was the trap; wrapping is the fix */
    align-items: center;
    gap: 4px 10px;
}

.tb-dir__role {
    font-family: var(--font-display);
    font-weight: 700;
    font-size: 11px;
    letter-spacing: 0.1em;
    color: var(--steel);
}

.tb-dir__fact {
    font-family: var(--font-mono);
    font-size: 11px;
    color: var(--ink-soft);
}

.tb-dir__quick {
    flex: none;
    display: grid;
    place-items: center;
    width: 44px;
    height: 44px;
    border-radius: 50%;
    border: 1px solid var(--line-hard);
    background: var(--paper);
    color: var(--accent-deep);
    font-size: 18px;
    text-decoration: none;
}

.tb-dir__quick:active { background: var(--accent-soft); }

.tb-dir__contacts {
    border-top: 1px solid var(--line);
    padding: 2px 10px 8px;
}

.tb-dir__contact {
    display: flex;
    align-items: stretch;
    gap: 8px;
    border-bottom: 1px dashed var(--line);
}

.tb-dir__contact:last-child { border-bottom: none; }

.tb-dir__contact-main {
    flex: 1;
    min-width: 0;
    display: flex;
    flex-direction: column;
    justify-content: center;
    gap: 1px;
    min-height: 52px;
    padding: 6px 0;
    text-decoration: none;
}

.tb-dir__contact-label {
    font-family: var(--font-mono);
    font-size: 10px;
    letter-spacing: 0.14em;
    color: var(--steel);
}

.tb-dir__contact-value {
    font-family: var(--font-body);
    font-weight: 600;
    font-size: 15px;
    color: var(--ink);
}

.tb-dir__contact-email {
    font-weight: 500;
    font-size: 14px;
    overflow-wrap: anywhere;
}

.tb-dir__contact-alt {
    flex: none;
    align-self: center;
    padding: 8px 12px;
    border: 1px solid var(--line-hard);
    border-radius: 999px;
    font-family: var(--font-display);
    font-weight: 700;
    font-size: 11px;
    letter-spacing: 0.1em;
    color: var(--accent-deep);
    text-decoration: none;
}

.tb-dir__contact-alt:active { background: var(--accent-soft); }

.tb-dir__contact-none {
    padding: 12px 0;
    font-family: var(--font-mono);
    font-size: 12px;
    color: var(--ink-soft);
}

/* The thumb rail. Fixed to the app COLUMN's right edge; letters with nobody
   under them go pale and dead, so the rail doubles as a map of the roster. */
.tb-dir__rail {
    position: fixed;
    top: 50%;
    transform: translateY(-50%);
    right: calc((100vw - min(100vw, 520px)) / 2 + 2px);
    display: flex;
    flex-direction: column;
    z-index: 20;
}

.tb-dir__rail-l {
    border: none;
    background: transparent;
    padding: 0 4px;
    min-width: 16px;

    /* ⚠️ THE PITCH IS A SHARE OF THE SCREEN, NOT A CONSTANT.
       Kirk, 2026-08-31, on a real handset showing all 19 of Charlie's letters:
       "we have the real estate to give a little more breathing room even with
       all letters."

       It was a flat 15px, which made the full 27-letter rail 405px on every
       device - a third of a modern phone, and the same absolute size on a 1024px
       tablet as on a 568px iPhone SE. 2.4vh instead means 27 letters always come
       to 27 x 2.4 = 64.8vh, so the rail keeps the SAME PROPORTION of the screen
       whatever the screen is, and gets its breathing room from the device rather
       than from a number somebody guessed.

       The clamp is what keeps that honest at both ends:
         floor 15px - never tighter than it is today, so a short viewport
                      (landscape) is no worse than it already was
         ceil  26px - a tablet does not need 40px letters; past this the rail
                      would start looking like a menu instead of an index

       Measured at 812px tall: 19.5px pitch, so 27 letters = 526px (65%) against
       405px (50%) before, and Charlie's 19 = 371px against 285px. */
    line-height: clamp(15px, 2.4vh, 26px);

    font-family: var(--font-mono);

    /* Scaled on the same principle - a 10.5px glyph in a 19.5px box reads as a
       speck. Deliberately a SHALLOWER curve than the pitch: the point of the
       extra room is space around the letters, not big type. */
    font-size: clamp(10.5px, 1.5vh, 13px);

    font-weight: 700;
    color: var(--accent-deep);
    text-align: center;
    cursor: pointer;
}

.tb-dir__rail-l:active { color: var(--accent); transform: scale(1.4); }

/* -- THE RAIL BREATHES WHEN THE FILTER SHRINKS IT ---------------------------
   Kirk, 2026-08-31, looking at a SALES filter that left four letters: "It's not
   even the size - it's spacing between letters that really would matter."

   Right, and it is the sharper diagnosis. At the packed 15px pitch, four letters
   are four ADJACENT 15px targets, so "D instead of C" is one thumb-width of
   error. Growing the glyph would not fix that on its own; separating the targets
   does, because a near-miss then lands in dead space instead of on the neighbour.

   So each tier spends its slack on BOTH: padding, which makes each letter a
   bigger target, and gap, which puts dead space between them. Roughly two thirds
   target, one third separation - all target and a slight miss still hits the
   wrong letter; all gap and the rail feels unresponsive.

   ⚠️ THE PACKED TIER IS THE WORST CASE, AND THAT IS THE SAFETY ARGUMENT. The
   rail is position:fixed and centred, so anything taller than the viewport clips
   at BOTH ends with no scrollbar to reveal it. Since the base pitch is 2.4vh
   (see .tb-dir__rail-l), 27 packed letters come to 27 x 2.4 = ~65vh - and every
   tier below applies to FEWER letters, so none of them can exceed it. Checked at
   all three clamp values, not just the middle one:

       tier      letters  padding  gap   pitch   total       total
                                                @812px      as %vh
       (packed)   15-27    0       0     19.5    27 -> 526    65
       --spaced   10-14    4px     3px   27.5    14 -> 424    52
       --airy      7-9     7px     5px   33.5     9 -> 341    42
       --thumb     <=6    10px     7px   39.5     6 -> 272    33

   So the tiers only ever make the rail SHORTER than the packed case they are
   carving out of. Four letters - the case in the SALES screenshot - lands at
   ~193px on a 812px screen, centred.

   ⚠️ CHANGE A NUMBER HERE AND RE-CHECK THE PACKED ROW AT THE CLAMP FLOOR (15px,
   which is any viewport under ~625px tall). That is the one combination where a
   tier could grow past its parent case, because the floor stops the base pitch
   shrinking any further while padding and gap keep adding. */

.tb-dir__rail--spaced { gap: 3px; }

.tb-dir__rail--spaced .tb-dir__rail-l {
    padding-block: 4px;
    min-width: 20px;
}

.tb-dir__rail--airy { gap: 5px; }

.tb-dir__rail--airy .tb-dir__rail-l {
    padding-block: 7px;
    min-width: 24px;
}

.tb-dir__rail--thumb { gap: 7px; }

.tb-dir__rail--thumb .tb-dir__rail-l {
    padding-block: 10px;
    min-width: 28px;
}

/* .tb-dir__rail-l.off REMOVED 2026-08-31. The rail used to render all 27 glyphs
   and grey out the ones nobody in the WHOLE ROSTER sat under. It now renders
   exactly the groups on screen, so an unreachable letter cannot occur and this
   rule had no caller left. */

/* ═══════════════════════════════════════════════════════════════════════════
   TEAM CHAT (the pop-up overlay) — Conversations + THE COMMANDER inside the
   applet. A sheet that floats over whatever page is open; closing it changes
   nothing underneath. Same ticket voice: paper, card rows, condensed display
   type, the ink band reserved for the Commander's own bubbles.
   ═══════════════════════════════════════════════════════════════════════════ */

/* The bar chip door. Goes accent when something is unread. */
.tb-chip--chat {
    /* The blip anchors to this chip's shoulder. */
    position: relative;
    border-color: var(--line-hard);

    /* ⚠️ AND IT MUST NOT CLIP, which .tb-chip's own `overflow: hidden` was
       doing - the blip sits at top:-5px/right:-6px and was being sliced off at
       the chip's edge, so a pea meant to sit ON the shoulder read as a square
       tucked INSIDE the corner (Kirk, 2026-09-06: "Can the bubble go outside
       the chat").

       Overriding it here and not on .tb-chip is the whole point: that
       overflow exists for the ellipsis on the TRUCK and NAME chips, where a
       long value really does have to be cut. This chip's text is the literal
       word CHAT - it can never need ellipsising, so it has nothing to lose. */
    overflow: visible;
}

/* UNREAD - same footprint as the quiet chip, always. The old "CHAT · 7"
   suffix grew the chip and clipped in iOS's tight bar; now the signal is
   the border flipping green + the blip. Green, not accent: the tech's name
   chip directly below is already solid orange, and two orange lumps in one
   corner read as one control. */
.tb-chip--chat-live {
    border-color: var(--ok);
    color: #fff;
    font-weight: 700;
}

/* THE BLIP - Kirk's "subtle visual disruptor". A pea on the chip's
   shoulder; the numeral inside is deliberately microscopic ("no point,
   but it's there if ya zoom") - the REAL count rides the title attribute.
   The ink ring cuts it cleanly off the dark bar, and the slow breathe
   draws the eye the way the sync-heartbeat nudge does, without shouting. */
.tb-chip__blip {
    position: absolute;
    top: -5px;
    right: -6px;
    min-width: 13px;
    height: 13px;
    padding: 0 2px;
    display: inline-flex;
    align-items: center;
    justify-content: center;
    border-radius: 999px;
    background: var(--ok);
    border: 2px solid var(--ink);
    color: #fff;
    font-family: var(--font-mono);
    font-size: 6px;
    font-weight: 700;
    line-height: 1;
    animation: tb-blip-breathe 2.8s ease-in-out infinite;
}

@keyframes tb-blip-breathe {

    0%,
    100% {
        transform: scale(1);
    }

    50% {
        transform: scale(1.3);
    }
}

/* The job page's row chip — an action, not a jump, so it wears the accent. */
.tb-jump__go--chat {
    background: var(--accent);
    border-color: var(--accent-deep);
    color: #fff;
    font-weight: 700;
}

.tb-jump__go--chat:active {
    background: var(--accent-deep);
}

/* The rolodex's chat row. */
.tb-dir__chatgo {
    width: 100%;
    border: none;
    background: transparent;
    text-align: left;
    cursor: pointer;
}

.tb-dir__chatgo .tb-dir__contact-value {
    color: var(--accent-deep);
}

/* ── The sheet ─────────────────────────────────────────────────────────── */

.tb-chatx__scrim {
    z-index: 60;
}

.tb-chatx {
    position: fixed;
    z-index: 61;
    inset: 0 0 0 auto;
    width: min(100vw, 520px);
    display: flex;
    flex-direction: column;
    background: var(--paper);
    border-left: 1px solid var(--line-hard);
    box-shadow: -12px 0 40px rgba(22, 35, 45, 0.25);
}

.tb-chatx__head {
    display: flex;
    align-items: center;
    gap: 8px;
    padding: 10px 12px;
    background: var(--ink);
    color: var(--paper);
    border-bottom: 3px solid var(--accent);
}

.tb-chatx__back {
    border: none;
    background: transparent;
    color: var(--paper);
    font-size: 24px;
    line-height: 1;
    padding: 0 6px;
    cursor: pointer;
}

.tb-chatx__title {
    flex: 1;
    min-width: 0;
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
    font-family: var(--font-display);
    font-weight: 700;
    font-size: 17px;
    letter-spacing: 0.06em;
}

.tb-chatx__jobchip {
    flex: none;
    padding: 2px 6px;
    border-radius: 4px;
    background: var(--accent);
    color: #fff;
    font-family: var(--font-mono);
    font-size: 9.5px;
    font-weight: 700;
    letter-spacing: 0.1em;
}

.tb-chatx__x {
    border: none;
    background: transparent;
    color: var(--paper);
    font-size: 15px;
    padding: 6px;
    cursor: pointer;
}

.tb-chatx__error {
    margin: 8px 12px 0;
}

/* ── Rooms ─────────────────────────────────────────────────────────────── */

.tb-chatx__body {
    flex: 1;
    min-height: 0;
    overflow-y: auto;
    padding: 10px 12px 16px;
}

.tb-chatx__new {
    display: flex;
    align-items: center;
    justify-content: center;
    gap: 8px;
    width: 100%;
    min-height: 46px;
    margin-bottom: 10px;
    background: var(--card);
    border: 1px dashed var(--line-hard);
    border-radius: var(--corner);
    font-family: var(--font-display);
    font-weight: 700;
    font-size: 13px;
    letter-spacing: 0.12em;
    color: var(--ink-soft);
    cursor: pointer;
}

.tb-chatx__new:active { background: var(--accent-soft); }

.tb-chatx__new-glyph { color: var(--accent-deep); font-size: 16px; }

.tb-chatx__empty {
    padding: 14px 4px;
    font-family: var(--font-mono);
    font-size: 12px;
    color: var(--ink-soft);
}

.tb-chatx__room {
    display: flex;
    align-items: center;
    gap: 8px;
    width: 100%;
    min-height: var(--tap);
    margin-bottom: 6px;
    padding: 10px;
    background: var(--card);
    border: 1px solid var(--line);
    border-radius: var(--corner);
    text-align: left;
    cursor: pointer;
}

.tb-chatx__room:active { background: var(--accent-soft); }

.tb-chatx__room-main {
    flex: 1;
    min-width: 0;
    display: flex;
    flex-direction: column;
    gap: 2px;
}

.tb-chatx__room-title {
    display: flex;
    align-items: center;
    gap: 6px;
    font-family: var(--font-body);
    font-weight: 700;
    font-size: 15px;
    color: var(--ink);
}

.tb-chatx__room-last {
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
    font-size: 12.5px;
    color: var(--ink-soft);
}

.tb-chatx__room-last b { font-weight: 600; margin-right: 3px; }

.tb-chatx__room-side {
    flex: none;
    display: flex;
    flex-direction: column;
    align-items: flex-end;
    gap: 4px;
}

.tb-chatx__room-when {
    font-family: var(--font-mono);
    font-size: 10px;
    color: var(--steel);
}

.tb-chatx__pill {
    min-width: 20px;
    padding: 1px 6px;
    border-radius: 999px;
    background: var(--accent);
    color: #fff;
    font-family: var(--font-mono);
    font-size: 11px;
    font-weight: 700;
    text-align: center;
}

/* ── The thread ── column-reverse pins the newest message to the bottom and
   keeps it there as messages arrive - zero scroll scripting. The markup
   renders newest-first; this flips it visually. ─────────────────────────── */

.tb-chatx__thread {
    flex: 1;
    min-height: 0;
    overflow-y: auto;
    overscroll-behavior: contain;
    display: flex;
    flex-direction: column-reverse;
    padding: 10px 12px;
    gap: 6px;
}

.tb-chatx__row {
    display: flex;
    justify-content: flex-start;
}

.tb-chatx__row.mine { justify-content: flex-end; }

.tb-chatx__bubble {
    max-width: 84%;
    display: flex;
    flex-direction: column;
    gap: 2px;
    padding: 8px 10px;
    background: var(--card);
    border: 1px solid var(--line);
    border-radius: var(--corner);
    border-bottom-left-radius: 4px;
}

.tb-chatx__row.mine .tb-chatx__bubble {
    background: var(--accent-soft);
    border-color: var(--line-hard);
    border-radius: var(--corner);
    border-bottom-right-radius: 4px;
}

/* THE COMMANDER wears the ink band - unmistakably the machine member. */
.tb-chatx__row.cmdr .tb-chatx__bubble {
    background: var(--ink);
    border-color: var(--ink);
    color: var(--paper);
}

.tb-chatx__row.cmdr .tb-chatx__text,
.tb-chatx__row.cmdr .tb-chatx__text a {
    color: var(--paper);
}

.tb-chatx__row.cmdr .tb-chatx__when { color: var(--line-hard); }

.tb-chatx__sender {
    font-family: var(--font-display);
    font-weight: 700;
    font-size: 11px;
    letter-spacing: 0.08em;
    color: var(--steel);
}

.tb-chatx__row.cmdr .tb-chatx__sender { color: var(--accent); }

.tb-chatx__tochip {
    align-self: flex-start;
    padding: 1px 6px;
    border-radius: 4px;
    background: var(--paper);
    border: 1px solid var(--line-hard);
    font-family: var(--font-mono);
    font-size: 9.5px;
    letter-spacing: 0.08em;
    color: var(--accent-deep);
}

.tb-chatx__text {
    font-size: 14.5px;
    line-height: 1.35;
    color: var(--ink);
    overflow-wrap: anywhere;
}

.tb-chatx__text a { color: var(--accent-deep); }

.tb-chatx__when {
    align-self: flex-end;
    font-family: var(--font-mono);
    font-size: 9.5px;
    color: var(--steel);
}

.tb-chatx__sys {
    align-self: center;
    padding: 2px 10px;
    font-family: var(--font-mono);
    font-size: 11px;
    color: var(--ink-soft);
    text-align: center;
}

.tb-chatx__thinking {
    align-self: flex-start;
    padding: 8px 10px;
    border-radius: var(--corner);
    background: var(--ink);
    color: var(--accent);
    font-family: var(--font-mono);
    font-size: 11.5px;
    letter-spacing: 0.06em;
    animation: tb-chatx-pulse 1.4s ease-in-out infinite;
}

@keyframes tb-chatx-pulse {
    0%, 100% { opacity: 1; }
    50% { opacity: 0.55; }
}

/* ── The composer ──────────────────────────────────────────────────────── */

.tb-chatx__composer {
    display: flex;
    align-items: center;
    gap: 6px;
    padding: 8px 10px calc(8px + env(safe-area-inset-bottom, 0px));
    background: var(--card);
    border-top: 1px solid var(--line-hard);
}

.tb-chatx__cmdr-toggle {
    flex: none;
    width: 42px;
    height: 42px;
    border-radius: 50%;
    border: 1px solid var(--line-hard);
    background: var(--paper);
    color: var(--steel);
    font-size: 17px;
    cursor: pointer;
}

.tb-chatx__cmdr-toggle.on {
    background: var(--ink);
    border-color: var(--ink);
    color: var(--accent);
}

.tb-chatx__input {
    flex: 1;
    min-width: 0;
    min-height: 42px;
    padding: 0 12px;
    border: 1px solid var(--line-hard);
    border-radius: 999px;
    background: var(--paper);
    font-family: var(--font-body);
    font-size: 16px; /* the iOS zoom floor */
    color: var(--ink);
}

.tb-chatx__send {
    flex: none;
    min-height: 42px;
    padding: 0 14px;
    border: none;
    border-radius: 999px;
    background: var(--accent);
    color: #fff;
    font-family: var(--font-display);
    font-weight: 700;
    font-size: 12.5px;
    letter-spacing: 0.1em;
    cursor: pointer;
}

.tb-chatx__send:disabled {
    background: var(--line-hard);
    cursor: default;
}

/* ── The new-chat picker ───────────────────────────────────────────────── */

.tb-chatx__picksearch { margin-bottom: 8px; }

.tb-chatx__picked {
    display: flex;
    flex-wrap: wrap;
    gap: 6px;
    margin-bottom: 8px;
}

.tb-chatx__pickchip {
    padding: 6px 10px;
    border: 1px solid var(--accent-deep);
    border-radius: 999px;
    background: var(--accent);
    color: #fff;
    font-family: var(--font-display);
    font-weight: 600;
    font-size: 12px;
    letter-spacing: 0.04em;
    cursor: pointer;
}

.tb-chatx__person {
    display: flex;
    align-items: center;
    gap: 8px;
    width: 100%;
    min-height: 50px;
    margin-bottom: 5px;
    padding: 8px 10px;
    background: var(--card);
    border: 1px solid var(--line);
    border-radius: var(--corner);
    text-align: left;
    cursor: pointer;
}

.tb-chatx__person.picked {
    border-color: var(--accent);
    box-shadow: inset 3px 0 0 var(--accent);
}

.tb-chatx__person-name {
    flex: 1;
    min-width: 0;
    font-weight: 600;
    font-size: 15px;
    color: var(--ink);
}

.tb-chatx__person-role {
    flex: none;
    font-family: var(--font-display);
    font-weight: 700;
    font-size: 10.5px;
    letter-spacing: 0.08em;
    color: var(--steel);
}

.tb-chatx__person-kind {
    flex: none;
    padding: 2px 6px;
    border-radius: 4px;
    background: var(--paper);
    border: 1px solid var(--line);
    font-family: var(--font-mono);
    font-size: 9px;
    letter-spacing: 0.08em;
    color: var(--ink-soft);
}

.tb-chatx__start {
    width: 100%;
    margin-top: 10px;
}

/* The dispatcher move on the rooms view - a room from a bare job number. */
.tb-chatx__jobgo {
    display: flex;
    gap: 6px;
    margin-bottom: 10px;
}

.tb-chatx__jobgo-in {
    flex: 1;
    min-width: 0;
    min-height: 42px;
    padding: 0 12px;
    border: 1px solid var(--line-hard);
    border-radius: var(--corner);
    background: var(--card);
    font-family: var(--font-mono);
    font-size: 16px; /* the iOS zoom floor */
    color: var(--ink);
}

.tb-chatx__jobgo-btn {
    flex: none;
    min-height: 42px;
    padding: 0 12px;
    border: 1px solid var(--accent-deep);
    border-radius: var(--corner);
    background: var(--accent);
    color: #fff;
    font-family: var(--font-display);
    font-weight: 700;
    font-size: 11.5px;
    letter-spacing: 0.08em;
    cursor: pointer;
}

.tb-chatx__jobgo-btn:disabled {
    background: var(--line-hard);
    border-color: var(--line-hard);
    cursor: default;
}

/* ADD PERSON - the room grows on demand ("add the estimator if wanted"). */
.tb-chatx__addbtn {
    flex: none;
    width: 32px;
    height: 32px;
    border-radius: 50%;
    border: 1px solid var(--accent);
    background: transparent;
    color: var(--accent);
    font-size: 18px;
    line-height: 1;
    cursor: pointer;
}

.tb-chatx__addbtn:active { background: var(--accent); color: #fff; }

/* The post-add confirmation line - lives in the thread until dismissed or
   the view moves on. A button so tapping it away is natural. */
.tb-chatx__flashline {
    align-self: center;
    border: 1px solid var(--ok);
    border-radius: 999px;
    padding: 5px 12px;
    background: var(--ok-soft);
    color: var(--ok);
    font-family: var(--font-mono);
    font-size: 11.5px;
    cursor: pointer;
}

/* Offline honesty: the tiles + checklist actions stay VISIBLE but disabled
   when the wire is dead - a blank page reads as broken, a disabled button
   reads as the truth. */
.tb-home__tile:disabled .tb-home__tile-glyph { color: var(--line-hard); }

.tb-home__offline {
    margin-top: 6px;
    font-family: var(--font-mono);
    font-size: 11px;
    color: var(--steel);
    text-align: center;
}

.tb-wf__go:disabled,
.tb-wf__peek:disabled,
.tb-wf__add:disabled {
    opacity: 0.45;
    cursor: default;
}

/* The chat sheet's close target earns thumb-sized geometry, and daylight
   between ADD (+) and CLOSE (x) so a glove never hits the wrong one. */
.tb-chatx__x {
    width: 38px;
    height: 38px;
    font-size: 19px;
    margin-left: 10px;
    padding: 0;
}

/* The picked-person chip's remove x - was riding the name's own font size. */
.tb-chatx__pickchip-x {
    font-size: 15px;
    font-weight: 700;
    margin-left: 8px;
}

/* ── THE PWA LAYER (2026-08-26) - self-update + build fine print ────────── */

/* UPDATE READY - the one tap that installs a waiting build. A fixed bar at
   the FOOT of the screen (a sticky top bar would fight the sticky header for
   the same edge): visible from every page, covers nothing being edited, and
   sits BELOW every scrim/overlay (40+) so open sheets keep their word.
   Accent because it is an INVITATION, not an alarm. */
.tb-update {
    position: fixed;
    left: 0;
    right: 0;
    bottom: 0;
    z-index: 30;
    display: block;
    width: 100%;
    padding: 12px 16px calc(12px + env(safe-area-inset-bottom));
    background: var(--accent);
    color: #fff;
    border: none;
    border-top: 2px solid var(--ink);
    font-family: "Barlow Condensed", sans-serif;
    font-size: 17px;
    font-weight: 700;
    letter-spacing: 0.06em;
    cursor: pointer;
}

/* The fine print at the foot of HOME: build stamp + the manual update pull.
   Deliberately quiet - it exists for the "Kirk says there's a fix" phone
   call, not for every day. */
.tb-home__ver {
    display: flex;
    flex-wrap: wrap;
    align-items: center;
    justify-content: center;
    gap: 10px;
    margin-top: 18px;
    padding: 10px 0 4px;
    border-top: 1px dashed rgba(22, 35, 45, 0.25);
}

.tb-home__ver-stamp {
    font-family: "IBM Plex Mono", monospace;
    font-size: 11px;
    font-weight: 600;
    letter-spacing: 0.08em;
    color: rgba(22, 35, 45, 0.55);
}

.tb-home__ver-check {
    background: transparent;
    border: 1px solid rgba(22, 35, 45, 0.35);
    border-radius: 999px;
    padding: 4px 12px;
    font-family: "Barlow Condensed", sans-serif;
    font-size: 13px;
    font-weight: 700;
    letter-spacing: 0.06em;
    color: rgba(22, 35, 45, 0.7);
    cursor: pointer;
}

.tb-home__ver-check:disabled {
    opacity: 0.5;
    cursor: default;
}

.tb-home__ver-msg {
    flex-basis: 100%;
    text-align: center;
    font-size: 12.5px;
    color: rgba(22, 35, 45, 0.7);
}

/* ── LOOKUP: the in-box clear (2026-08-26) ──────────────────────────────
   Lives INSIDE the box's right end; clears the TEXT only. The wrap takes
   over the input's flex slot so GO keeps its place. */
.tb-searchwrap {
    position: relative;
    flex: 1;
    min-width: 0;
}

.tb-searchwrap .tb-search {
    width: 100%;
    padding-right: 40px;
}

.tb-search__x {
    position: absolute;
    top: 50%;
    right: 4px;
    transform: translateY(-50%);
    width: 34px;
    height: 34px;
    display: inline-flex;
    align-items: center;
    justify-content: center;
    background: transparent;
    border: none;
    border-radius: 999px;
    color: var(--ink-soft);
    font-size: 15px;
    font-weight: 700;
    cursor: pointer;
}

/* ── JOB PHOTOS: the kind filter (2026-08-26) ───────────────────────────
   Chips in the popup - tap applies and closes. Filled = the active pick,
   the same on/off language the SEARCH NOTES toggle speaks. */
.tb-docfilter {
    display: flex;
    flex-wrap: wrap;
    gap: 8px;
}

.tb-docfilter__opt {
    display: inline-flex;
    align-items: center;
    gap: 6px;
    padding: 9px 14px;
    background: var(--card);
    border: 1.5px solid var(--line-hard);
    border-radius: 999px;
    font-family: var(--font-display);
    font-size: 14px;
    font-weight: 700;
    letter-spacing: 0.05em;
    color: var(--ink);
    cursor: pointer;
}

.tb-docfilter__opt--on {
    background: var(--ink);
    border-color: var(--ink);
    color: var(--paper);
}

.tb-docfilter__n {
    font-family: var(--font-mono);
    font-size: 11px;
    font-weight: 600;
    color: var(--ink-soft);
}

.tb-docfilter__opt--on .tb-docfilter__n {
    color: rgba(243, 239, 230, 0.7);
}

/* ══ THE LOCATION OPT-IN CARD ═════════════════════════════════════════════
   Kirk, 2026-08-31: "If no-geo is there, card, get them to opt-in. We want to
   do this carefully."

   Rendered by MainLayout above @Body and gated to /home - see ShowGeoCard for
   why it appears nowhere else. It replaces nothing: the NO GEO chip in the bar
   stays as the standing way back, and both open the same LocationPrompt.

   ⚠️ IT IS NOT A MODAL AND MUST NEVER BECOME ONE. No scrim, no fixed position,
   no pointer-events trapping. It sits in the page flow at the top of Home so
   every button underneath it still works. The whole reason the fleet ended up
   on NO GEO was a permission ask that read as friction and got dismissed; a
   card that blocks the app would earn the same reflex and deserve it. */

.tb-geocard {
    margin: 0 0 14px;
    padding: 12px 14px;
    background: var(--accent-soft);
    border: 2px solid var(--accent);
    border-left-width: 6px;
}

/* Refused is a DIFFERENT thing to say, so it gets a different colour: the
   browser will never prompt again, and the card's job changes from inviting to
   giving directions. */

.tb-geocard--off {
    background: var(--paper);
    border-color: var(--danger);
}

.tb-geocard__head {
    display: flex;
    align-items: center;
    gap: 8px;
}

.tb-geocard__mark {
    flex: 0 0 auto;
    font-size: 16px;
    line-height: 1;
    color: var(--accent);
}

.tb-geocard--off .tb-geocard__mark {
    color: var(--danger);
}

.tb-geocard__title {
    font-family: var(--font-display);
    font-weight: 700;
    font-size: 13px;
    letter-spacing: 0.08em;
    color: var(--ink);
}

.tb-geocard__body {
    margin-top: 6px;
    font-size: 13px;
    line-height: 1.45;
    color: var(--ink-soft);
}

/* The two buttons sit right-aligned with the primary last, matching every other
   foot in the app (the upload sheet, the confirm sheets) so a thumb already
   knows where the affirmative one is. */

.tb-geocard__foot {
    display: flex;
    justify-content: flex-end;
    gap: 8px;
    margin-top: 12px;
}

/* ══ THE MADLIBS SENTENCE ═════════════════════════════════════════════════
   A form that reads as English instead of as a stack of labelled boxes:

       I want to [Create v] a [Dispatch v] for [(search) Woodway Point Condos]
       at [706 Augusta Dr v]. Send [Butch v] on [Mon 9/1 v] at [9:00 AM v]
       for [Line Clean v]. PO [_______].

   A tech reads the whole request in one pass and can see what is still missing
   without decoding six field labels - the sentence itself says what each slot
   is for.

   ⚠️ THE BLANK IS THE ONE GENUINELY NEW CONTROL IN THE APP, and that is why
   this is its own block rather than a reuse. Every option class we already own
   (.tb-xfer__opt, .tb-taskpick__opt, .tb-catpick__opt, .tb-docfilter__opt,
   .tb-wf__pick) is a block or flex box built to sit in a GRID or a ROW. Not one
   of them sits on a text baseline inside running copy, which is the entire job
   here. Borrowing one would have meant fighting its display and its margins on
   every rule, and the next change to either control would have been a coin
   toss - the same call the file already made at .tb-taskpick. */

.tb-madlib {
    font-family: var(--font-body);
    font-weight: 500;
    font-size: 17px;
    color: var(--ink);
    margin: 0 0 4px;
}

/* One sentence line. The line-height looks absurd written down and is not:
   running text is mixing with 40px-tall inline controls, so anything tighter
   makes consecutive lines' blanks collide and the sentence stops looking like a
   sentence. Nothing in the app has had to do this before - the nearest prose
   blocks (.tb-confirm__body at 1.45, .tb-wf__confirm-text at 1.35) are set for
   words only and are far too tight to borrow. */
.tb-madlib__line {
    line-height: 2.1;
    margin: 0 0 2px;
}

/* THE INLINE BLANK - a tappable slot living on the sentence's baseline.

   inline-flex so the value and its caret stay a single unit that cannot break
   across a line, and vertical-align: baseline so the slot's own text sits on
   the same rule as the words either side of it. (middle would centre the BOX
   on the x-height and float the value visibly above the sentence.)

   40px minimum: the de-facto floor for a small option button in this app, and
   these are tapped with a glove in a crawlspace.

   17px keeps it over the 16px iOS line whichever element the markup lands on -
   anything smaller and a blank rendered as an <input> would zoom Safari in on
   focus and never zoom back out. */
.tb-madlib__blank {
    display: inline-flex;
    align-items: center;
    gap: 6px;
    vertical-align: baseline;
    min-height: 40px;

    /* nowrap + a cap + ellipsis: "Woodway Point Condos" is longer than a phone
       line, and a blank that wrapped internally would tear the sentence in
       half. Capping the BLANK rather than the line lets the sentence keep
       wrapping around it normally - only the value inside gets shortened.
       14em ~ 240px at this size, which leaves room for words on both sides of
       it on a 375px handset. */
    max-width: min(100%, 14em);
    white-space: nowrap;
    overflow: hidden;
    text-overflow: ellipsis;

    padding: 4px 10px;
    font-family: var(--font-display);
    font-weight: 700;
    font-size: 17px;
    letter-spacing: 0.04em;
    text-align: left;
    color: var(--ink);
    background: var(--card);
    border: 2px solid var(--line-hard);
    cursor: pointer;
    touch-action: manipulation;
}

/* ⚠️ text-overflow IS DECLARED TWICE ON PURPOSE. It only applies to block
   containers, and a flex container is not one - so the rule on the blank above
   buys the hard clip (overflow: hidden) but not the "...". The child rule is
   what actually draws the ellipsis. Declaring both means the blank degrades to
   a clean clip rather than to overflowing text if the markup ever puts a bare
   text node in there instead of a span.

   The caret is excluded by name so it can never be the thing that gets
   shortened - it is the affordance, not the value. */
.tb-madlib__blank > span:not(.tb-madlib__caret) {
    min-width: 0;
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
}

/* ⚠️ COMPOUND, NOT BARE. --empty / --filled / :disabled are all modifiers of
   .tb-madlib__blank and would sit at (0,1,0) written bare - the same
   equal-specificity cascade-order bug the file has already been bitten by twice
   (.tb-xfer__opt--done.--on, .tb-sect__find--on:active). Compounded they are
   (0,2,0) and beat the base rule regardless of what gets appended below them
   later. */

/* Not filled yet, and it should read as an INVITATION rather than as an empty
   control. Dashed on three sides says "slot"; the one solid orange edge is the
   line you write on, which is the only thing in a paper job ticket that has
   ever meant "put a value here". */
.tb-madlib__blank.tb-madlib__blank--empty {
    color: var(--steel);
    background: var(--paper);
    border: 2px dashed var(--line-hard);
    border-bottom: 2px solid var(--accent);
}

/* Holds a value.

   ⚠️ THE APP HAD NO EMPTY-VS-FILLED VOCABULARY BEFORE THIS. Every existing
   --on means "selected among peers" (ink fill, paper text) and that is the
   wrong sentence here - a filled blank is not competing with anything, it is
   simply answered. The only soft fill in the app is .tb-otp__channel--on, and
   it means exactly this: accent-soft ground, accent-deep text, accent border -
   settled, legible, and clearly still tappable to change. Borrowed wholesale so
   the two go on meaning the same thing. */
.tb-madlib__blank.tb-madlib__blank--filled {
    color: var(--accent-deep);
    background: var(--accent-soft);
    border: 2px solid var(--accent);
}

/* The sentence is progressive - you cannot pick a building before you have
   picked a customer - so a later blank can be un-fillable for a perfectly
   ordinary reason. INERT, NOT BROKEN: it keeps the dashed slot shape so it
   still reads as part of the sentence, and only loses the orange write-on-me
   line and some contrast. Nothing red, nothing struck through; there is no
   error here, just a turn that has not come round yet.

   Written after --empty deliberately: both are (0,2,0), and a disabled blank is
   always an empty one, so the later rule has to be this one. */
.tb-madlib__blank:disabled {
    color: var(--ink-soft);
    background: var(--paper);
    border: 2px dashed var(--line);
    opacity: 0.5;
    cursor: not-allowed;
}

/* The small "v". currentColor rather than a token so it follows the blank
   through every state above without four more rules; flex-shrink:0 so the
   ellipsis eats the value and never the affordance. */
.tb-madlib__caret {
    flex-shrink: 0;
    font-size: 11px;
    line-height: 1;
    color: currentColor;
    opacity: 0.6;
}

.tb-madlib__hint {
    margin: 2px 0 0;
    font-family: var(--font-mono);
    font-size: 12px;
    font-weight: 500;
    line-height: 1.35;
    color: var(--steel);
}

/* The chosen value read back inside prose on the confirm step - "Send BUTCH on
   MON 9/1" rather than a second table of fields. Small and bold: it is an
   emphasis inside a sentence, not a chip.

   ⚠️ IT MUST WRAP, AND THAT IS WHY IT IS NOT A .tb-tag. .tb-tag sets
   white-space: nowrap, and that exact property had to be overridden at
   .tb-tag--po because a non-wrapping tag pushed the whole card sideways on a
   375px screen. A customer name is longer than a PO number. overflow-wrap:
   anywhere so even one unbroken 30-character string bends rather than shoves. */
.tb-madlib__said {
    font-family: var(--font-display);
    font-weight: 700;
    font-size: 14px;
    letter-spacing: 0.04em;
    color: var(--accent-deep);
    white-space: normal;
    overflow-wrap: anywhere;
}

/* ══ THE IN-SHEET PICKER ══════════════════════════════════════════════════
   What a madlib blank opens: search a customer, a building, a tech, or pick a
   task code - all of it inside .tb-sheet, the bottom sheet.

   ⚠️ THIS BLOCK MUST NEVER SET A z-index, AND THAT IS THE WHOLE REASON IT
   EXISTS. .tb-taskpick and .tb-catpick are z 41; .tb-sheet is z 61. Reusing
   either container would paint the picker UNDERNEATH the sheet that opened it -
   a control the tech can hear responding and cannot see. These rules live
   inside the sheet's stacking context and inherit it, so any z-index added here
   later is at best noise and at worst a new bug. Same call, same reasoning as
   .tb-taskpick vs .tb-catpick: they look alike, they are not the same control. */

/* Search row inside the sheet body - input then GO. Same shape as
   .tb-searchrow / .tb-search, restated as its own class rather than reached for
   with a second class in the markup, because this one lives under a foot and a
   scroll container that those know nothing about. */
.tb-wpick__search {
    display: flex;
    gap: 8px;
    margin-bottom: 12px;
}

/* 16px is not cosmetic - below it iOS Safari zooms the page on focus and never
   zooms back, the same floor .tb-search and .tb-eqfind__box live under. The
   margin kill is because .tb-input carries 16px of bottom margin for stacked
   forms, which in a flex row is just a gap in the wrong axis. */
.tb-wpick__search .tb-input {
    margin-bottom: 0;
    font-size: 16px;
}

.tb-wpick__search .tb-btn {
    width: auto;
    min-width: 72px;
    flex-shrink: 0;
}

/* A search RESULT - a customer, a building, a tech. Full width and left
   aligned because the eye scans a result list down its left edge; the 4px
   accent border is the app's standing "this row is a thing you can take", from
   .tb-wf__pick. 52px minimum with two lines of content inside it. */
.tb-wpick__row {
    display: block;
    width: 100%;
    min-height: 52px;
    margin-bottom: 8px;
    padding: 10px 12px;
    text-align: left;
    color: var(--ink);
    background: var(--paper);
    border: 1px solid var(--line);
    border-left: 4px solid var(--accent);
    border-radius: 10px;
    cursor: pointer;
    touch-action: manipulation;
}

.tb-wpick__row:active {
    background: var(--accent-soft);
}

/* The name. Deliberately NOT ellipsized - this is the thing being chosen, and a
   tech picking between "Woodway Point Condos" and "Woodway Point Condos II"
   needs to see the whole of both. It wraps; the row grows. */
.tb-wpick__rowtop {
    display: block;
    font-family: var(--font-display);
    font-weight: 700;
    font-size: 16px;
    letter-spacing: 0.02em;
    color: var(--ink);
}

/* The disambiguator - an address, an employee number. Ellipsized on one line
   because unlike the name it is a hint, and a two-line address in every row
   would halve how many results fit on a phone. */
.tb-wpick__rowsub {
    display: block;
    max-width: 100%;
    margin-top: 2px;
    font-family: var(--font-mono);
    font-size: 12px;
    font-weight: 500;
    line-height: 1.3;
    color: var(--steel);
    white-space: nowrap;
    overflow: hidden;
    text-overflow: ellipsis;
}

/* TWO COLUMNS for the task codes, the same shape as .tb-taskpick__grid and for
   the same reason: every option carries a second line, and ragged two-line
   chips are far harder to scan than a grid. */
/* A chip grid is a BLOCK inside the sheet, not the whole sheet, so it needs air
   underneath it - without this the next thing down butts straight against it. */
.tb-wpick__grid {
    display: grid;
    grid-template-columns: 1fr 1fr;
    gap: 8px;
    margin-bottom: 12px;
}

/* THREE ACROSS, for a row of mode chips rather than a list of choices.
   BOARDS / JOB # / SEARCH in a two-column grid left SEARCH stranded on a row of
   its own beside a gap - which reads as a layout fault and, worse, spends a
   whole row of height on a screen that has a keyboard across the bottom of it. */
.tb-wpick__grid--three {
    grid-template-columns: repeat(3, 1fr);
}


.tb-wpick__opt {
    display: flex;
    flex-direction: column;
    align-items: flex-start;
    gap: 1px;
    /* min-width:0 lets a grid item be narrower than its own text, which is what
       makes the description's ellipsis below possible at all. */
    min-width: 0;
    min-height: 52px;
    padding: 7px 9px;
    text-align: left;
    color: var(--ink);
    background: var(--card);
    border: 2px solid var(--line-hard);
    cursor: pointer;
    touch-action: manipulation;
}

.tb-wpick__opt:active {
    background: var(--accent-soft);
}

/* Chosen. Ink fill + paper text - the app's one and only word for "selected
   among peers" (.tb-tab--on, .tb-xfer__opt--on, .tb-taskpick__opt--on,
   .tb-docfilter__opt--on). Compounded rather than bare so nothing appended
   after it can win on cascade order at equal specificity. */
.tb-wpick__opt.tb-wpick__opt--on {
    color: var(--paper);
    background: var(--ink);
    border-color: var(--ink);
}

.tb-wpick__code {
    font-family: var(--font-display);
    font-weight: 700;
    font-size: 15px;
    letter-spacing: 0.06em;
    text-transform: uppercase;
}

/* The half a tech actually reads - "HSBL" means nothing, "Hose Bibb Leaking"
   means everything. One ellipsized line: they run to 30 characters and a cell
   is about half a phone wide. */
.tb-wpick__desc {
    max-width: 100%;
    font-size: 11px;
    font-weight: 500;
    line-height: 1.25;
    opacity: 0.78;
    white-space: nowrap;
    overflow: hidden;
    text-overflow: ellipsis;
}

/* Nothing matched. Mono and muted, the same quiet voice as .tb-wf__empty and
   .tb-dir__empty - an empty result is information, not a failure, and it should
   never shout louder than the search box that caused it. */
/* The filter's own commentary - it says which rung of the ladder ran, so a list
   that is showing something other than exactly what the box says never does it
   silently. See ShownSites. */
.tb-wpick__note {
    margin: 0 0 8px;
    padding: 0 2px;
    font-family: var(--font-mono);
    font-size: 12px;
    color: var(--steel);
}

.tb-wpick__empty {
    padding: 14px 4px;
    font-family: var(--font-mono);
    font-size: 12px;
    color: var(--ink-soft);
}

/* ── THE SENTENCE'S FULL STOP ─────────────────────────────────────────────
   ⚠️ A PERIOD AFTER A BLANK WILL WRAP ONTO A LINE OF ITS OWN, and it looks
   exactly like a rendering fault. Seen on a real handset 2026-08-31: the
   "Send [me] on [Thu 9/3] at [10:00 AM] for [New Install]." line put its
   final "." alone on the next row.

   The blank is an inline-flex box with a max-width, so the browser is free to
   break between it and the text node following it. Wrapping the blank AND its
   punctuation in a nowrap span removes that break opportunity without stopping
   the sentence wrapping everywhere else - the break can still happen BEFORE the
   span, which is what keeps a long line readable. */
.tb-madlib__stop {
    white-space: nowrap;
}

/* -- THE TITLE-FILTER RAIL, PANNABLE WITH A MOUSE --------------------------
   Kirk, 2026-08-31, after demonstrating the directory to Charlie's: "Would you
   please make the title filters scroll right and left with a mouse click?"

   .tb-dir__chips is overflow-x:auto with a HIDDEN scrollbar, which is exactly
   right for a phone - a thumb swipes it. With a mouse there is nothing to grab
   and the vertical wheel belongs to the page, so every filter past the right
   edge was unreachable. Charlie's already cuts WAREHOUSE off mid-word.

   The rail wraps the row and puts an arrow either side. Behaviour and the
   port-from-RCS reasoning live in wwwroot/js/page.js.

   ⚠️ NAMED __chiprail, NOT __rail. `.tb-dir__rail` IS ALREADY TAKEN - it is the
   fixed-position A-Z rolodex strip at line 7447 (position:fixed;
   flex-direction:column; z-index:20), whose buttons are .tb-dir__rail-l.

   This was written as .tb-dir__rail first, and that one name broke BOTH things
   at once - caught by measuring the live DOM, not by reading. Appended later, it
   reset the letter strip's flex-direction to `row`, laying the A-Z index out
   horizontally off the right edge of the screen; and it inherited the
   `position: fixed` it had not overridden, which is why the chip row measured
   629px wide inside a 240px parent. Two regressions, one name. */
.tb-dir__chiprail {
    display: flex;
    align-items: center;
    gap: 4px;
    /* Takes over the flex-item duty .tb-dir__chips used to hold in .tb-dir__bar,
       so the row still collapses rather than growing to fit every chip. */
    flex: 1;
    min-width: 0;
}

/* -- the arrows --
   ⚠️ IN FLOW, NOT OVERLAID. The RCS carousel floats its arrows ON the rail
   because a card is big enough to spare the room. A filter chip is not: an
   overlaid arrow would sit on top of a title and make it unreadable and
   un-tappable, so these take their own space beside the row.

   ⚠️ 34px, WHICH IS UNDER THE APP'S 40px OPTION FLOOR, DELIBERATELY. That floor
   exists for thumbs, and these only ever render for a POINTER - page.js reports
   nowhere-to-go on a touch-only device, which hides them. Keeping them near chip
   height also stops the whole bar growing a row taller on a phone that never
   sees them. */
.tb-dir__chipnav {
    flex: none;
    width: 34px;
    height: 34px;
    display: flex;
    align-items: center;
    justify-content: center;
    padding: 0;
    background: var(--card);
    border: 1px solid var(--line-hard);
    border-radius: 999px;
    font-family: var(--font-display);
    font-size: 20px;
    line-height: 1;
    color: var(--ink);
    cursor: pointer;
    touch-action: manipulation;
}

.tb-dir__chipnav:active {
    background: var(--accent-soft);
    border-color: var(--accent);
}

/* Kept in place rather than hidden at the ends of the rail: a control that
   vanishes mid-interaction moves the other arrow under the cursor, and the next
   click then pans the wrong way. */
.tb-dir__chipnav:disabled {
    color: var(--line-hard);
    border-color: var(--line);
    background: transparent;
    cursor: default;
}

/* -- drag-to-pan --
   Applied by page.js only on a pointer device, so a phone never gets a grab
   cursor it cannot use. */
.tb-dir__chips.pannable {
    cursor: grab;
}

.tb-dir__chips.dragging {
    cursor: grabbing;
    /* ⚠️ WITHOUT THIS A DRAG SELECTS THE CHIP LABELS instead of panning, and the
       row fills with blue highlight. */
    user-select: none;
}

/* ══ THE SUPPLY HOUSE, REACHABLE ═══════════════════════════════════════════
   Kirk, 2026-09-05: the address opens MAPS, the phones dial, the emails open
   mail. Inline on the CREATE A PURCHASE ORDER sentence rather than behind the
   ContactSheet modal, because a tech mid-sentence should not have to open a
   dialog to see where they are being sent - the vendor was just picked and the
   branch is the thing they are confirming.

   Lighter than .tb-contacts__row on purpose: this sits under a sentence rather
   than in a sheet of its own, so it gets a hairline between rows and no
   min-height block. Still a 44px target. */
.tb-vcontact {
    margin: 4px 0 10px;
    border-top: 1px solid var(--line);
}

.tb-vcontact__row {
    display: flex;
    align-items: center;
    gap: 10px;
    width: 100%;
    min-height: 44px;
    padding: 6px 2px;
    color: var(--ink);
    text-decoration: none;
    border-bottom: 1px solid var(--line);
    touch-action: manipulation;
}

.tb-vcontact__row:active {
    background: var(--ink);
    color: var(--paper);
}

.tb-vcontact__icon {
    flex-shrink: 0;
    width: 16px;
    text-align: center;
    font-size: 15px;
    color: var(--steel);
}

.tb-vcontact__row:active .tb-vcontact__icon,
.tb-vcontact__row:active .tb-vcontact__who,
.tb-vcontact__row:active .tb-vcontact__go {
    color: var(--paper);
}

/* min-width: 0 or the ellipsis never fires - a flex item defaults to
   min-width: auto and refuses to shrink under its own content. */
.tb-vcontact__val {
    flex: 1;
    min-width: 0;
    font-family: var(--font-mono);
    font-size: 13px;
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
}

/* An email is the one value that genuinely does not fit on a phone, and the
   half a tech needs to recognise it is the FRONT. Ellipsis, never wrap. */
.tb-vcontact__val--mail {
    font-size: 12px;
}

.tb-vcontact__who {
    font-family: var(--font-display);
    font-weight: 700;
    font-size: 11px;
    letter-spacing: 0.08em;
    text-transform: uppercase;
    color: var(--steel);
    margin-right: 6px;
}

.tb-vcontact__go {
    flex-shrink: 0;
    font-family: var(--font-display);
    font-weight: 700;
    font-size: 11px;
    letter-spacing: 0.08em;
    color: var(--steel);
}

/* ══ PURCHASE ORDERS ON A JOB ══════════════════════════════════════════════
   The panel under the crew's notes, and the receipt gate on the way to
   COMPLETE. Two surfaces, one idea: a purchase order with nothing billed
   against it and no receipt photo is a spent number nobody can account for. */
.tb-pos__row {
    display: flex;
    align-items: center;
    gap: 10px;
    padding: 10px 12px;
    border-bottom: 2px dashed var(--line);
}

.tb-pos__row:last-child {
    border-bottom: none;
}

/* min-width: 0 or a long supply-house name pushes the badge off the row - a
   flex item defaults to min-width: auto and refuses to shrink. */
.tb-pos__main {
    flex: 1;
    min-width: 0;
}

.tb-pos__no {
    display: block;
    font-family: var(--font-mono);
    font-weight: 700;
    font-size: 15px;
    color: var(--ink);
}

.tb-pos__vendor {
    display: block;
    margin-top: 1px;
    font-size: 13px;
    color: var(--ink);
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
}

.tb-pos__meta {
    display: block;
    margin-top: 1px;
    font-family: var(--font-display);
    font-size: 11px;
    letter-spacing: 0.06em;
    text-transform: uppercase;
    color: var(--steel);
}

/* ⚠️ A BUTTON, NOT A LABEL. "NO RECEIPT" is the one row state a tech can do
   something about, so tapping it opens the camera with the PO's stamp already
   welded on - the same door the gate uses. */
.tb-pos__need {
    flex-shrink: 0;
    padding: 8px 10px;
    min-height: 44px;
    border: 2px solid var(--danger);
    background: rgba(201, 42, 42, 0.06);
    color: var(--danger);
    font-family: var(--font-display);
    font-weight: 700;
    font-size: 11px;
    letter-spacing: 0.08em;
    touch-action: manipulation;
}

.tb-pos__need:active {
    background: var(--danger);
    color: var(--paper);
}

.tb-pos__ok {
    flex-shrink: 0;
    font-family: var(--font-display);
    font-weight: 700;
    font-size: 11px;
    letter-spacing: 0.08em;
    color: var(--steel);
}

/* The list inside the receipt gate. Sits in a confirm sheet that already has
   its own padding, so this only needs to separate the rows from the prose. */
.tb-poask {
    margin: 12px 0 0;
    border-top: 2px dashed var(--line);
}

.tb-poask__row {
    display: flex;
    align-items: center;
    gap: 10px;
    padding: 10px 0;
    border-bottom: 2px dashed var(--line);
}

.tb-poask__id {
    flex: 1;
    min-width: 0;
}

/* ⚠️ .tb-btn IS width:100% BY DEFAULT and flex-shrink:0 does not undo that -
   the buttons ate half the row and read as the main event, when the thing a
   tech needs to read is WHICH purchase order. Sized to the thumb and no wider. */
.tb-poask__row .tb-btn {
    flex: 0 0 auto;
    width: auto;
    min-width: 96px;
    padding-left: 16px;
    padding-right: 16px;
}

.tb-poask__no {
    display: block;
    font-family: var(--font-mono);
    font-weight: 700;
    font-size: 16px;
    color: var(--ink);
}

.tb-poask__where {
    display: block;
    font-size: 13px;
    color: var(--steel);
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
}

/* The locked stamp in front of the upload sheet's name box. Reads as part of
   the field rather than a separate control, because it is not one - the tech
   cannot change it and should not try. */
.tb-up__namerow {
    display: flex;
    align-items: stretch;
    gap: 0;
}

.tb-up__prefix {
    display: flex;
    align-items: center;
    flex-shrink: 0;
    padding: 0 10px;
    border: 2px solid var(--line);
    border-right: none;
    background: var(--line);
    font-family: var(--font-mono);
    font-weight: 700;
    font-size: 13px;
    color: var(--ink);
    white-space: nowrap;
}

.tb-up__namerow .tb-input {
    flex: 1;
    min-width: 0;
}

/* The line total, called out inside the meta line - a tech scanning the panel
   is looking for the money before anything else on the row. */
.tb-pos__money {
    color: var(--ink);
}

/* Two words stacked, so the most actionable badge on the row stays a thumb
   target without pushing the vendor name off the card. */
.tb-pos__need {
    line-height: 1.25;
    text-align: center;
}

/* The basement-copy strip on the purchase-order panel. Quiet - it is a caveat,
   not an alarm; the panel is still useful and the numbers are still real, they
   are just not confirmed with ESC on this visit. */
.tb-pos__stale {
    margin: 0;
    padding: 8px 12px;
    border-bottom: 2px dashed var(--line);
    font-family: var(--font-display);
    font-size: 11px;
    letter-spacing: 0.06em;
    text-transform: uppercase;
    color: var(--steel);
}

/* ══ THE FIELD-NOTES WINDOW ════════════════════════════════════════════════
   Kirk, 2026-09-05: "when a couple techs get all wordy, which we want, the
   dispatch navigation gets really ugly ... If we have an ongoing dispatch for
   weeks this will be unreal for the techs to view."

   The notes are the richest thing on a job page and they were unbounded, so
   they pushed the visits, equipment, purchase orders and photos off the bottom.
   Now they own a window and scroll inside it.

   ⚠️ dvh, NOT vh. On iOS the URL bar makes vh taller than what you can actually
   see, so a 60vh block is partly under the chrome - and this one is a scroll
   container, which means the bottom of the list would sit somewhere unreachable.
   Same reason the picker sheet is anchored in dvh.

   ⚠️ AND A max-height, NOT height. A job with two short notes should be two
   short notes, not an empty box with a scrollbar. It only becomes a window when
   there is enough to fill one. */
.tb-fn__list {
    max-height: 60dvh;
    overflow-y: auto;
    /* Keeps a flick inside the notes from dragging the job page behind it once
       the list hits its end - the same containment the contact sheet needs. */
    overscroll-behavior: contain;
    /* The scroll edge lands mid-note otherwise and reads as a rendering fault
       rather than as "there is more". */
    scroll-padding-block: 8px;
}

/* A hairline so the window reads as a window. Without it a scrolled list looks
   like the card is simply cut off. */
.tb-fn__list::-webkit-scrollbar {
    width: 10px;
}

.tb-fn__list::-webkit-scrollbar-thumb {
    background: var(--line-hard);
    border: 3px solid var(--card);
    border-radius: 5px;
}

/* Roomier editor - Kirk asked for "a little larger". 8k of end-of-day notes
   through a six-row slot was a keyhole. Still capped in dvh so the keyboard
   never pushes the SAVE button off an iPhone. */
.tb-fned__box {
    min-height: 34dvh;
    max-height: 46dvh;
}

/* The FUTURE line in the ASSIGN job picker. A rule with its label sitting on
   it, so the eye reads a boundary rather than another row. */
.tb-wpick__split {
    display: flex;
    align-items: center;
    gap: 10px;
    margin: 14px 0 10px;
    font-family: var(--font-display);
    font-weight: 700;
    font-size: 11px;
    letter-spacing: 0.12em;
    color: var(--steel);
}

.tb-wpick__split::before,
.tb-wpick__split::after {
    content: "";
    flex: 1;
    height: 2px;
    background: var(--line);
}

/* ══ THE ON-CALL ROTA ══════════════════════════════════════════════════════ */

/* A NAME ON THE ROTA IS A DOOR. Each one links to that person in the directory,
   scoped by EmpNo - so the rota never restates a phone number the directory
   already owns.

   ⚠️ The comma between names is a SIBLING, not an ::after on the link. A ::after
   inside the <a> sits inside the underline and drags it through the punctuation,
   which reads as a typo at arm's length. */
.tb-oc__person {
    color: inherit;
    text-decoration: none;
    border-bottom: 1px dotted var(--line-hard);
}

.tb-oc__person:active { color: var(--accent); }

/* THE LEADER, IN BOLD. Kirk: "we could identify a leader ... decorate the leader
   name (bold it)". Weight only - no colour, no badge: on a row that is already
   carrying a date, a team name and sometimes a NOW tag, one more colour is one
   too many, and bold reads at arm's length in a van. */
.tb-oc__person--lead {
    font-weight: 700;
    border-bottom-color: var(--accent);
}

/* THE RENAME PENCIL. Kirk: "a pencil or some subtle marker ... it could be a
   small edit link or a pencil on the team management line."

   SUBTLE IS THE REQUIREMENT, not a compromise on it: a team is named once and
   read forever, so this must not compete with the name it edits. Muted, no
   border, no background - it reads as punctuation until somebody looks for it,
   and then it is exactly where they looked. */
.tb-oc__pencil {
    margin-left: 6px;
    padding: 2px 4px;
    border: none;
    background: transparent;
    color: var(--steel);
    font-size: 13px;
    line-height: 1;
    opacity: 0.55;
    cursor: pointer;
}

.tb-oc__pencil:active {
    opacity: 1;
    color: var(--accent);
}

/* Editing in place: the box takes the room, the two answers sit beside it. */
.tb-oc__rename {
    display: flex;
    align-items: center;
    gap: 6px;
}

.tb-oc__renamebox {
    flex: 1 1 auto;
    min-width: 0;
    margin: 0;
}

.tb-oc__renameok,
.tb-oc__renamex {
    flex-shrink: 0;
    min-width: 34px;
    min-height: 34px;
    padding: 0 8px;
    border: 2px solid var(--line-hard);
    background: transparent;
    font-size: 15px;
    line-height: 1;
    cursor: pointer;
}

/* ⚠️ THE TICK IS THE COMMITTING ONE, so it carries the weight - ✓ and ✕ at the
   same value is a coin toss at arm's length on a bright day. */
.tb-oc__renameok {
    border-color: var(--ok);
    color: var(--ok);
    font-weight: 700;
}

.tb-oc__renameok:disabled {
    opacity: 0.4;
    border-color: var(--line-hard);
    color: var(--steel);
}

.tb-oc__renamex { color: var(--steel); }

/* TEAM and LEAD share the width EDIT used to have. */
.tb-oc__teambtns {
    display: flex;
    gap: 6px;
    flex-shrink: 0;
}


/* THE HEADER BADGE. Green like the chat blip - the bar's other accent is the
   tech's own orange name chip, and two orange lumps in one corner read as one
   control. Solid rather than outlined because unlike CHAT this is not a place
   to go, it is a fact about right now. */
.tb-chip--oncall {
    background: var(--ok);
    border-color: var(--ok);
    color: #fff;
    font-weight: 700;
    /* 11px, matching NoGeo rather than the 12px chips. Kirk: "make the text a
       tiny bit smaller for On Call - like NoGeo is smaller." Both are STATES the
       bar is reporting; CHAT is a door you tap. Sizing them together says that
       without a word, and it buys the wordmark a few px back. */
    font-size: 11px;
    /* No letter-spacing since it went mixed-case - the tracking existed to open
       up ALL CAPS and now only costs width in the tightest bar we have. */
}

.tb-oc__head {
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: 10px;
    padding: 14px 2px 8px;
}

.tb-oc__headline {
    font-family: var(--font-display);
    font-weight: 700;
    font-size: 22px;
    letter-spacing: 0.06em;
    color: var(--ink);
}

.tb-oc__admin {
    padding: 8px 14px;
    min-height: 44px;
    border: 2px solid var(--line-hard);
    background: transparent;
    color: var(--steel);
    font-family: var(--font-display);
    font-weight: 700;
    font-size: 12px;
    letter-spacing: 0.1em;
    touch-action: manipulation;
}

/* THE ANSWER, WHEN IT IS YES. Loud on purpose - this is the one thing most
   people opened the page to see, and it earns the space precisely because it
   is not on screen the rest of the time. */
.tb-oc__now {
    padding: 14px;
    background: var(--ok);
    border-color: var(--ok);
}

.tb-oc__nowlead {
    display: block;
    font-family: var(--font-display);
    font-weight: 700;
    font-size: 20px;
    letter-spacing: 0.08em;
    color: #fff;
}

.tb-oc__nowteam {
    display: block;
    margin-top: 2px;
    font-family: var(--font-mono);
    font-size: 14px;
    color: #fff;
}

.tb-oc__nowwith {
    display: block;
    margin-top: 6px;
    font-size: 13px;
    color: rgba(255, 255, 255, 0.88);
}

/* ── THE 28-DAY STRIP ──────────────────────────────────────────────────────
   Austin: "this does have to work on the phone, so there is space requirements
   if you make a mini-calendar."

   So: not a grid. A real month grid spends seven columns telling a tech the
   dates they already know, to surface the one thing they do not - whose week it
   is. A band of days does that in one row, scrolls with a thumb, and always
   starts TODAY rather than on the 1st. */
.tb-oc__strip {
    display: flex;
    gap: 3px;
    overflow-x: auto;
    /* Keeps a flick along the strip from dragging the page sideways. */
    overscroll-behavior-x: contain;
    padding: 4px 2px 10px;
}

.tb-oc__day {
    flex: 0 0 auto;
    width: 34px;
    padding: 5px 0;
    text-align: center;
    background: var(--steel);
    color: var(--paper);
    border: 2px solid transparent;
}

/* Nobody on call. Quiet, not alarming - an uncovered day is usually just a day
   the office has not got to yet. */
.tb-oc__day--free {
    background: transparent;
    color: var(--line-hard);
    border-color: var(--line);
}

/* YOURS. The only colour on the strip that means "you", matching the banner. */
.tb-oc__day--mine {
    background: var(--ok);
    color: #fff;
}

/* ⚠️ TODAY IS A RING, NEVER A FILL - the fill already carries whose day it is,
   and overriding it would trade the answer for the date. */
.tb-oc__day--today {
    border-color: var(--accent);
}

.tb-oc__dow {
    display: block;
    font-family: var(--font-display);
    font-size: 9px;
    letter-spacing: 0.06em;
    opacity: 0.85;
}

.tb-oc__dom {
    display: block;
    font-family: var(--font-mono);
    font-size: 13px;
    font-weight: 700;
    line-height: 1.1;
}

/* ── THE BLOCKS ── */
.tb-oc__row,
.tb-oc__team-row {
    display: flex;
    align-items: center;
    gap: 10px;
    padding: 10px 12px;
    border-bottom: 2px dashed var(--line);
}

.tb-oc__row:last-child,
.tb-oc__team-row:last-child {
    border-bottom: none;
}

.tb-oc__row--now {
    border-left: 4px solid var(--ok);
}

/* min-width: 0 or a long member list pushes the YOU tag off the row. */
.tb-oc__main {
    flex: 1;
    min-width: 0;
}

.tb-oc__when {
    display: block;
    font-family: var(--font-display);
    font-weight: 700;
    font-size: 15px;
    color: var(--ink);
}

.tb-oc__nowtag {
    margin-left: 6px;
    padding: 1px 6px;
    background: var(--ok);
    color: #fff;
    font-size: 10px;
    letter-spacing: 0.08em;
}

.tb-oc__team {
    display: block;
    margin-top: 1px;
    font-size: 14px;
    color: var(--ink);
}

.tb-oc__who,
.tb-oc__note {
    display: block;
    margin-top: 1px;
    font-size: 12px;
    color: var(--steel);
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
}

.tb-oc__tag {
    flex-shrink: 0;
    padding: 3px 8px;
    background: var(--ok);
    color: #fff;
    font-family: var(--font-display);
    font-weight: 700;
    font-size: 11px;
    letter-spacing: 0.08em;
}

.tb-oc__bin {
    flex-shrink: 0;
    width: 44px;
    height: 44px;
    border: 2px solid var(--danger);
    background: transparent;
    color: var(--danger);
    font-size: 16px;
    touch-action: manipulation;
}

/* ══ THE HEADER'S SHRINK ORDER ═════════════════════════════════════════════
   Kirk, 2026-09-06, on a 375px iPhone: "The OnCALL can't fit with the other
   stuff."

   The bar is space-between with brand | chips | who, and NOTHING said which
   should give way - so the flex default let every item shrink equally and
   .tb-chip's own overflow:hidden turned the badge into "ON C…". A truncated
   safety reminder is worse than none: it reads as a rendering fault, and the
   one thing it had to say is the half that got cut.

   ⚠️ SO THE BRAND YIELDS AND THE CHIPS DO NOT. That is the honest priority -
   CHARLIE'S PLUMBING / TECH BILLING is decoration on a screen the tech already
   knows they are looking at, and ONCALL is the reason the badge exists. The
   brand already carries min-width:0, so it could always shrink; it simply was
   never told it should go first. */
.tb-chip--chat,
.tb-chip--oncall {
    flex-shrink: 0;
}

/* And when the brand does yield, it has to yield GRACEFULLY - without this the
   two lines just spill under the chips rather than shortening. */
.tb-bar__co,
.tb-bar__app {
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
}

/* The right-hand group. ⚠️ IT CONTAINS ONLY .tb-bar__who - the On Call chip is
   NOT inside it (see MainLayout.razor). Written for a grouping that the markup
   never actually made, and kept because the 6px gap is still correct for what
   it does hold. Do not trust the old claim that ONCALL is tucked in here. */
.tb-bar__right {
    display: flex;
    align-items: center;
    gap: 6px;
    min-width: 0;
}
