/* ============================================================
     LITM Management System — styles.html
     Shared stylesheet for all pages
  ============================================================ */

  /* Self-hosted — PORTING.md: "The prototype links Google Fonts — that link
     does not go into the app." Variable build (weight 400..700), the SINGLE
     face Character (#444/#406, owner-approved 2026-10-03) uses for both
     --font-sans and --font-mono: see preview-themes.css's own "ONE font"
     rule. Loaded here (not per-route) because this file is the one global
     <link> every page — legacy and React — already pays for. */
  @font-face {
    font-family: 'Red Hat Text';
    font-style: normal;
    font-weight: 400 700;
    font-display: swap;
    src: url('/vendor/fonts/redhattext-latin.woff2') format('woff2');
  }

  /* ==========================================================================
     THE TOKEN LAYER (#350)
     ==========================================================================
     Before this block the admin app had ONE custom property (`--tl-accent`)
     and 228 distinct colour literals spread over three surfaces: this
     stylesheet, the page-local `<style>` blocks that 18 of 22 page modules
     inject, and inline `style=` inside the modules. A source audit of this
     file alone saw a third of it.

     ⚠️ THE NAMES ARE NOT INVENTED HERE. Eleven of them are copied from
     `public-client/pay.css`, which already had a 14-name `:root` block while
     this sheet had one property. AGENTS.md rule 7(b) forbids the client site
     importing anything from here, so the two files stay two files — but they
     do NOT get two vocabularies. `functions/test/pay_page_brand_tokens.test.js`
     reads both files from disk in CI and asserts, per shared name, that both
     define it, that they define it to the SAME value, and that each file
     actually references it outside its own `:root` block. Change a value here
     and that test names the client page as the thing you just broke.

     What this block is NOT: a normalisation pass. Every substitution made when
     these names were introduced replaced a literal with the token holding
     EXACTLY that literal's value; the only value changes were near-neighbour
     merges of ≤5/255 per channel with an identical role, each one listed in
     the #350 comment. Visibly-different drift — the second red `#c0392b`
     above all — is still a literal, on purpose, because deciding it is the
     owner's call and not a refactor's.
  */
  :root {
    /* ---- shared with public-client/pay.css; pinned by CI ---------------- */
    --brand: #e6007e;       /* the invoice magenta: title, table header, balance */
    --ink: #1d1d1f;         /* body ink; also the dark ground of header/toast/.btn-dark */
    --ink-soft: #6e6e73;    /* labels, captions, secondary prose */
    --bg: #f5f5f7;          /* the page ground */
    --card: #ffffff;        /* sheets, cards, table grounds */
    --line: #e5e5ea;        /* section rules */
    --rule: #f0f0f0;        /* the lighter rule between rows and inside chips */
    --warn-bg: #fdecea;     /* .iv-recon-warn — the invoice sheet's warning idiom */
    --warn-line: #f5c6c2;
    --warn-ink: #b3261e;
    /* #1403 — the line-items table header bar's own fill/text, separate
       from --brand/--on-dark: a tenant whose --brand is itself light (a
       pastel accent) needs a DIFFERENT pair for this bar to stay legible.
       Default to --brand/--on-dark, so a tenant with no theme override (the
       ordinary case) paints this bar exactly as before via plain CSS
       var() — no JS required. The SAME derivation `public-client/pay.css`
       already carries (#1394) for the client-facing document; InvoicePage.jsx's
       own per-sheet theme override (`inv.theme`) sets both here too. */
    --header-fill: var(--brand);
    --header-text: var(--on-dark);
    --paid-ink: #1d7d3f;    /* .iv-recon-ok */

    /* ---- admin only ---------------------------------------------------- */
    --ink-mid: #2e2829;     /* the dark secondary ink; also .btn-dark's ground */
    --ink-mute: #6e6765;    /* the most-used colour in the app: tertiary text */
    --ink-faint: #78706e;   /* placeholder, disabled, empty-state */
    --on-dark: #ffffff;     /* ink and rules ON a dark ground. Same value as
                               --card, a different role: one is paper, the
                               other is what you write on the header with. */
    --bg-soft: #f3f1f0;     /* the subtler ground: table stripes, expanded rows */
    --control-line: #d6d0cd;/* input/button borders — NOT `--rule`: that name is
                               already spoken for by pay.css, at #f0f0f0. */
    --accent: #c4006b;      /* Character (#444/#406, owner-approved 2026-10-03):
                               links, focus, primary action */
    --attn: #c4006b;        /* Character has one accent, not a separate amber —
                               see preview-themes.css's own `--attn: var(--accent)`
                               rule. NOT renamed to keep ~550 existing
                               `var(--attn)` call sites working unchanged. */
    --danger: #b91c1c;      /* errors, destructive actions */
    --ok: #15803d;          /* positive/confirmed states (the invoice sheet's
                               own paid green is --paid-ink, and is a different,
                               deliberately unmerged colour) */

    --radius: 4px;          /* pay.css stays at 12px (DIVERGES list in
                               pay_page_brand_tokens.test.js) — Character's
                               radius is an admin-app decision, not the
                               client-facing sheet's. */

    /* ---- type (#358) ---------------------------------------------------
       Derived from COMPUTED styles in 28 rendered harness screens, not from
       this file: 1,027 elements that carry their own text, at four viewport
       widths. The ramp below is the head of the measured distribution and it
       covers 614 of the 658 font-size declarations in the admin app. The 44
       it does not cover are listed in `docs/GUIDE_TYPE_SCALE.md` and are
       still literals on purpose — the same rule the colour layer used: a
       visibly-different value is drift, and deciding it is the owner's call,
       not a refactor's.

       ⚠️ THERE IS NO LEADING RAMP HERE, AND THAT IS A MEASUREMENT, NOT AN
       OMISSION. 990 of the 1,027 text-bearing elements compute
       `line-height: normal`. `normal` is resolved from the USED FONT'S
       metrics, so it is not one number: measured on this stack it is 16px at
       13px (1.231) but 18px at 16px (1.125) and 15px at 12px (1.250) — not
       constant, not even monotonic — and at 13px it is 16px in the body
       stack, 15px in Arial and 15px in the mono stack, on the same machine.
       Writing any of those in tells one platform's font metrics to every
       other platform. So the two names below are the two leadings the app
       actually SETS; the other 22 explicit ones are drift, and the 990
       `normal` ones stay `normal`. GUIDE_TYPE_SCALE.md carries the table.
    */
    --font-sans: 'Red Hat Text', -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
    --font-mono: 'Red Hat Text', ui-monospace, SFMono-Regular, Menlo, monospace;

    --text-3xs: 9px;
    --text-2xs: 10px;
    --text-xs: 11px;        /* the caption/label size: 114 declarations */
    --text-sm: 12px;        /* 169 */
    --text-base: 13px;      /* THE BODY SIZE: 209 declarations, the most-used
                               value in the app. Note it is not 16px and not
                               on Tailwind's default ramp — see the guide. */
    --text-lg: 14px;
    --text-xl: 15px;
    --text-2xl: 16px;
    --text-3xl: 18px;
    --text-4xl: 20px;
    --text-5xl: 24px;

    --weight-normal: 400;
    --weight-medium: 500;
    --weight-semibold: 600; /* 133 declarations — our real "bold" */
    --weight-bold: 700;

    --leading-none: 1;      /* the RESET, not a leading: chips, badges, icon
                               buttons, anything whose box is its own height */
    --leading-normal: 1.5;  /* the one prose leading anybody chose twice */

    --track-tight: -0.3px;  /* the display lockups: header logo, .iv-brand */
    --track-caps: 0.4px;    /* uppercase micro-labels. ⚠️ Seven other values
                               express the SAME role (0.25/0.3/0.5/0.6px and
                               0.02/0.03/0.04/0.05/0.06em) across 57 more
                               declarations. They are NOT merged into this
                               token: 0.3px→0.4px on an 11-character caps
                               label moves it, and this layer does not move
                               anything. The list is in the guide. */

    /* ====================================================================
       "Dashboard — Calm" — ADDITIVE NAMES ONLY (design port, #391)
       ====================================================================
       Every name below is NEW. None of them existed anywhere in this repo
       before this block: grepping `firebase_frontend/public`,
       `firebase_frontend/public-client`, `firebase_frontend/src` and
       `.design-sync/` for `--st-`, `--tint-`, `--shell`, `--presence`,
       `--today`, `--weight-heavy`, `--weight-black`, `--track-mono` and
       `--track-display` returned ZERO hits. An unreferenced custom property
       paints nothing, so adding them moves no pixel on any screen — which is
       exactly why they are allowed in `:root` while the design's NEUTRAL
       RE-VALUES are not.

       ⚠️ THE RE-VALUES ARE NOT HERE ON PURPOSE. The same design proposes new
       values for `--bg`/`--bg-soft`/`--line`/`--rule`/`--control-line`/
       `--ink`/`--ink-mid`/`--ink-soft`/`--ink-mute`/`--ink-faint`, `--radius`
       8px→6px, and two new font stacks. Those are declared on the React
       dashboard's own root instead (`src/routes/dashboard/dashboard.css`,
       `.dash-page`), because ~690 `var(--…)` call sites across 20+ un-ported
       legacy screens read these names, and because five of them (`--ink`,
       `--ink-soft`, `--bg`, `--line`, `--rule`) are in the CI-pinned block at
       the top of this file — re-valuing them here fails
       `functions/test/pay_page_brand_tokens.test.js`, which asserts this file
       and `public-client/pay.css` define them to the SAME value.
    */

    /* Status vocabulary. Each pair is one ink (also the 3px chip spine) and
       one tint (the ground behind it). Read through a `data-st` attribute, so
       there is exactly ONE mapping from status to colour in the app rather
       than a colour prop per component. `--brand` is deliberately NOT part of
       this family: it belongs to the invoice and the logo.
       Character (#444/#406, owner-approved 2026-10-03): denser tints — the
       -100 step, not the old desaturated -50-ish step, every ink still
       ≥4.5:1 on its own tint (tools/token-contrast.mjs). */
    --st-ready: #15803d;      --tint-ready: #dcfce7;
    --st-prep: #b45309;       --tint-prep: #fef3c7;
    --st-confirmed: #1d4ed8;  --tint-confirmed: #dbeafe;
    --st-pipeline: #6d28d9;   --tint-pipeline: #ede9fe;  /* inquiry/proposal/negotiation — and "overdue" / "nobody assigned yet", the one place this screen raises its voice */
    --st-idle: #57504f;       --tint-idle: #f3f1f0;      /* neutral rows, unfiltered state; also the ground for --st-off */
    --st-off: #6e6765;                                   /* lost, cancelled — rides --tint-idle */

    --today: var(--card);     /* Character: today's cell reads from the border,
                                  not a tint (preview-themes.css). */

    /* Application shell scale — a dark surface the --ink-* ramp cannot
       express. Character (2026-10-03): the shell is now the SAME light
       surface as the rest of the app (no separate dark bar) — referenced by
       `src/routes/topnav/topnav.css`'s `.tnd` scope. */
    --shell: #ffffff;         /* bar ground */
    --shell-2: #f3f1f0;       /* inactive tab, search field, buttons */
    --shell-3: #e7e3e1;       /* hover */
    --shell-line: #e7e3e1;    /* borders on the bar */
    --shell-line-2: #d6d0cd;  /* raised borders, avatar edge */
    --shell-ink: #1b1718;     /* primary text on the bar */
    --shell-soft: #57504f;    /* icons */
    --shell-mute: #6e6765;    /* secondary text */
    --presence: #4a9d7f;      /* the online dot */

    /* Character tops headings out at 600 — the existing --weight-semibold
       step. These two names stay defined (133+ call sites read
       --weight-heavy/--weight-black) but no longer say anything the
       four-step ramp couldn't already say. */
    --weight-heavy: 600;      /* tab names, section titles */
    --weight-black: 600;      /* wordmark, month title, event title, day number */

    /* Two tracking values the existing pair cannot express. --track-mono is
       RELATIVE on purpose: the mono micro-labels are set at three different
       sizes (9/10/11px) and a px value cannot serve all three. --track-caps
       (0.4px) is untouched and still owns the legacy screens' caps labels. */
    --track-mono: 0.06em;     /* uppercase mono micro-labels */
    --track-display: -0.4px;  /* anything at 20px and up in weight 800/900 */
  }

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

  /* (Removed @view-transition { navigation: auto } — issue #139: the SPA
     navigates client-side (History API), so it never benefited from
     cross-document transitions, but the browser surfaced the superseded
     transition as an unhandled "AbortError: Transition was skipped" on real
     page loads — console noise with no upside here.) */

  /* iOS Safari renders date/time inputs as NATIVE controls with their own
     intrinsic width, and it ignores text-align on them — so on iPhone the date
     field both overflowed its card (wider than every neighbouring input) and
     showed its value centred. `text-align: left` alone never fixed it because
     the native control isn't laid out like a text input at all.
     -webkit-appearance:none drops the native rendering (the tap-to-open date
     picker still works), and min-width:0 lets it actually shrink — a flex/grid
     child defaults to min-width:auto, which refuses to go below the control's
     intrinsic width and is what pushed it past the card edge. */
  input[type="date"], input[type="time"], input[type="datetime-local"] {
    -webkit-appearance: none;
    appearance: none;
    min-width: 0;
    max-width: 100%;
    text-align: left;
  }

  body {
    font-family: var(--font-sans);
    background: var(--bg);
    color: var(--ink);
    min-height: 100vh;
  }

  /* --- Header --- */
  .header {
    background: var(--ink);
    color: var(--on-dark);
    padding: 16px 24px;
    display: flex;
    align-items: center;
    gap: 16px;
  }
  .header-logo {
    font-size: var(--text-3xl);
    font-weight: var(--weight-semibold);
    letter-spacing: var(--track-tight);
  }
  .header-logo span { color: var(--attn); }
  .user-badge {
    margin-left: auto;
    display: flex;
    align-items: center;
    gap: 8px;
    flex-shrink: 0;
    position: relative; /* #189 — anchors .user-menu */
  }
  /* #189 — the badge became the handle for the account menu. It is a <button>
     now, so it has to be stripped back to looking exactly like the badge did. */
  .user-badge-btn {
    display: flex; align-items: center; gap: 8px;
    background: none; border: 0; padding: 2px; margin: 0;
    font: inherit; color: inherit; cursor: pointer; border-radius: var(--radius);
  }
  .user-badge-btn:hover { background: rgba(255,255,255,0.08); }
  .user-badge-btn:focus-visible { outline: 2px solid var(--attn); outline-offset: 2px; }
  .user-menu {
    position: absolute; top: calc(100% + 8px); right: 0; z-index: 60;
    min-width: 210px; padding: 6px;
    background: #2c2c2e; border: 1px solid #48484a; border-radius: 10px;
    box-shadow: 0 8px 24px rgba(0,0,0,0.35);
  }
  .user-menu-who { padding: 8px 10px 10px; border-bottom: 1px solid #48484a; margin-bottom: 6px; }
  .user-menu-name  { font-size: var(--text-base); font-weight: var(--weight-semibold); color: var(--on-dark); }
  .user-menu-email { font-size: var(--text-xs); color: #a1a1a6; word-break: break-all; }
  .user-menu-item {
    display: block; width: 100%; text-align: left;
    padding: 9px 10px; border: 0; border-radius: 7px;
    background: none; color: #f2f2f7; font: inherit; font-size: var(--text-base); cursor: pointer;
  }
  .user-menu-item:hover { background: var(--ink-mid); }
  .user-menu-item:focus-visible { outline: 2px solid var(--attn); outline-offset: -2px; }
  #user-menu-signout { color: #ff9f9f; }
  .user-avatar {
    width: 32px;
    height: 32px;
    border-radius: 50%;
    background: var(--attn);
    color: var(--on-dark);
    font-size: var(--text-base);
    font-weight: var(--weight-bold);
    display: flex;
    align-items: center;
    justify-content: center;
    flex-shrink: 0;
  }
  .user-info { line-height: 1.2; }
  .user-name  { font-size: var(--text-base); font-weight: var(--weight-medium); color: var(--on-dark); }
  .user-email { font-size: var(--text-xs); color: #a1a1a6; }
  /* #160 — current access tier (from the ID-token claim). Its own flex child of
     .user-badge, NOT inside .user-info, so it survives the mobile rule that
     hides name/email — the tier stays visible on every screen size. */
  .user-level {
    font-size: var(--text-2xs); font-weight: var(--weight-bold); text-transform: uppercase; letter-spacing: 0.5px;
    padding: var(--space-1) var(--space-3); border-radius: var(--radius); white-space: nowrap; flex-shrink: 0;
    background: var(--ink-mid); color: var(--control-line);
  }
  .user-level-manager    { background: #1f3a5f; color: #7fb0f0; }
  .user-level-admin      { background: #4a3a10; color: #f5c451; }
  .user-level-owner      { background: #4a2a10; color: var(--attn); }
  .user-level-superadmin { background: #3d1f52; color: #c99cf0; }
  .header-actions { display: flex; gap: 8px; flex-wrap: wrap; }

  /* "A newer build is deployed" link, revealed by shared/version.js. Styled to
     be noticeable but not loud — this is information, not an alarm, and an SPA
     tab running yesterday's code is a nuisance rather than an emergency. No
     animation on purpose: it sits in the header for as long as it takes. */
  .header-update-link {
    font-size: var(--text-sm); font-weight: var(--weight-semibold); color: #f5c451; text-decoration: none;
    padding: 5px 10px; border: 1px solid rgba(245, 196, 81, 0.45); border-radius: var(--radius);
    white-space: nowrap; flex-shrink: 0; margin-left: 8px;
  }
  .header-update-link:hover { background: rgba(245, 196, 81, 0.12); }

  /* The ONLY search affordance on the legacy `.header` (#539). An <a> now, not
     a <button>: it navigates to `/search` through the delegated router link
     handler instead of opening the deleted ⌘K command palette overlay, so the
     two font/line-height declarations that only mattered for a <button>'s own
     UA styling are gone and `text-decoration:none` takes their place. The
     `.palette-*` rules that used to follow this one went with the file they
     styled — the top bar's field carries its own styling in topnav.css. */
  .header-search-btn {
    display: inline-flex; align-items: center;
    background: none; border: 1px solid rgba(255,255,255,0.25); border-radius: var(--radius);
    color: var(--on-dark); font-size: var(--text-lg); line-height: var(--leading-none); padding: 7px 10px;
    text-decoration: none; cursor: pointer;
    flex-shrink: 0; margin-left: 8px;
  }
  .header-search-btn:hover { background: rgba(255,255,255,0.12); }

  /* --- Page-level title bar ---
     Replaces the old per-page `.header` (same dark styling as the shell's
     own header, rendered a second time inside #page-content — i.e. beside
     the sidebar, not above it — which read as two stacked dark bars with
     the sidebar wedged between them; see GitHub issue #3, point 3). Just a
     light in-flow title row now; the "← Back" link is gone too, redundant
     once every page already has the persistent sidebar for navigation. */
  .page-title-bar {
    display: flex; align-items: center; gap: 12px; flex-wrap: wrap;
    padding: 20px 24px 4px;
  }
  .page-title { font-size: var(--text-4xl); font-weight: var(--weight-bold); color: var(--ink); flex: 1; }
  @media (max-width: 640px) {
    .page-title-bar { padding: 16px 16px 4px; }
    .page-title { font-size: 17px; flex-basis: 100%; }
  }

  /* Header is a single nowrap flex row by default (fine on desktop) — on
     narrow screens logo+user badge don't fit on one line and used to get
     silently clipped/overlapped instead of wrapping. Letting it wrap, and
     trimming the user badge down to just the avatar, is enough to keep
     everything reachable without a horizontal scrollbar appearing inside
     the header itself. (Used to also cover back-link/title/action-button
     layout here too, back when every page rendered its own copy of this
     same dark `.header` — see .page-title-bar's own comment for why that's
     gone now.) */
  @media (max-width: 640px) {
    .header { flex-wrap: wrap; padding: 12px 16px; row-gap: 8px; }
    .header-logo { font-size: var(--text-2xl); }
    .user-badge { order: 1; margin-left: auto; }
    .user-badge .user-info { display: none; }
  }

  /* --- Toolbar --- */
  .toolbar {
    background: var(--card);
    border-bottom: 1px solid var(--line);
    padding: 12px 24px;
    display: flex;
    align-items: center;
    gap: 12px;
    flex-wrap: wrap;
    /* Aligned to the same content column as .main below it — otherwise on
       a capped-width desktop layout the toolbar's buttons stay pinned to
       the viewport edge while the content beneath is centered, which reads
       as a layout accident rather than a deliberate design. */
    max-width: 1180px;
    margin: 0 auto;
    box-sizing: border-box;
  }

  /* --- Buttons --- */
  .btn {
    padding: 8px 16px;
    border-radius: var(--radius);
    font-size: var(--text-base);
    font-weight: var(--weight-semibold);
    cursor: pointer;
    border: none;
    transition: opacity 0.15s;
    text-decoration: none;
    display: inline-flex;
    align-items: center;
    gap: 4px;
  }
  /* `.btn` declares `display: inline-flex`, which is an author declaration
     and beats the UA stylesheet's `[hidden] { display: none }` — a button
     marked `hidden` stayed laid out and visible. Same root cause as the
     centred-overlay incident and the #333 pay-link card (both fixed locally
     below/elsewhere); fixed globally here so no future `.btn` needs its own
     one-off `[hidden]` override, and so #334's owner-only re-parse buttons
     (invoice_edit.js `#btn-reparse` / `#btn-reparse-ai`) stop being visible
     to non-owners while the server-side requireOwner() gate is the actual
     access control. */
  .btn[hidden] { display: none; }
  .btn:hover { opacity: 0.85; }
  .btn:disabled, .btn[disabled] { opacity: 0.45 !important; pointer-events: none; cursor: not-allowed; }
  .btn-primary   { background: var(--ink); color: var(--on-dark); margin-left: auto; }
  .btn-secondary { background: var(--card); color: var(--ink); border: 1px solid var(--control-line); }
  .btn-secondary:hover { background: var(--bg); }
  .btn-dark      { background: var(--ink-mid); color: var(--on-dark); }
  .btn-ghost     { background: transparent; color: var(--ink-soft); border: 1px solid transparent; }
  .btn-danger    { background: var(--card); color: var(--danger); border: 1px solid var(--control-line); }
  .btn-sm        { padding: 5px 10px; font-size: var(--text-sm); }
  .nav-badge {
    position: absolute; top: -6px; right: -6px;
    background: var(--danger); color: var(--on-dark); font-size: var(--text-2xs); font-weight: var(--weight-bold);
    min-width: 16px; height: 16px; border-radius: var(--radius);
    display: flex; align-items: center; justify-content: center; padding: 0 4px;
  }

  /* --- View toggle --- */
  .view-toggle {
    display: flex;
    border: 1px solid var(--control-line);
    border-radius: var(--radius);
    max-width: 100%;
    overflow-x: auto;
    overflow-y: hidden;
    -webkit-overflow-scrolling: touch;
    scrollbar-width: none;
  }
  .view-toggle::-webkit-scrollbar { display: none; }
  .view-toggle button {
    padding: 7px 16px;
    border: none;
    background: var(--card);
    font-size: var(--text-base);
    cursor: pointer;
    color: var(--ink-soft);
    transition: all 0.15s;
    flex: 0 0 auto;
    white-space: nowrap;
  }
  .view-toggle button.active {
    background: var(--ink);
    color: var(--on-dark);
  }

  /* --- Filter select --- */
  .filter-select {
    padding: 7px 12px;
    border: 1px solid var(--control-line);
    border-radius: var(--radius);
    font-size: var(--text-base);
    background: var(--card);
    color: var(--ink);
    cursor: pointer;
    max-width: 100%;
    box-sizing: border-box;
  }

  /* --- #1137 /invoices filter selects: a TAP target, not a typed field --
     Measured (Chromium + WebKit headless): a synthetic click fired inside
     an absolutely-positioned ::after never reaches a native <select> —
     only a tap on its own painted box does, so the ::after hit extension
     above (`button, [role=button], .btn, .hit`) is a no-op on one. A real
     44px target is a <label> wrapper instead, the same mechanism this
     file already relies on for a checkbox's <label>: a tap anywhere in it
     reaches the <select> inside. `--hit-min` is 0 on a desktop (no-op) and
     44px on a phone; the select itself (`.iv-filter-select`, excluded
     from the shared field rule below) keeps its own small, unchanged,
     drawn size. */
  .iv-filter-wrap { display: inline-flex; align-items: center; min-height: var(--hit-min); }

  /* #1134 — the "Show past events" checkbox beside the filter selects. */
  .filter-toggle {
    display: inline-flex;
    align-items: center;
    gap: 6px;
    font-size: var(--text-base);
    color: var(--ink-mute);
    cursor: pointer;
    white-space: nowrap;
  }

  /* Author `display: inline-flex` above beats the UA `[hidden]{display:none}`
     rule (origin precedence, not specificity) — same trap `.btn[hidden]`
     documents near the top of this file. Needed once a caller toggles this
     with the `hidden` attribute instead of `style.display` (#1181 follow-up). */
  .filter-toggle[hidden] { display: none; }

  /* --- Main content --- */
  /* Constrained + centered on wide desktop screens — full-bleed content on
     an ultrawide monitor forces the eye to travel further than it needs to
     and reads as dated; the header/toolbar bars above stay full-width
     (that's a separate, deliberate choice), only the actual page content
     is capped. Below the cap this is a no-op (padding-only, same as
     before). */
  .main { padding: 24px; max-width: 1180px; margin: 0 auto; box-sizing: border-box; }

  /* --- Loading --- */
  .loading {
    text-align: center;
    padding: 60px;
    color: var(--ink-soft);
    font-size: var(--text-lg);
  }

  /* --- Equalizer loader (shared) ---
     On-brand "DJ/AV company" loading state: 5 bars bouncing like an audio
     equalizer, staggered so they don't move in lockstep. Markup produced by
     common.js's equalizerLoaderHtml(caption) — see that function's header
     comment for the exact structure this styles. Kept small/tasteful
     (not a full-screen takeover) since it replaces a plain "Loading..."
     inline in the page body, not a splash screen. */
  .litm-eq-loader {
    display: flex;
    flex-direction: column;
    align-items: center;
    justify-content: center;
    gap: 14px;
    padding: 60px 20px;
  }
  .litm-eq-bars {
    display: flex;
    align-items: flex-end;
    justify-content: center;
    gap: 5px;
    height: 36px;
  }
  .litm-eq-bar {
    display: inline-block;
    width: 6px;
    height: 100%;
    border-radius: 3px;
    background: var(--attn);
    transform-origin: bottom;
    animation: litm-eq 0.9s ease-in-out infinite;
  }
  .litm-eq-bar:nth-child(1) { animation-delay: 0s; }
  .litm-eq-bar:nth-child(2) { animation-delay: 0.12s; }
  .litm-eq-bar:nth-child(3) { animation-delay: 0.24s; }
  .litm-eq-bar:nth-child(4) { animation-delay: 0.36s; }
  .litm-eq-bar:nth-child(5) { animation-delay: 0.48s; }
  @keyframes litm-eq {
    0%, 100% { transform: scaleY(0.25); }
    50%      { transform: scaleY(1); }
  }
  .litm-eq-caption {
    color: var(--ink-soft);
    font-size: var(--text-lg);
  }
  @media (prefers-reduced-motion: reduce) {
    .litm-eq-bar { animation: none; transform: scaleY(0.7); }
  }

  /* --- Cards --- */
  .card {
    background: var(--card);
    border-radius: 12px;
    padding: 20px;
    box-shadow: 0 1px 3px rgba(0,0,0,0.08);
    margin-bottom: 16px;
  }
  .card-title {
    font-size: var(--text-xs);
    font-weight: var(--weight-bold);
    text-transform: uppercase;
    letter-spacing: 0.6px;
    color: var(--ink-soft);
    margin-bottom: 14px;
  }

  /* --- Status badges --- */
  .status-badge {
    display: inline-block;
    padding: 3px 10px;
    border-radius: 20px;
    font-size: var(--text-xs);
    font-weight: var(--weight-semibold);
    text-transform: uppercase;
    letter-spacing: 0.3px;
  }
  .status-inquiry   { background: #fff3cd; color: #856404; }
  .status-confirmed { background: #d1e7dd; color: #0a3622; }
  .status-prep      { background: #cfe2ff; color: #084298; }
  .status-ready     { background: #d0f0c0; color: #1a5c2a; }
  .status-closed    { background: #e9ecef; color: #495057; }
  .status-cancelled { background: #f8d7da; color: #842029; }
  .status-merged    { background: #e9ecef; color: #495057; }
  .status-pending-deletion { background: var(--bg); color: var(--ink-mute); }
  .row-pending-deletion { opacity: 0.55; }
  .row-pending-deletion strong { text-decoration: line-through; }

  /* --- Fields (view mode) --- */
  .field { margin-bottom: 14px; }
  .field:last-child { margin-bottom: 0; }
  .field label {
    display: block;
    font-size: var(--text-xs);
    font-weight: var(--weight-semibold);
    text-transform: uppercase;
    letter-spacing: var(--track-caps);
    color: var(--ink-soft);
    margin-bottom: 4px;
  }
  .field-value {
    font-size: var(--text-lg);
    color: var(--ink);
    line-height: var(--leading-normal);
    min-height: 20px;
    white-space: pre-wrap;
  }
  .field-value.empty { color: var(--ink-faint); font-style: italic; }
  .field-value a { color: var(--accent); text-decoration: none; }
  .field-value a:hover { text-decoration: underline; }
  /* #204 R1 — provenance marker for a value that is stored at more than one
     level (the logistics facts on the event card live on the event AND on the
     venue/space). Present only when the displayed value FELL THROUGH to the
     venue, so the unmarked case stays the common one. Sits inside
     .field-value, which means edit mode hides it along with its parent — there
     the raw override input is shown with the venue value as a placeholder. */
  .field-value .field-src {
    display: inline-block;
    margin-left: 8px;
    padding: 1px 6px;
    border-radius: 4px;
    background: #f2f2f7;
    color: var(--ink-mute);
    font-size: var(--text-xs);
    font-style: normal;
    white-space: nowrap;
    vertical-align: 1px;
  }

  /* --- Form inputs --- */
  .field input,
  .field select,
  .field textarea {
    width: 100%;
    padding: 9px 12px;
    border: 1px solid var(--control-line);
    border-radius: var(--radius);
    font-size: var(--text-lg);
    font-family: inherit;
    color: var(--ink);
    background: var(--card);
    transition: border-color 0.15s;
    display: none;
    /* iOS Safari centers the displayed value in date/time inputs by default
       (unlike text inputs) — force left alignment so they match every other
       field instead of looking randomly shifted. */
    text-align: left;
  }
  .field input:focus,
  .field select:focus,
  .field textarea:focus {
    outline: none;
    border-color: var(--attn);
  }
  .field textarea { resize: vertical; min-height: 80px; }

  /* Edit mode — show inputs, hide values */
  body.edit-mode .field-value { display: none; }
  /* A handful of .field-value elements have no edit-mode input counterpart
     at all — they're read-only by design (Additional Spaces on Overview;
     Load-in/Parking/Tech Notes in Logistics' Venue Details, whose value
     may come from either the venue or the space with no way to tell which
     — see renderVenueDetails) — so they must stay visible instead of
     following every other .field-value into hiding while editing. */
  body.edit-mode .field-value.static { display: block; }
  body.edit-mode .field input,
  body.edit-mode .field select,
  body.edit-mode .field textarea { display: block; }
  body.edit-mode .field .time-range { display: flex; align-items: center; gap: 8px; }
  body.edit-mode .field .time-range input { display: block; flex: 1; }
  .field .time-range { display: none; }
  .time-sep { color: var(--ink-soft); font-size: var(--text-lg); flex-shrink: 0; }
  body.edit-mode .edit-hidden  { display: none; }
  body.edit-mode .edit-visible { display: flex; }
  .edit-only { display: none; }
  body.edit-mode .edit-only { display: block; }
  .edit-visible { display: none; }

  /* Delete keeps its layout slot in both modes (visibility, not display) —
     the mode-toggle sits right next to it in .header-actions, and toggling
     Delete's DOM presence shrank/grew the whole right-aligned action group,
     shifting the toggle sideways every time you switched modes. */
  #btn-delete { visibility: hidden; }
  body.edit-mode #btn-delete { visibility: visible; }

  /* New event form — always show inputs */
  .new-event-form .field input,
  .new-event-form .field select,
  .new-event-form .field textarea { display: block; }
  .new-event-form .field-value { display: none; }

  /* --- Invoice edit/print page (DJEP-style) --- */
  .invoice-sheet {
    background: var(--card);
    max-width: 800px;
    margin: 0 auto;
    padding: 48px 56px;
    border-radius: 12px;
    box-shadow: 0 1px 3px rgba(0,0,0,0.08);
  }
  .iv-brand-header {
    display: flex;
    justify-content: space-between;
    align-items: flex-start;
    border-bottom: 3px solid var(--brand);
    padding-bottom: 20px;
    margin-bottom: 28px;
  }
  .iv-brand { font-size: var(--text-4xl); font-weight: var(--weight-bold); color: var(--brand); letter-spacing: var(--track-tight); }
  .iv-brand span { color: var(--ink); }
  .iv-brand-tagline { font-size: var(--text-xs); color: var(--ink-soft); margin-top: 2px; }
  .iv-title-block { text-align: right; }
  .iv-title { font-size: 28px; font-weight: 800; color: var(--brand); letter-spacing: 1px; }
  .iv-id { font-size: var(--text-base); color: var(--ink-soft); margin-top: 4px; }

  .iv-meta-grid { display: grid; grid-template-columns: 1fr 1fr; gap: 24px; margin-bottom: 28px; }
  .iv-meta-col-title {
    font-size: var(--text-xs); font-weight: var(--weight-bold); text-transform: uppercase;
    letter-spacing: 0.6px; color: var(--brand); margin-bottom: 8px;
  }
  .iv-meta-row { display: flex; justify-content: space-between; gap: 12px; font-size: var(--text-base); margin-bottom: 4px; }
  .iv-meta-row .iv-label { color: var(--ink-soft); flex-shrink: 0; }
  .iv-meta-row .iv-value { font-weight: var(--weight-semibold); text-align: right; }
  .iv-meta-row select, .iv-meta-row input { display: none; width: auto; padding: 4px 8px; font-size: var(--text-base); text-align: right; }
  /* Only hide a row's read-only span if it actually has an input/select to
     replace it with — client/venue/event-date/guests etc. have no editable
     counterpart and must stay visible in edit mode, not go blank. */
  body.edit-mode .iv-meta-row:has(select, input) .iv-value { display: none; }
  body.edit-mode .iv-meta-row select,
  body.edit-mode .iv-meta-row input { display: inline-block; }

  .iv-table-wrap { max-width: 100%; overflow-x: auto; -webkit-overflow-scrolling: touch; }
  .iv-table { width: 100%; border-collapse: collapse; margin-bottom: 8px; }
  .iv-table th {
    background: var(--header-fill); color: var(--header-text); font-size: var(--text-xs); text-transform: uppercase;
    letter-spacing: var(--track-caps); padding: 8px 10px; text-align: left;
    -webkit-print-color-adjust: exact; print-color-adjust: exact;
  }
  .iv-table th.iv-num, .iv-table td.iv-num { text-align: right; }
  .iv-table td { padding: 8px 10px; font-size: var(--text-base); border-bottom: 1px solid var(--rule); vertical-align: middle; }
  .iv-table input { display: none; padding: 4px 6px; font-size: var(--text-base); width: 100%; }
  /* Same rule as .iv-meta-row's above: hide a cell's read-only span only
     when the cell has an input to replace it. The line TOTAL cell is
     computed (qty x price, LineItemsTable.jsx) and has no input, so the
     old blanket `.iv-table .iv-value` rule blanked the whole Total column
     for as long as the invoice was in edit mode. */
  body.edit-mode .iv-table td:has(input) .iv-value { display: none; }
  body.edit-mode .iv-table input { display: block; }
  /* #267 — DJEP's opening category caption ("AV Only - Private Event") is a
     section label, not a $0.00 item. Read mode paints it as one: no rule under
     it (the caption belongs WITH the rows beneath), no money columns. Edit mode
     is untouched — the inputs are still there, so a row classified wrongly can
     be corrected in place. */
  .iv-table tr.iv-item-header td { border-bottom: none; padding-bottom: 2px; }
  .iv-table tr.iv-item-header td:first-child .iv-value {
    font-weight: var(--weight-bold); font-size: var(--text-xs); text-transform: uppercase;
    letter-spacing: var(--track-caps); color: var(--ink-soft);
  }
  .iv-row-remove { display: none; color: var(--danger); cursor: pointer; font-size: var(--text-2xl); text-align: center; }
  body.edit-mode .iv-row-remove { display: block; }
  .iv-add-row { display: none; margin-top: 4px; }
  body.edit-mode .iv-add-row { display: inline-flex; }

  .iv-totals { margin-left: auto; width: 280px; margin-top: 16px; }
  .iv-totals-row { display: flex; justify-content: space-between; padding: 6px 0; font-size: var(--text-base); }
  .iv-totals-row.iv-grand {
    border-top: 2px solid var(--ink); margin-top: 4px; padding-top: 10px;
    font-size: var(--text-2xl); font-weight: 800; color: var(--brand);
  }
  /* IE-17 (#391 slice 3, found porting invoice_edit.js to React) — `.iv-totals-row`
     declares `display: flex` directly, exactly the trap `.iv-original`/
     `.iv-payments`/`.iv-paylink`/`.iv-terms-policy` already needed their own
     `[hidden]` override for (see each one's own comment): an attribute
     selector in the UA stylesheet loses to ANY author rule of equal or lower
     specificity, so `#iv-paid-row`/`#iv-balance-row`'s `.hidden = true` was a
     complete no-op — both rows rendered on EVERY invoice, including ones with
     no payments and no ledger rows, contradicting the comment two lines above
     `renderBalance` ("Both rows hide when there is nothing to say... an unpaid
     invoice should not grow a '$0.00 paid' line it never had"). Worse than
     merely visible: `renderBalance` returns before writing `#iv-paid`/
     `#iv-balance`'s text in that branch, so the two rows kept showing
     whatever the STATIC template's hardcoded "$0.00" last held, never the
     real total. Confirmed empirically (`INV-267-CAPTION`, harness): legacy
     printed "Balance due $0.00" on a $4,630 invoice with zero payments. */
  .iv-totals-row[hidden] { display: none; }
  /* Invoice math-verification indicator (internal, no-print): green when our
     computed total reconciles with the parsed Amount, red when it doesn't. */
  .iv-recon { margin-top: 10px; font-size: var(--text-sm); line-height: 1.4; border-radius: 6px; padding: 8px 10px; }
  .iv-recon-ok { color: var(--paid-ink); }
  .iv-recon-warn { color: var(--warn-ink); background: var(--warn-bg); border: 1px solid var(--warn-line); font-weight: var(--weight-semibold); }

  /* Free-text blocks lifted off the source document (Details / Terms). Prose,
     so: left-aligned, line breaks preserved (a run-of-show paragraph's line
     structure is content), and never wider than a comfortable measure. */
  .iv-doc-prose { margin-top: 24px; }
  .iv-doc-prose-body {
    font-size: 12.5px; line-height: 1.55; color: var(--ink);
    white-space: pre-wrap; overflow-wrap: anywhere; max-width: 62em;
  }

  .iv-notes { margin-top: 28px; font-size: var(--text-sm); color: var(--ink-soft); border-top: 1px solid var(--line); padding-top: 16px; }
  .iv-notes textarea { display: none; width: 100%; }
  body.edit-mode .iv-notes .iv-value { display: none; }
  body.edit-mode .iv-notes textarea { display: block; }
  .iv-notes .iv-value.empty { color: var(--ink-faint); font-style: italic; }

  /* Invoice status lives in the app chrome (no-print), not the printable body —
     it's an internal workflow field, not something a client should see. */
  .iv-status-select { display: none; width: auto; padding: 6px 10px; font-size: var(--text-sm); }
  body.edit-mode .iv-status-view { display: none; }
  body.edit-mode .iv-status-select { display: inline-block; }

  /* --- #256: the original PDF beside our reconstruction --------------------
     Closed, .iv-compare is a plain block and the page is byte-for-byte the
     layout it has always had: one centred 800px .invoice-sheet. Only
     .iv-compare-open turns it into two columns, and only the JS adds that
     class — and only when the viewport is wide enough for two documents.
     Below that width the same button opens the shared overlay instead, so
     there is no half-width unreadable pane at any size.

     minmax(0, 1fr), never a bare 1fr: a bare fr track's minimum is its
     content's min-content width, so the invoice table could push this row
     past the viewport and blow out the page — the same trap .two-col's own
     comment documents. */
  /* The page's usual 1180px reading measure is right for ONE document and wrong
     for two: capped there, each column tops out around 550px however wide the
     screen is, and with the 208px sidebar alongside it the invoice's
     description column collapses to ~100px — measured, not guessed. Only the
     split lifts the cap, so every other page (and this one with the original
     closed) keeps the measure it was designed with. */
  .main:has(.iv-compare-open) { max-width: none; }
  .iv-compare-open {
    display: grid;
    grid-template-columns: minmax(0, 1fr) minmax(0, 1fr);
    gap: 20px;
    align-items: start;
  }
  /* The sheet stops being CENTRED in split mode — `margin: 0 auto` would push it
     away from the original it is meant to be read against, leaving the gutter in
     the middle of the comparison. It may grow past its usual 800px to fill the
     column, but not without limit: a document still has a reading measure, and
     on a 2560px screen an uncapped sheet is a 1140px-wide invoice. */
  .iv-compare-open .invoice-sheet { margin: 0; max-width: 900px; }
  /* Sticky so the original stays put while our (usually longer) sheet scrolls
     past it. A line item near the bottom of our list is exactly the one you
     need the original still on screen for. */
  .iv-original {
    position: sticky;
    top: 16px;
    display: flex;
    flex-direction: column;
    background: #2b2c2f;
    border-radius: 12px;
    overflow: hidden;
    /* Tall enough to read a page of an invoice at a glance, capped so the pane
       never exceeds the window and forces the whole page to scroll to reach the
       bottom of the document. */
    height: calc(100vh - 32px);
  }
  /* `display: flex` above BEATS the UA stylesheet's [hidden]{display:none} — a
     class selector outranks an attribute selector on the same element, so the
     closed pane kept its full 100vh of layout and pushed the invoice a
     screen-height down the page. Caught by opening it, not by reading it. */
  .iv-original[hidden] { display: none; }
  /* Below 1400px a real PDF opens in the shared overlay (InvoicePage.jsx), so
     the pane only ever carries a MESSAGE there ("no original stored", a Drive
     link, an error) — and opening it as a second column squeezed the invoice
     to ~150px on a phone and widened the page past the screen (#1137
     journeys). There it is a short note above the sheet instead. */
  @media (max-width: 1399px) {
    .iv-compare-open { display: block; }
    .iv-compare-open .iv-original { position: static; height: auto; margin-bottom: var(--space-8); }
    .iv-compare-open .invoice-sheet { margin: 0 auto; max-width: var(--layout-sheet); }
  }
  .iv-original-head {
    display: flex; align-items: center; justify-content: space-between; gap: 12px;
    padding: 8px 10px 8px 14px; flex: 0 0 auto;
    background: rgba(32, 33, 36, 0.96); color: #e8eaed;
    border-bottom: 1px solid rgba(255, 255, 255, 0.08);
  }
  .iv-original-title { font-size: var(--text-sm); font-weight: var(--weight-semibold); color: #e8eaed; }
  .iv-original-actions { display: flex; align-items: center; gap: 6px; flex: 0 0 auto; }
  /* .btn-ghost and .btn-icon are styled for a light surface; this bar is dark
     for the same reason the preview overlay's is — a white bar would put an
     edge back around the document. Scoped to this pane only. */
  .iv-original-actions .btn,
  .iv-original-actions .btn-icon {
    color: #e8eaed; background: transparent; border-color: rgba(255,255,255,0.22);
  }
  .iv-original-actions .btn:hover,
  .iv-original-actions .btn-icon:hover {
    background: rgba(255,255,255,0.12); border-color: rgba(255,255,255,0.4); color: var(--on-dark);
  }
  .iv-original-body { flex: 1 1 auto; min-height: 0; display: flex; }
  /* A percentage height would collapse to zero inside a flex column without the
     min-height:0 above; the iframe carries the whole remaining height. */
  .iv-original-body iframe { width: 100%; height: 100%; border: 0; background: transparent; }
  /* The explanation shown when there is nothing to render — no stored PDF, a
     legacy Drive link, an access tier below Manager, a failed read. 4 of the 65
     live invoices land here, so this is a normal state, not an error screen. */
  .iv-original-empty {
    margin: auto; padding: 24px; max-width: 380px; text-align: center;
    font-size: var(--text-base); line-height: var(--leading-normal); color: var(--ink-faint);
  }
  .iv-original-empty a { color: #7ab6ff; }

  /* --- #342: the ledger card, and the two totals rows it explains ----------
     The provenance line under the balance is app chrome (.no-print in the
     markup): a client's copy carries the money, not which of our two records
     answered. */
  .iv-totals-basis { font-size: 11.5px; line-height: 1.45; color: var(--ink-soft); margin-top: 6px; text-align: right; }
  .iv-payments { margin-top: 24px; border-top: 1px solid var(--line); padding-top: 12px; }
  .iv-payments[hidden] { display: none; }
  /* #1283 — "Record payment", beside the card's own title. `.no-print` in
     the markup, same reasoning `.iv-paylink` already states: a money-entry
     CONTROL, never something a client's printed copy should carry. */
  .iv-payments-head { display: flex; align-items: baseline; justify-content: space-between; gap: 10px; flex-wrap: wrap; }
  .iv-pay-record { font-size: var(--text-sm); }
  /* #1308 — "Terms accepted …", beside the card's own title, same quiet
     mono-link shape as `.iv-pay-void` below (a courtesy control, not a
     warning — most invoices never carry one, and that must read as
     "nothing to see" rather than an error). */
  .iv-accept-note {
    border: 0; background: transparent; padding: 0;
    font-size: var(--text-sm); color: var(--ink-soft); cursor: pointer;
  }
  .iv-accept-note:hover { color: var(--ink); text-decoration: underline; }
  .iv-accept-note:disabled { color: var(--ink-faint); cursor: default; text-decoration: none; }
  /* A manual (box-ledger) row's own "void" control — quiet, inline with the
     row it corrects. */
  .iv-pay-void {
    margin-top: 6px; border: 0; background: transparent; padding: 0;
    font-size: var(--text-sm); color: #c0392b; cursor: pointer;
  }
  .iv-pay-void:hover { text-decoration: underline; }
  .iv-pay-void:disabled { color: var(--ink-faint); cursor: default; text-decoration: none; }
  .iv-payments-status { font-size: 12.5px; line-height: 1.55; color: var(--ink); overflow-wrap: anywhere; }
  .iv-payments-rows { margin-top: 10px; display: grid; gap: 8px; }
  .iv-pay-row {
    border: 1px solid var(--line); border-radius: 10px; padding: 10px 12px; background: #fbfbfd;
    /* min-width:0 so a long processor object id shrinks the box's content
       instead of pushing the sheet wider than the phone. */
    min-width: 0;
  }
  .iv-pay-head {
    display: flex; flex-wrap: wrap; align-items: baseline; justify-content: space-between; gap: 8px;
  }
  .iv-pay-kind { font-size: var(--text-base); font-weight: var(--weight-semibold); color: var(--ink); }
  .iv-pay-gross { font-size: var(--text-xl); font-weight: var(--weight-semibold); color: var(--paid-ink); font-variant-numeric: tabular-nums; }
  /* Money LEAVING (refund, chargeback, ACH return) must not read as a smaller
     payment. The sign is the direction; the colour only repeats it. */
  .iv-pay-out { color: #c0392b; }
  .iv-pay-sub { font-size: var(--text-sm); line-height: var(--leading-normal); color: var(--ink-soft); margin-top: 3px; overflow-wrap: anywhere; }
  /* THE PROCESSOR'S OWN OBJECT ID. 27 unbreakable characters (ch_3U6eIn…) in a
     card that has to survive a 390px screen — `anywhere` rather than
     `break-word`, because break-word will not split a token that has no break
     opportunity in it at all, and the page would scroll sideways. */
  .iv-pay-oid {
    margin-top: 5px; font-family: var(--font-mono);
    font-size: 11.5px; color: var(--ink-mid); overflow-wrap: anywhere; word-break: break-all;
  }
  .iv-pay-warn { color: #b26a00; font-weight: var(--weight-semibold); }
  /* The document's claim disagreeing with the ledger. Not an error — a question
     with a named answer, so it reads as a note and not as a failure. */
  .iv-payments-conflict {
    margin-top: 10px; padding: 10px 12px; border-radius: 10px;
    border: 1px solid var(--attn); background: #fff8ec;
    font-size: var(--text-sm); line-height: 1.55; color: #7a4a00; overflow-wrap: anywhere;
  }
  .iv-payments-conflict[hidden] { display: none; }

  /* --- #333: the payment-link card, at the foot of the sheet ---------------
     Sits below the version history, .no-print in the markup: it describes a
     capability, and a token must never be able to reach a printed invoice. */
  .iv-paylink { margin-top: 24px; border-top: 1px solid var(--line); padding-top: 12px; }
  /* The card itself is toggled with the `hidden` attribute; this card is not
     a `.btn`, so it needs its own override the same way `.iv-original` and
     `.iv-payments` above do. The two buttons inside it are `.btn`s and were
     also toggled via `hidden` — that half is now covered by the global
     `.btn[hidden]` rule (#334), so the `.iv-paylink [hidden]` descendant
     clause below is redundant for them but kept for any non-`.btn` child
     this card hides in future. */
  .iv-paylink[hidden], .iv-paylink [hidden] { display: none; }
  .iv-paylink-status { font-size: 12.5px; line-height: 1.55; color: var(--ink); overflow-wrap: anywhere; }
  .iv-paylink-sub { font-size: var(--text-sm); line-height: var(--leading-normal); color: var(--ink-soft); margin-top: 4px; overflow-wrap: anywhere; }
  .iv-paylink-actions { display: flex; flex-wrap: wrap; gap: 8px; margin-top: 10px; }
  /* THE ONCE-ONLY REVEAL. Loud on purpose — it is the only moment the link is
     readable, and a Manager who scrolls past it has no way back except minting
     a new one, which kills the link they were looking for. */
  .iv-paylink-reveal {
    margin-top: 12px; padding: 12px;
    border: 1px solid var(--attn); border-radius: 10px; background: #fff8ec;
  }
  .iv-paylink-once { font-size: var(--text-base); font-weight: var(--weight-semibold); line-height: 1.45; color: #7a4a00; }
  .iv-paylink-copyrow { display: flex; flex-wrap: wrap; gap: 8px; margin-top: 10px; }
  /* flex-basis 220px with min-width:0 so the row WRAPS on a phone instead of
     pushing the sheet wider — a bare `flex:1` here cannot shrink below the
     token's min-content width and would overflow the page at 390px. */
  .iv-paylink-input {
    flex: 1 1 220px; min-width: 0; padding: 8px 10px;
    font-family: var(--font-mono); font-size: var(--text-sm);
    border: 1px solid var(--control-line); border-radius: var(--radius); background: var(--card); color: var(--ink);
  }
  .iv-paylink-note { font-size: var(--text-sm); line-height: var(--leading-normal); color: var(--ink-soft); margin-top: 8px; overflow-wrap: anywhere; }

  /* --- #346: the payment SCHEDULE card ------------------------------------
     Printed, unlike .iv-paylink: what is owed and by when is part of the
     invoice. The EDITOR inside it carries .no-print in the markup.
     Every horizontal run here wraps rather than widening the sheet — the
     card has to sit inside a 390px phone with zero horizontal overflow, and
     a bare `flex: 1` on the controls cannot shrink below its min-content
     width (see .iv-paylink-input above for the same lesson). */
  .iv-terms-policy { margin-top: 24px; border-top: 1px solid var(--line); padding-top: 12px; }
  .iv-terms-policy[hidden] { display: none; }
  .iv-tp-badge { font-size: var(--text-sm); line-height: var(--leading-normal); margin-top: 4px; overflow-wrap: anywhere; }
  .iv-tp-badge-default { color: #7a4a00; background: #fff8ec; border: 1px solid #f5d9a8; border-radius: var(--radius); padding: 6px 8px; }
  .iv-tp-badge-pinned { color: var(--ink-soft); }
  .iv-tp-rule { font-size: var(--text-base); line-height: var(--leading-normal); margin-top: 8px; color: var(--ink); overflow-wrap: anywhere; }
  /* The editor exists only in edit mode — the same rule .iv-table's inputs
     follow, expressed once here rather than per control. */
  .iv-tp-editor { display: none; }
  body.edit-mode .iv-tp-editor { display: block; margin-top: 10px; }
  .iv-tp-field { display: flex; flex-wrap: wrap; align-items: center; gap: 6px; margin-top: 8px; }
  .iv-tp-field > label { flex: 0 0 92px; font-size: var(--text-sm); color: var(--ink-soft); }
  .iv-tp-controls { display: flex; flex-wrap: wrap; align-items: center; gap: 6px; min-width: 0; }
  .iv-tp-unit { font-size: var(--text-sm); color: var(--ink-soft); }
  .iv-tp-input {
    padding: 5px 7px; font-size: 12.5px; border: 1px solid var(--control-line);
    border-radius: 7px; background: var(--card); color: var(--ink); min-width: 0;
  }
  .iv-tp-num { width: 66px; }
  .iv-tp-date { width: 148px; }
  .iv-tp-actions { margin-top: 10px; }
  .iv-tp-rows { margin-top: 10px; }
  .iv-tp-row {
    display: flex; flex-wrap: wrap; gap: 4px 10px; align-items: baseline;
    padding: 6px 0; border-top: 1px solid var(--rule); font-size: var(--text-base);
  }
  .iv-tp-row-label { flex: 1 1 120px; min-width: 0; font-weight: var(--weight-semibold); }
  .iv-tp-row-due { flex: 1 1 150px; min-width: 0; color: var(--ink-soft); overflow-wrap: anywhere; }
  .iv-tp-row-amt { flex: 0 1 auto; margin-left: auto; text-align: right; }
  .iv-tp-pct { font-weight: var(--weight-normal); color: var(--ink-soft); }
  .iv-tp-covered { color: #34a853; font-size: var(--text-sm); }
  /* An unresolved due date is LOUD. #323 shipped a value computed from zero
     because nothing said it had failed; a grey blank here would be the same
     mistake with a date on it. */
  .iv-tp-unresolved { color: var(--warn-ink); }
  .iv-tp-fallback { color: #7a4a00; }
  .iv-tp-notices { margin-top: 8px; }
  .iv-tp-notice { font-size: var(--text-sm); line-height: var(--leading-normal); color: var(--ink-soft); margin-top: 6px; overflow-wrap: anywhere; }
  .iv-tp-notice-loud {
    color: #7a4a00; background: #fff8ec; border: 1px solid #f5d9a8;
    border-radius: var(--radius); padding: 6px 8px;
  }

  /* #1038 slice 4 (#695) — typed payment terms + bill-to editors. Whole
     custom panels, not a single input swap, so each needs its own named
     edit-mode toggle — the same shape `.iv-tp-editor` above already uses
     for the identical reason. */
  .iv-terms-editor { display: none; margin: 8px 0 16px; }
  body.edit-mode .iv-terms-editor { display: block; }
  .iv-terms-radios { display: flex; flex-direction: column; gap: 6px; margin-bottom: 8px; }
  .iv-terms-radios label { display: flex; align-items: center; gap: 8px; font-size: var(--text-sm); cursor: pointer; }
  .iv-terms-radios input[type="radio"] { width: auto; margin: 0; }
  .iv-terms-net, .iv-terms-duerow, .iv-terms-deposit {
    display: flex; align-items: center; gap: 8px; margin: 6px 0; flex-wrap: wrap; font-size: var(--text-sm);
  }
  .iv-terms-net input, .iv-terms-deposit input { width: 72px; padding: 5px 7px; font-size: 12.5px; border: 1px solid var(--control-line); border-radius: 7px; background: var(--card); color: var(--ink); }
  .iv-terms-duerow input[type="date"] { padding: 5px 7px; font-size: 12.5px; border: 1px solid var(--control-line); border-radius: 7px; background: var(--card); color: var(--ink); }
  .iv-terms-quick { display: flex; gap: 4px; flex-wrap: wrap; }
  .iv-terms-quick button {
    font-size: var(--text-xs); border: 1px solid var(--line); background: var(--card);
    border-radius: 12px; padding: 3px 9px; cursor: pointer; color: var(--ink-soft);
  }
  .iv-terms-quick button:hover { border-color: var(--accent); color: var(--accent); }
  .iv-terms-preview {
    font-style: italic; color: var(--ink-soft); border-left: 2px solid var(--line);
    padding-left: 10px; margin-top: 8px; font-size: var(--text-sm);
  }
  .iv-terms-preview small {
    display: block; text-transform: uppercase; letter-spacing: .06em;
    font-size: 10px; color: var(--ink-faint); margin-bottom: 2px; font-style: normal;
  }

  .iv-billto-editor { display: none; margin-top: 8px; }
  body.edit-mode .iv-billto-editor { display: block; }
  .iv-billto-row { display: flex; align-items: center; gap: 8px; margin-bottom: 4px; }
  .iv-billto-row .iv-label { flex: 0 0 64px; text-transform: capitalize; }
  .iv-billto-row input {
    flex: 1; min-width: 0; padding: 5px 7px; font-size: 12.5px;
    border: 1px solid var(--control-line); border-radius: 7px; background: var(--card); color: var(--ink);
  }
  .iv-billto-row input.iv-billto-missing { border-style: dashed; }
  .iv-billto-ask { margin-top: 6px; font-size: var(--text-sm); color: var(--ink-faint); cursor: not-allowed; }

  /* The internal memo reuses `.iv-notes`'s own layout (it is one, with a
     second instance's own title/colour) but is never printed — `no-print`
     in the markup, same as the original-PDF pane. */
  .iv-memo .iv-meta-col-title { color: var(--warn-ink); }

  @media print {
    .no-print { display: none !important; }
    body { background: var(--card); }
    .main { padding: 0; }
    .invoice-sheet { box-shadow: none; padding: 0; max-width: none; margin: 0; border-radius: 0; }
    /* The split must not survive into print. .iv-original is .no-print already,
       but a grid with one hidden column still reserves the empty track and the
       sheet prints on half the page. */
    .iv-compare-open { display: block; }
  }

  /* --- Two column layout --- */
  .two-col {
    display: grid;
    /* minmax(0, 1fr), not bare 1fr: a bare 1fr column's minimum size is its
       content's min-content width, not 0 — the same gotcha flexbox items
       have by default. A wide-enough child (e.g. the Timeline card's fixed-
       pixel-width custom-field-row grid) can then force this column past
       its fair 50% share and blow the whole two-col row past the viewport.
       minmax(0, 1fr) lets the column actually shrink to its allotted
       share, pushing any real overflow onto the child to handle instead of
       the page. */
    grid-template-columns: minmax(0, 1fr) minmax(0, 1fr);
    gap: 16px;
    align-items: start;
  }
  @media (max-width: 768px) {
    .two-col { grid-template-columns: 1fr; }
  }
  /* Game Plan and Timeline side by side, same visual size — Game Plan is
     "the main thing being written" (direct feedback), so its card should
     read as equally prominent as the Timeline card next to it, not a small
     afterthought above it. align-items:stretch makes both cards match the
     taller one's height; the textarea then flexes to fill its card instead
     of leaving empty space below a short one. */
  .two-col.stretch {
    align-items: stretch;
  }
  .two-col.stretch > .card {
    display: flex;
    flex-direction: column;
  }
  .two-col.stretch > .card > .field {
    flex: 1;
    display: flex;
    flex-direction: column;
  }
  .two-col.stretch > .card textarea {
    flex: 1;
    min-height: 260px;
  }

  /* Compact multi-column field grid for the Logistics card — was one long
     vertical stack of full-width fields, now several short fields per row
     so the whole card reads at a glance instead of scrolling. */
  .logistics-grid {
    display: grid;
    grid-template-columns: repeat(4, 1fr);
    gap: 4px 16px;
  }
  .logistics-grid .field.span-2 { grid-column: span 2; }
  @media (max-width: 768px) {
    .logistics-grid { grid-template-columns: 1fr; }
    .logistics-grid .field.span-2 { grid-column: span 1; }
  }

  /* --- Event page header --- */
  .event-header { margin-bottom: 20px; }
  /* Keep id/title/date/status glanceable while scrolling within a tab —
     scoped to #page-event only, #page-new's header stays normal flow. */
  #page-event .event-header {
    position: sticky;
    top: 0;
    background: var(--bg);
    z-index: 50;
    padding: 12px 0;
    margin-bottom: 12px;
    border-bottom: 1px solid var(--line);
  }

  /* --- Event page tabs --- */
  .tabs-nav {
    display: flex;
    gap: 4px;
    overflow-x: auto;
    margin-bottom: 16px;
    border-bottom: 1px solid var(--line);
  }
  .tab-btn {
    padding: 10px 16px;
    border: none;
    background: transparent;
    font-size: var(--text-base);
    font-weight: var(--weight-semibold);
    color: var(--ink-soft);
    cursor: pointer;
    white-space: nowrap;
    border-bottom: 2px solid transparent;
    transition: color 0.15s, border-color 0.15s;
  }
  .tab-btn:hover { color: var(--ink); }
  .tab-btn.active { color: var(--ink); border-bottom-color: var(--attn); }
  .tab-panel { display: none; }
  .tab-panel.active { display: block; }
  .event-id {
    font-size: var(--text-sm);
    font-family: monospace;
    color: var(--ink-soft);
    margin-bottom: 4px;
  }
  .event-title {
    font-size: var(--text-5xl);
    font-weight: var(--weight-bold);
    margin-bottom: 8px;
  }
  .event-meta {
    display: flex;
    align-items: center;
    gap: 12px;
    flex-wrap: wrap;
  }
  .event-date {
    font-size: var(--text-xl);
    color: var(--ink-mid);
    font-weight: var(--weight-medium);
  }
  /* Sticky-header summary (venue / staff count / next checkpoint) — glanceable
     from any tab instead of a click into Logistics or Staff. */
  .event-summary-bar {
    display: flex;
    flex-wrap: wrap;
    gap: 6px 14px;
    margin-top: 8px;
  }
  .summary-chip {
    font-size: 12.5px;
    color: var(--ink-soft);
    white-space: nowrap;
  }

  /* --- Events table (list view) --- */
  .events-table {
    width: 100%;
    background: var(--card);
    border-radius: 12px;
    overflow: hidden;
    box-shadow: 0 1px 3px rgba(0,0,0,0.08);
    border-collapse: collapse;
    /* Deliberate column proportions (see the <colgroup> in
       dashboard-page.js) instead of auto layout — auto-layout was dumping
       any leftover width into whichever column had the least content
       (usually Staff/Lead), leaving visible dead space inside those cells
       rather than distributing it across the row on purpose. */
    table-layout: fixed;
  }
  .events-table td { overflow-wrap: break-word; }
  .events-table th {
    padding: 12px 16px;
    text-align: left;
    font-size: var(--text-xs);
    font-weight: var(--weight-semibold);
    text-transform: uppercase;
    letter-spacing: 0.5px;
    color: var(--ink-soft);
    border-bottom: 1px solid var(--rule);
    background: var(--bg-soft);
  }
  .events-table td {
    padding: 14px 16px;
    font-size: var(--text-lg);
    border-bottom: 1px solid var(--bg);
    vertical-align: middle;
  }
  .events-table tr:last-child td { border-bottom: none; }
  .events-table tr:hover td { background: var(--bg-soft); cursor: pointer; }

  /* --- Lead cell (events table) — kept as compact chips on both desktop
     and mobile; without this the bare <select> below fell back to the
     browser's native control chrome, which on mobile Safari renders as a
     full-width grey button wildly out of step with the assigned-lead chip
     next to it. */
  .lead-static {
    display: inline-flex;
    align-items: center;
    gap: 6px;
    font-size: var(--text-base);
    max-width: 100%;
  }
  .lead-static span { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
  .lead-edit-btn {
    flex: 0 0 auto;
    border: none;
    background: transparent;
    color: var(--ink-mute);
    cursor: pointer;
    font-size: var(--text-sm);
    line-height: var(--leading-none);
    padding: 3px 5px;
    border-radius: 5px;
  }
  .lead-edit-btn:hover { background: var(--rule); color: var(--ink); }
  .lead-just-saved { animation: lead-saved-flash 1.2s ease; }
  @keyframes lead-saved-flash {
    0% { background: #e3f6e8; }
    100% { background: transparent; }
  }

  /* Per-field autosave feedback (event.js core scalar fields + day/custom
     timeline fields) — field goes visibly inert the instant it saves, then
     a green flash on the border/background fades back to normal. This is
     the reference implementation for the pattern documented in
     plans/GUIDE_FIELD_AUTOSAVE_PATTERN.md. */
  .field-saving { background: var(--rule) !important; color: var(--ink-mute) !important; }
  .field-just-saved { animation: field-saved-flash 1.1s ease; }
  @keyframes field-saved-flash {
    0%   { border-color: var(--ink-mute); background: var(--rule); }
    30%  { border-color: #34c759; background: #eafaf0; }
    100% { border-color: var(--control-line); background: var(--card); }
  }
  .lead-select {
    appearance: none;
    -webkit-appearance: none;
    max-width: min(100%, 160px);
    width: auto;
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
    padding: 6px 12px;
    border: 1px solid var(--control-line);
    border-radius: var(--radius);
    font-size: var(--text-base);
    background: var(--card);
    color: var(--ink);
    cursor: pointer;
    box-sizing: border-box;
  }
  .lead-select.lead-unassigned {
    color: var(--danger);
    border-color: #ffb3ad;
    background: #fff5f5;
    font-weight: var(--weight-semibold);
  }
  .mli-lead { font-size: var(--text-sm); color: var(--ink-soft); }
  .mli-lead.lead-unassigned { color: var(--danger); font-weight: var(--weight-semibold); }

  /* Same reflow pattern as equipment.html's table: below 640px a fixed-column
     table can't shrink to fit without becoming illegible, so each row
     becomes its own stacked card instead - cells go full-width, each with a
     small uppercase data-label standing in for the header that's no longer
     there. Uses the full screen width (no horizontal scroll, no dead margin)
     - the whole point of this pass. */
  @media (max-width: 640px) {
    #all-events-section { background: transparent; box-shadow: none; padding: 0; }
    .events-table { box-shadow: none; border-radius: 0; }
    .events-table thead { display: none; }
    .events-table, .events-table tbody, .events-table tr, .events-table td { display: block; width: 100%; box-sizing: border-box; }
    .events-table tr:not(.row-divider) { position: relative; padding: 10px 16px 10px 28px; border-bottom: 1px solid var(--line); }
    .events-table td { padding: 3px 0; border-bottom: none; }
    .events-table td[data-label]:empty { display: none; }
    .events-table td[data-label]::before {
      content: attr(data-label);
      display: block;
      font-size: var(--text-2xs);
      color: #aeaeb2;
      text-transform: uppercase;
      letter-spacing: 0.03em;
    }
    /* Status accent moves from the first stacked cell (desktop's left
       border on td:first-child) to a strip spanning the whole card. A plain
       border-left on the row rendered short/cut-off in practice (confirmed
       on a real phone) - an absolutely-positioned strip pinned to both the
       top and bottom of the row is immune to whatever caused that, since
       its height comes directly from the row's own box, not from a border
       that has to visually track content flow. */
    /* Cancel the desktop td:first-child border-left. Matching its
       specificity wasn't enough on its own - that unconditional rule lives
       LATER in this file (the ".events-table tr.row-status-X td:first-child"
       block further down), and equal specificity ties resolve by source
       order, not by which one is "more specific to mobile" - so it kept
       winning regardless, leaving a second, short border-height stripe
       rendering right on top of the new full-height ::before one, glued
       against the DATE text since that first cell has no left padding of
       its own. !important is the actual fix here, not just belt-and-suspenders. */
    .events-table tr.row-status-inquiry td:first-child,
    .events-table tr.row-status-confirmed td:first-child,
    .events-table tr.row-status-prep td:first-child,
    .events-table tr.row-status-ready td:first-child,
    .events-table tr.row-status-closed td:first-child,
    .events-table tr.row-status-cancelled td:first-child {
      border-left: none !important;
    }
    .events-table tr.row-status-inquiry::before,
    .events-table tr.row-status-confirmed::before,
    .events-table tr.row-status-prep::before,
    .events-table tr.row-status-ready::before,
    .events-table tr.row-status-closed::before,
    .events-table tr.row-status-cancelled::before {
      content: '';
      position: absolute;
      left: 0; top: 0; bottom: 0;
      width: 4px;
    }
    .events-table tr.row-status-inquiry::before   { background: #856404; }
    .events-table tr.row-status-confirmed::before { background: #0a3622; }
    .events-table tr.row-status-prep::before      { background: #084298; }
    .events-table tr.row-status-ready::before     { background: #1a5c2a; }
    .events-table tr.row-status-closed::before    { background: var(--ink-soft); }
    .events-table tr.row-status-cancelled::before { background: #842029; }
  }

  /* ==========================================================================
     DATA TABLE (#1181) — the shared Kaneo-style table for Invoices and All
     events (`shared/data_table.js`). `.events-table` above stays
     `pages/history.js`'s own (the Past events screen this issue does not
     touch, plus its `row-status-*` accent bars, shared with that screen) —
     this is a separate, denser vocabulary, not a restyle of it. The old
     `#all-events-section`-scoped compact-card block that used to live here
     is gone with it: that id no longer exists once `all-events.js` renders
     `.dt-table` instead of `.events-table`.
     Tokens only: `--st`/`--stbg` (the shared `[data-st]` status vocabulary
     below, extended with the invoice statuses this screen needs),
     `--space-*`/`--control-h-*`/`--radius-pill`/`--shadow-sm` (tokens.css's
     size layer) for geometry, the legacy `:root` palette for colour.
     ========================================================================== */
  .dt-wrap {
    background: var(--card);
    border-radius: var(--radius-sheet);
    box-shadow: var(--shadow-sm);
    overflow: hidden;
  }
  .dt-scroll { overflow-x: auto; }
  .dt-table { width: 100%; border-collapse: collapse; }
  .dt-table thead { position: sticky; top: 0; z-index: 2; background: var(--card); }
  .dt-th {
    padding: var(--space-5) var(--space-6);
    text-align: left;
    font-size: var(--text-2xs);
    font-weight: var(--weight-bold);
    color: var(--ink-mute);
    border-bottom: 1px solid var(--line);
    white-space: nowrap;
  }
  .dt-th-right { text-align: right; }
  .dt-sort-btn {
    display: inline-flex;
    align-items: center;
    gap: var(--space-2);
    background: none;
    border: none;
    padding: 0;
    font: inherit;
    text-transform: inherit;
    letter-spacing: inherit;
    color: inherit;
    cursor: pointer;
  }
  .dt-th-right .dt-sort-btn { flex-direction: row-reverse; }
  .dt-sort-btn:hover, .dt-th.is-sorted .dt-sort-btn { color: var(--ink); }
  .dt-sort-arrow { font-size: var(--text-2xs); color: var(--ink); min-width: 8px; display: inline-block; }
  .dt-table tbody tr { border-bottom: 1px solid var(--rule); }
  .dt-table tbody tr:last-child { border-bottom: none; }
  .dt-table td {
    padding: var(--space-4) var(--space-6);
    font-size: var(--text-base);
    color: var(--ink);
    vertical-align: middle;
  }
  .dt-th-right, td.dt-cell-right { text-align: right; }
  .dt-row-clickable { cursor: pointer; }
  .dt-row-clickable:hover, .dt-row-clickable:focus-within { background: var(--bg-soft); }
  .dt-table tbody tr:focus-visible { outline: 2px solid var(--accent); outline-offset: -2px; }
  .dt-table tr.row-divider td, .dt-table tr.dt-group-row td {
    padding: var(--space-3) var(--space-6);
    font-size: var(--text-xs);
    font-weight: var(--weight-bold);
    text-transform: none;
    color: var(--ink-mute);
    background: var(--bg);
  }
  .dt-table tr.row-divider:hover td, .dt-table tr.dt-group-row:hover td { background: var(--bg); cursor: default; }

  .dt-pill {
    display: inline-block;
    padding: 3px 10px;
    border-radius: var(--radius-pill);
    font-size: var(--text-xs);
    font-weight: var(--weight-semibold);
    background: var(--stbg);
    color: var(--st);
    white-space: nowrap;
  }
  .dt-num { display: inline-block; font-variant-numeric: tabular-nums; font-feature-settings: 'tnum' 1; }
  /* #1184 §2 — the events list's service-tag indicator: a small colored
     dot per tag (`all-events.js`'s `_tagsCellHtml`), the legacy-page
     equivalent of the card's lucide icons — this stack has no
     icon-component library. The base rule is the fallback color only:
     the REAL per-tag color (tenant data, the vocabulary's own hex) is
     written by `_renderTagStyles` into a generated `<style>` ELEMENT as
     `.dt-tagdot[data-tag="…"] { background: #rrggbb }`, never a `style="…"`
     HTML attribute — design-token-lint's inline-style gate bans that
     attribute outright in this legacy stack, tenant value or not. */
  .dt-tagdot {
    display: inline-block;
    width: 8px;
    height: 8px;
    border-radius: 50%;
    margin-right: 4px;
    vertical-align: middle;
    background: var(--st-idle);
  }
  /* The generic "nothing here" dash a data-table cell falls back to (e.g.
     all-events.js's Staff column with no one booked) — one class instead of
     a one-off inline `style="color:var(--ink-faint)"` per caller (#1181
     follow-up: inline styles → design tokens). */
  .dt-cell-empty { color: var(--ink-faint); }

  .dt-row-actions {
    display: inline-flex;
    gap: var(--space-3);
    opacity: 0;
    transition: opacity var(--motion-fast) var(--motion-ease);
  }
  .dt-table tbody tr:hover .dt-row-actions,
  .dt-table tbody tr:focus-within .dt-row-actions {
    opacity: 1;
  }
  .dt-row-action {
    background: none;
    border: 1px solid var(--control-line);
    border-radius: var(--radius);
    padding: 2px 8px;
    font-size: var(--text-xs);
    font-weight: var(--weight-semibold);
    color: var(--ink-soft);
    cursor: pointer;
  }
  .dt-row-action:hover { border-color: var(--accent); color: var(--accent); }

  .dt-skeleton-row td { padding: var(--space-4) var(--space-6); }
  .dt-skeleton {
    display: block;
    height: 14px;
    border-radius: var(--radius);
    background: linear-gradient(90deg, var(--bg-soft) 25%, var(--line) 37%, var(--bg-soft) 63%);
    background-size: 400% 100%;
    animation: dt-shimmer 1.4s ease infinite;
  }
  @keyframes dt-shimmer {
    0% { background-position: 100% 50%; }
    100% { background-position: 0 50%; }
  }

  .dt-empty { padding: var(--space-13) var(--space-8); text-align: center; color: var(--ink-mute); }
  .dt-empty-title { font-size: var(--text-lg); font-weight: var(--weight-semibold); color: var(--ink-soft); margin-bottom: var(--space-2); }
  .dt-empty-sub { font-size: var(--text-sm); }

  /* --- Toolbar: filter chips / search / date range ------------------------ */
  .dt-toolbar { display: flex; flex-wrap: wrap; align-items: center; gap: var(--space-6); margin-bottom: var(--space-6); }
  .dt-toolbar-group { display: inline-flex; flex-wrap: wrap; align-items: center; gap: var(--space-3); }
  /* Author `display: inline-flex` beats the UA `[hidden]{display:none}`
     rule — same trap `.btn[hidden]` documents near the top of this file.
     Needed once a caller (the brand-chips group) toggles this with the
     `hidden` attribute instead of `style.display` (#1181 follow-up). */
  .dt-toolbar-group[hidden] { display: none; }
  .dt-chips { display: inline-flex; flex-wrap: wrap; gap: var(--space-3); }
  .dt-chip {
    display: inline-flex;
    align-items: center;
    gap: var(--space-2);
    height: var(--control-h-sm);
    padding: 0 var(--control-px-sm);
    border-radius: var(--radius-pill);
    border: 1px solid var(--control-line);
    background: var(--card);
    color: var(--ink-soft);
    font-size: var(--text-xs);
    font-weight: var(--weight-semibold);
    cursor: pointer;
    white-space: nowrap;
  }
  .dt-chip:hover { border-color: var(--ink); color: var(--ink); }
  .dt-chip.is-active { background: var(--ink); border-color: var(--ink); color: var(--on-dark); }
  .dt-chip-count { font-size: var(--text-2xs); font-weight: var(--weight-bold); opacity: 0.75; }
  .dt-search, .dt-date {
    min-height: var(--field-min-h);
    height: var(--control-h-sm);
    padding: 0 var(--control-px-sm);
    border-radius: var(--radius);
    border: 1px solid var(--control-line);
    background: var(--card);
    color: var(--ink);
    font-size: var(--fs-field);
    box-sizing: border-box;
  }
  .dt-search { min-width: 180px; }
  .dt-daterange { display: inline-flex; align-items: center; gap: var(--space-2); }
  .dt-daterange-sep { color: var(--ink-faint); }

  /* Phone: a dense table degrades to a readable row list, not a horizontal
     scroll — the same reflow `.events-table`'s own mobile block uses,
     written once more at tokens.json's own phone breakpoint (767px) with
     every size read from a token (docs/GUIDE_MOBILE_TOKENS.md); `.events-table`
     predates that rule and stays unconverted (history.js's own, out of this
     issue's scope). */
  @media (max-width: 767px) {
    .dt-scroll { overflow-x: visible; }
    .dt-table thead { display: none; }
    .dt-table, .dt-table tbody, .dt-table tr, .dt-table td { display: block; width: 100%; box-sizing: border-box; }
    .dt-table tbody tr { padding: var(--space-5) var(--control-px); border-bottom: 1px solid var(--line); }
    .dt-table tr.row-divider, .dt-table tr.dt-group-row { padding: var(--space-3) var(--control-px); }
    .dt-table td { padding: var(--space-1) 0; }
    .dt-table td[data-label]:empty { display: none; }
    .dt-table td[data-label]::before {
      content: attr(data-label);
      display: block;
      font-size: var(--fs-meta);
      color: var(--ink-faint);
    }
    .dt-th-right, td.dt-cell-right { text-align: left; }
    .dt-row-actions { opacity: 1; margin-top: var(--space-2); }
    .dt-toolbar { gap: var(--space-4); }
    .dt-search { min-width: 0; flex: 1 1 auto; }
  }


  /* Status color-coding on the event list — same palette as the calendar
     chips (.cal-event.status-X below), just as a left accent bar instead of
     a full-row background so the row stays easy to read. */
  .events-table tr.row-status-inquiry   td:first-child { border-left: 4px solid #856404; }
  .events-table tr.row-status-confirmed td:first-child { border-left: 4px solid #0a3622; }
  .events-table tr.row-status-prep      td:first-child { border-left: 4px solid #084298; }
  .events-table tr.row-status-ready     td:first-child { border-left: 4px solid #1a5c2a; }
  .events-table tr.row-status-closed    td:first-child { border-left: 4px solid var(--ink-soft); }
  .events-table tr.row-status-cancelled td:first-child { border-left: 4px solid #842029; }

  /* Past events stay in the list (info may still be needed) but read as
     de-emphasized at a glance — muted, not hidden. */
  .events-table tr.row-past { opacity: 0.55; filter: grayscale(35%); }

  .events-table tr.row-divider td {
    padding: 6px 16px;
    font-size: var(--text-xs);
    font-weight: var(--weight-bold);
    text-transform: uppercase;
    letter-spacing: 0.5px;
    color: var(--ink-mute);
    background: var(--bg);
  }
  .events-table tr.row-divider:hover td { background: var(--bg); cursor: default; }
  .drive-link {
    color: var(--accent);
    text-decoration: none;
    font-size: var(--text-base);
  }
  .drive-link:hover { text-decoration: underline; }
  .amount { font-weight: var(--weight-semibold); color: var(--ink); }

  /* --- Calendar --- */
  .calendar-container {
    background: var(--card);
    border-radius: 12px;
    overflow: hidden;
    box-shadow: 0 1px 3px rgba(0,0,0,0.08);
    /* Capped and centered — full-bleed across a wide desktop viewport read as
       oversized even though the information density itself was fine. */
    max-width: 760px;
    margin: 0 auto;
  }
  .calendar-nav {
    display: flex;
    align-items: center;
    justify-content: space-between;
    padding: 16px 20px;
    border-bottom: 1px solid var(--rule);
  }
  .calendar-nav h2 { font-size: var(--text-2xl); font-weight: var(--weight-semibold); }
  .cal-nav-btn {
    padding: 6px 14px;
    border: 1px solid var(--control-line);
    border-radius: var(--radius);
    background: var(--card);
    font-size: var(--text-base);
    cursor: pointer;
  }
  .calendar-grid {
    display: grid;
    grid-template-columns: repeat(7, minmax(0, 1fr)) /* equal days; a long chip may not widen its column (#1137) */;
  }
  .cal-day-header {
    padding: 10px;
    text-align: center;
    font-size: var(--text-xs);
    font-weight: var(--weight-semibold);
    text-transform: uppercase;
    letter-spacing: 0.5px;
    color: var(--ink-soft);
    border-bottom: 1px solid var(--rule);
    background: var(--bg-soft);
  }
  .cal-day {
    min-height: 58px;
    padding: 6px;
    border-right: 1px solid var(--rule);
    border-bottom: 1px solid var(--rule);
  }
  .cal-day:nth-child(7n) { border-right: none; }
  .cal-day-num {
    font-size: var(--text-sm);
    color: var(--ink-soft);
    margin-bottom: 4px;
  }
  .cal-day.today .cal-day-num {
    background: var(--attn);
    color: var(--on-dark);
    width: 22px;
    height: 22px;
    border-radius: 50%;
    display: flex;
    align-items: center;
    justify-content: center;
    font-weight: var(--weight-semibold);
  }
  .cal-day.other-month { background: var(--bg-soft); }
  .cal-day.other-month .cal-day-num { color: var(--ink-faint); }
  .cal-event {
    background: var(--ink);
    color: var(--on-dark);
    border-radius: 4px;
    padding: 3px 6px;
    font-size: var(--text-xs);
    margin-bottom: 2px;
    cursor: pointer;
    line-height: 1.3;
    overflow: hidden;
    white-space: nowrap;
    text-overflow: ellipsis;
  }
  .cal-event.status-confirmed { background: #0a3622; }
  .cal-event.status-inquiry   { background: #856404; }
  .cal-event.status-prep      { background: #084298; }
  .cal-event.status-ready     { background: #1a5c2a; }
  .cal-event.pending-deletion { background: var(--ink-soft); opacity: 0.6; text-decoration: line-through; }
  .cal-event.status-cancelled { background: #842029; }
  .cal-event.is-past { opacity: 0.5; filter: grayscale(50%); }
  .cal-event-multiday { border-left: 3px solid transparent; padding-left: 4px; }
  .cal-event-day-label { display: block; font-size: var(--text-3xs); opacity: 0.75; font-weight: var(--weight-semibold); text-transform: uppercase; letter-spacing: 0.3px; }
  .cal-event-highlight { outline: 2px solid var(--ink); outline-offset: 1px; box-shadow: 0 1px 4px rgba(0,0,0,0.35); }
  .cal-event-more {
    font-size: var(--text-2xs);
    color: var(--ink-mute);
    padding: 1px 6px;
  }
  /* #822 — the DJEP calendar layer's own chip: visually distinct from a real
     .cal-event on purpose. This is raw, unresolved feed data (DJEP's own
     "(Select A Room)" literal included), never an event we created — a solid
     fill next to real event chips would claim a confidence this data does not
     have. Dashed outline, no fill, its own muted color. */
  .cal-event-djep {
    background: transparent;
    color: var(--ink-soft);
    border: 1px dashed var(--ink-mute);
    border-radius: 4px;
    padding: 2px 6px;
    font-size: var(--text-xs);
    margin-bottom: 2px;
    line-height: 1.3;
    overflow: hidden;
    white-space: nowrap;
    text-overflow: ellipsis;
    cursor: default;
  }
  .cal-event-djep-tag {
    font-size: var(--text-3xs);
    font-weight: var(--weight-semibold);
    text-transform: uppercase;
    letter-spacing: 0.3px;
    opacity: 0.75;
    margin-right: 3px;
  }

  /* Keep the classic 7-column grid shape on phones (not a list) — just make
     each cell short enough that a full month fits as a compact rectangle
     spanning the screen width, instead of towering down the page. */
  @media (max-width: 480px) {
    .calendar-container { max-width: none; }
    .cal-day { min-width: 0; }
    .cal-nav-btn { padding: 5px 10px; font-size: var(--text-sm); }
    .calendar-nav h2 { font-size: var(--text-lg); }
    .cal-nav-btn { padding: 5px 10px; font-size: var(--text-sm); }
    .cal-day-header { padding: 6px 2px; font-size: var(--text-3xs); }
    .cal-day { min-height: 40px; padding: 3px; }
    .cal-day-num { font-size: var(--text-2xs); margin-bottom: 1px; }
    .cal-day.today .cal-day-num { width: 18px; height: 18px; font-size: var(--text-2xs); }
    .cal-event { font-size: 8px; padding: 1px 3px; margin-bottom: 1px; }
    .cal-event-more { font-size: 8px; padding: 0 3px; }
  }

  /* --- Dashboard detail overlay --- */
  .detail-overlay {
    display: none;
    position: fixed;
    inset: 0;
    background: rgba(0,0,0,0.4);
    z-index: 100;
    align-items: center;
    justify-content: center;
  }
  .detail-overlay.open { display: flex; }
  .detail-panel {
    background: var(--card);
    border-radius: 16px;
    width: 560px;
    max-width: 95vw;
    max-height: 85vh;
    overflow-y: auto;
    padding: 28px;
    position: relative;
  }
  .detail-close {
    position: absolute;
    top: 16px;
    right: 16px;
    background: var(--bg);
    border: none;
    border-radius: 50%;
    width: 30px;
    height: 30px;
    font-size: var(--text-2xl);
    cursor: pointer;
    display: flex;
    align-items: center;
    justify-content: center;
    color: var(--ink-soft);
  }
  .detail-title {
    font-size: var(--text-3xl);
    font-weight: var(--weight-bold);
    margin-bottom: 4px;
    padding-right: 36px;
  }
  .detail-date {
    font-size: var(--text-base);
    color: var(--ink-soft);
    margin-bottom: 16px;
  }
  .detail-grid {
    display: grid;
    grid-template-columns: 1fr 1fr;
    gap: 12px;
    margin-bottom: 16px;
  }
  .detail-field label {
    display: block;
    font-size: var(--text-xs);
    font-weight: var(--weight-semibold);
    text-transform: uppercase;
    letter-spacing: 0.5px;
    color: var(--ink-soft);
    margin-bottom: 3px;
  }
  .detail-field p { font-size: var(--text-lg); color: var(--ink); }
  .detail-notes {
    background: var(--bg);
    border-radius: var(--radius);
    padding: 12px;
    font-size: var(--text-base);
    color: var(--ink-mid);
    white-space: pre-wrap;
    line-height: var(--leading-normal);
  }
  .detail-actions {
    display: flex;
    gap: 8px;
    margin-top: 16px;
  }

  /* --- Invoice line items --- */
  .line-items { margin-top: 8px; }
  .line-item {
    display: flex;
    justify-content: space-between;
    align-items: center;
    padding: 8px 0;
    border-bottom: 1px solid var(--bg);
    font-size: var(--text-base);
  }
  .line-item:last-child { border-bottom: none; }
  /* #267 — the invoice's opening category caption, not an item. No bottom rule
     (it belongs WITH the rows under it) and no qty/price cells at all. */
  .line-item.line-item-caption { border-bottom: none; padding: 10px 0 2px; }
  .line-item.line-item-caption .line-item-name {
    color: var(--ink-mute); font-size: var(--text-xs); font-weight: var(--weight-bold);
    text-transform: uppercase; letter-spacing: var(--track-caps);
  }
  .line-item-name  { color: var(--ink); flex: 1; }
  .line-item-qty   { color: var(--ink-soft); margin: 0 12px; }
  .line-item-total { font-weight: var(--weight-semibold); }
  .invoice-total {
    display: flex;
    justify-content: space-between;
    padding-top: 10px;
    margin-top: 4px;
    border-top: 2px solid var(--ink);
    font-weight: var(--weight-bold);
    font-size: var(--text-xl);
  }

  /* --- #342: the money on the event's invoice card ------------------------
     Small by design — the card's job is "has anything arrived, and can I find
     it on Stripe's side", not a ledger view. The full detail is one click away
     on /invoice/:id. */
  .ev-pay-title {
    font-size: var(--text-xs); font-weight: var(--weight-bold); color: var(--ink-mute);
    text-transform: uppercase; letter-spacing: var(--track-caps); margin-bottom: 6px;
  }
  .ev-pay-balance { font-size: var(--text-base); font-weight: var(--weight-semibold); color: var(--paid-ink); margin-bottom: 8px; }
  .ev-pay-row {
    border: 1px solid var(--rule); border-radius: var(--radius); padding: 8px 10px; margin-bottom: 6px;
    background: #fbfbfd; min-width: 0;
  }
  .ev-pay-line { display: flex; flex-wrap: wrap; justify-content: space-between; gap: 6px; }
  .ev-pay-kind { font-size: var(--text-sm); font-weight: var(--weight-semibold); color: var(--ink); }
  .ev-pay-amt { font-size: var(--text-base); font-weight: var(--weight-bold); color: var(--paid-ink); font-variant-numeric: tabular-nums; }
  .ev-pay-out { color: #c0392b; }
  .ev-pay-sub { font-size: 11.5px; line-height: 1.45; color: var(--ink-soft); margin-top: 2px; overflow-wrap: anywhere; }
  /* `ch_3U6eInBNzWh7RtYn2aa0yTAz` has no break opportunity in it, and this card
     lives in a narrow column on a 390px phone. `anywhere` + break-all is what
     stops it pushing the whole page sideways. */
  .ev-pay-oid {
    margin-top: 3px; font-family: var(--font-mono);
    font-size: var(--text-xs); color: var(--ink-mid); overflow-wrap: anywhere; word-break: break-all;
  }
  .ev-pay-conflict {
    margin-top: 6px; padding: 8px 10px; border-radius: var(--radius);
    border: 1px solid var(--attn); background: #fff8ec;
    font-size: 11.5px; line-height: var(--leading-normal); color: #7a4a00; overflow-wrap: anywhere;
  }
  .ev-pay-err {
    font-size: 11.5px; line-height: var(--leading-normal); color: #c0392b; overflow-wrap: anywhere;
  }

  /* --- Contacts --- */
  .contact-card {
    border: 1px solid var(--rule);
    border-radius: 10px;
    padding: 14px;
    margin-bottom: 10px;
    position: relative;
  }
  .contact-card:last-child { margin-bottom: 0; }
  .contact-name {
    font-size: var(--text-lg);
    font-weight: var(--weight-semibold);
    margin-bottom: 2px;
  }
  .contact-role {
    font-size: var(--text-xs);
    text-transform: uppercase;
    letter-spacing: var(--track-caps);
    color: var(--ink-soft);
    margin-bottom: 8px;
  }
  .contact-detail {
    font-size: var(--text-base);
    color: var(--ink-mid);
    margin-bottom: 3px;
    display: flex;
    align-items: center;
    gap: 6px;
  }
  .contact-detail a { color: var(--accent); text-decoration: none; }

  /* --- Add contact form --- */
  .add-contact-form {
    border: 1px dashed var(--control-line);
    border-radius: 10px;
    padding: 14px;
    margin-top: 10px;
    display: none;
  }
  .add-contact-form.open { display: block; }
  .form-row {
    display: grid;
    grid-template-columns: 1fr 1fr;
    gap: 10px;
    margin-bottom: 10px;
  }
  .form-row-label {
    display: block;
    font-size: var(--text-xs);
    font-weight: var(--weight-semibold);
    text-transform: uppercase;
    letter-spacing: var(--track-caps);
    color: var(--ink-soft);
    margin-bottom: 4px;
  }
  .form-row input {
    width: 100%;
    padding: 8px 10px;
    border: 1px solid var(--control-line);
    border-radius: var(--radius);
    font-size: var(--text-base);
    font-family: inherit;
  }
  .form-row input:focus {
    outline: none;
    border-color: var(--attn);
  }
  .form-actions-row {
    display: flex;
    gap: 8px;
    justify-content: flex-end;
  }

  /* --- Links row --- */
  .links-row {
    display: flex;
    gap: 10px;
    flex-wrap: wrap;
  }
  #provision-row { display: flex; gap: 8px; align-items: center; flex-wrap: wrap; }
  .link-btn {
    display: inline-flex;
    align-items: center;
    gap: 6px;
    padding: 8px 14px;
    border: 1px solid var(--control-line);
    border-radius: var(--radius);
    font-size: var(--text-base);
    color: var(--ink);
    text-decoration: none;
    background: var(--card);
    transition: background 0.15s;
  }
  .link-btn:hover { background: var(--bg); }
  /* #299 — the "this job's mail address was reissued" notice, under the links
     row. `flex-basis: 100%` because .links-row is a flex row and this must sit
     on its own line rather than squeeze in beside the buttons. */
  .ev-job-mail-note {
    flex-basis: 100%;
    font-size: var(--text-sm);
    color: var(--ink-mute);
    margin-top: 2px;
  }

  .staff-warn-dot { color: var(--danger); font-size: var(--text-2xl); line-height: var(--leading-none); }
  .dash-section { background: var(--card); border-radius: 12px; padding: 16px; box-shadow: 0 1px 3px rgba(0,0,0,0.08); }

  /* --- Urgency summary strip --- */
  .urgency-strip {
    display: grid;
    grid-template-columns: repeat(4, 1fr);
    gap: 12px;
    margin-bottom: 16px;
    max-width: 760px;
    margin-left: auto;
    margin-right: auto;
  }
  @media (max-width: 640px) {
    .urgency-strip { grid-template-columns: repeat(2, 1fr); }
  }
  .urgency-pill {
    text-align: center;
    padding: 12px 8px;
    border-radius: 10px;
    background: var(--bg);
    cursor: pointer;
    border: 2px solid transparent;
    transition: border-color 0.15s, transform 0.1s;
    user-select: none;
  }
  .urgency-pill:hover { transform: translateY(-1px); }
  .urgency-pill.active { border-color: var(--ink); }
  .urgency-count { font-size: 22px; font-weight: var(--weight-bold); color: var(--ink); }
  .urgency-label { font-size: var(--text-sm); font-weight: var(--weight-semibold); color: var(--ink-mid); margin-top: 2px; }
  .urgency-sub   { font-size: var(--text-xs); color: var(--ink-mute); }
  .urgency-now  { background: #fde8e6; }
  .urgency-now  .urgency-count { color: #c0392b; }
  .urgency-soon { background: #fff3cd; }
  .urgency-soon .urgency-count { color: #856404; }
  .urgency-mid  { background: #eef3fb; }
  .urgency-far  { background: var(--bg); }

  /* --- Calendar + mini list side-by-side --- */
  .calendar-with-list {
    display: flex;
    gap: 16px;
    align-items: flex-start;
    flex-wrap: wrap;
    /* Left-aligned, not centered - centering a fixed-width calendar next to
       a flexible list reads as an accident of layout math, not a deliberate
       choice, once the viewport is wider than the pair's combined width. */
    justify-content: flex-start;
  }
  .calendar-with-list .calendar-container { flex: 1 1 600px; max-width: 760px; }
  /* Same selector as above, later in the file, so it actually wins the
     cascade — an earlier attempt at this same override lived inside the
     max-width:480px query further up the stylesheet and silently lost to
     the rule immediately above, since a same-specificity rule declared
     later always wins regardless of which one sits inside a media query. */
  @media (max-width: 480px) {
    .calendar-with-list .calendar-container { flex: 1 1 100%; max-width: none; }
  }
  .calendar-mini-list {
    /* Fills whatever width remains next to the calendar - no fixed cap, no
       inner scrollbox. A long month just grows the card taller; the page
       itself scrolls, not a box nested inside it. */
    flex: 1 1 320px;
    background: var(--card);
    border-radius: 12px;
    padding: 16px;
    box-shadow: 0 1px 3px rgba(0,0,0,0.08);
  }
  .mini-list-title {
    font-size: var(--text-xs);
    font-weight: var(--weight-bold);
    text-transform: uppercase;
    letter-spacing: 0.6px;
    color: var(--ink-soft);
    margin-bottom: 10px;
  }
  .mini-list-item {
    display: block;
    width: 100%;
    text-align: left;
    background: var(--card);
    /* Always bordered, not just in the desktop 2-col grid - a single mobile
       column of cards with no border between them reads as one continuous
       run of text, not separate events. */
    border: 1px solid #ececef;
    padding: 10px 12px;
    border-radius: var(--radius);
    margin-bottom: 8px;
    cursor: pointer;
    font-family: inherit;
    box-sizing: border-box;
  }
  .mini-list-item:last-child { margin-bottom: 0; }
  .mini-list-item:hover { background: var(--bg); }
  .mli-name { font-size: var(--text-xs); color: var(--ink-soft); margin-top: 1px; }
  .mli-date { font-size: var(--text-xs); color: var(--ink-mute); margin-top: 1px; }
  .mini-list-week-divider {
    font-size: var(--text-2xs); font-weight: var(--weight-bold); text-transform: uppercase; letter-spacing: 0.5px;
    color: #aeaeb2; padding: 10px 6px 4px;
  }
  .mini-list-week-divider:first-child { padding-top: 0; }

  /* Desktop: tiles in a 2-column grid instead of a single narrow scrolling
     column. No inner scrollbox - however many events the month has, the
     grid just grows taller and the page scrolls past it like everything
     else, instead of trapping the list in its own little scrolling pane. */
  @media (min-width: 700px) {
    .calendar-mini-list { max-width: 900px; }
    #mini-list-items {
      display: grid;
      grid-template-columns: 1fr 1fr;
      gap: 10px;
    }
    #mini-list-items .mini-list-item { margin-bottom: 0; } /* grid gap handles spacing instead */
    #mini-list-items .mini-list-week-divider { grid-column: 1 / -1; }
  }

  .btn-provision {
    display: inline-flex;
    align-items: center;
    gap: 6px;
    padding: 8px 14px;
    border: 1.5px dashed #34c759;
    border-radius: var(--radius);
    font-size: var(--text-base);
    color: var(--ok);
    background: #f0fdf4;
    cursor: pointer;
    transition: background 0.15s, opacity 0.15s;
  }
  .btn-provision:hover { background: #dcfce7; }
  .btn-provision:disabled { opacity: 0.5; cursor: default; }
  .btn-recheck {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    /* Matches .btn-provision's padding/border-radius (8px 14px / 8px) so the
       two sit at the same height and corner-rounding when shown side by side
       (e.g. folder exists but chat doesn't: "Create Chat Space" + recheck). */
    padding: 8px 14px;
    border: 1px solid var(--control-line);
    border-radius: var(--radius);
    font-size: var(--text-lg);
    background: transparent;
    cursor: pointer;
    color: var(--ink-soft);
    transition: background 0.15s, opacity 0.15s;
    vertical-align: middle;
  }
  .btn-recheck:hover { background: var(--bg); color: var(--ink); }
  .btn-recheck:disabled { opacity: 0.45; pointer-events: none; }

  /* --- Save bar --- */
  .save-bar {
    position: sticky;
    bottom: 0;
    background: var(--card);
    border-top: 1px solid var(--line);
    padding: 14px 24px;
    display: none;
    gap: 10px;
    align-items: center;
    z-index: 10;
  }
  .save-bar.visible { display: flex; }
  .save-bar-msg {
    flex: 1;
    font-size: var(--text-base);
    color: var(--ink-soft);
  }

  /* --- Timeline --- */
  .timeline-view { display: block; }
  /* Checkpoint NAME is the thing that should register first at a glance —
     "what happens here", not "what time is it" — so it carries the visual
     weight, not the time value (direct feedback: with many rows on screen
     it wasn't obvious what was the primary vs secondary piece of info). */
  /* white-space:nowrap + ellipsis (not wrap) is load-bearing, not cosmetic —
     row height feeds _timelineMeasureRowGap (measures a live rendered row's
     getBoundingClientRect().height to get minGapPx for the collision/
     cascade math). A row that's free to wrap to 2 lines on a narrow
     viewport (this row type has no fixed width; the 2-column split halves
     it further) measures ~2x taller, and WHICH row the measurement happens
     to sample (the first .tl-drag-row/.tl-dot-row still in the DOM from the
     previous render) is arbitrary — some labels wrap, some don't, so
     minGapPx swung between renders and the cascade pushed different rows
     different, seemingly random amounts on mobile. Pinning every row to one
     line makes the measured height constant regardless of viewport/column
     width, which is what the cascade math has always assumed. min-width:0
     is required for text-overflow:ellipsis to actually kick in on a flex
     item instead of the browser just letting it overflow at its natural
     content width.  */
  .tl-label { font-weight: var(--weight-semibold); color: var(--ink); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; min-width: 0; }
  .tl-time  { font-weight: var(--weight-normal); color: var(--ink-soft); white-space: nowrap; flex-shrink: 0; }
  /* Custom (ad-hoc) fields sit lighter than fixed checkpoints now that both
     are merged into one chronological list — same idea as the hollow vs
     solid dot below, just at the label level. */
  .tl-label.cf { font-weight: var(--weight-medium); color: var(--ink-mid); }

  /* Dot-timeline — vertical connecting line with one dot per checkpoint.
     View mode only now — edit mode's interactive counterpart is the
     draggable ruler below (.tl-drag-view), not this one; the raw field
     list this used to sit alongside no longer exists as a visible surface
     at all (see _openTlFieldPopover/_openTlAddMenu). `!important` is needed
     because renderTimeline()
     unconditionally sets these elements' inline style.display to toggle
     single-/multi-day mode — a plain class-selector rule would lose to that
     inline style regardless of source order. Time-proportional row spacing
     (issue #29, 2026-07-20) — reverses the earlier "deliberately uniform,
     not time-proportional" decision now that a real hour ruler is wanted as
     the visual foundation for a future drag-and-drop feature. */
  body.edit-mode .timeline-view,
  body.edit-mode .days-view {
    display: none !important;
  }
  .tl-dot-timeline {
    --tl-accent: var(--attn);
    position: relative;
  }
  .tl-dot-timeline::before {
    content: '';
    position: absolute;
    left: 39px; /* 5px + the 34px hour-ruler column reserved to its left */
    top: 11px;
    bottom: 11px;
    width: 2px;
    background: var(--line);
  }
  /* Pale hour/half-hour ruler pinned to the left, behind the dots — ticks
     are absolutely positioned per-instance (inline top:%, see
     _timelineRulerBuild); pointer-events:none so it never steals clicks
     meant for the dots/rows on top of it. */
  .tl-hour-ruler {
    position: absolute;
    left: 0;
    top: 0;
    bottom: 0;
    width: 34px;
    pointer-events: none;
  }
  .tl-hour-tick {
    position: absolute;
    left: 0;
    right: 0;
  }
  .tl-hour-tick::after {
    content: '';
    position: absolute;
    right: 2px;
    top: 0;
    width: 8px;
    height: 1px;
    background: var(--control-line);
  }
  .tl-hour-tick-label {
    position: absolute;
    right: 12px;
    top: 0;
    transform: translateY(-50%);
    font-size: var(--text-xs);
    color: var(--ink-faint);
    white-space: nowrap;
  }
  /* Day-change marker (issue #29) — replaces the old per-row +1/+2 badge:
     one clear "the day changed here" line across the FULL width (not just
     the ruler column, unlike an hour tick) so it reads as a break in the
     schedule rather than another time mark. Sits above the dots/ruler
     (z-index) since it's the more important signal when both coincide. */
  .tl-day-boundary {
    position: absolute;
    left: 0;
    right: 0;
    height: 0;
    border-top: 1px dashed var(--ink-faint);
    z-index: 1;
  }
  /* Anchored to the RIGHT edge, opposite side from a row's own label text
     (which starts right after the ruler) — a checkpoint landing exactly on
     a boundary (e.g. dragged to midnight) then overlaps the row's TIME text
     at worst, not its more prominent label, and the two rarely coincide in
     practice since a row's time is short and right-aligned already. */
  .tl-day-boundary-label {
    position: absolute;
    right: 0;
    top: -8px;
    background: var(--card);
    padding: 0 0 0 6px;
    font-size: var(--text-2xs);
    font-weight: var(--weight-semibold);
    color: var(--ink-mute);
    white-space: nowrap;
  }
  /* Gap compression (issue #29) — an inter-checkpoint gap over the
     threshold (_timelineRulerBuild's compressGaps) gets squeezed to a fixed
     small height; this overlay visually fades the connecting line/ticks to
     nothing through the middle of that squeezed band and back, so a long
     idle stretch reads as "compressed here" rather than acres of empty
     ruler. #fff matches .card's own background — this only reads correctly
     sitting on that surface, same assumption view-mode's boundary label
     background already makes. */
  .tl-gap-fade {
    position: absolute;
    left: 0;
    right: 0;
    background: linear-gradient(to bottom, transparent 0%, var(--card) 35%, var(--card) 65%, transparent 100%);
    pointer-events: none;
    z-index: 1;
  }
  .tl-dot-row {
    position: absolute;
    left: 34px; /* shifted right by .tl-hour-ruler's width */
    right: 0;
    transform: translateY(-50%); /* top:% is set inline per row */
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: 10px;
    padding: 7px 0 7px 22px;
    font-size: var(--text-lg);
  }
  .tl-dot-row::before {
    content: '';
    position: absolute;
    left: 0;
    top: 50%;
    transform: translateY(-50%);
    width: 12px;
    height: 12px;
    box-sizing: border-box;
    border-radius: 50%;
    background: var(--tl-accent);
    border: 2px solid var(--tl-accent);
  }
  /* Custom fields keep the hollow ring (matches edit mode's
     .custom-field-row::before); standard checkpoints get a solid dot. */
  .tl-dot-row.cf::before {
    background: var(--card);
  }

  /* 2-column cluster layout (issue #41 item 5) — a run of 2+ near/same-time
     rows (see _timelineResolveClusteredRowPositions) splits into a left and
     right half of the row's usual left:34px/right:0 full width, with a
     small gap between, instead of stacking endlessly in one column (which
     visually implied "different times" for checkpoints actually at/near
     the same time). Deliberately does NOT touch .tl-dot-row::before/
     .tl-dot-drag's own left:0/-6px dot offset — those stay relative to the
     row's own (now half-width) box, so a column-B row's dot naturally lands
     away from the shared spine (.tl-dot-timeline::before, fixed at
     left:39px) with no extra CSS needed. Applies to every row type that can
     be column-assigned: the read-only .tl-dot-row, the draggable
     .tl-drag-row, and their shared .tl-note-line. */
  .tl-dot-row.tl-col-a,
  .tl-drag-row.tl-col-a,
  .tl-note-line.tl-col-a {
    right: calc(50% + 3px);
  }
  .tl-dot-row.tl-col-b,
  .tl-drag-row.tl-col-b,
  .tl-note-line.tl-col-b {
    left: calc(50% + 3px);
  }

  /* Interactive edit-mode ruler (issue #29 phase 2) — draggable counterpart
     to the read-only .tl-dot-timeline above. .tl-drag-view wraps a
     .tl-dot-timeline built by _renderFlatDragTimeline; visibility is toggled
     in _applyMultiDayVisibility alongside #single-day-editor, not here. */
  .tl-drag-view {
    position: relative; /* positioning context for .tl-add-time-fab */
    margin-bottom: 10px;
  }
  .tl-drag-row {
    position: absolute;
    left: 34px;
    right: 0;
    transform: translateY(-50%);
    display: flex;
    align-items: center;
    gap: 10px;
    padding-left: 22px;
    font-size: var(--text-base);
    border-radius: 6px;
  }
  /* Whole-row grab target (issue #41 item 4a) — the small .tl-dot-drag dot
     still works, but real-user feedback was "not obvious it's draggable";
     _wireDragTimelineDelegation now starts a drag from anywhere on the row,
     so this hover state is the visual affordance that makes that
     discoverable. Same subtle-highlight language as other hoverable rows
     elsewhere on this page (.mini-list-item, .djep-row-head). :hover
     naturally covers hovering .tl-label too, since it's a descendant of
     this row with no pointer-events:none of its own. */
  .tl-drag-row:hover {
    cursor: grab;
    background: var(--bg);
  }
  .tl-drag-row:active { cursor: grabbing; }
  /* A real element, not a ::before — pseudo-elements can't receive pointer
     events, and this one has to (drag target). Sized bigger than the
     visible dot (::after) so the actual hit target is comfortably larger
     than the 12px mark, same "generous tap target, small visual mark"
     split .tl-note-btn already uses elsewhere on this page. */
  .tl-dot-drag {
    position: absolute;
    left: -6px;
    top: 50%;
    transform: translateY(-50%);
    width: 20px;
    height: 20px;
  }
  .tl-dot-drag::after {
    content: '';
    position: absolute;
    left: 4px;
    top: 4px;
    width: 12px;
    height: 12px;
    box-sizing: border-box;
    border-radius: 50%;
    background: var(--tl-accent, var(--attn));
    border: 2px solid var(--tl-accent, var(--attn));
  }
  .tl-dot-drag.cf::after { background: var(--card); }
  .tl-dot-drag.draggable {
    cursor: grab;
    touch-action: none; /* dragging must not also scroll the page on touch */
  }
  .tl-dot-drag.draggable:active,
  .tl-dot-drag.draggable.dragging { cursor: grabbing; }
  .tl-dot-drag.draggable.dragging::after {
    box-shadow: 0 0 0 5px rgba(245, 166, 35, 0.25);
  }

  .tl-day-header {
    display: flex;
    align-items: center;
    gap: 6px;
    font-weight: var(--weight-semibold);
    color: var(--ink);
    font-size: var(--text-lg);
    margin: 12px 0 2px;
  }
  .tl-day-header:first-child { margin-top: 0; }
  .tl-day-phase-dot {
    flex-shrink: 0;
    width: 8px;
    height: 8px;
    border-radius: 50%;
    background: var(--tl-accent, var(--ink-mute));
  }

  /* Precise-editing popover (issue #29) — was a permanently-visible block
     whenever body.edit-mode was set; now click-to-reveal (see
     _openTlFieldPopover/_closeTlFieldPopover), floating near whichever dot
     was clicked on the interactive ruler above it, so the two no longer
     duplicate the same 8 checkpoints on screen at once. display/position
     are JS-driven now (element.style.display/left/top), not body.edit-mode
     — nothing here reacts to that class anymore. */
  .timeline-edit {
    display: none;
    position: fixed;
    z-index: 50;
    flex-direction: column;
    gap: 6px;
    background: var(--card);
    border-radius: 10px;
    box-shadow: 0 8px 30px rgba(0, 0, 0, 0.18);
    padding: 14px;
    /* .tl-row's grid columns need ~342px of actual layout width (measured
       via getBoundingClientRect — the nominal 90+120+52+28px + 3*10px gaps
       column math undercounts the time12-group's real min-content width) —
       380px total (with 14px padding each side) is the smallest box that
       shows a single spotlighted row with no horizontal scrollbar (issue
       #29 UX redesign, 2026-07-20). */
    width: 380px;
    max-height: 70vh;
    overflow-y: auto;
  }
  .tl-popover-header { display: flex; justify-content: flex-end; margin-bottom: -4px; }
  /* Multi-day mode only (see _tlPopoverDayContext) — day-row chrome (date,
     day type, notes — now in the day-settings box, not this list) is
     hidden while a single field is spotlighted, so this is the only
     remaining clue which day the field belongs to. */
  .tl-popover-daylabel {
    font-size: var(--text-sm); font-weight: var(--weight-semibold); color: var(--ink-mute); margin: -2px 0 2px;
  }
  .tl-popover-close {
    background: none; border: none; cursor: pointer; font-size: var(--text-lg);
    color: var(--ink-mute); padding: 4px; line-height: var(--leading-none);
  }
  .tl-popover-close:hover { color: var(--ink); }
  /* Brief flash on whichever row a dot-click opened the popover to, so the
     eye lands on the right field among all 8+N rows. */
  .tl-popover-highlight { animation: tl-popover-flash 0.9s ease; }
  @keyframes tl-popover-flash {
    0% { background: #fff4e0; }
    100% { background: transparent; }
  }
  /* Always-visible entry point for adding a checkpoint — the only way in
     when the ruler has zero dots yet (a brand-new event/day has nothing to
     click), and a general "add a time" affordance otherwise. Opens
     _openTlAddMenu, not the field popover directly. */
  .tl-add-time-fab {
    position: absolute; top: 4px; right: 4px;
    width: 26px; height: 26px; border-radius: 50%;
    background: var(--card); border: 1px solid var(--line); box-shadow: 0 1px 3px rgba(0,0,0,0.1);
    color: var(--ink-mute); font-size: var(--text-xl); line-height: var(--leading-none); cursor: pointer;
  }
  .tl-add-time-fab:hover { color: var(--ink); border-color: var(--control-line); }
  /* Floating menu of untimed checkpoints + "new custom field" (see
     _openTlAddMenu) — same floating-box visual language as .timeline-edit,
     just a plain list of buttons instead of an editable row. */
  .tl-add-menu {
    display: flex; flex-direction: column; gap: 2px;
    position: fixed; z-index: 50;
    background: var(--card); border-radius: 10px;
    box-shadow: 0 8px 30px rgba(0, 0, 0, 0.18);
    padding: 6px; min-width: 200px; max-height: 60vh; overflow-y: auto;
  }
  .tl-add-menu-item {
    display: block; width: 100%; text-align: left;
    background: none; border: none; border-radius: 6px;
    padding: 8px 10px; font-size: var(--text-base); color: var(--ink); cursor: pointer;
  }
  .tl-add-menu-item:hover { background: var(--bg); }
  .tl-add-menu-custom { color: var(--accent); }
  /* Day-level settings (date/type/notes/remove-day) — a small, deliberately
     separate floating box from the add-menu/field popover above, since it
     edits day metadata rather than a checkpoint (see _openTlDaySettings and
     the day-settings-btn comment in _renderDayDragTimeline). */
  .tl-day-settings {
    display: flex; flex-direction: column; gap: 8px;
    position: fixed; z-index: 50;
    background: var(--card); border-radius: 10px;
    box-shadow: 0 8px 30px rgba(0, 0, 0, 0.18);
    padding: 14px; width: 260px; max-height: 70vh; overflow-y: auto;
  }
  .tl-day-remove-btn { color: var(--ink-faint); margin-top: 4px; }
  .tl-day-settings-btn {
    margin-left: auto;
    background: none; border: none; cursor: pointer;
    color: var(--ink-faint); font-size: var(--text-base); padding: 2px 4px;
  }
  .tl-day-settings-btn:hover { color: var(--ink-mute); }
  /* Grid, not flex: alignment across rows ("выровнено относительно друг
     друга") is a column-track guarantee, not something flex-wrap can ever
     deliver — flex sizes each row from its own content, so the same field
     lands at a different x position row to row. Every .tl-row shares the
     identical template below, so label/time/note-icon line up exactly;
     the note field is explicitly placed on its own full-width row 2 for
     EVERY row (never sometimes-inline), which is what removed the
     "notes and time randomly overlapping" inconsistency. */
  /* Fixed pixel tracks, not auto: an "auto" grid track sizes off each
     item's own max-content contribution, and a flex container's (i.e.
     .time12-group's) max-content contribution isn't reliably pinned to
     its children's explicit widths in every browser's grid track-sizing
     pass — live-tested and confirmed to balloon .time12-group out to
     ~236px on some rows despite .time12-text's own width:64px. A fixed
     column width sidesteps that entirely and is also the more direct way
     to guarantee every row's time control lands at the same x. */
  .tl-row {
    display: grid; grid-template-columns: 90px 120px 52px 28px;
    align-items: center; column-gap: 10px; row-gap: 6px;
  }
  /* Same "the name is the important part" reasoning as .tl-label (view
     mode) — dark + semibold instead of the muted grey a plain label
     usually gets, so it doesn't read as secondary to the time control
     next to it. */
  .tl-row > span:first-child { grid-row: 1; grid-column: 1; font-size: var(--text-base); font-weight: var(--weight-semibold); color: var(--ink); }
  .tl-row > .time12-group { grid-row: 1; grid-column: 2; }
  .tl-row > .tl-dayshift { grid-row: 1; grid-column: 3; }
  .tl-row > .tl-note-btn { grid-row: 1; grid-column: 4; }
  .tl-row > .tl-note-input { grid-row: 2; grid-column: 1 / -1; }
  /* AI timeline hint (issue #54 follow-up, event.js's _placeTimelineHints) —
     explicit full-width row 3, same reasoning as .tl-note-input's own
     grid-row:2 above: .tl-row is a strict fixed-column grid, so a 5th child
     with no explicit placement would fall into the auto-placement algorithm
     and land squeezed into a leftover ~90px cell instead of reading as a
     normal hint. Placed as a CHILD of .tl-row (not a sibling) specifically
     so it hides/shows together with the row's own popover spotlight
     toggle — see _placeTimelineHints' own comment. Reuses .ai-field-hint's
     look untouched; only the layout placement is row-specific. */
  .tl-row > .ai-timeline-hint { grid-row: 3; grid-column: 1 / -1; margin-top: 0; }
  /* "✕ Clear this time" (#253) — returns a STANDARD checkpoint to not-set, the
     one thing the eight fixed rows could not do (custom rows have had their own
     ✕ all along). Its own full-width grid row 4, for the same reason
     .ai-timeline-hint takes row 3 and .tl-note-input row 2: this grid's four
     columns are fixed pixel tracks that already need ~342px of the popover's
     352px, so a fifth icon column does not fit — and 380px is what fits a
     390px phone, so the popover cannot simply grow. It is also deliberately
     NOT a bare ✕ in the custom row's position: styles.css states that glyph as
     the cue that a row is custom, and the day-settings box's "✕ Remove this
     day" is this UI's existing shape for a labelled row-scoped removal.
     Hidden by default; _openTlFieldPopover shows it only for a checkpoint that
     actually has a time. */
  .tl-row > .tl-clear-btn {
    grid-row: 4; grid-column: 1 / -1; margin-top: 2px;
    color: var(--ink-faint); border-color: var(--line);
  }
  .tl-row > .tl-clear-btn:hover { color: var(--danger); border-color: var(--danger); }

  /* 12-hour time entry (edit mode) — a compact text field ("9:00") plus an
     AM/PM toggle button, wrapping a hidden native <input type="time"> that
     stays the real 24h value (see _time12Html in event.js). Always exactly
     H:MM, so both pieces stay narrow rather than flex-growing into empty
     space. Explicit width (not just child widths) pins this group's own
     box regardless of how any parent grid track sizes it — see the .tl-row
     comment above for the exact ballooning bug this guards against. */
  .time12-group { display: inline-flex; align-items: center; gap: 4px; width: 120px; }
  /* time12-group now sits in its own auto-sized grid column (.tl-row /
     .custom-field-row above), so it's never squeezed by a crowded row —
     that was the root cause of the live-reported "2:3(" truncated-time
     bug under the old flex layout. flex-shrink:0 stays here as a plain
     internal safeguard between the two fixed-size children of this
     (still-flex) group, not as a fix for outer row pressure. */
  /* Qualified as .time12-group .time12-text, not just .time12-text: this
     input is nested inside a .field ancestor on some rows (the day-rows
     wrapper), and plain ".field input" (one class + one type selector)
     outranks a bare one-class ".time12-text" on specificity alone,
     regardless of source order — confirmed live via getMatchedCSSRules,
     it was silently winning and stretching this input to 100% width,
     which in turn pushed .ampm-toggle out past .time12-group's own fixed
     width, overlapping the day-offset-chip in the next grid column
     entirely (not a "missing button", a covered one — elementFromPoint at
     the chip's own coordinates returned the AM/PM button sitting on top of
     it). Two classes beats one class + one type on the specificity table. */
  .time12-group .time12-text {
    width: 64px; flex-shrink: 0; padding: 6px 6px; border: 1px solid var(--control-line); border-radius: var(--radius);
    font-size: var(--text-base); font-family: inherit; outline: none; text-align: center;
  }
  .time12-text:focus { border-color: var(--accent); }

  /* AM/PM as a small sliding segmented control — same visual pattern as
     the page-level View/Edit .mode-toggle, just sized down. Two real
     buttons (not one button whose label swaps) so each half is directly
     clickable to that state, matching .mode-toggle's own interaction. */
  .ampm-toggle {
    position: relative; display: inline-flex; flex-shrink: 0;
    width: 52px; height: 30px; background: var(--line); border-radius: var(--radius); padding: 2px;
  }
  .ampm-toggle-thumb {
    position: absolute; top: 2px; left: 2px; width: calc(50% - 2px); height: calc(100% - 4px);
    background: var(--card); border-radius: 6px; box-shadow: 0 1px 2px rgba(0,0,0,0.18);
    transition: transform 0.2s cubic-bezier(0.4,0,0.2,1);
  }
  .ampm-toggle[data-period="PM"] .ampm-toggle-thumb { transform: translateX(100%); }
  .ampm-toggle-opt {
    position: relative; z-index: 1; flex: 1; border: none; background: transparent;
    font-size: var(--text-2xs); font-weight: var(--weight-bold); color: var(--ink-mute); cursor: pointer; border-radius: 6px;
    transition: color 0.15s ease;
  }
  .ampm-toggle[data-period="AM"] .ampm-toggle-opt[data-period="AM"],
  .ampm-toggle[data-period="PM"] .ampm-toggle-opt[data-period="PM"] { color: var(--ink); }

  /* Note field: always its own full-width grid row (see .tl-row /
     .custom-field-row's grid-row:2 placement above) — every row reserves
     the same slot for it, so it never overlaps or randomly wraps relative
     to the time control the way it could under the old flex layout. */
  .tl-note-btn {
    display: none; border: none; background: none; padding: 2px 4px; font-size: var(--text-base);
    line-height: var(--leading-none); cursor: pointer; border-radius: 6px; opacity: 0.55;
  }
  .tl-note-btn:hover { opacity: 1; background: var(--rule); }
  .tl-note-input {
    width: 100%; box-sizing: border-box; padding: 6px 8px; border: 1px solid var(--control-line); border-radius: var(--radius);
    font-size: var(--text-base); font-family: inherit; outline: none; color: #48484a;
  }
  .tl-note-input:focus { border-color: var(--accent); }
  /* Positioned like .tl-dot-row (its preceding sibling) now that rows are
     time-proportional rather than stacked in flow — top:% is set inline
     per note (row's own percent + a fixed nudge to clear the row above),
     see the tl-note-line usages in renderTimeline. */
  .tl-note-line {
    position: absolute;
    left: 34px;
    right: 0;
    padding-left: 22px;
    font-size: var(--text-sm); color: var(--ink-mute);
    white-space: normal;
  }

  /* Desktop: notes are a standard, always-visible field (like the existing
     day/event Notes fields) — no click needed. Mobile: collapsed behind
     the 🗒 toggle to save space, same as any note already typed still
     auto-opens (see toggleTimelineNote / the *NoteField setters). Only
     visibility toggles here now — the grid template already gives the
     note field its own full-width row at every breakpoint, so there's no
     flex-basis/order tug-of-war left to manage. */
  @media (min-width: 641px) {
    .tl-note-input { display: block !important; }
  }
  @media (max-width: 640px) {
    .tl-note-btn { display: inline-flex; }
  }

  /* Additional-spaces row (Logistics tab) — space picker + its own notes/
     game-plan textarea, roomy by design (no crowding fixes needed like the
     Timeline custom-field-rows since there's no time12 control competing
     for width here). */
  .event-space-row {
    display: flex; align-items: flex-start; gap: 10px; background: var(--bg);
    border-radius: 10px; padding: 10px; margin-bottom: 8px;
  }
  .event-space-row:last-child { margin-bottom: 0; }
  /* Select-or-type combobox (Combobox class), not a plain <select> — issue
     #6: picking a brand-new space used to mean leaving this page for
     Venues, adding it there, then coming back. Fixed flex-basis matches
     the old select's width so the row layout is otherwise unchanged. */
  .event-space-row .event-space-cb { flex: 0 0 180px; }
  .event-space-row textarea { flex: 1; min-width: 0; min-height: 60px; }
  .event-space-row .btn-icon { opacity: 1; flex-shrink: 0; margin-top: 6px; }

  .day-row { background: var(--bg); border-radius: 10px; padding: 10px; margin-bottom: 8px; }
  .day-row:last-child { margin-bottom: 0; }
  .day-row-times { display: flex; align-items: center; gap: 8px; }
  .day-row-times input { flex: 1; }
  .day-row-times span { color: var(--ink-mute); }
  .day-row-schedule { display: flex; flex-direction: column; gap: 6px; margin-top: 6px; }

  /* Custom field rows (name + time + day-offset + note + remove) — same
     grid-alignment reasoning as .tl-row above. Six children, each pinned
     to an explicit row/column so placement never depends on auto-flow:
     label | time | day-offset chip | note-icon | remove — all row 1 —
     then the note input on its own full-width row 2. The label column is
     minmax(90px,1fr): 1fr sizes off the row's own (identical-per-row)
     container width, not each row's own text length, so labels of very
     different lengths still line up at the same x position. The other
     four columns are fixed pixel widths, not auto — see .tl-row's own
     comment for the .time12-group auto-track ballooning bug this avoids. */
  .custom-field-row {
    display: grid; grid-template-columns: minmax(90px, 1fr) 120px 52px 28px 30px;
    align-items: center; column-gap: 10px; row-gap: 6px;
  }
  .custom-field-row > .cf-label-wrap { grid-row: 1; grid-column: 1; }
  .custom-field-row > .time12-group { grid-row: 1; grid-column: 2; }
  .custom-field-row > .tl-dayshift { grid-row: 1; grid-column: 3; }
  .custom-field-row > .tl-note-btn { grid-row: 1; grid-column: 4; }
  .custom-field-row > .btn-icon { grid-row: 1; grid-column: 5; }
  .custom-field-row > .tl-note-input { grid-row: 2; grid-column: 1 / -1; }

  /* Custom field name: plain text once set, hover (+ a beat, so a quick
     mouse pass-over doesn't flicker a pencil) reveals an edit button — see
     _cfLabelHtml/_revealCustomFieldLabelEdit in event.js. A brand new field
     (still unnamed) shows the input directly instead, via the .editing
     class already present on the wrapper. min-width keeps the label text
     itself from being squeezed to a sliver; the grid column above (not a
     flex-basis fight with siblings) is what now guarantees this column's
     actual width. */
  .cf-label-wrap { min-width: 0; display: flex; align-items: center; gap: 4px; position: relative; }
  /* Lighter than .tl-row's standard-checkpoint label (font-weight:600,
     #1d1d1f) — same "custom field reads as secondary" cue as the hollow
     dot (.custom-field-row::before) and view mode's .tl-label.cf. */
  .cf-label-text {
    font-size: var(--text-base); font-weight: var(--weight-medium); color: var(--ink-mid); overflow: hidden; text-overflow: ellipsis;
    white-space: nowrap; flex: 1; min-width: 0;
  }
  .cf-label-edit-btn {
    border: none; background: none; padding: 2px 4px; font-size: var(--text-sm); line-height: var(--leading-none);
    cursor: pointer; border-radius: 6px; opacity: 0; flex-shrink: 0;
    transition: opacity 0.15s ease 0.4s;
  }
  .cf-label-wrap:hover .cf-label-edit-btn { opacity: 0.6; }
  .cf-label-edit-btn:hover { opacity: 1 !important; background: var(--rule); }
  .cf-label-wrap.editing .cf-label-text,
  .cf-label-wrap.editing .cf-label-edit-btn { display: none; }
  .cf-label-wrap:not(.editing) .cf-label-input { display: none; }
  .cf-label-input { flex: 1; min-width: 0; }

  /* Dot + connecting-line visual the read-only preview used to show as a
     separate block, now folded directly onto the editable rows (direct
     feedback: still want the "time flows top to bottom" look, just not
     duplicated). ONE continuous line per container — standard checkpoints
     and custom fields are now merged into a single chronological order
     (see _mergedTimelineRows) and rendered as direct siblings, so a single
     .timeline-edit/.day-row-schedule::before line correctly spans all of
     them; there's no longer a separate custom-only sub-container to draw
     a second segment for. */
  /* .timeline-edit itself is already position:fixed (it's the popover
     container, see above) — only .day-row-schedule needs relative here as
     a positioning context for its own ::before line. */
  .day-row-schedule { position: relative; }
  .timeline-edit::before, .day-row-schedule::before {
    content: ''; position: absolute; left: 5px; top: 11px; bottom: 11px; width: 2px; background: var(--line);
  }
  .tl-row, .custom-field-row { position: relative; padding-left: 22px; }
  /* top is a fixed 15px (half of the 30px .ampm-toggle/.time12-text row),
     not top:50%+translateY — the row's own box now often spans TWO grid
     rows (checkpoint row + full-width note row below it), and 50% would
     center the dot against that whole taller box instead of just against
     the checkpoint/time row it actually marks.
     Standard checkpoints get a SOLID dot, custom fields a HOLLOW ring —
     now that both live in one merged, interleaved list, this is the only
     remaining cue distinguishing "fixed checkpoint" from "ad-hoc field"
     (the old cue, a separate line segment, no longer exists — see above).
     Custom rows also keep their own always-visible ✕ remove button, which
     standard rows never show, as a second reinforcing cue. */
  .tl-row::before, .custom-field-row::before {
    content: ''; position: absolute; left: 0; top: 15px; transform: translateY(-50%);
    width: 12px; height: 12px; box-sizing: border-box; border-radius: 50%;
    border: 2px solid var(--attn);
  }
  .tl-row::before { background: var(--attn); }
  .custom-field-row::before { background: var(--card); }

  /* "+ Add custom field" / "+ Add day" — indented to start at the same x
     position as the dotted rows above them (22px, matching .tl-row's own
     padding-left) instead of spanning the full card edge-to-edge, which
     read as wider than the timeline it sits under and crossed the
     connecting line. "+ Add day" gets the identical treatment (same
     weight, same alignment) rather than the heavier default button look
     it had before — it already reads as a separate action by virtue of
     sitting below the day list rather than inside any one day's rows. */
  .tl-add-field-btn { margin-left: 22px; width: calc(100% - 22px); margin-top: 8px; }
  /* These rows only exist in edit mode, so there's no list to declutter —
     unlike .contact-card/.staff-card's hover-to-reveal, the remove button
     should just always be visible here. */
  .custom-field-row .btn-icon,
  .day-row .btn-icon { opacity: 1; flex-shrink: 0; }

  /* Day-shift control for a checkpoint (issue #29) — replaces the old
     same-day/next-day chip. Two pale hover-reveal arrows (◀ previous day /
     ▶ next day) plus a small badge showing the current signed day count
     (blank when 0). Arrows stay near-invisible until the row is hovered, so
     8+ rows of controls don't clutter the timeline at rest — only surfacing
     when the user is actually looking at that row. New custom fields still
     default to a +1 shift (see addCustomTimeField/addDayCustomField's
     comment: a custom time point is more often something after midnight
     than earlier the same day) so the badge is visible without any hover
     needed on a fresh field. */
  .tl-dayshift {
    display: inline-flex; align-items: center; justify-content: center;
    gap: 1px; flex-shrink: 0;
  }
  .tl-day-arrow {
    border: none; background: none; padding: 0; width: 16px; height: 16px;
    font-size: var(--text-3xs); line-height: var(--leading-none); color: var(--ink-mute); cursor: pointer;
    border-radius: 4px; opacity: 0; transition: opacity 0.15s ease, background 0.15s ease;
  }
  .tl-row:hover .tl-day-arrow,
  .custom-field-row:hover .tl-day-arrow { opacity: 0.35; }
  .tl-day-arrow:hover { opacity: 1 !important; background: var(--rule); }
  /* :hover never fires reliably on touch — same reasoning as .tl-note-btn's
     own mobile breakpoint above; stay faintly visible instead of requiring
     a hover gesture that doesn't really exist on a phone/tablet. */
  @media (max-width: 640px) {
    .tl-day-arrow { opacity: 0.35; }
  }
  .tl-dayshift-badge {
    display: inline-block; min-width: 14px; text-align: center;
    font-size: var(--text-2xs); font-weight: var(--weight-bold); color: var(--attn);
  }
  /* Same badge in view mode, next to the time it applies to. */
  .tl-nextday-badge {
    display: inline-block;
    font-size: var(--text-2xs);
    font-weight: var(--weight-bold);
    color: var(--on-dark);
    background: var(--attn);
    border-radius: var(--radius);
    padding: 1px 6px;
    margin-left: 4px;
    vertical-align: 1px;
  }

  /* --- Combobox --- */
  .cb { position: relative; }
  .cb-input {
    width: 100%;
    padding: 8px 10px;
    border: 1px solid var(--control-line);
    border-radius: var(--radius);
    font-size: var(--text-lg);
    font-family: inherit;
    background: var(--card);
    color: var(--ink);
    outline: none;
    box-sizing: border-box;
  }
  .cb-input:focus { border-color: var(--accent); box-shadow: 0 0 0 3px rgba(0,122,255,.12); }
  .cb-list {
    display: none;
    position: absolute;
    top: calc(100% + 4px);
    left: 0; right: 0;
    background: var(--card);
    border: 1px solid var(--control-line);
    border-radius: 10px;
    box-shadow: 0 8px 24px rgba(0,0,0,.12);
    max-height: 220px;
    overflow-y: auto;
    z-index: 500;
  }
  .cb-list.open { display: block; }
  .cb-item {
    padding: 9px 12px;
    font-size: var(--text-lg);
    cursor: pointer;
    color: var(--ink);
    border-bottom: 1px solid var(--bg);
  }
  .cb-item:last-child { border-bottom: none; }
  .cb-item:hover { background: var(--bg); }
  /* #353 — the KEYBOARD cursor (ArrowUp/ArrowDown), which is a different thing
     from :hover: the mouse can be resting anywhere while somebody arrows down
     the list, so the two have to be tellable apart. Same ground as hover plus
     an accent bar down the leading edge — an inset shadow rather than a real
     border so the row does not shift by 3px as the cursor passes over it.
     Works on .cb-new too, which is children[0] and therefore navigable. */
  .cb-item.cb-active { background: var(--bg); box-shadow: inset 3px 0 0 var(--accent); }
  .cb-item mark { background: #fff3b0; color: inherit; border-radius: 2px; }
  /* "Create new" is rendered before the match list — give it its own
     background + a heavier bottom border so it doesn't read as just another
     (blue-tinted) match, which risked creating a duplicate record on a typo. */
  .cb-new {
    color: var(--accent);
    font-weight: var(--weight-semibold);
    background: #f0f6ff;
    border-bottom: 2px solid #d1e4ff;
  }
  .cb-new:hover { background: #e5f0ff; }

  /* Hide combobox inputs in view mode (same as other field inputs) */
  .field .cb { display: none; }
  body.edit-mode .field .cb { display: block; }
  .new-event-form .field .cb { display: block; }
  /* add-contact form cb always visible when form is open */
  .add-contact-form .cb { display: block; }

  /* Lead — directly interactive in view mode too (no edit-mode entry
     needed): the combobox is always shown; the plain-text value (#fv-lead)
     is superseded by it and stays hidden in both modes. Both the .cb
     wrapper AND its inner input need un-hiding — the input is separately
     caught by the base view-mode `.field input { display:none }` rule. The
     ID selector out-specifies both, no !important needed. */
  #fv-lead { display: none; }
  #cb-lead-edit .cb { display: block; }
  #cb-lead-edit .cb .cb-input { display: block; }
  #cb-lead-edit.lead-unassigned .cb-input { color: var(--danger); border-color: #ffb3ad; font-weight: var(--weight-semibold); }
  #cb-lead-edit.lead-unassigned .cb-input::placeholder { color: var(--danger); opacity: 1; }

  /* --- Drive file list --- */
  .section-label {
    font-size: var(--text-xs);
    font-weight: var(--weight-semibold);
    text-transform: uppercase;
    letter-spacing: .05em;
    color: var(--ink-mute);
    margin-bottom: 8px;
  }
  .file-item {
    display: flex;
    align-items: center;
    gap: 8px;
    padding: 6px 0;
    border-bottom: 1px solid var(--bg);
    font-size: var(--text-base);
  }
  /* --- Contact card --- */
  .contact-card-header {
    display: flex;
    align-items: flex-start;
    justify-content: space-between;
    margin-bottom: 4px;
  }
  .btn-icon {
    background: none;
    border: none;
    cursor: pointer;
    padding: 10px;
    margin: -10px;
    font-size: var(--text-lg);
    opacity: 0;
    transition: opacity .15s;
    flex-shrink: 0;
  }
  .contact-card:hover .btn-icon { opacity: 1; }
  .staff-card:hover .btn-icon { opacity: 1; }
  /* Hover doesn't exist on touch — always show the icon there instead of
     hiding an action behind a gesture the device can't perform. */
  @media (hover: none) {
    .btn-icon { opacity: 1; }
  }
  .contact-edit-form { display: flex; flex-direction: column; gap: 6px; }
  .contact-edit-row  { display: flex; gap: 8px; }
  .contact-edit-row .field-input { flex: 1; }

  /* Base style for standalone inputs outside .field wrappers */
  .field-input {
    width: 100%;
    box-sizing: border-box;
    padding: 9px 12px;
    border: 1px solid var(--control-line);
    border-radius: var(--radius);
    font-size: var(--text-lg);
    font-family: inherit;
    color: var(--ink);
    background: var(--card);
    transition: border-color 0.15s;
    text-align: left;
  }
  .field-input:focus { outline: none; border-color: var(--attn); }

  /* --- Staff cards --- */
  .staff-card {
    border: 1px solid var(--ink-mid);
    border-radius: 10px;
    padding: 10px 12px;
    margin-bottom: 8px;
  }
  .staff-card:last-child { margin-bottom: 0; }
  .staff-card-main { display: flex; align-items: flex-start; justify-content: space-between; margin-bottom: 2px; }
  .staff-name { font-size: var(--text-lg); font-weight: var(--weight-semibold); color: var(--ink); }
  .staff-lead-badge {
    display: inline-block;
    font-size: var(--text-2xs);
    font-weight: var(--weight-bold);
    letter-spacing: 0.5px;
    text-transform: uppercase;
    color: var(--on-dark);
    background: var(--attn);
    border-radius: var(--radius);
    padding: 2px 7px;
    margin-left: 6px;
    vertical-align: 2px;
  }
  .staff-role { font-size: var(--text-sm); color: var(--ink-mute); margin-top: 1px; }
  .staff-detail { font-size: var(--text-base); color: var(--ink-mute); margin-top: 4px; }
  .staff-detail a { color: var(--accent); text-decoration: none; }
  .staff-edit-row { display: flex; gap: 8px; }
  .staff-edit-row .field-input { flex: 1; }

  .file-item:last-child { border-bottom: none; }
  .file-icon { flex-shrink: 0; font-size: var(--text-2xl); }
  .file-name { flex: 1; color: var(--accent); text-decoration: none; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
  .file-name:hover { text-decoration: underline; }
  /* Only shown for files inside a subfolder (e.g. "Client Docs") — a flat
     event-root file has nothing here, so the list doesn't get noisy for
     the common case. */
  .file-folder {
    color: var(--ink-mute); font-size: var(--text-xs); flex-shrink: 0; background: var(--bg);
    padding: 2px 6px; border-radius: 6px;
  }
  .file-size { color: var(--ink-mute); font-size: var(--text-sm); flex-shrink: 0; }

  /* --- Documents collected from email (#249 follow-up) ---
     Its OWN classes rather than reusing .file-item: a Drive row is one line
     (icon, link, size) while these carry a second metadata line and up to two
     actions, and the Drive list is explicitly not to be disturbed. */
  /* TWO COLUMNS on desktop, one on a phone. minmax(0, 1fr) rather than a bare
     1fr: a bare fr track's minimum is min-content, so a long filename would
     widen the column and push the grid past the card instead of ellipsising
     inside it. */
  /* Purpose group headings on the Files tab. Quiet — they organise, they are not
     the content. */
  .att-group-head {
    display: flex; align-items: center; gap: 7px;
    margin: 14px 0 6px; font-size: 11.5px; font-weight: var(--weight-bold);
    letter-spacing: .04em; text-transform: uppercase; color: var(--ink-soft);
  }
  .att-group-head:first-child { margin-top: 4px; }
  .att-group-head-quiet { color: #a1a1a6; font-weight: var(--weight-semibold); text-transform: none; letter-spacing: 0; }
  .att-group-count {
    font-weight: var(--weight-semibold); color: var(--ink-mute); background: #f2f2f4;
    border-radius: 999px; padding: 0 6px; font-size: 10.5px; letter-spacing: 0;
  }
  .att-purpose-note { color: #6a5acd; font-style: italic; }

  .att-grid {
    display: grid;
    grid-template-columns: repeat(2, minmax(0, 1fr));
    gap: 8px;
    margin-top: 4px;
  }
  @media (max-width: 640px) {
    .att-grid { grid-template-columns: minmax(0, 1fr); }
  }
  /* THE CHIP. It has to look pressable — that is the whole point of the change
     — so it carries a border, a background and a hover/active state, unlike the
     borderless log row it replaced. */
  .att-chip {
    display: flex;
    align-items: flex-start;
    gap: 8px;
    padding: 10px;
    border: 1px solid var(--line);
    border-radius: 10px;
    background: var(--card);
    font-size: var(--text-base);
    cursor: pointer;
    transition: border-color .12s, box-shadow .12s, transform .06s;
  }
  .att-chip:hover  { border-color: var(--accent); box-shadow: 0 1px 6px rgba(0,0,0,.07); }
  .att-chip:active { transform: translateY(1px); }
  .att-chip:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }
  /* A file we hold but cannot serve to the browser. Inert, and it must LOOK
     inert — the previous markup rendered it identically to an openable row and
     only withheld the button, so the difference was invisible until the click
     that did nothing. */
  .att-chip-dead {
    cursor: default; background: var(--bg-soft); border-style: dashed; opacity: .75;
  }
  .att-chip-dead:hover { border-color: var(--line); box-shadow: none; }
  /* #260 — a file we do not hold at all, only a link to. It is LIVE (the owner
     asked for "как будто это файл"), so it keeps every pressable affordance of a
     stored chip; the dotted left edge is the only thing that says the bytes are
     somewhere else. Deliberately NOT .att-chip-dead: an inert grey chip would
     read as "we failed", when the truth is "here it is, on Drive". */
  .att-chip-link { border-left: 3px dotted var(--accent); }
  /* In-flight. The chip keeps its layout (a spinner replacing its contents
     would collapse the grid row mid-click); only the cursor and opacity move. */
  .att-busy { opacity: .55; cursor: progress; }
  /* The ↓ button. Small, square and quiet — it is the secondary gesture now
     that the chip itself opens the file. */
  .att-dl {
    flex-shrink: 0;
    width: 26px; height: 26px; line-height: var(--leading-none);
    display: flex; align-items: center; justify-content: center;
    border: 1px solid var(--control-line); border-radius: 7px;
    background: var(--card); color: #48484a; font-size: var(--text-lg); cursor: pointer;
    transition: background .12s, border-color .12s;
  }
  .att-dl:hover { background: var(--bg); border-color: var(--accent); color: var(--accent); }
  .att-dl:disabled { opacity: .5; cursor: default; }

  /* The invoice card's "look at the original" control. Quiet — the invoice id
     beside it is still the way to the full page; this only answers "is this the
     right document?" without leaving the event. */
  .inv-peek {
    margin-left: 8px; padding: 1px 7px;
    border: 1px solid var(--control-line); border-radius: 6px; background: var(--card);
    color: #48484a; font-size: var(--text-xs); font-family: inherit; cursor: pointer;
  }
  .inv-peek:hover:not(:disabled) { background: var(--bg); border-color: var(--accent); color: var(--accent); }
  .inv-peek:disabled { opacity: .55; cursor: default; }

  .att-item {
    display: flex;
    align-items: flex-start;
    gap: 8px;
    padding: 8px 0;
    border-bottom: 1px solid var(--bg);
    font-size: var(--text-base);
  }
  .att-item:last-child { border-bottom: none; }
  /* min-width:0 so a long filename ellipsises instead of pushing the actions
     off the card — a flex child defaults to min-width:auto and refuses to
     shrink below its content. */
  .att-main { flex: 1; min-width: 0; }
  .att-name {
    color: var(--ink); font-weight: var(--weight-medium);
    overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
  }
  .att-meta {
    display: flex; flex-wrap: wrap; align-items: center; gap: 5px;
    margin-top: 2px; font-size: var(--text-xs); color: var(--ink-mute);
  }
  .att-dot { color: var(--control-line); }
  .att-kind {
    font-weight: var(--weight-semibold); letter-spacing: .04em;
    background: var(--bg); border-radius: 5px; padding: 1px 5px;
  }
  /* The read/not-read badge. Green ONLY for a real extraction — everything
     else is neutral grey rather than a warning colour, because "stored but not
     parsed" is a normal, useful outcome and must not read as a failure. */
  .att-read { font-weight: var(--weight-semibold); }
  .att-read-ok   { color: #15803d; }
  .att-read-none { color: var(--ink-mute); }
  .att-read-fail { color: #b45309; }
  .att-actions { display: flex; align-items: center; gap: 8px; flex-shrink: 0; }
  .att-download { white-space: nowrap; }
  .att-drive { color: var(--accent); text-decoration: none; font-size: var(--text-sm); white-space: nowrap; }
  .att-drive:hover { text-decoration: underline; }
  .att-note  { color: var(--ink-mute); font-size: var(--text-xs); font-style: italic; }
  .att-empty { color: var(--ink-faint); font-size: var(--text-base); font-style: italic; padding: 4px 0; }
  .att-error { color: var(--danger); font-size: var(--text-base); padding: 4px 0; }
  .att-foot {
    display: flex; align-items: center; justify-content: space-between;
    gap: 10px; margin-top: 10px; padding-top: 8px;
    border-top: 1px solid var(--bg); font-size: var(--text-xs); color: var(--ink-mute);
  }
  .att-collect {
    flex-shrink: 0; background: none; border: 1px solid var(--control-line); border-radius: 7px;
    padding: 4px 9px; font-size: var(--text-xs); font-family: inherit; color: var(--ink); cursor: pointer;
  }
  .att-collect:hover:not(:disabled) { background: var(--bg); }
  .att-collect:disabled { opacity: .5; cursor: default; }

  /* --- Upload zone --- */
  .upload-zone { margin-top: 12px; }
  .upload-zone-inner {
    border: 1.5px dashed var(--control-line);
    border-radius: 10px;
    padding: 14px;
    text-align: center;
    font-size: var(--text-base);
    color: var(--ink-mute);
    cursor: pointer;
    transition: border-color .15s, background .15s;
  }
  .upload-zone-inner:hover { border-color: var(--accent); color: var(--accent); background: #f0f6ff; }
  .upload-zone-inner.drag-over { border-color: var(--accent); background: #e5f0ff; color: var(--accent); }

  .upload-item {
    display: flex;
    align-items: center;
    justify-content: space-between;
    padding: 6px 0;
    font-size: var(--text-base);
    border-bottom: 1px solid var(--bg);
  }
  .upload-item-name { color: var(--ink); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; flex: 1; }
  .upload-item-status { flex-shrink: 0; font-size: var(--text-sm); margin-left: 8px; }
  .upload-item-status.uploading { color: var(--ink-mute); }
  .upload-item-status.done      { color: #34c759; }
  .upload-item-status.error     { color: var(--danger); }

  .drive-link-row {
    display: flex;
    gap: 8px;
    margin-top: 10px;
    align-items: center;
  }
  @media (max-width: 480px) {
    .drive-link-row { flex-wrap: wrap; }
    .drive-link-row .drive-link-input { flex-basis: 100%; }
  }
  .drive-link-input {
    flex: 1;
    padding: 7px 10px;
    border: 1px solid var(--control-line);
    border-radius: var(--radius);
    font-size: var(--text-base);
    font-family: inherit;
    outline: none;
    color: var(--ink);
  }
  .drive-link-input:focus { border-color: var(--accent); }
  .drive-link-input::placeholder { color: var(--ink-faint); }

  /* --- Delete modal --- */
  .modal-overlay {
    display: none;
    position: fixed;
    inset: 0;
    background: rgba(0,0,0,0.5);
    z-index: 300;
    align-items: center;
    justify-content: center;
  }
  .modal-overlay.open { display: flex; }
  .modal {
    background: var(--card);
    border-radius: 16px;
    width: 360px;
    max-width: 92vw;
    padding: 32px 28px 24px;
    text-align: center;
  }
  .modal-icon { font-size: 40px; margin-bottom: 12px; }
  .modal-title { font-size: var(--text-3xl); font-weight: var(--weight-bold); margin-bottom: 8px; }
  .modal-body { font-size: var(--text-lg); color: var(--ink-soft); margin-bottom: 24px; line-height: 1.4; }
  .modal-actions { display: flex; gap: 10px; justify-content: center; }
  .modal-actions .btn { min-width: 110px; }
  .modal-countdown { padding-top: 4px; }
  .countdown-text { font-size: var(--text-xl); font-weight: var(--weight-semibold); margin-bottom: 14px; color: var(--ink); }
  .countdown-bar-wrap {
    height: 4px;
    background: var(--bg);
    border-radius: 2px;
    overflow: hidden;
    margin-bottom: 20px;
  }
  .countdown-bar {
    height: 100%;
    background: var(--danger);
    border-radius: 2px;
    width: 100%;
  }

  /* --- Photo lightbox (#358) ------------------------------------------------
     Three page modules injected their own copy of this — venues.js,
     equipment.js and insurance-report.js — and insurance-report.js's copy
     even carried the comment "kept in sync by hand (separate page module, no
     shared component)". Normalised, five of the six rules were identical
     across all three files, character for character; the sixth,
     `.photo-lightbox-header`, was identical in equipment/insurance-report and
     lacked four declarations in venues, because venues puts a plain name in
     the header while the other two put a chip beside a model.

     So THE INTERSECTION lives here and each page keeps only its own delta:
     equipment.js and insurance-report.js still declare the four flex
     properties locally. Nothing here conflicts with those four, so the union
     each page sees is exactly the rule it had before — proven by forcing the
     overlay open in the harness (it is display:none in every fixture, so the
     ordinary render capture covers none of it) and diffing 25 computed
     properties on all four nodes at 1280 and 390. Zero deltas.

     ⚠️ If you ever add `display`/`gap`/`align-items`/`flex-wrap` to
     `.photo-lightbox-header` HERE, venues silently becomes a flex row: a
     page-local block sits inside #page-content and therefore later in
     document order than this file, so it WINS at equal specificity — but it
     can only win on properties it actually declares. */
  .photo-lightbox {
    display: none; position: fixed; inset: 0; background: rgba(0,0,0,0.92);
    z-index: 400; flex-direction: column; align-items: center; justify-content: center;
    padding: 60px 20px 20px; box-sizing: border-box;
  }
  .photo-lightbox.open { display: flex; }
  .photo-lightbox-header {
    position: absolute; top: 18px; left: 20px; right: 60px;
    color: var(--on-dark); font-size: var(--text-xl); font-weight: var(--weight-semibold);
  }
  .photo-lightbox-close {
    position: absolute; top: 12px; right: 16px; background: rgba(255,255,255,0.15);
    color: var(--on-dark); border: none; border-radius: 50%; width: 36px; height: 36px;
    font-size: var(--text-2xl); cursor: pointer; display: flex; align-items: center; justify-content: center;
  }
  .photo-lightbox-close:hover { background: rgba(255,255,255,0.28); }
  .photo-lightbox-img { max-width: 92vw; max-height: 82vh; object-fit: contain; border-radius: var(--radius); }

  /* --- Toast --- */
  .toast {
    position: fixed;
    bottom: 80px;
    left: 50%;
    transform: translateX(-50%);
    background: var(--ink);
    color: var(--on-dark);
    padding: 10px 20px;
    border-radius: 20px;
    font-size: var(--text-base);
    opacity: 0;
    transition: opacity 0.3s;
    pointer-events: none;
    z-index: 200;
  }
  .toast.show { opacity: 1; }
  .toast.loading { background: var(--ink-mid); }
  .toast.error { background: var(--danger); }
  .toast.loading::after { content: ''; display: inline-block; width: 8px; height: 8px; margin-left: 8px; border: 2px solid rgba(255,255,255,0.3); border-top-color: var(--card); border-radius: 50%; animation: spin 0.7s linear infinite; vertical-align: middle; }
  @keyframes spin { to { transform: rotate(360deg); } }

  /* --- Empty state --- */
  .empty-state {
    text-align: center;
    padding: 60px 20px;
    color: var(--ink-soft);
  }
  .empty-state h3 { font-size: var(--text-2xl); margin-bottom: 8px; color: var(--ink); }
  .empty-state p  { font-size: var(--text-lg); }

  /* --- Task checklist --- */
  #card-tasks { margin-top: 0; }
  /* Same fix as .events-table: a fixed-column table can't reflow to a narrow
     screen without the text overlapping the card edge (Notes/Date columns
     were spilling past the card boundary on iPhone) — scroll it instead. */
  #tasks-list { overflow-x: auto; -webkit-overflow-scrolling: touch; }
  .tasks-table { width: 100%; min-width: 480px; border-collapse: collapse; font-size: var(--text-base); }
  .tasks-table th { font-size: var(--text-xs); font-weight: var(--weight-semibold); color: var(--ink-mute); text-transform: uppercase; letter-spacing: .4px; padding: 0 8px 8px; text-align: left; border-bottom: 1px solid var(--line); }
  .tasks-table td { padding: 8px 8px; border-bottom: 1px solid var(--bg); vertical-align: middle; }
  .tasks-table tr:last-child td { border-bottom: none; }
  .tasks-table td.tc-status { width: 112px; }
  .tasks-table td.tc-ball { width: 92px; }
  .tasks-table td.tc-date { width: 100px; white-space: nowrap; }
  .tasks-table td.tc-remove { width: 28px; }
  .task-status-pill { display: inline-block; font-size: var(--text-sm); font-weight: var(--weight-medium); padding: 3px 10px; border-radius: 20px; white-space: nowrap; }
  select.task-status-pill { border: none; cursor: pointer; font-size: var(--text-sm); font-weight: var(--weight-medium); padding: 4px 10px; border-radius: 20px; appearance: none; -webkit-appearance: none; }
  .task-row.task-done td { color: var(--ink-mute); }
  /* Task-name column is the 3rd td (Status, Ball, Task) — strike that one,
     not the Status/Ball pills before it. */
  .task-row.task-done td:nth-child(3) { text-decoration: line-through; }
  .task-row.task-done td:nth-child(-n+2) { text-decoration: none; }
  .task-input { width: 100%; font-size: var(--text-base); padding: 4px 8px; border: 1px solid var(--control-line); border-radius: 6px; background: var(--card); color: var(--ink); box-sizing: border-box; }
  .task-input:focus { border-color: var(--accent); outline: none; }
  .task-input[type="date"] { width: 130px; }
  .task-status-select { font-size: var(--text-sm); padding: 3px 4px; border: 1px solid var(--control-line); border-radius: 6px; }
  .btn-task-remove { background: none; border: none; color: var(--ink-faint); font-size: var(--text-2xl); cursor: pointer; padding: 0 4px; line-height: var(--leading-none); }
  .btn-task-remove:hover { color: var(--danger); }
  .tasks-progress { font-size: var(--text-sm); font-weight: var(--weight-medium); color: var(--ink-mute); }
  .tasks-progress.all-done { color: #22c55e; }
  /* #169 standard-checklist picker (_openTaskTemplatePicker). Same floating-box
     language as .tl-add-menu — body-anchored and position:fixed so the tasks
     card's overflow can't clip it — but a checkbox list with a confirm step,
     since the point is picking several items in one pass. max-height keeps all
     17 rows reachable on a short viewport without the panel outgrowing it. */
  .task-picker {
    display: flex; flex-direction: column;
    position: fixed; z-index: 50;
    background: var(--card); border-radius: 10px;
    box-shadow: 0 8px 30px rgba(0, 0, 0, 0.18);
    padding: 10px; width: 280px; max-height: 70vh;
  }
  .task-picker-head { display: flex; align-items: center; gap: 8px; padding: 2px 4px 8px; }
  .task-picker-title { font-size: var(--text-xs); font-weight: var(--weight-semibold); color: var(--ink-mute); text-transform: uppercase; letter-spacing: var(--track-caps); }
  .task-picker-all { margin-left: auto; background: none; border: none; padding: 0; font-size: var(--text-xs); color: var(--accent); cursor: pointer; }
  .task-picker-list { flex: 1; overflow-y: auto; display: flex; flex-direction: column; gap: 1px; }
  .task-picker-item {
    display: flex; align-items: center; gap: 8px;
    padding: 6px 8px; border-radius: 6px;
    font-size: var(--text-base); color: var(--ink); cursor: pointer;
  }
  .task-picker-item:hover { background: var(--bg); }
  .task-picker-name { flex: 1; }
  /* Already on the event: shown, never offered — see the comment in
     _openTaskTemplatePicker for why these aren't filtered out. */
  .task-picker-item.is-present { color: var(--ink-mute); cursor: default; }
  .task-picker-item.is-present:hover { background: none; }
  .task-picker-flag { font-size: var(--text-xs); color: var(--ink-faint); white-space: nowrap; }
  .task-picker-foot { display: flex; align-items: center; gap: 8px; padding: 10px 4px 2px; border-top: 1px solid var(--bg); margin-top: 8px; }
  .task-picker-count { font-size: var(--text-xs); color: var(--ink-mute); }
  .task-picker-foot .btn { margin-left: auto; }
  .task-picker-foot .btn:disabled { opacity: .4; cursor: default; }
  /* Status chip felt oversized on phones — the horizontal-scroll table
     (see #tasks-list note above) shows Status+Task first and nothing else
     without scrolling, so the pill dominates that initial view. Shrink it
     specifically at phone widths rather than reflowing the table (reflow
     was already tried here and reverted — see the #tasks-list comment). */
  @media (max-width: 480px) {
    .tasks-table td.tc-status { width: 96px; }
    .tasks-table td.tc-ball { width: 80px; }
    .task-status-pill, select.task-status-pill { font-size: var(--text-xs); padding: 3px 7px; }
  }

  /* --- Misc --- */
  .required-mark { color: var(--danger); margin-left: 2px; }
  .page { padding: 24px; display: none; max-width: 1180px; margin: 0 auto; box-sizing: border-box; }
  .page.active { display: block; }

  /* --- DJEP inline field hints --- */
  .djep-hint {
    display: flex; align-items: center; justify-content: space-between; gap: 8px;
    margin-top: 6px; padding: 5px 9px; background: #fff8ed; border: 1px solid #ffd580;
    border-radius: 7px; font-size: var(--text-sm); color: var(--ink-soft);
  }
  .djep-hint strong { color: var(--ink); }

  /* --- #281 the pipeline clock, under the event card's Status field ---
     Three states. Plain grey = the lead is inside its stage's window (or the
     invoice still has time to run). Amber = past it. `unknown` is its own
     state on purpose: rows stamped before the clock existed have no entry
     time, and the card says so rather than showing a number derived from
     CreatedAt, which would be a different fact wearing this one's clothes. */
  .stage-clock { margin-top: 6px; font-size: var(--text-sm); color: var(--ink-soft); }
  .stage-clock.stale { color: #8a3b1b; font-weight: var(--weight-semibold); }
  .stage-clock.unknown { color: var(--ink-mute); font-style: italic; }
  .djep-hint .btn { flex-shrink: 0; }
  .djep-hint .btn:disabled { opacity: 0.6; cursor: default; }

  /* --- AI field-suggestions (event.js, issue #54) --- redesigned per owner
     feedback: the original shipped a big stacked list of every pending
     suggestion inside #card-ai-suggestions, which read as an unreadable
     "wall". Now #card-ai-suggestions is a slim ONE-LINE action bar (a
     single Accept-all/Reject-all/Refresh control for every field, not
     per-field controls — explicitly what the owner asked for), and the
     actual suggested values show as small inline hints dropped next to
     each field's own input (see .ai-field-hint below and
     _placeInlineFieldHints() in event.js). Same soft indigo "AI
     suggestion" language emails.js already established for its own
     thread-suggestion card, so the two features still read as one system
     rather than two different visual dialects for the same idea. */
  /* Slim "AI + email" strip (no bulk Accept-all/Reject-all — owner). Just a
     small label + a refresh icon; per-field pills do the applying. */
  #card-ai-suggestions.card { background: var(--bg); border: 1px solid #ececf3; padding: 6px 12px; }
  .ai-sugg-strip { display: flex; align-items: center; justify-content: space-between; gap: 10px; }
  .ai-sugg-strip-label { font-size: var(--text-sm); font-weight: var(--weight-semibold); color: var(--ink-soft); }
  .ai-sugg-strip-loading { font-size: var(--text-sm); color: var(--ink-mute); }

  /* #205 slice 5 — the correspondence DIGEST. A third visual register, and it
     has to stay distinguishable from the two above at a glance: the hint pills
     are values to click, the lead-action card is a decision to make, this is
     prose to READ and nothing here is actionable. So: no rail, no tint, no
     accent colour competing with the teal callout — quiet paper, generous
     measure, the type doing the work.

     The candidate block is the exception, and deliberately loud about it: an
     amber rail plus a dashed edge, because everything in it comes from mail
     nobody has attached to this event yet. It may simply be about a different
     event. That has to be legible in peripheral vision, or the block reads as
     fact and quietly becomes misinformation on the card. */
  .ai-digest-block { padding: 2px 0 10px; }
  .ai-digest-block + .ai-digest-block { border-top: 1px solid #ececf0; padding-top: 12px; margin-top: 4px; }
  .ai-digest-head {
    display: flex; align-items: baseline; justify-content: space-between; gap: 10px;
    flex-wrap: wrap; margin-bottom: 6px;
  }
  .ai-digest-kind {
    font-size: var(--text-xs); font-weight: var(--weight-bold); color: var(--ink-soft);
    letter-spacing: 0.04em; text-transform: uppercase;
  }
  .ai-digest-meta { font-size: var(--text-xs); color: #a1a1aa; font-variant-numeric: tabular-nums; }
  /* max-width in ch keeps the measure readable on a wide desktop card; the
     model writes paragraphs, and a 1400px-wide line is unreadable. */
  .ai-digest-body { font-size: var(--text-base); line-height: 1.55; color: #1f2937; max-width: 78ch; }
  .ai-digest-body p { margin: 0 0 8px; }
  .ai-digest-body p:last-child { margin-bottom: 0; }
  /* The model writes markdown; a tiny subset of it is rendered (see
     _digestProseHtml). Bold carries the model's own section headings, so it has
     to read as a heading without becoming a second type scale. */
  .ai-digest-body strong { color: #111827; font-weight: 650; }

  /* SECTION HEADINGS. The model organises every digest into named sections
     ("Who is who", "Dates & access", "Audio/visual") and until 2026-08-15 they
     rendered as bold body text — same size, same colour, so nine sections read
     as one undifferentiated column and the card looked like a log file.
     A label plus a hairline running to the edge: the cheapest device that makes
     a long summary scannable, and it costs no colour the card wasn't already
     entitled to (the same pink label used for section titles on the invoice
     sheet). Nothing here becomes clickable — this is still prose to READ. */
  .ai-digest-h {
    display: flex; align-items: center; gap: 8px;
    font-size: var(--text-xs); font-weight: var(--weight-bold); text-transform: uppercase; letter-spacing: 0.06em;
    color: var(--brand); margin: 18px 0 7px;
  }
  .ai-digest-h::after {
    content: ''; flex: 1 1 auto; height: 1px;
    background: linear-gradient(90deg, #f2d6e6, rgba(242, 214, 230, 0));
  }
  .ai-digest-h:first-child { margin-top: 0; }
  .ai-digest-h strong { color: inherit; font-weight: inherit; }
  /* A heading too long to be a label is a title: keep its case, drop the
     accent, let size carry it. In practice this is the digest's opening line. */
  .ai-digest-h-long {
    text-transform: none; letter-spacing: -0.01em; font-size: 13.5px;
    color: #111827; margin-bottom: 8px;
  }
  .ai-digest-h-long::after { background: linear-gradient(90deg, #e8e8ee, rgba(232, 232, 238, 0)); }

  /* LISTS. Custom markers with a hanging indent, so a wrapped line stays
     aligned with the text above it rather than sliding under the bullet — the
     items here are long (a whole AV requirement per line). One nesting level,
     which is all the model emits; its marker is a dash, so the two levels are
     told apart by shape and not only by indent. */
  .ai-digest-body ul { margin: 0 0 10px; padding-left: 18px; }
  .ai-digest-body ul:last-child { margin-bottom: 0; }
  .ai-digest-body li { margin: 0 0 3px; }
  .ai-digest-body .ai-digest-list { list-style: none; margin: 0 0 10px; padding: 0; }
  .ai-digest-body .ai-digest-list:last-child { margin-bottom: 0; }
  .ai-digest-body .ai-digest-list > li {
    position: relative; padding-left: 15px; margin: 0 0 4px;
  }
  .ai-digest-body .ai-digest-list > li::before {
    content: ''; position: absolute; left: 3px; top: 0.6em;
    width: 4px; height: 4px; border-radius: 50%; background: var(--ink-faint);
  }
  .ai-digest-body .ai-digest-sublist { list-style: none; margin: 4px 0 0; padding: 0; }
  .ai-digest-body .ai-digest-sublist > li {
    position: relative; padding-left: 15px; margin: 0 0 3px;
  }
  .ai-digest-body .ai-digest-sublist > li::before {
    content: ''; position: absolute; left: 2px; top: 0.72em;
    width: 6px; height: 1px; border-radius: 0; background: var(--ink-faint);
  }
  .ai-digest-body code {
    font-family: var(--font-mono);
    font-size: 11.5px; background: #f4f4f6; border-radius: 3px; padding: 1px 4px;
    word-break: break-all; /* filenames and message ids are long and must not widen the card */
  }
  /* The documents the summary was written from, as things you can open. A
     footnote's weight, not a toolbar's: it sits under the prose it explains and
     must not compete with it. */
  .ai-digest-docs {
    display: flex; flex-wrap: wrap; align-items: center; gap: 6px;
    margin-top: 12px; padding-top: 10px; border-top: 1px dashed #ececf0;
  }
  .ai-digest-docs-label {
    font-size: var(--text-2xs); font-weight: var(--weight-bold); letter-spacing: 0.06em; text-transform: uppercase;
    color: #a1a1aa; margin-right: 2px;
  }
  .ai-digest-doc {
    display: inline-flex; align-items: center; gap: 5px; max-width: 100%;
    border: 1px solid var(--line); border-radius: var(--radius); background: var(--card);
    font-family: inherit; font-size: 11.5px; color: #3f3f46;
    padding: 3px 9px; cursor: pointer; text-align: left;
    overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
  }
  .ai-digest-doc:hover:not(:disabled) { background: var(--bg); border-color: var(--ink-faint); color: #111827; }
  .ai-digest-doc:disabled { opacity: 0.6; cursor: progress; }
  .ai-digest-doc .file-icon { flex: 0 0 auto; }

  .ai-digest-unconfirmed {
    border-left: 3px solid #d97706;
    border-bottom: 1px dashed #e5c07b;
    padding-left: 10px; margin-left: -2px;
  }
  .ai-digest-unconfirmed .ai-digest-kind { color: #b45309; }
  .ai-digest-foot { margin-top: 8px; font-size: var(--text-sm); }
  .ai-digest-foot a { color: #b45309; text-decoration: none; font-weight: var(--weight-semibold); }
  .ai-digest-foot a:hover { text-decoration: underline; }
  .ai-digest-note {
    margin-top: 10px; padding-top: 8px; border-top: 1px solid #f0f0f3;
    font-size: var(--text-xs); color: #a1a1aa; line-height: 1.45;
    display: flex; align-items: flex-start; justify-content: space-between; gap: 12px;
  }
  /* Quiet by design: re-reading the mail is an LLM call, so the control should
     be findable without inviting idle clicks. */
  .ai-digest-refresh {
    flex: 0 0 auto; border: 1px solid #e0e0e5; background: var(--card); border-radius: 6px;
    padding: 4px 9px; font-size: var(--text-xs); color: var(--ink-soft); cursor: pointer; white-space: nowrap;
  }
  .ai-digest-refresh:hover:not(:disabled) { background: #f6f6f8; color: #1f2937; }
  .ai-digest-refresh:disabled { opacity: 0.55; cursor: default; }

  /* #135 Phase 3B — AI LEAD-ACTION card. Deliberately distinct from the fact
     hint pills (.ai-field-hint): a solid teal-tinted callout with a colored
     left rail, so a stage-advance / next-action PROPOSAL reads as a decision
     to make, not a field value to click. */
  .litm-lead-action-card {
    border-left: 3px solid #0ea5a5;
    background-color: #f2fbfb;
    background-image: linear-gradient(115deg, rgba(14,165,165,0.06) 0%, rgba(56,189,248,0.06) 60%, rgba(14,165,165,0.06) 100%);
  }
  .litm-lead-action-head {
    display: flex; align-items: center; gap: 6px;
    font-size: var(--text-sm); font-weight: var(--weight-bold); color: #0f766e; letter-spacing: 0.02em;
    text-transform: uppercase; margin-bottom: 8px;
  }
  .litm-lead-action-row {
    display: flex; align-items: center; justify-content: space-between; gap: 12px;
    padding: 8px 0; flex-wrap: wrap;
  }
  .litm-lead-action-row + .litm-lead-action-row { border-top: 1px solid rgba(14,165,165,0.15); }
  .litm-lead-action-row-applied { opacity: 0.6; }
  .litm-lead-action-text { font-size: var(--text-base); color: #1f2937; flex: 1 1 220px; min-width: 0; }
  .litm-lead-action-text strong { color: #0f766e; }
  .litm-lead-action-date { color: #6b7280; font-weight: var(--weight-medium); }
  .litm-lead-action-why { font-size: var(--text-sm); color: #6b7280; margin-top: 3px; }
  .litm-lead-action-controls { display: flex; align-items: center; gap: 6px; flex-shrink: 0; }
  .litm-lead-action-applied { font-size: var(--text-sm); font-weight: var(--weight-semibold); color: #16a34a; }
  .ai-sugg-refresh {
    flex-shrink: 0; background: transparent; border: none; cursor: pointer;
    font-size: var(--text-xl); line-height: var(--leading-none); color: #7b5cff; padding: 2px 6px; border-radius: 6px;
    transition: transform 200ms ease, background 150ms ease;
  }
  .ai-sugg-refresh:hover { background: rgba(123,92,255,0.10); transform: rotate(90deg); }

  /* Clickable per-field AI hint pill — injected as the last child of the
     targeted .field wrapper, right after its edit-mode input, same
     placement pattern as .djep-hint above. Not gated by body.edit-mode
     (unlike .field-value/.field input) so it stays visible in both View
     and Edit. Small and tasteful (not a full-width banner) so a card with
     several suggestions doesn't turn back into the old unreadable "wall".
     Shimmering violet→orange gradient reads as "AI-generated"; an inset
     shadow gives it a slightly recessed, tappable feel instead of sitting
     flush on the page. The source-email quote is a title-attribute tooltip,
     not shown inline.
     Per-hint LIFECYCLE (issue #54 follow-up): the outer .ai-field-hint is a
     plain <span>, not a <button> — it wraps TWO sibling <button>s
     (.ai-field-hint-apply, the clickable "apply this value" surface, and
     .ai-hint-dismiss, the small "×" close control) rather than nesting one
     button inside another, which is invalid HTML (see event.js's
     _aiHintPillHtml comment). The hover/title tooltip lives on this outer
     span, which is NEVER given the `disabled` attribute (spans can't be
     disabled), so hover still works even when the inner apply button is
     disabled (mid-save, or permanently once .ai-field-hint-applied) —
     pointer-events are intentionally never fully switched off anywhere
     here. */
  .ai-field-hint {
    display: inline-flex; align-items: center; flex-wrap: wrap; gap: 2px;
    max-width: 100%; box-sizing: border-box;
    margin-top: 6px; padding: 2px 4px 2px 10px; border-radius: var(--radius);
    /* Very faint AI tint — barely brighter than the card background (owner);
       ~7% color alpha over a near-white base, so the shimmer reads as a
       subtle sheen, not a murky block. */
    background-color: #fbfaff;
    background-image: linear-gradient(115deg, rgba(123,92,255,0.07) 0%, rgba(180,92,255,0.07) 33%, rgba(255,111,174,0.07) 55%, rgba(255,138,61,0.07) 80%, rgba(123,92,255,0.07) 100%);
    background-size: 260% 260%;
    /* Slow, slow shimmer (owner). */
    animation: ai-hint-shimmer 22s ease-in-out infinite;
    box-shadow: inset 0 0 0 1px rgba(123,92,255,0.10);
    transition: transform 120ms ease, box-shadow 150ms ease, filter 150ms ease;
  }
  .ai-field-hint:not(.ai-field-hint-applied):hover,
  .ai-field-hint:not(.ai-field-hint-applied):focus-within {
    animation-duration: 11s;
    filter: brightness(1.02) saturate(1.1);
    transform: translateY(-1px);
    box-shadow: inset 0 0 0 1px rgba(123,92,255,0.20), 0 2px 6px rgba(123,92,255,0.12);
  }
  .ai-field-hint:active { transform: translateY(0) scale(0.98); filter: brightness(1.05); }

  /* Inner apply surface — carries the actual button semantics/click
     handler; reset to look like plain pill content, not a native button. */
  .ai-field-hint-apply {
    display: inline-flex; align-items: center; gap: 6px;
    border: none; background: transparent; padding: 4px 2px;
    -webkit-appearance: none; appearance: none;
    font: inherit; font-size: var(--text-sm); line-height: 1.4; text-align: left;
    color: #2b1a4a; cursor: pointer;
  }
  .ai-field-hint-apply:focus-visible { outline: none; }
  /* Disabled covers BOTH the transient "saving…" state (btnEl.disabled set
     by applyFieldSuggestionUI/applyTimelineSuggestionUI for the duration of
     the save) and the permanent 'applied' state below — same dimmed look
     reads fine for either since both mean "not clickable right now". */
  .ai-field-hint-apply:disabled { cursor: not-allowed; opacity: 0.65; }

  /* Applied entries (issue #54 per-hint lifecycle): stays visible — NEVER
     removed — but dimmed/grayscaled and inert, so a human can always see
     where an applied value came from (hover still shows the quote/who/when
     tooltip on the outer span). No shimmer (it's resolved, not "new"); no
     "×" dismiss control (nothing left to dismiss). */
  .ai-field-hint-applied {
    animation: none;
    opacity: 0.6;
    filter: grayscale(55%);
    box-shadow: inset 0 0 0 1px rgba(0,0,0,0.08);
  }
  .ai-field-hint-applied .ai-field-hint-apply { cursor: default; }
  .ai-field-hint-check { color: #16a34a; font-weight: var(--weight-bold); flex-shrink: 0; }

  /* "×" dismiss control (pending hints only) — small, quiet, only draws
     attention on hover. */
  .ai-hint-dismiss {
    flex-shrink: 0; border: none; background: transparent; cursor: pointer;
    color: #9a94ad; font-size: var(--text-xl); line-height: var(--leading-none); padding: 3px 7px;
    border-radius: 6px; -webkit-appearance: none; appearance: none;
    transition: background 120ms ease, color 120ms ease;
  }
  .ai-hint-dismiss:hover, .ai-hint-dismiss:focus-visible {
    background: rgba(220,38,38,0.10); color: #dc2626; outline: none;
  }

  .ai-field-hint-emoji { flex-shrink: 0; }
  /* Text sits on a soft translucent plate rather than directly on the
     gradient — keeps it legible at every point of the shimmer instead of
     depending on picking one "safe" text color for a moving background. */
  /* Value sits on a light plate for legibility over the soft gradient.
     Current-value/strikethrough removed — hints show only the email-derived
     info, not a diff against the field (owner). */
  .ai-field-hint-value {
    border-radius: 5px; padding: 1px 4px;
    white-space: pre-wrap; word-break: break-word;
    color: #1c1330; font-weight: var(--weight-semibold);
  }

  @keyframes ai-hint-shimmer {
    0% { background-position: 0% 50%; }
    50% { background-position: 100% 50%; }
    100% { background-position: 0% 50%; }
  }

  /* --- AI timeline hints VISIBLE STRIP (event.js, issue #54 follow-up) ---
     Primary, always-on surface for suggested checkpoint times — sits right
     under the Timeline card's own title, so it's discoverable without
     opening a checkpoint's popover first (see _renderTimelineHintsStrip()
     in event.js). Each chip reuses .ai-field-hint's exact gradient-pill
     look; this wrapper just lays several of them out in a row that wraps. */
  .tl-ai-hint-strip {
    display: flex; flex-wrap: wrap; align-items: center; gap: 8px;
    margin-bottom: 10px;
  }
  .tl-ai-hint-strip-label { font-size: var(--text-sm); font-weight: var(--weight-semibold); color: var(--ink-soft); flex-basis: 100%; }
  .tl-ai-hint-strip .ai-field-hint { margin-top: 0; }

  /* iOS Safari auto-zooms the page on focus of any text field whose
     computed font-size is under 16px — this floors every text-entry
     control app-wide so focusing a field never triggers that zoom. */
  @media (max-width: 640px) {
    input:not([type="checkbox"]):not([type="radio"]):not([type="range"]),
    select,
    textarea {
      font-size: var(--text-2xl) !important;
    }
  }

/* SPA page-transition cross-fade (shared/router.js) — softens the instant
   DOM swap between pages into a brief dip/recover instead of an abrupt
   cut, so navigating doesn't read as a full-page reload. */
#page-content {
  transition: opacity 150ms ease;
}
/* STATUS VOCABULARY — ONE mapping, global, read through `data-st`.
   Every Calm surface (dashboard chips, event header, money/tasks panels, and
   whatever Claude Design builds from the synced library) resolves a status
   colour through the same two custom properties, set by the same attribute.
   The tokens above are global; until 2026-09-02 this mapping was duplicated
   per page (`.dash-page […]` in dashboard.css, `.ev-page […]` in event.css)
   with DIFFERENT vocabularies — the dashboard knew `pipeline`/`off`, the card
   knew `inquiry`/`proposal`/`negotiation`/`lost`/`cancelled` — so a chip
   with a dashboard tone inside the card, or any chip outside both pages,
   silently rendered uncoloured. This is the union, unscoped; the per-page
   copies are gone. Which TONE a status gets is still decided in code
   (statusVocabulary.mjs); this is the only place a tone becomes a colour. */
[data-st] { --st: var(--st-idle); --stbg: var(--tint-idle); }
[data-st="ready"] { --st: var(--st-ready); --stbg: var(--tint-ready); }
[data-st="prep"] { --st: var(--st-prep); --stbg: var(--tint-prep); }
[data-st="confirmed"] { --st: var(--st-confirmed); --stbg: var(--tint-confirmed); }
[data-st="pipeline"],
[data-st="inquiry"],
[data-st="proposal"],
[data-st="negotiation"] { --st: var(--st-pipeline); --stbg: var(--tint-pipeline); }
[data-st="off"],
[data-st="lost"],
[data-st="cancelled"],
[data-st="merged"] { --st: var(--st-off); --stbg: var(--tint-idle); }
/* #1181 — invoice statuses, the same ramp read by `.dt-pill[data-st]`
   (`shared/data_table.js`): draft rides the default `--st-idle` above
   (explicit here anyway, so the vocabulary states every value it covers
   rather than relying on an unmatched attribute falling through); sent is
   "awaiting", same amber as a pipeline lead; paid is done, same green as
   ready; partially-paid is the pipeline purple — #1181's own derived "some
   but not all of the amount collected" bucket AND (#1403) the box's own
   literal `partially_paid` stored value (`invoices.js`'s `_statusBucket`
   normalises both to this one hyphenated bucket); overdue (#1403 — one of
   the box's own five canon statuses, `mail-api::mcp::invoices::STATUSES`,
   simply missing a rule here until now) reuses the reconciliation
   warning's own red, the one invoice-sheet tone that already means "this
   needs a human"; void rides the same muted tone cancelled/lost does. */
[data-st="draft"] { --st: var(--st-idle); --stbg: var(--tint-idle); }
[data-st="sent"] { --st: var(--st-prep); --stbg: var(--tint-prep); }
[data-st="paid"] { --st: var(--st-ready); --stbg: var(--tint-ready); }
[data-st="partially-paid"] { --st: var(--st-pipeline); --stbg: var(--tint-pipeline); }
[data-st="overdue"] { --st: var(--warn-ink); --stbg: var(--warn-bg); }
[data-st="void"] { --st: var(--st-off); --stbg: var(--tint-idle); }

/* TRIAGE TONE VOCABULARY — a SEPARATE attribute from `[data-st]` above, on
   purpose (#483, the mail-Calm port's README): `data-st` names an event's
   STATUS; `data-tone` names a triage judgement (act / lead / weak /
   unknown) that the Emails page and the event card's mail overlay attach
   to a thread or a reason code — a different vocabulary that happens to
   want the same colour ramp. Mapped to the SAME `--st-*`/`--tint-*` tokens
   rather than overloading `data-st` with values that are not statuses.
   Default (`unknown`, or any value not named below) rides `--st-idle` —
   nothing here is uncertain by omission, "unknown" is its own real state. */
[data-tone] { --st: var(--st-idle); --stbg: var(--tint-idle); }
[data-tone="act"] { --st: var(--st-pipeline); --stbg: var(--tint-pipeline); }
[data-tone="lead"] { --st: var(--st-ready); --stbg: var(--tint-ready); }
[data-tone="weak"] { --st: var(--st-prep); --stbg: var(--tint-prep); }
[data-tone="unknown"] { --st: var(--st-idle); --stbg: var(--tint-idle); }

#page-content.page-transitioning {
  opacity: 0.35;
}

/* Event page Edit/View mode switch — a sliding segmented control instead
   of separate Edit/Done buttons. Same dip/recover feel as the SPA page
   transition above, applied to #page-event itself while the mode swap
   happens underneath, so the flip from all-inputs to all-plain-text (or
   back) doesn't read as an abrupt cut. */
#page-event {
  transition: opacity 150ms ease;
}
#page-event.mode-transitioning {
  opacity: 0.35;
}
.mode-toggle {
  position: relative;
  display: inline-flex;
  background: var(--line);
  border-radius: 18px;
  padding: 3px;
  width: 136px;
  height: 32px;
  flex-shrink: 0;
}
.mode-toggle-thumb {
  position: absolute;
  top: 3px;
  left: 3px;
  width: calc(50% - 3px);
  height: calc(100% - 6px);
  background: var(--card);
  border-radius: 15px;
  box-shadow: 0 1px 3px rgba(0, 0, 0, 0.18);
  transition: transform 0.25s cubic-bezier(0.4, 0, 0.2, 1);
}
.mode-toggle[data-active="edit"] .mode-toggle-thumb {
  transform: translateX(100%);
}
.mode-toggle-opt {
  position: relative;
  z-index: 1;
  flex: 1;
  border: none;
  background: transparent;
  font-size: var(--text-base);
  font-weight: var(--weight-semibold);
  color: var(--ink-mute);
  cursor: pointer;
  border-radius: 15px;
  transition: color 0.2s ease;
}
.mode-toggle[data-active="view"] .mode-toggle-opt[data-mode="view"],
.mode-toggle[data-active="edit"] .mode-toggle-opt[data-mode="edit"] {
  color: var(--ink);
}

/* ===================================================================
   Read-only "linked emails" sidebar on the event page (issue #54 follow-up)
   A right-edge toggle opens a slide-in drawer; available from any event
   section. Light-theme palette, matches the rest of the app. Mobile: the
   drawer goes full-width.
   =================================================================== */
.ev-emails-toggle {
  position: fixed; right: 0; top: 50%; transform: translateY(-50%);
  z-index: 890;
  display: flex; flex-direction: column; align-items: center; gap: 4px;
  padding: 12px 6px; border: none; border-radius: 10px 0 0 10px;
  background: var(--accent); color: var(--on-dark); cursor: pointer;
  box-shadow: 0 2px 10px rgba(0,0,0,0.18);
  font-size: var(--text-sm); font-weight: var(--weight-semibold); letter-spacing: 0.02em;
}
.ev-emails-toggle:hover { background: #0069d9; }
.ev-emails-toggle-icon { font-size: var(--text-3xl); line-height: var(--leading-none); }
.ev-emails-toggle-label { writing-mode: vertical-rl; text-orientation: mixed; }

.ev-emails-backdrop {
  position: fixed; inset: 0; z-index: 900;
  background: rgba(0,0,0,0.32);
  opacity: 0; transition: opacity 0.22s ease;
}
.ev-emails-backdrop.open { opacity: 1; }

/* --- The messenger: ONE window, two panes (owner asked for the Discord shape).
   It used to be a 440px drawer pinned to the right edge, and reading a thread
   opened a SECOND overlay on top of it — so switching conversations meant
   closing the reader to reach a list that had been behind it the whole time.
   Now the list and the conversation are side by side and switching is one
   click. Centred rather than edge-pinned, because at this size a panel stuck to
   one side reads as a sidebar that grew, not as a window. */
.ev-emails-drawer {
  position: fixed; inset: 0; margin: auto; z-index: 901;
  width: min(1240px, 94vw); height: min(88vh, 940px);
  background: var(--card); border-radius: 14px; box-shadow: 0 24px 60px rgba(0,0,0,0.3);
  display: flex; flex-direction: column; overflow: hidden;
  opacity: 0; transform: scale(0.985); transition: opacity 0.18s ease, transform 0.18s ease;
}
.ev-emails-drawer.open { opacity: 1; transform: scale(1); }
/* ⚠️ THE `hidden` ATTRIBUTE DOES NOT WORK ON ITS OWN HERE, and getting this
   wrong took the whole page down for a moment.
   `hidden` is only `display: none` from the UA stylesheet, and ANY author
   `display` declaration outranks it — this rule set declares `display: flex`.
   That was harmless while the drawer was pinned to the right edge and pushed
   off-screen with translateX(100%): invisible AND out of the way. The centred
   window is not out of the way — closed, it sat dead centre at full size with
   opacity 0, swallowing every click in a 1240px-wide area of the page.
   So the attribute is given teeth explicitly, and pointer-events covers the
   ~250ms between the class coming off and `hidden` going on. */
.ev-emails-drawer[hidden] { display: none; }
.ev-emails-drawer:not(.open) { pointer-events: none; }
.ev-emails-drawer.open { pointer-events: auto; }
/* Same trap, same fix, for the backdrop — it has no author `display` today, so
   `hidden` happens to work, but it is one edit away from not working and it
   covers the entire viewport. */
.ev-emails-backdrop[hidden] { display: none; }
.ev-emails-backdrop:not(.open) { pointer-events: none; }

/* min-height:0 on the pane row, or a flex child with its own scroll container
   refuses to shrink below its content and both panes grow the window instead
   of scrolling inside it. */
.ev-em-panes { flex: 1 1 auto; min-height: 0; display: flex; }
.ev-em-panes > .ev-emails-list {
  flex: 0 0 336px; border-right: 1px solid #ececf0; background: #fbfbfd;
}
.ev-em-convo { flex: 1 1 auto; min-width: 0; display: flex; flex-direction: column; }
.ev-em-convo-head {
  flex: 0 0 auto; display: flex; align-items: center; gap: 8px;
  padding: 12px 16px; border-bottom: 1px solid #ececf0;
}
.ev-em-convo-titles { min-width: 0; }
.ev-em-convo-subject {
  font-size: 14.5px; font-weight: 650; color: var(--ink);
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
.ev-em-convo-meta {
  font-size: 11.5px; color: var(--ink-mute); margin-top: 2px;
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
/* Mobile only — on desktop both panes are always visible, so a back button
   would point at nothing. */
.ev-em-convo-back { display: none; }
.ev-em-convo-body {
  flex: 1 1 auto; min-height: 0; overflow-y: auto; overscroll-behavior: contain;
  padding: 16px 18px 20px; background: var(--bg);
  display: flex; flex-direction: column; gap: 10px;
}
.ev-em-convo-empty {
  margin: auto; color: #a1a1a6; font-size: var(--text-base); font-style: italic;
}
/* The selected row. Without it, on a window where the two panes scroll
   independently, nothing ties the conversation on the right to a row on the
   left. */
.ev-em-thread.selected { border-color: var(--accent); box-shadow: 0 0 0 1px var(--accent) inset; }
/* A row whose dismissal is in flight. Faded rather than removed, so the list
   geometry stays put while the call runs — deleting it immediately makes the
   next row jump under a cursor that is already moving toward it, which is how
   you mis-click the row after the one you meant. */
.ev-em-thread-pending { opacity: 0.45; pointer-events: none; transition: opacity 0.15s ease; }
/* #259 — just accepted, and still sitting in the Unconfirmed group because the
   list is not repainted mid-burst (moving rows under a travelling cursor is the
   thing the coalesced refresh exists to avoid). The green edge says the answer
   landed; the heading's count is re-derived at the same moment. */
.ev-em-thread-answered { box-shadow: inset 3px 0 0 var(--ok); }

.ev-emails-head {
  display: flex; align-items: center; gap: 8px;
  padding: 14px 16px; border-bottom: 1px solid var(--line); flex: 0 0 auto;
}
.ev-emails-title { font-size: var(--text-xl); font-weight: var(--weight-bold); color: var(--ink); flex: 1; }
.ev-emails-refresh, .ev-emails-close {
  border: none; background: transparent; cursor: pointer;
  color: var(--ink-soft); border-radius: 6px; line-height: var(--leading-none);
}
.ev-emails-refresh { font-size: 17px; padding: 4px 7px; }
.ev-emails-close { font-size: var(--text-5xl); padding: 0 6px; }
.ev-emails-refresh:hover, .ev-emails-close:hover { background: var(--rule); color: var(--ink); }

.ev-emails-list { flex: 1 1 auto; overflow-y: auto; padding: 8px; }
.ev-emails-empty { padding: 20px 12px; color: var(--ink-mute); font-size: var(--text-base); text-align: center; }

/* #259 — the two groups. The list used to be one undivided run of rows in which
   "unconfirmed" was a per-row badge only, so a card whose summary talked about
   23 unanswered conversations showed nothing a reader could recognise as them.
   Ordering inside each group is still pure recency (see getEventThreadsCore's
   comment); this only draws the line between the settled history and the pile
   that is waiting on a human. The heading is sticky because that pile can be
   two dozen rows long and the label has to survive the scroll. */
.ev-em-group + .ev-em-group { margin-top: 14px; }
.ev-em-group-head {
  position: sticky; top: 0; z-index: 1;
  display: flex; align-items: baseline; flex-wrap: wrap; gap: 6px;
  margin: 0 0 8px; padding: 7px 10px; border-radius: var(--radius);
  background: #f2f2f4; color: var(--ink-soft);
}
.ev-em-group-title { font-size: 11.5px; font-weight: var(--weight-bold); text-transform: uppercase; letter-spacing: 0.03em; }
.ev-em-group-count {
  margin-left: auto; font-size: var(--text-xs); font-weight: var(--weight-bold);
  background: rgba(0,0,0,0.08); border-radius: 999px; padding: 1px 7px;
}
.ev-em-group-note { flex: 1 0 100%; font-size: var(--text-xs); font-weight: var(--weight-normal); text-transform: none; letter-spacing: 0; opacity: 0.9; }
/* Amber, matching the "AI-matched" badge and the digest's own unconfirmed
   block: the same claim should look the same wherever it is made. */
.ev-em-group-unconfirmed .ev-em-group-head { background: #fff3e0; color: #b26a00; }
.ev-em-group-unconfirmed .ev-em-group-count { background: rgba(178,106,0,0.14); }

.ev-em-thread { border: 1px solid var(--line); border-radius: 10px; margin-bottom: 8px; overflow: hidden; background: var(--card); }
.ev-em-thread-head {
  display: block; width: 100%; text-align: left; cursor: pointer;
  border: none; background: transparent; padding: 10px 12px;
}
.ev-em-thread-head:hover { background: var(--bg-soft); }
.ev-em-thread-top { display: flex; align-items: center; gap: 7px; margin-bottom: 3px; }
.ev-em-thread-subject { font-size: 13.5px; font-weight: var(--weight-semibold); color: var(--ink); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.ev-em-thread-meta { font-size: 11.5px; color: var(--ink-mute); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }

.ev-em-badge { flex: 0 0 auto; font-size: var(--text-2xs); font-weight: var(--weight-bold); letter-spacing: 0.02em; padding: 1px 6px; border-radius: 999px; text-transform: uppercase; }
.ev-em-badge-linked { background: #e3f0ff; color: #0062cc; }
.ev-em-badge-matched { background: #fff3e0; color: #b26a00; }
/* #155 — post-hoc reconciler "likely match", unconfirmed (distinct from the
   classifier's own AI-matched). Purple = "review needed". */
.ev-em-badge-review { background: #efeaff; color: #5b3fd7; }
/* Ops — working correspondence AI-matched to the event (shown since
   2026-08-10). Deliberately the quietest badge of the four: it's the most
   common row and it's context, not a decision. Neutral grey so it never reads
   as the amber "AI-matched" client inquiry sitting next to it. */
.ev-em-badge-ops { background: #f2f2f4; color: var(--ink-soft); }
/* #205 slice 3 — "Possible": one of several events this email might belong to.
   The same purple family as Review because both ask the same question of a
   human, but unfilled — Review is a deterministic score against THIS event,
   Possible is only "the model listed it while choosing another". It must read
   as weaker than Review sitting directly above it in the same list. */
.ev-em-badge-possible { background: transparent; color: #7a6bbf; box-shadow: inset 0 0 0 1px #d9d2f5; }
/* #214 — "Auto-attached": a link nobody confirmed. Reads as attached (the same
   green family as the human badge, because it IS attached and drives the card),
   but outlined rather than filled, so a row worth a second look is separable at
   a glance from one a person vouched for. */
.ev-em-badge-auto { background: transparent; color: var(--ok); box-shadow: inset 0 0 0 1px #b7e6c6; }
/* --- The per-thread ⋮ menu ---------------------------------------------------
   Replaces the full-width Detach button that used to sit on every ATTACHED row.
   Owner: while scrolling a settled list, a destructive control that wide gets
   tapped by accident — and detach is not a cheap mistake, it passes rejectEvent
   and teaches the matcher that a correct pairing was wrong. */
.ev-em-menu-wrap { position: absolute; top: 6px; right: 6px; z-index: 2; }
/* The row is position:relative ONLY so the wrap above can anchor to it. */
.ev-em-thread { position: relative; }
/* ⚠️ .ev-em-thread sets overflow:hidden (it clips the expanded body's
   background into the rounded corners), which would clip the dropdown to the
   row's own height and render it as a sliver. Lifted only while a menu is
   open — a permanent overflow:visible would lose the corner clipping the row
   was given it for. */
.ev-em-thread.menu-open { overflow: visible; }
.ev-em-kebab {
  width: 26px; height: 26px; line-height: var(--leading-none); padding: 0;
  display: flex; align-items: center; justify-content: center;
  border: 1px solid transparent; border-radius: 7px;
  background: transparent; color: var(--ink-mute); font-size: var(--text-2xl); cursor: pointer;
}
.ev-em-kebab:hover { background: #f2f2f4; color: #48484a; }
.ev-em-menu-wrap.open .ev-em-kebab { background: #f2f2f4; border-color: var(--line); color: var(--ink); }
.ev-em-menu {
  display: none; position: absolute; top: 30px; right: 0;
  min-width: 232px; padding: 4px;
  background: var(--card); border: 1px solid var(--line); border-radius: 10px;
  box-shadow: 0 8px 24px rgba(0,0,0,.14);
}
.ev-em-menu-wrap.open .ev-em-menu { display: block; }
.ev-em-menu-item {
  display: block; width: 100%; text-align: left;
  padding: 8px 10px; border: 0; border-radius: 7px;
  background: none; color: var(--ink); font-size: 12.5px; cursor: pointer;
}
.ev-em-menu-item:hover:not(:disabled) { background: var(--bg); }
.ev-em-menu-item:disabled { opacity: .55; cursor: default; }
/* The one line that stops "not relevant" being read as "delete". Deliberately
   inside the menu rather than in a tooltip: the distinction between "stays
   attached" and "detached" is the whole reason both items exist. */
.ev-em-menu-note { padding: 2px 10px 6px; font-size: var(--text-xs); color: var(--ink-mute); line-height: 1.35; }
.ev-em-menu-sep { height: 1px; margin: 4px 0; background: var(--rule); }
.ev-em-menu-danger { color: var(--warn-ink); }
.ev-em-menu-danger:hover:not(:disabled) { background: #fdeeee; }
/* Room for the kebab, so a long subject ellipsises before it reaches the
   button instead of sliding underneath it. */
.ev-em-thread-head { padding-right: 34px; }
/* A thread the AI has been told not to read. Quiet, not alarming — the state is
   deliberate and normal. */
.ev-em-badge-muted { background: #f2f2f4; color: var(--ink-mute); box-shadow: inset 0 0 0 1px var(--line); }
.ev-em-thread-muted .ev-em-thread-subject { color: var(--ink-soft); }

.ev-em-review-actions { display: flex; gap: 6px; padding: 0 12px 8px 12px; }
.ev-em-review-btn { flex: 1 1 auto; font-size: var(--text-sm); font-weight: var(--weight-semibold); padding: 5px 8px; border-radius: 7px; border: 1px solid transparent; cursor: pointer; }
.ev-em-review-btn:disabled { opacity: 0.5; cursor: default; }
.ev-em-review-confirm { background: #eafaf0; color: var(--ok); border-color: #b7e6c6; }
.ev-em-review-confirm:hover:not(:disabled) { background: #d7f5e2; }
.ev-em-review-dismiss { background: var(--bg); color: var(--ink-mute); border-color: var(--line); }
.ev-em-review-dismiss:hover:not(:disabled) { background: #efeff1; }

/* #159 — a change to the EVENT reported in an email (postponed/cancelled/moved).
   Loud on purpose: easy to miss in a thread, expensive to miss in practice. */
.ev-em-change { margin: 0 12px 8px; padding: 8px 10px; border-radius: var(--radius); background: #fff6e5; border: 1px solid #f0d9a8; }
.ev-em-change-cancel { background: #ffefef; border-color: #f2c3c3; }
.ev-em-change-head { font-size: var(--text-sm); font-weight: var(--weight-bold); color: #8a5a00; }
.ev-em-change-cancel .ev-em-change-head { color: var(--warn-ink); }
/* #574 — a verbatim quoted excerpt from a real email, unbounded and possibly
   containing a single long unbroken token (a URL, a confirmation code): with
   no wrap rule of its own this relied entirely on `.ev-em-thread`'s ancestor
   `overflow: hidden` to hide the tail rather than degrading gracefully, which
   silently clips the sentence mid-word with no ellipsis and no visible sign
   anything is missing. `overflow-wrap` lets a token that cannot fit on its
   own line break instead of overflowing. */
.ev-em-change-quote { font-size: var(--text-sm); font-style: italic; color: var(--ink-soft); margin-top: 4px; overflow-wrap: break-word; }
.ev-em-change-note { font-size: var(--text-xs); color: var(--ink-mute); margin-top: 4px; }

.ev-em-thread-body { display: none; border-top: 1px solid var(--rule); padding: 8px 12px 4px; background: var(--bg-soft); }
.ev-em-thread.expanded .ev-em-thread-body { display: block; }

/* --- Bubbles: ours right, theirs left --------------------------------------
   Direction comes from the server, never from sniffing our own domain in the
   browser — see _emailMessageHtml. An unknown direction falls LEFT, because
   claiming a client's message as ours is the worse of the two errors.
   #574 second pass — `min(72%, 48rem)`, not a bare `78%`: a percentage alone
   keeps growing on a very wide panel, and the rule is a bubble never blows
   past a comfortable reading measure regardless of screen size. `width:
   fit-content` is stated explicitly rather than relied upon — `.ev-em-msg`
   is `display: flex` with default (stretch) cross-axis alignment, and cross-
   axis here is VERTICAL (row direction), so width already comes from
   content, but nothing upstream should be able to silently turn this into a
   full-width block by changing that default. */
.ev-em-msg { display: flex; }
.ev-em-msg-ours { justify-content: flex-end; }
.ev-em-bubble {
  max-width: min(72%, 48rem); width: fit-content; padding: 9px 12px 10px; border-radius: 14px;
  background: var(--card); border: 1px solid var(--line);
  border-bottom-left-radius: 5px;
}
/* Ours: tinted and mirrored, so the side is legible even in a screenshot with
   no colour — the tail corner swaps with the alignment. */
.ev-em-msg-ours .ev-em-bubble {
  background: #e7f0ff; border-color: #cfe0ff;
  border-bottom-left-radius: 14px; border-bottom-right-radius: 5px;
}
.ev-em-msg-head { display: flex; justify-content: space-between; gap: 10px; margin-bottom: 4px; }
.ev-em-msg-from { font-size: 11.5px; font-weight: 650; color: #4a4a4f; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.ev-em-msg-date { font-size: var(--text-xs); color: var(--ink-mute); flex: 0 0 auto; }
.ev-em-msg-body { font-size: var(--text-base); line-height: var(--leading-normal); color: var(--ink); white-space: pre-wrap; word-break: break-word; }
/* Who an outbound message went to. Quiet, above the text — it answers "who did
   we ask?" on a bubble with no reply under it. */
.ev-em-msg-to {
  font-size: var(--text-xs); color: var(--ink-soft); margin: 0 0 5px;
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
.ev-em-msg-to-more { color: var(--ink-mute); }
/* What the invoice DOCUMENT stated, beside our own client record. Subordinate
   on purpose — our record is the maintained truth, this is one PDF's claim. */
.iv-on-doc { font-weight: var(--weight-normal); font-size: 11.5px; color: var(--ink-mute); font-style: italic; }

/* DJEP writes PROSE into the DUE DATE line ("50% due now, balance due 9/1/26"),
   so this value is not a date and must not be laid out like one: right-aligned
   in a narrow slot it wrapped mid-phrase and read as a broken field. Let the
   label and the value share the row and wrap as text. */
.iv-meta-row { align-items: baseline; }
.iv-meta-row .iv-value { text-align: right; overflow-wrap: anywhere; hyphens: none; }
.iv-meta-row .iv-value.iv-value-prose { text-align: left; font-weight: var(--weight-medium); }

/* The unconfirmed digest block, collapsed. `details/summary` rather than a
   hand-rolled toggle: it is keyboard- and screen-reader-correct for free, and
   the open state survives a re-render of the surrounding card. */
/* THREE columns, not two: the marker is a grid item of its own. With
   `1fr auto` the ::before filled column 1 outright, so the warning label was
   auto-placed into column 2 (hard right, a screen away from the marker) and the
   message count dropped onto a second row under it. It read as two unrelated
   fragments. Marker | label | count, one row, is what it was always meant to
   be. */
.ai-digest-unconfirmed > .ai-digest-toggle {
  cursor: pointer; list-style: none;
  display: grid; grid-template-columns: auto 1fr auto; align-items: baseline; gap: 6px;
}
.ai-digest-unconfirmed > .ai-digest-toggle::-webkit-details-marker { display: none; }
.ai-digest-unconfirmed > .ai-digest-toggle::before {
  content: '▸'; grid-row: 1; grid-column: 1;
  color: #a1a1a6; font-size: var(--text-2xs);
}
.ai-digest-unconfirmed > .ai-digest-toggle > .ai-digest-kind { grid-row: 1; grid-column: 2; }
.ai-digest-unconfirmed > .ai-digest-toggle > .ai-digest-meta { grid-row: 1; grid-column: 3; }
.ai-digest-unconfirmed[open] > .ai-digest-toggle::before { content: '▾'; }
/* The model's one-line reason to open it, or not. Spans both columns on its own
   row so a long sentence never squeezes the message count. */
.ai-digest-doubt {
  grid-column: 1 / -1; grid-row: 2;
  font-size: 11.5px; font-weight: var(--weight-normal); color: var(--ink-soft); font-style: italic;
  line-height: 1.4; margin-top: 3px;
}

/* #259 — the unconfirmed digest block's way into the mail it was written from.
   A button, not a link, because it no longer navigates anywhere: it opens the
   messenger on this card and selects the source conversation. */
.ai-digest-open-mail {
  border: 1px solid #d9d2f5; border-radius: var(--radius); background: var(--card);
  color: #6a5acd; font-size: var(--text-sm); font-family: inherit;
  padding: 4px 10px; cursor: pointer;
}
.ai-digest-open-mail:hover { background: #f6f4ff; border-color: #6a5acd; }
/* What the message carried. A LISTING, not controls — see _msgAttachmentsHtml
   for why these are deliberately not buttons. */
.ev-em-msg-atts { display: flex; flex-wrap: wrap; gap: 6px; margin-top: 8px; }
.ev-em-msg-att {
  display: inline-flex; align-items: center; gap: 5px;
  max-width: 100%; padding: 3px 8px;
  border: 1px solid rgba(0,0,0,0.10); border-radius: var(--radius);
  background: rgba(255,255,255,0.75);
  font-size: 11.5px; color: var(--ink-mid);
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
.ev-em-msg-att-icon { flex: 0 0 auto; }
.ev-em-msg-att-size { color: var(--ink-mute); font-variant-numeric: tabular-nums; }

@media (max-width: 560px) {
  /* Full screen on a phone, with the ✕ guaranteed to stay reachable — 100vh is
     the height with the browser chrome RETRACTED, so it would push the header
     under the URL bar. Same correction as the document preview. */
  .ev-em-bubble { max-width: 88%; }
}

@media (max-width: 640px) {
  /* Full screen on a phone, and the two panes become ONE column: the list, or
     the conversation with a back arrow. Selecting a thread flips between them
     (JS toggles .convo-open) — without this both panes would try to share a
     390px screen and neither would be usable. */
  .ev-emails-drawer {
    width: 100vw; max-width: 100vw; height: 100vh; height: 100dvh;
    border-radius: 0; inset: 0; margin: 0;
  }
  .ev-em-panes > .ev-emails-list { flex: 1 1 auto; border-right: none; }
  .ev-em-convo { display: none; }
  .ev-emails-drawer.convo-open .ev-emails-list { display: none; }
  .ev-emails-drawer.convo-open .ev-em-convo { display: flex; }
  .ev-em-convo-back {
    display: flex; align-items: center; justify-content: center;
    flex: 0 0 auto; width: 36px; height: 36px; padding: 0;
    border: 1px solid var(--line); border-radius: 9px; background: var(--card);
    color: var(--ink); font-size: 22px; line-height: var(--leading-none); cursor: pointer;
  }
  .ev-emails-toggle { padding: 10px 5px; font-size: var(--text-xs); }
  /* #259 — the group heading is sticky, and on a 390px screen the three-line
     version (wrapped title, count, note) measured 98px of a 726px list: an
     eighth of the reading area permanently occupied by a caption. The title and
     the number are the part that carries the meaning; the sentence under them
     is what goes. */
  .ev-em-group-note { display: none; }
}

  /* #245 — the readiness chip. Sits last in the summary bar so it reads as a
     verdict on everything left of it. Three states plus a contested one; the
     contested state is amber rather than red on purpose — new mail proposing a
     change is normal traffic, not a failure, and a red chip on every incoming
     email would be trained away within a week. */
  .readiness-chip { font-variant-numeric: tabular-nums; cursor: help; }
  .readiness-detail { opacity: 0.55; font-size: 0.92em; }
  .readiness-ready     { color: #15803d; }
  .readiness-partial   { color: #a16207; }
  .readiness-thin      { color: #b91c1c; }
  .readiness-contested { color: #b45309; font-weight: var(--weight-semibold); }

  /* Quick look at a collected document, over the card rather than in a new tab.
     Reuses .modal-overlay's backdrop and open/closed behaviour; the sheet is its
     own size because .modal is built for a small confirmation dialog and a floor
     plan in a 400px box is not a preview, it is a thumbnail. */
  /* BORDERLESS, the way Drive does it (owner, 2026-08-14): the toolbar is stuck
     to the top of the SCREEN, the document floats in the middle at whatever size
     it is, there is no frame around it, and everything behind is simply dimmed.
     The previous version was a white card with rounded corners and a shadow —
     a document inside a document, which is exactly the border he means.
     The sheet still exists as a flex column, but it is now full-bleed and
     TRANSPARENT: it lays out the bar and the content, and contributes no
     surface of its own. */
  .attachment-preview { background: rgba(20, 20, 22, 0.88); }
  .attachment-preview .ap-sheet {
    background: transparent; border-radius: 0; width: 100vw; height: 100vh;
    display: flex; flex-direction: column; overflow: hidden; box-shadow: none;
  }
  /* The toolbar. Dark rather than white, because it now sits ON the dimmed
     backdrop rather than on a white card — a white bar would reintroduce the
     edge the change removes. */
  .attachment-preview .ap-head {
    display: flex; align-items: center; justify-content: space-between; gap: 12px;
    padding: 10px 14px 10px 18px; flex: 0 0 auto;
    background: rgba(32, 33, 36, 0.96); color: #e8eaed;
    border-bottom: 1px solid rgba(255, 255, 255, 0.08);
  }
  .attachment-preview .ap-name {
    font-size: var(--text-base); font-weight: var(--weight-semibold); color: #e8eaed;
    overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
  }
  /* The controls have to be legible on the dark bar; .btn-ghost and .btn-icon
     are both styled for a light surface. Scoped to this overlay only. */
  .attachment-preview .ap-actions .btn,
  .attachment-preview .ap-actions .btn-icon {
    color: #e8eaed; background: transparent; border-color: rgba(255,255,255,0.22);
  }
  .attachment-preview .ap-actions .btn:hover,
  .attachment-preview .ap-actions .btn-icon:hover {
    background: rgba(255,255,255,0.12); border-color: rgba(255,255,255,0.4); color: var(--on-dark);
  }
  /* IMAGES get their own renderer — see the markup comment. Centred at natural
     size, never upscaled, never wider or taller than the space left under the
     bar. `object-fit: contain` plus max-* is what keeps a panorama and a
     portrait both borderless. */
  .attachment-preview .ap-audio,
  .attachment-preview .ap-video { display: none; }
  /* Audio is a control strip, not a document — it gets a comfortable width and
     sits centred rather than filling the stage. */
  .attachment-preview.ap-is-audio .ap-stage iframe,
  .attachment-preview.ap-is-audio .ap-img { display: none; }
  .attachment-preview.ap-is-audio .ap-audio { display: block; width: min(560px, 90%); }
  .attachment-preview.ap-is-video .ap-stage iframe,
  .attachment-preview.ap-is-video .ap-img { display: none; }
  .attachment-preview.ap-is-video .ap-video {
    display: block; max-width: 100%; max-height: 100%;
  }
  .attachment-preview .ap-img { display: none; }
  .attachment-preview.ap-is-image .ap-stage iframe { display: none; }
  .attachment-preview.ap-is-image .ap-img {
    display: block; max-width: 100%; max-height: 100%; object-fit: contain;
  }
  .attachment-preview .ap-actions { display: flex; align-items: center; gap: 6px; flex: 0 0 auto; }
  /* The iframe carries the whole remaining height — a percentage height would
     collapse to zero inside a flex column without this. */
  /* `contain` stops a scroll that reaches the document's end from continuing
     into the page behind — the reason a two-page timeline used to drag the
     event card along with it. The JS also locks body overflow; this handles the
     trackpad-momentum case the lock alone does not. */
  /* THE STAGE — the dim area the document floats in, and the thing you click to
     close. Without it the document filled the viewport and there was nowhere to
     click at all. */
  .attachment-preview .ap-stage {
    flex: 1 1 auto; min-height: 0;
    display: flex; align-items: center; justify-content: center;
    padding: 24px;
  }
  /* The document itself: CONSTRAINED, not full-bleed. The width cap is what
     leaves dim margin on either side — that margin is the close affordance and
     it is also what stops a floor plan being stretched across a 1920px screen.
     `transparent` background because Chrome's PDF viewer paints its own; a grey
     of ours would show only as a seam. */
  .attachment-preview .ap-stage iframe {
    width: min(1080px, 100%); height: 100%; border: 0; background: transparent;
    overscroll-behavior: contain;
  }
  .attachment-preview .ap-sheet { overscroll-behavior: contain; }

  @media (max-width: 560px) {
    /* Desktop is already full-bleed since the borderless change, so this block
       now only carries what is genuinely phone-specific: the dvh correction and
       a bigger close target. */
    .attachment-preview .ap-sheet { width: 100vw; height: 100vh; border-radius: 0; }
    /* dvh, where supported, is what actually delivers that: 100vh on mobile
       Safari/Chrome is the height with the browser chrome RETRACTED, so a
       100vh sheet is taller than the visible viewport and the bottom of it —
       and on a short page the header — sits under the URL bar. Declared second
       so it simply overrides on browsers that understand it. */
    .attachment-preview .ap-sheet { height: 100dvh; }
    /* A light sticky header needs its own text/border colours restored —
     * without this, `.ap-name`/`.ap-actions` kept the DARK-toolbar
     * `#e8eaed` text (line 4353-4365 above) against this now-light
     * background: 1.21:1, invisible (mobile-journeys contrast check,
     * #1204 — the first journey to ever script opening a real attachment
     * preview, which is why this was never caught before). */
    .attachment-preview .ap-head { position: sticky; top: 0; background: var(--card); color: var(--ink); z-index: 1; }
    .attachment-preview .ap-name { color: var(--ink); }
    .attachment-preview .ap-actions .btn,
    .attachment-preview .ap-actions .btn-icon { color: var(--ink-soft); border-color: var(--control-line); }
    .attachment-preview .ap-actions .btn:hover,
    .attachment-preview .ap-actions .btn-icon:hover { background: var(--bg); border-color: var(--control-line); color: var(--ink); }
    /* A bigger tap target for the one control that gets a phone user out. */
    .attachment-preview .ap-close { min-width: 40px; min-height: 40px; font-size: 17px; }
    .attachment-preview .ap-name { max-width: 45vw; }
  }

/* --- Sidebar (Sidebar.jsx / app/index.html's #app-sidebar nav) — added
   2026-07-16, replaces the per-page top-button row. Originally inlined in
   app/index.html's <style> (the only page that used it); moved here so the
   component is stylable from outside that one page (design-sync coverage,
   #372 follow-up). No rule changed — verbatim extraction. */
.app-sidebar {
  width: 208px; flex-shrink: 0; background: var(--bg-soft); border-right: 1px solid var(--line);
  padding: 12px; overflow-y: auto;
  display: flex; flex-direction: column;
}
/* Section dividers read by weight and a top rule, not shouting caps —
   Character (#444/#406): sentence case, no uppercase micro-labels. */
.sidebar-section {
  font-size: var(--text-xs); letter-spacing: normal; color: var(--ink-mute);
  padding: 16px 10px 6px; margin-top: 4px; font-weight: var(--weight-bold);
  border-top: 1px solid var(--line);
}
.sidebar-section:first-child { margin-top: 0; padding-top: 2px; border-top: none; }
/* Each link is a real button-weight row (icon + label, rounded fill on
   hover/active) rather than a thin text line — the icon alone does most
   of the work of reading as "a section of the app", not a form field. */
.sidebar-link {
  display: flex; align-items: center; gap: 10px;
  padding: 9px 10px; margin: 2px 0; border-radius: var(--radius);
  font-size: var(--text-lg); font-weight: var(--weight-medium); color: var(--ink-mid); text-decoration: none;
  transition: background 0.12s, color 0.12s;
}
.sidebar-link-icon { font-size: var(--text-xl); width: 18px; text-align: center; flex-shrink: 0; }
.sidebar-link:hover { background: var(--bg-soft); }
.sidebar-link.active { background: var(--bg-soft); color: var(--ink); font-weight: var(--weight-semibold); }
/* Desktop collapse-to-icons (GitHub issue #3, point 2) — default open
   per explicit direction, state remembered in localStorage so it stays
   collapsed/open across navigation and reloads. Scoped to a min-width
   query (not just "not mobile") so its width rule can never accidentally
   win a specificity fight against the mobile off-canvas drawer's own
   width rule below if the class is still set while the viewport is
   narrow. */
.sidebar-collapse-btn {
  display: flex; align-items: center; justify-content: center; gap: 10px;
  width: 100%; margin-top: auto; padding: 9px 10px; border: none;
  border-top: 1px solid var(--line); background: none; color: var(--ink-mute);
  font-size: var(--text-base); cursor: pointer; font-family: inherit;
}
.sidebar-collapse-btn:hover { background: #eeeeef; color: var(--ink-mid); }
@media (min-width: 641px) {
  .app-sidebar.collapsed { width: 60px; }
  .app-sidebar.collapsed .sidebar-section,
  .app-sidebar.collapsed .sidebar-link-label { display: none; }
  .app-sidebar.collapsed .sidebar-link { justify-content: center; }
}
/* Mobile: off-canvas drawer, hidden by default, toggled by the hamburger
   button (app/index.html), positioned to start right below the header.
   The hamburger button itself and .sidebar-backdrop stay in app/index.html
   — they are page-shell/mobile-nav concerns, not this component. */
@media (max-width: 640px) {
  .app-sidebar {
    /* Mobile drawer: fixed overlay above ALL page content. z-index must
       beat the event card's sticky header (#page-event .event-header,
       z-index:50) or that header bleeds THROUGH the open drawer. */
    display: none; position: fixed; top: 0; left: 0; z-index: 1000;
    width: 78%; max-width: 280px; height: 100%; overflow-y: auto; background: var(--card);
    border-right: none; box-shadow: 2px 0 16px rgba(0,0,0,0.18);
  }
  body.sidebar-open .app-sidebar { display: block; }
}

/* ==========================================================================
   HIT AREA ≠ VISUAL SIZE (#1137)
   ==========================================================================
   A control is drawn at its token height (--control-h-*: 34px on a phone)
   and TAPPED through an invisible ::after that grows it to --hit-min (44px
   on a phone — iOS's minimum target; 0 on a desktop, where this adds
   nothing). The extension is centred and only ever grows a box that is
   smaller than --hit-min; a box already that big gets inset 0. Percentages
   in top/bottom are of the control's own height, left/right of its width,
   because the control is the ::after's containing block.
   Zero specificity: a control that is itself absolute/sticky keeps that
   (it is still a containing block), and none of this outranks a component.
   `.hit` is the opt-in for anything else (a link, a label).
   ========================================================================== */
:where(button, [role="button"], .btn, .hit) { position: relative; }
:where(button, [role="button"], .btn, .hit)::after {
  content: '';
  position: absolute;
  top: min(0px, calc((100% - var(--hit-min)) / 2));
  bottom: min(0px, calc((100% - var(--hit-min)) / 2));
  left: min(0px, calc((100% - var(--hit-min)) / 2));
  right: min(0px, calc((100% - var(--hit-min)) / 2));
}

/* Text fields: the field IS its target (it holds 16px text on a touch
   screen), so it is drawn at --field-min-h (auto on a desktop).
   :not(.iv-filter-select) — /invoices' filter dropdowns are TAPPED-open,
   never typed into, and drawing one at 44px read as an oversized control
   (owner, real iPhone, #1137); their own rule below gives them a real
   44px target a different way. */
#page-content :is(select:not(.iv-filter-select), input:not([type]), input[type="text"], input[type="search"], input[type="email"],
  input[type="number"], input[type="tel"], input[type="url"], input[type="date"], input[type="time"]) {
  min-height: var(--field-min-h);
}

/* A touch screen at ANY width (#1137 journeys: a phone in landscape is 852px
   wide and gets the desktop layout): iOS Safari zooms into a field whose
   text is under 16px and leaves the page zoomed. --fs-field is that floor
   (tokens.json `coarse`); a mouse never matches, so desktop is untouched. */
@media (pointer: coarse) {
  input:not([type="checkbox"]):not([type="radio"]):not([type="range"]):not([type="color"]):not([type="file"]):not([type="button"]):not([type="submit"]):not([type="reset"]):not([type="image"]),
  select,
  textarea { font-size: max(var(--fs-field), 1em) !important; }
}

/* ==========================================================================
   ≤767px — LEGACY SCREENS' PHONE LAYOUT (#1088/#1137). Layout only; every
   size here is a token.
   ========================================================================== */
@media (max-width: 767px) {
  /* A segmented control WRAPS instead of scrolling: /history's five range
     presets ran off the screen edge with nothing to say the row moved. */
  .view-toggle { flex-wrap: wrap; overflow: visible; }
  .view-toggle button { flex: 1 1 auto; }
  /* The toast sits above the bottom tab bar, inside the screen's width.
     Over an open sheet common.js's _placeToast moves it above the sheet's
     footer (an inline bottom). Centred by margin, not translateX(-50%): with
     left:50% it could only be half the screen wide. */
  .toast {
    bottom: calc(var(--tnd-tabbar-h, 0px) + var(--space-6));
    left: var(--page-pad);
    right: var(--page-pad);
    width: fit-content;
    max-width: calc(100vw - 2 * var(--page-pad));
    margin: 0 auto;
    transform: none;
    box-sizing: border-box;
    text-align: center;
  }
  /* /events' "Show past events" (#1134): the whole label is the target. */
  #page-content .filter-toggle { padding: 0 var(--space-2); }
  /* The invoice editor's Bill To / Details columns: two `1fr` tracks grew to
     their widest field in edit mode and pushed the page to 550px. */
  .iv-meta-grid { grid-template-columns: minmax(0, 1fr); }
  /* The sheet's side padding is a printed page's margin; on a phone it left
     the line items 252px wide. */
  .invoice-sheet { padding: var(--space-10) var(--page-pad); }
  .iv-table th, .iv-table td { padding-left: var(--space-3); padding-right: var(--space-3); }
}
