:root{--navy:#032b49;--navy-2:#005080;--red:#e11d2a;--ink:#16222f;--muted:#5f7288;--line:#e4eaf1;--bg:#eaf0f6;--green:#0f9d58;--amber:#e08600}
/* --navy is a SURFACE token as often as an ink one (.ghead, .gbtn, .bigbtn, .rbtn.patrol, .cp-btn,
   .gsave, .gib-f.on, .g-sheet .seg button.on all paint a navy BACKGROUND with white text on it), so
   the dark theme can never redefine --navy itself. --navy-ink is the same colour used the other way
   round: navy AS TEXT, on a card or on the page. Dark redefines only this one, which is why the
   news eyebrow, the Copilot heading and the break title stop being #032b49-on-#1a2231 (1.10:1).
   Reach for --navy-ink for any new `color:`; keep --navy for any new `background:`. */
:root{--navy-ink:#032b49}
*{box-sizing:border-box;-webkit-tap-highlight-color:transparent}
body{margin:0;font-family:-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,Helvetica,Arial,sans-serif;color:var(--ink);background:var(--bg)}
.hidden{display:none!important}
input,select,textarea,button{font-family:inherit;font-size:16px}

/* --- Generated identity discs (avatar.js) ------------------------------------------------------
   avSvg() emits `<span class="pav">` with a gradient <svg> disc, the initials, and an optional
   photo on top. The officer app does not load styles.css, so the primitive needs its own home here.

   It is declared THIS EARLY on purpose. Every per-context avatar class further down the file
   (.gib-av, .gmem-av, .gvn-av, .g-ment-av, .gdir-av, .ghg-av/.gm-av/.gpf-av2) already carries a
   width/height/font-size tuned against a real phone, and each of those is a single class — the same
   specificity as `.pav`. Declared first, `.pav` supplies only what is MISSING (the stacking context
   and the three layers) and every measured geometry below still wins. Move this block to the bottom
   and it silently reskins eight contexts at once.

   ⚠ `av-row` is BOTH the 36px step of avatar.js's size ladder and an unrelated availability-row rule
   at the top of this file. The ladder steps below are written `.pav.av-*` so they cannot be reached
   by accident — but never pass `cls:'av-row'` from guard.js, because `.av-row`'s own padding,
   background and box-shadow would land on the disc. Pass `size:36` and the context's own class. */
.pav{position:relative;display:inline-flex;align-items:center;justify-content:center;flex:none;
  border-radius:50%;overflow:hidden;color:#fff;font-family:var(--font-display,inherit);font-weight:800;
  /* --avc is the palette hex avSvg writes inline. The <svg> covers the disc completely, so this is
     only ever seen if the SVG failed to paint — hence the house gradient behind it. */
  background:var(--avc,var(--grad-primary,var(--navy)))}
.pav-g{position:absolute;inset:0;width:100%;height:100%}
.pav-wm{fill:#fff;opacity:.10}
.pav-i{position:relative;z-index:1;line-height:1}
.pav img{position:absolute;inset:0;width:100%;height:100%;object-fit:cover;z-index:2;border-radius:inherit}
/* The size ladder, mirroring avatar.js's AV_SIZES. Used only where a context has no sized class of
   its own; everywhere else guard.js passes `size:` and the existing class still draws the box. */
.pav.av-xs{width:18px;height:18px;font-size:9px}
.pav.av-sm{width:22px;height:22px;font-size:9.5px}
.pav.av-md{width:26px;height:26px;font-size:11px}
.pav.av-row{width:36px;height:36px;font-size:14px}
.pav.av-hd{width:48px;height:48px;font-size:18px}
.pav.av-lg{width:76px;height:76px;font-size:27px}
.pav.av-mem{width:28px;height:28px;font-size:11.5px}
/* On the dark surface a mid-tone disc and the card behind it sit close enough that the circle can
   lose its edge; a hairline INSIDE the disc restores it. It rides on ::after rather than on `.pav`
   itself for two reasons: box-shadow is not additive, so on `.pav` it would wipe out a container's
   own shadow (.gpf-av2 has one), and ::after draws ABOVE the photo layer instead of under it. */
[data-theme="dark"] .pav::after{content:"";position:absolute;inset:0;border-radius:inherit;z-index:3;
  pointer-events:none;box-shadow:inset 0 0 0 1px rgba(255,255,255,.10)}

/* Login */
.glogin{min-height:100vh;display:flex;align-items:center;justify-content:center;background:linear-gradient(160deg,var(--navy),var(--navy-2));padding:24px}
.glogin form{background:#fff;border-radius:18px;padding:28px 22px;width:100%;max-width:360px;display:flex;flex-direction:column;gap:14px;box-shadow:0 12px 40px rgba(0,0,0,.3)}
.gbrand{text-align:center;line-height:1;margin-bottom:4px}
.gbrand .u{font-family:var(--font-display,inherit);font-weight:800;font-size:26px;color:var(--heading,#0b2036);letter-spacing:.5px}
.gbrand .s{font-family:var(--font-display,inherit);font-weight:600;font-size:10px;color:var(--blue,#0a6bb0);letter-spacing:3px}
.gbrand .t{font-size:12px;color:var(--muted);margin-top:6px;letter-spacing:1px}
.glogin input{padding:14px;border:1px solid var(--line);border-radius:12px}
.glogin button{padding:15px;background:var(--grad-primary,var(--navy));color:#fff;border:none;border-radius:12px;font-weight:700;font-size:16px;box-shadow:0 6px 16px rgba(0,80,128,.28)}
.gerror{color:var(--red);font-size:13px;text-align:center;min-height:16px}
/* App shell */
.gapp{max-width:480px;margin:0 auto;min-height:100vh;background:var(--bg);display:flex;flex-direction:column}
.ghead{position:sticky;top:0;z-index:5;background:var(--navy);color:#fff;padding:14px 18px;display:flex;align-items:center;justify-content:space-between}
.ghead .u{font-family:var(--font-display,inherit);font-weight:800;font-size:16px;letter-spacing:.5px}.ghead .u span{color:#4aa3dd;font-size:9px;letter-spacing:2.5px;display:block;font-weight:600}
.ghead .who{font-size:13px;color:#9dbdd6;text-align:right}
.ghead .who a{color:#9dbdd6;font-size:11px;text-decoration:underline}
.gmain{flex:1;min-height:0;display:flex;flex-direction:column;padding:16px;padding-bottom:calc(64px + env(safe-area-inset-bottom,0px))}
.gview{display:none}.gview.on{display:block}
/* Duty */
.duty-card{background:#fff;border-radius:16px;padding:22px;text-align:center;box-shadow:0 1px 3px rgba(20,30,50,.08)}
.duty-status{font-size:13px;color:var(--muted);text-transform:uppercase;letter-spacing:1px}
.duty-state{font-size:26px;font-weight:800;margin:6px 0}
.duty-state.on{color:var(--green)}.duty-state.off{color:var(--muted)}
.duty-since{font-size:14px;color:var(--muted)}
.bigbtn{display:block;width:100%;border:none;border-radius:14px;padding:18px;font-size:18px;font-weight:700;color:#fff;margin-top:16px}
.bigbtn.in{background:var(--green)}.bigbtn.out{background:var(--red)}
.section-title{font-size:13px;color:var(--muted);text-transform:uppercase;letter-spacing:1px;margin:22px 4px 8px;font-weight:700}
.shift-card{background:#fff;border-radius:14px;padding:14px 16px;margin-bottom:10px;box-shadow:0 1px 3px rgba(20,30,50,.06);border-left:4px solid var(--navy)}
.shift-card .site{font-weight:700;font-size:15px}
.shift-card .meta{font-size:13px;color:var(--muted);margin-top:3px}
.shift-card .post{display:inline-block;background:#eef1f8;color:var(--navy-2);font-size:12px;padding:2px 8px;border-radius:20px;margin-top:6px}
.empty{text-align:center;color:var(--muted);padding:30px 10px;font-size:14px}
.gsub{display:flex;gap:6px;margin:4px 0 10px}
.gsub button{flex:1;background:#fff;border:1px solid #e3e8ef;border-radius:10px;padding:9px 4px;font-size:13px;font-weight:700;color:var(--muted)}
.gsub button.on{background:var(--navy);color:#fff;border-color:var(--navy)}
.shift-card.open{border-left-color:var(--red)}
.gbtn{background:var(--navy);color:#fff;border:0;border-radius:10px;padding:10px 14px;font-size:14px;font-weight:700}
.gbtn.sm{padding:7px 12px;font-size:13px}
.gbtn.ghost{background:#eef1f8;color:var(--navy)}
.gbtn.wfull{display:block;width:100%;margin-top:10px}
.confirm-row{display:flex;gap:8px;margin-top:10px}
.confirm-row .gbtn{flex:1}
.st{display:inline-block;font-size:12px;font-weight:700;padding:2px 9px;border-radius:12px;margin-top:8px}
.st.ok{background:#d8f3df;color:#1a7a3e}.st.no{background:#fde0e0;color:#b3261e}
.tasks{margin:10px 0 2px;border-top:1px solid #eef1f6;padding-top:8px}
.tk{display:flex;align-items:center;gap:8px;font-size:14px;padding:5px 0}
.tk.done{color:var(--muted);text-decoration:line-through}
.tk input{width:18px;height:18px}
.swap-panel{margin-top:10px;display:flex;flex-direction:column;gap:8px;background:#f5f7fb;padding:10px;border-radius:10px}
.swap-panel select,.swap-panel input{padding:9px;border:1px solid #e3e8ef;border-radius:8px;font-size:14px}
.av-row{display:flex;justify-content:space-between;align-items:center;gap:10px;background:#fff;border-radius:10px;padding:10px 12px;margin-bottom:8px;font-size:13px;box-shadow:0 1px 3px rgba(20,30,50,.05)}
.av-add{margin-top:16px;display:flex;flex-direction:column;gap:8px}
.av-add select,.av-add input{padding:10px;border:1px solid #e3e8ef;border-radius:9px;font-size:15px}
.ck2{display:flex;align-items:center;gap:8px;font-size:14px}
.ck2 input{width:18px;height:18px}
/* Was `display:flex` from when this element WAS the two-up row. It is now a wrapper: an inner .grow2
   holds From/To and a .gtm-hint line sits under them, so a flex box here would put the hint beside
   the fields. gAvail's show/hide writes `display:block` inline, which outranks this either way — the
   rule is corrected rather than deleted so the stylesheet does not describe a layout that is gone. */
#av-times{display:block}
.qr-scan{position:fixed;inset:0;background:#000;z-index:2000;display:flex;align-items:center;justify-content:center;overflow:hidden}
.qr-scan video{width:100%;height:100%;object-fit:cover}
.qr-frame{position:absolute;width:66vw;height:66vw;max-width:300px;max-height:300px;border:3px solid #fff;border-radius:18px;box-shadow:0 0 0 100vmax rgba(0,0,0,.5)}
.qr-hint{position:absolute;bottom:118px;left:0;right:0;text-align:center;color:#fff;font-size:15px;font-weight:600;text-shadow:0 1px 3px #000;padding:0 20px}
.qr-close{position:absolute;bottom:38px;left:50%;transform:translateX(-50%);background:#fff;color:#0b2036;border:0;border-radius:24px;padding:12px 30px;font-size:15px;font-weight:700}
.po-panel{margin-top:8px}
.po-orders{white-space:pre-wrap;font-size:13px;line-height:1.5;background:#f6f8fb;border-radius:8px;padding:10px;margin:6px 0}
.po-line{font-size:13px;margin:4px 0}
.po-files{display:flex;flex-direction:column;gap:6px;margin-top:6px}
.po-file{display:flex;align-items:center;gap:8px;font-size:13px;text-decoration:none;color:var(--navy);background:#eef3fb;border-radius:8px;padding:8px 10px;min-height:var(--tap,44px)}
/* The filename and, under it, "Uploaded by <name> · <date time>". Two lines inside one tap target,
   which is why .po-file grew the --tap floor and a flex row: the row is one link the whole way
   across, and the officer's thumb must not have to find the filename specifically. min-width:0 is
   what lets a long post-order name ellipsise instead of pushing the icon off the card. */
.po-file-t{display:flex;flex-direction:column;min-width:0;flex:1}
.po-file-t>*{overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
.po-file-by{font-size:11px;line-height:1.35;opacity:.72;font-weight:400}
[data-theme="dark"] .po-orders{background:#141b28}[data-theme="dark"] .po-file{background:#1a2231;color:#cfe0ff}
.rw-cat{margin:8px 0}
.rw-item{display:flex;justify-content:space-between;align-items:center;gap:10px;padding:6px 0;border-top:1px solid var(--line,#eee)}
.gbtn.sm{padding:6px 12px;font-size:13px}
.course-les,.course-quiz{background:#fff;border-radius:12px;padding:12px;margin:8px 0;box-shadow:0 1px 3px rgba(20,30,50,.06)}
.course-q{margin:8px 0;border-top:1px solid var(--line,#eee);padding-top:8px}
.cq-text{font-weight:600;margin-bottom:6px}
.cq-opt{display:block;padding:6px 0;font-size:14px}
[data-theme="dark"] .course-les,[data-theme="dark"] .course-quiz{background:#1a2231}
.pay-cards{display:flex;gap:10px;flex-wrap:wrap}
.pay-card{flex:1;min-width:96px;background:#fff;border-radius:12px;padding:12px;text-align:center;box-shadow:0 1px 3px rgba(20,30,50,.06)}
.pay-card .pl{font-size:11px;color:var(--muted);text-transform:uppercase;letter-spacing:.5px}
.pay-card .pv{font-size:19px;font-weight:800;color:var(--navy);margin-top:3px}
[data-theme="dark"] .pay-card{background:#1a2231}[data-theme="dark"] .pay-card .pv{color:#e8eef7}
.ann-card{background:#fff;border-radius:14px;padding:14px;margin-bottom:12px;box-shadow:0 1px 3px rgba(20,30,50,.06)}
.ann-card.pinned{border-left:4px solid #e08600}
.ann-head{display:flex;align-items:center;gap:8px;margin-bottom:4px}
.ann-kind{font-weight:800;font-size:13px;color:var(--navy-ink)}
.ann-title{font-size:15px;font-weight:700}
.ann-body{white-space:pre-wrap;font-size:14px;line-height:1.5;margin-top:4px}
.ann-img{max-width:100%;border-radius:10px;margin-top:8px}
/* The two pictures this app paints INLINE inside a card — an announcement photo and a course
   lesson's image — both open the lightbox now, and a picture that opens has to say so. On a phone
   the image is already the full width of the card, so the tap target needs nothing; the cursor is
   the only affordance a supervisor on a laptop in Field mode ever gets, and without it a memo photo
   reads as decoration. .course-img keeps its geometry inline where it is rendered; this rule adds
   the affordance and nothing else, so neither picture moves a pixel. */
.ann-img,.course-img{cursor:zoom-in}
.ann-reacts{display:flex;gap:6px;margin-top:10px;align-items:center}
.ann-react{background:#eef1f8;border:1px solid transparent;border-radius:16px;padding:6px 13px;font-size:15px}
.ann-react.on{background:#dbe9ff;border-color:#7aa7ff;font-weight:700}
.ann-cbtn{background:#eef1f8;border:0;border-radius:16px;padding:6px 13px;font-size:14px;margin-left:auto}
.ann-comments{margin-top:10px;border-top:1px solid var(--line,#e6e9f0);padding-top:8px}
.ann-cmt{font-size:13px;margin-bottom:6px}
.ann-cadd{display:flex;gap:6px;margin-top:8px}
.ann-cadd input{flex:1;padding:8px 10px;border:1px solid var(--line,#d8dde8);border-radius:10px}
.ann-cadd button{border:0;background:var(--navy,#032b49);color:#fff;border-radius:10px;padding:8px 12px}
[data-theme="dark"] .ann-react{background:#232f42}[data-theme="dark"] .ann-cbtn{background:#232f42}[data-theme="dark"] .ann-react.on{background:#26407a;border-color:#4a74c8}
.gnav button .ic{font-size:17px}
/* Report */
.rbtns{display:grid;grid-template-columns:1fr 1fr;gap:12px}
.rbtn{border:none;border-radius:14px;padding:20px 12px;font-weight:700;font-size:15px;color:#fff;display:flex;flex-direction:column;align-items:center;gap:6px}
.rbtn .ic{font-size:26px;width:auto;height:auto;line-height:1;display:block}
.rbtn.patrol{background:var(--navy)}.rbtn.incident{background:var(--red)}
.gfield{display:flex;flex-direction:column;gap:6px;margin-bottom:12px}
.gfield label{font-size:13px;color:var(--muted);font-weight:600}
.gfield input,.gfield select,.gfield textarea{padding:13px;border:1px solid var(--line);border-radius:12px;background:#fff}
.gfield-row{display:grid;grid-template-columns:1fr 1fr;gap:10px}
.gsave{width:100%;background:var(--navy);color:#fff;border:none;border-radius:12px;padding:15px;font-weight:700;font-size:16px}
.glink{background:none;border:none;color:var(--navy-2);text-decoration:underline;font-size:14px}
.rep-item{background:#fff;border-radius:12px;padding:12px 14px;margin-bottom:8px;box-shadow:0 1px 3px rgba(20,30,50,.06)}
.rep-item .top{display:flex;justify-content:space-between;align-items:center}
.rep-item .ty{font-weight:700;font-size:14px}
.tag{font-size:11px;padding:2px 8px;border-radius:20px;font-weight:600}
.tag.patrol{background:#e9eefb;color:var(--navy-2)}.tag.security{background:#fcebeb;color:#a32d2d}.tag.parking{background:#fff3df;color:#9a6a00}.tag.maintenance{background:#e6f1fb;color:#185fa5}
.rep-item .nar{font-size:13px;color:var(--muted);margin-top:5px;overflow-wrap:anywhere}
/* The whole narrative, revealed by .rep-more. pre-wrap keeps the officer's own paragraphs;
   overflow-wrap keeps an unbroken run from widening the card on a phone. */
.rep-item .rep-full{font-size:13.5px;line-height:1.5;color:var(--ink);margin-top:7px;white-space:pre-wrap;overflow-wrap:anywhere}
/* .glink carries word-break:break-all for the URLs it was written for (guard.css §chat). On a
   button whose label is three plain words that only ever produces an ugly mid-word break. */
.rep-item .rep-more{word-break:normal}
/* pointer-events:none is LOAD-BEARING: the toast is hidden with opacity only and sits exactly over
   the chat compose input — without it, an invisible box eats taps on the text bar after any toast. */
/* Motion (house tokens, section 16): slide + fade IN over --t-med, fade OUT over --t-fast with the
   translate reset held until the fade has finished, so a toast never visibly slides back down while
   the next one is already coming up. The translateX(-50%) is the centring and is carried through
   BOTH states on purpose — dropping it from either one throws the toast at the left edge. */
.toast{position:fixed;left:50%;bottom:96px;transform:translateX(-50%) translateY(10px);background:var(--ink);color:#fff;padding:12px 18px;border-radius:24px;font-size:14px;z-index:20;opacity:0;transition:opacity var(--t-fast) var(--ease),transform 0s linear var(--t-fast);pointer-events:none}
.toast.show{opacity:1;transform:translateX(-50%) translateY(0);transition:opacity var(--t-med) var(--ease),transform var(--t-med) var(--ease)}
@media (prefers-reduced-motion:reduce){
  .toast{transform:translateX(-50%);transition:opacity var(--t-fast) linear}
  .toast.show{transform:translateX(-50%);transition:opacity var(--t-fast) linear}
}
/* Bottom nav */
.gnav{position:fixed;bottom:0;left:50%;transform:translateX(-50%);width:100%;max-width:480px;background:#fff;border-top:1px solid var(--line);display:flex;z-index:10}
.gnav button{flex:1;background:none;border:none;padding:10px 0 14px;color:var(--muted);font-size:11px;font-weight:600;display:flex;flex-direction:column;align-items:center;gap:3px}
.gnav button .ic{font-size:22px}
.gnav button.on{color:var(--navy)}
.photo-chip{display:inline-flex;align-items:center;gap:6px;background:#eef1f8;border-radius:20px;padding:5px 12px;font-size:13px;margin:0 6px 6px 0}
.photo-chip button{background:none;border:none;color:var(--muted);font-size:14px}

/* 5-tab nav sizing */
.gnav button{font-size:10px}
.gnav .ic{font-size:19px}

/* Tour / checkpoints */
.cp-card{background:#fff;border-radius:14px;padding:14px 16px;margin-bottom:10px;display:flex;align-items:center;justify-content:space-between;box-shadow:0 1px 3px rgba(20,30,50,.06);border-left:4px solid var(--muted)}
.cp-card.done{border-left-color:var(--green)}
.cp-name{font-weight:700;font-size:15px}
.cp-sub{font-size:13px;color:var(--muted);margin-top:2px}
.cp-card.done .cp-sub{color:var(--green)}
.cp-btn{background:var(--navy);color:#fff;border:none;border-radius:10px;padding:10px 18px;font-weight:700;font-size:14px}
.shot-box{background:#fff;border-radius:14px;padding:14px;margin-bottom:12px;box-shadow:0 1px 3px rgba(20,30,50,.06)}
.shot-box .break-btns{display:flex;gap:8px}.shot-box .gbtn{flex:1;text-align:center}
.cov-card{background:#fff;border-radius:14px;padding:14px;margin-bottom:10px;border-left:4px solid #e08600;box-shadow:0 1px 3px rgba(20,30,50,.06)}
.cov-q{font-weight:700;font-size:15px;margin-bottom:4px}
.cov-btns{display:flex;gap:8px;margin-top:8px}.cov-btns .gbtn{flex:1}
.cov-done{font-size:13px;color:var(--green,#0f9d58);margin-top:6px}
.note-card input{width:100%;border:1px solid var(--line);border-radius:10px;padding:10px;font-size:15px;box-sizing:border-box;margin-bottom:8px}
.note-card .il{display:block;font-size:12px;color:var(--muted);margin:4px 0 2px}
[data-theme="dark"] .shot-box,[data-theme="dark"] .cov-card{background:#1a2231}
.gprofile{display:flex;align-items:center;gap:12px;background:#fff;border-radius:14px;padding:12px 14px;margin-bottom:12px;box-shadow:0 1px 3px rgba(20,30,50,.06)}
.gpf-name{font-weight:800;font-size:16px;color:var(--navy-ink)}
.gpf-edit{font-size:12px;color:#2a6cff;cursor:pointer}
[data-theme="dark"] .gprofile{background:#1a2231}
.photo-grid{display:flex;gap:8px;flex-wrap:wrap}
.photo-thumb{position:relative;width:74px;height:74px;border-radius:10px;overflow:hidden;border:1px solid var(--line,#e6e9f0)}
.photo-thumb img{width:100%;height:100%;object-fit:cover}
.photo-thumb button{position:absolute;top:2px;right:2px;background:rgba(0,0,0,.6);color:#fff;border:0;border-radius:50%;width:20px;height:20px;font-size:12px;line-height:1;cursor:pointer}
.photo-add{display:inline-block;margin-top:8px;background:var(--grad-primary,#032b49);color:#fff;border-radius:10px;padding:10px 16px;font-weight:700;font-size:14px;cursor:pointer}
.ai-card{background:#fff;border-radius:14px;padding:14px;margin:14px 0;box-shadow:0 1px 3px rgba(20,30,50,.06)}
.ai-head{font-weight:800;font-size:15px;color:var(--navy-ink)}
.ai-sub{font-size:12px;color:var(--muted);margin:2px 0 8px}
.gai-chips{display:flex;flex-wrap:wrap;gap:6px;margin:0 0 10px}
.gai-chip{background:#eef3fa;border:1px solid #d7e2f0;color:#2b4a6f;border-radius:999px;padding:6px 11px;font-size:12px;cursor:pointer;text-align:left}
[data-theme="dark"] .gai-chip{background:#182338;border-color:#28405e;color:#9db8d8}
.ai-card textarea{width:100%;border:1px solid var(--line);border-radius:10px;padding:10px;font-size:14px;box-sizing:border-box;margin-bottom:8px}
.ai-out{margin-top:10px}
.ai-reply{background:#eef3fb;border-radius:10px;padding:10px;font-size:14px;line-height:1.5;white-space:pre-wrap}
.ai-err{color:#c0182a;font-size:13px}
[data-theme="dark"] .ai-card{background:#1a2231}[data-theme="dark"] .ai-reply{background:#141b28}
.break-box{background:#fff;border-radius:14px;padding:14px;margin-bottom:12px;box-shadow:0 1px 3px rgba(20,30,50,.06)}
.break-box.on{background:#fff7e6;border:1px solid #f0c674}
.break-t{font-size:13px;font-weight:700;margin-bottom:8px;color:var(--navy-ink)}
.break-sub{font-size:12px;color:var(--muted);margin:-2px 0 8px}
.badge-wrap{display:flex;flex-wrap:wrap;gap:6px;margin:2px 0 10px}
.badge-chip{display:inline-flex;align-items:center;gap:5px;padding:4px 9px;border-radius:999px;font-size:12px;font-weight:700;background:#eef1f5;color:#3b475c}
.badge-chip.gold{background:#fff4d6;color:#8a6d00}.badge-chip.silver{background:#eceff3;color:#556}.badge-chip.bronze{background:#f6e8dd;color:#8a5a2b}
[data-theme="dark"] .badge-chip{background:#222c3d;color:#c7d2e2}[data-theme="dark"] .badge-chip.gold{background:#3a3115;color:#f0c86a}
.gcard{background:#fff;border-radius:14px;padding:14px;margin-bottom:12px;box-shadow:0 1px 3px rgba(20,30,50,.06)}
[data-theme="dark"] .gcard{background:#1a2231}
.gwx-strip{display:flex;gap:5px;overflow-x:auto}
.gwx-day{flex:1 0 46px;text-align:center;padding:6px 2px;border-radius:9px;background:var(--panel,#f4f6fb)}
[data-theme="dark"] .gwx-day{background:#222c3d}
.gwx-dow{font-size:10px;font-weight:800;color:var(--muted);text-transform:uppercase}.gwx-ic{font-size:20px}.gwx-hilo{font-size:12px}
.gwx-adv{margin-top:8px;font-size:12px;line-height:1.4;color:#8a4b00;background:#fff2df;padding:8px 10px;border-radius:8px}
[data-theme="dark"] .gwx-adv{background:#3a2c14;color:#f0c078}
.gaw{font-size:12.5px;line-height:1.45}
.gai{margin-top:8px;font-size:12.5px;line-height:1.4;background:var(--panel,#f2f6ff);border-left:3px solid var(--navy,#22345f);padding:8px 10px;border-radius:6px}
[data-theme="dark"] .gai{background:#16233d;color:#d7dfec}
.break-btns{display:flex;gap:8px}
.break-btns .gbtn{flex:1;font-size:13px}
[data-theme="dark"] .break-box{background:#1a2231}[data-theme="dark"] .break-box.on{background:#2a2415;border-color:#6a5520}
.cp-iv{font-size:11px;background:#eef1f8;color:var(--navy);border-radius:10px;padding:2px 7px;margin-left:4px;font-weight:600}
.cp-hint{font-size:12px;color:var(--muted);margin:0 2px 12px}
.note-ov,.g-modal{position:fixed;inset:0;background:rgba(10,16,28,.55);display:flex;align-items:flex-end;justify-content:center;z-index:9999;padding:0}
.g-modal .note-card{background:#fff;border-radius:18px 18px 0 0;padding:20px;width:100%;max-width:560px;box-shadow:0 -4px 24px rgba(0,0,0,.25)}
[data-theme="dark"] .g-modal .note-card{background:#141b28}
.note-card{background:#fff;border-radius:18px 18px 0 0;padding:20px;width:100%;max-width:560px;box-shadow:0 -4px 24px rgba(0,0,0,.25)}
.note-t{font-weight:800;font-size:17px;color:var(--green,#0f9d58)}
.note-s{font-size:13px;color:var(--muted);margin:6px 0 10px}
.note-card textarea{width:100%;border:1px solid var(--line);border-radius:12px;padding:12px;font-size:15px;box-sizing:border-box}
.note-btns{display:flex;gap:10px;margin-top:12px}
.note-btns .gbtn{flex:1}
[data-theme="dark"] .note-card{background:#141b28}[data-theme="dark"] .cp-iv{background:#232f42;color:#cfe0ff}

/* Chat */
/* The messages view fills the space between header and bottom-nav so the compose
   bar sits flush above the nav (no dead gap) and the thread scrolls inside it. */
/* On the Chat tab → pin the app to the screen height so ONLY the list scrolls; the header (back),
   the Call/Radio/Pinned actions, and the compose bar all stay put. Without this the shell is
   min-height:100vh, so a long thread grows the whole page and the body scrolls the header out of
   reach. Scoped with :has to the TAB (#gv-messages.on), because the .gchat markup lingers in
   #gv-messages even on other tabs and the pin must not follow it there.
   ⚠ THE PIN COVERS THE CONVERSATION LIST TOO, AND IT HAS TO — it used to say
   `:has(#gv-messages.on .gchat)`, i.e. a THREAD only. On the list there was no .gchat, so .gapp fell
   back to min-height:100vh and .ginbox — `flex:1` of an auto-height column — came out exactly as
   tall as its own content. An overflow:auto box with nothing to scroll is still a scroll container,
   and `overscroll-behavior:contain` on it means a drag that starts anywhere over the list is
   swallowed instead of chaining to the page: the officer's conversation list would not move AT ALL.
   MEASURED at 390x844 with 18 conversations, before this change: document scrollHeight 1662 vs a
   844 viewport, a real wheel over the list left document.scrollTop at 0, and flipping ONLY
   overscroll-behavior to `auto` on .ginbox let the same gesture move the page 500px.
   Two ways to break it again: narrowing this selector back to a thread, or giving .gapp a height
   that is not bounded — either one un-bounds .ginbox and the swallow comes straight back. */
.gapp:has(#gv-messages.on){height:100vh;height:100dvh;overflow:hidden}
/* .gmain reserves 152px of bottom chrome everywhere else — 60px of tab bar plus the #g-sos FAB that
   sits above it (see the clearance note further down). gnav() HIDES #g-sos on this tab, so on Chat
   that reserve is dead space: MEASURED at 390x844 it left a 92px band of empty background between
   the compose bar (bottom 692) and the tab bar (top 784), which is exactly the "no dead gap" this
   section opens by promising. Reserve the nav, nothing more. */
.gapp:has(#gv-messages.on) .gmain{padding-bottom:calc(60px + env(safe-area-inset-bottom,0px))}
/* …and typing mode hides the nav as well, so it releases the reserve completely. Restated at this
   specificity because the :has() above carries its argument's id and would otherwise outrank
   section 21d's `body.g-typing .gmain{padding-bottom:0}`. */
body.g-typing .gapp:has(#gv-messages.on) .gmain{padding-bottom:0}
#gv-messages.on{display:flex;flex-direction:column;flex:1;min-height:0}
.gchat{display:flex;flex-direction:column;flex:1;min-height:0;position:relative}
/* overscroll-behavior:contain stops a top-of-thread pull from chaining into Chrome's pull-to-refresh
   (which reloaded the app mid-chat). It is only ever safe on a box that really can scroll — see the
   pin above, which is what makes that true for .ginbox. */
.ginbox{flex:1;min-height:0;overflow-y:auto;overscroll-behavior:contain}
.g-thread{flex:1;overflow-y:auto;display:flex;flex-direction:column;gap:8px;padding:6px 2px;overscroll-behavior:contain}
.gbubble{max-width:80%;padding:9px 13px;border-radius:14px;font-size:14px;line-height:1.4;white-space:pre-wrap;word-break:break-word}
.gbubble .bm{font-size:11px;opacity:.7;margin-bottom:2px}
.gbubble.mine{align-self:flex-end;background:var(--navy);color:#fff}
.gbubble.them{align-self:flex-start;background:#fff;border:1px solid var(--line)}
.g-compose{display:flex;gap:8px;padding-top:10px;align-items:center}
.g-compose input{flex:1;height:48px;padding:0 14px;border:1px solid var(--line);border-radius:12px;box-sizing:border-box}
.g-compose button{background:var(--navy);color:#fff;border:none;border-radius:12px;height:48px;padding:0 18px;font-weight:700;display:flex;align-items:center;justify-content:center;flex-shrink:0}

/* Guard chat attachments + compose buttons */
.gicon{background:#eef1f6;border:1px solid var(--line);border-radius:12px;width:48px;height:48px;flex-shrink:0;font-size:22px;cursor:pointer;display:flex;align-items:center;justify-content:center;padding:0;line-height:1}
.gicon.recording,.g-hbtn.recording{background:var(--red);color:#fff;border-color:var(--red);animation:gpulse 1s infinite}
@keyframes gpulse{0%,100%{opacity:1}50%{opacity:.55}}
.gatt-img{max-width:200px;max-height:200px;border-radius:10px;display:block;margin-top:5px}
.gatt-vid{max-width:230px;max-height:300px;border-radius:12px;display:block;margin-top:5px;background:#000}
/* Full-screen in-app meeting overlay (WhatsApp-feel) + channel live badge */
.gmeet-ov{position:fixed;inset:0;z-index:100050;background:#000;display:flex;flex-direction:column}
.gmeet-bar{display:flex;align-items:center;gap:8px;padding:calc(8px + env(safe-area-inset-top,0px)) 12px 8px;background:#0d2b42;color:#e8f2fb;font-size:14px;flex:0 0 auto}
.gmeet-dot{width:9px;height:9px;border-radius:50%;background:#2ecb6e;box-shadow:0 0 8px #2ecb6e}
.gmeet-x{border:none;border-radius:9px;background:#e04848;color:#fff;font-weight:800;padding:7px 14px;font-size:13px}
.gmeet-body{flex:1;min-height:0}
.gmeet-body iframe{width:100%;height:100%;border:0;display:block}
.gib-meet{flex:0 0 auto;display:inline-flex;align-items:center;gap:3px;padding:1px 6px;border-radius:8px;background:#1d9e57;color:#fff;font-size:10px;font-weight:800}
.gib-meet .ic{width:11px;height:11px;vertical-align:0}
/* Unified thread-header action row — EVERY chat action lives here; 8 buttons scroll sideways on
   narrow phones instead of squeezing (the compose bar below is just the text box + send). */
.gth2-acts{display:flex;gap:8px;overflow-x:auto;-webkit-overflow-scrolling:touch;scrollbar-width:none;padding-bottom:2px}
.gth2-acts::-webkit-scrollbar{display:none}
.g-hbtn{width:44px;height:44px;border-radius:50%;border:1.5px solid #d5e2ec;background:#fff;display:inline-flex;align-items:center;justify-content:center;color:#0d3a5c;flex:0 0 auto;padding:0}
.g-hbtn svg{width:21px;height:21px}
.g-hbtn:active{background:#eef4fb}
.g-hbtn-call{border-color:#1d9e57;color:#0d7a45}
.g-hbtn-meet{border-color:#4f7bff;color:#2b55d4}
.gatt-audio{margin-top:5px;height:38px;max-width:200px;display:block}
.gatt-file{display:inline-block;margin-top:5px;color:inherit;text-decoration:underline;font-size:13px}
.gbubble.mine .gatt-file{color:#fff}

/* fix: keep compose bar visible when chat overflows */
.g-thread{min-height:0}

/* fix: officer compose never overflows */
.g-compose input{min-width:0}
.g-compose>button{flex-shrink:0}

/* ══ Voice call overlay (call.js) — a phone CALL SCREEN, not a modal ═══════════════════════════════
   Owner report: "the call on the Android app — the text should not be there, it breaks the UI."
   This block was the admin console's modal treatment (still correct there, see the twin in
   styles.css) dropped onto a phone, and it did three things a call screen must never do. Measured in
   a headless Chromium at 375x812 against the real officer shell:
     · the scrim was rgba(15,20,35,.78) — TRANSLUCENT. The whole app read straight through the
       incoming-call screen: the header, "Riverbend Plaza — North Post", "18:00–06:00 · clocked in
       4h 12m ago", the tab labels. That show-through IS the text that should not be there.
     · z-index was 9999, but the radio bar is 100000 and its FAB 100009, so the always-on dispatch
       net painted ON TOP of a ringing call — elementFromPoint over the bar returned the bar, so the
       tap went to the radio, not to Answer.
     · the card was a fixed 300px with no wrap rule on .uc-name, so a caller string with no space in
       it (a number, an address-shaped id) overflowed by a measured 29px and painted outside the
       white card onto the scrim.
   It is now opaque, topmost, and uses the same palette as the app's other two full-screen call
   surfaces — #g-phone (the Telnyx softphone, rgba(1,25,43,.97)) and the native IncomingCallActivity
   (#0E1B2A) — so all three read as one screen. Deliberately dark in BOTH themes, like every phone
   call screen and like those two. .uc-* is officer-only; the admin console keeps its own copy. ══ */
.uc-overlay{position:fixed;inset:0;background:#01192b;display:flex;align-items:center;justify-content:center;z-index:100020;
  padding:24px calc(20px + env(safe-area-inset-right,0px)) calc(24px + env(safe-area-inset-bottom,0px)) calc(20px + env(safe-area-inset-left,0px));
  overflow-y:auto;overscroll-behavior:contain;font-family:inherit}
.uc-card{background:transparent;box-shadow:none;padding:0;width:100%;max-width:340px;text-align:center;color:#e6edf3}
/* the "Incoming call" kicker; .uc-sub on its own is the in-call status line ("Connecting…") */
.uc-lbl{font-size:13px;letter-spacing:.14em;text-transform:uppercase;color:#9fb4cc;margin-bottom:10px}
.uc-sub{color:#9fb4cc;font-size:15px}
/* Never let a caller name push the controls around: break inside an unbroken token, then clamp to
   three lines with an ellipsis. Both halves are needed — `anywhere` alone still grows the card. */
.uc-name{font-family:var(--font-display,inherit);font-size:26px;font-weight:800;color:#fff;margin:0 0 8px;line-height:1.25;
  overflow-wrap:anywhere;word-break:break-word;
  display:-webkit-box;-webkit-box-orient:vertical;-webkit-line-clamp:3;overflow:hidden}
.uc-timer{font-family:var(--font-mono,monospace);font-size:15px;color:#9fb4cc;font-weight:700;min-height:20px;margin-top:2px}
.uc-acts{display:flex;gap:14px;justify-content:center;margin-top:30px}
/* min-width:0 so a long label shrinks the button instead of widening the row off-screen */
.uc-b{flex:1 1 0;min-width:0;min-height:var(--tap,44px);padding:16px 10px;border:none;border-radius:14px;font-family:inherit;
  font-weight:700;font-size:16px;background:#123a57;color:#fff;cursor:pointer}
.uc-b.red{background:#c0182a;color:#fff}.uc-b.grn{background:#0d7a45;color:#fff}.uc-b.on{background:#0a6bb0;color:#fff}
.g-callbtn{background:#fff;color:var(--green);border:1px solid var(--green);border-radius:10px;padding:6px 12px;font-weight:700;font-size:13px}
[data-theme="dark"] .g-callbtn{background:transparent}
.gthread-actions button{display:inline-flex;align-items:center;gap:5px;white-space:nowrap;line-height:1.4}
.gthread-actions .ic{width:15px;height:15px;vertical-align:-2px}
.dir-call{display:inline-block;margin-top:4px;color:#0a6bb0;font-weight:600;text-decoration:none;font-size:13px}
/* Channels 2.0 (officer) */
.gchan-bar{display:flex;gap:6px;overflow-x:auto;padding:6px 2px;margin-bottom:6px}
.gchan-chip{flex:0 0 auto;border:1px solid #d3dae6;background:#fff;border-radius:16px;padding:5px 12px;font-size:13px;font-weight:600;color:#0b2036;white-space:nowrap}
.gdir-chip{margin-left:auto;border-style:dashed;color:#22345f}
/* ══ Chat inbox — WhatsApp/Connecteam row anatomy + Discord section organisation ═══════════════════
   The old list was a stack of bordered cards with a name, a preview and a red dot. A messenger row
   is a different animal: a round avatar, the name and the TIME on one line, the preview and the
   unread COUNT on the next, and a hairline divider inset to the avatar instead of a card border —
   which is what makes a long list read as a conversation list rather than a dashboard.
   Sections keep this app's uppercase muted header (which is also Discord's category treatment) and
   gain a caret, a channel count and their own unread total. ══ */
.ginbox{padding:2px 0}
/* max-width + hidden overflow: the row must never push its last button off the screen edge
   (owner screenshot 2026-08-10 — the staff Call button hung half off a phone viewport). Below
   480px the New/Call button drops its text and becomes icon-only; title/aria keep its name. */
.ginbox-top{display:flex;gap:8px;align-items:center;margin-bottom:9px;max-width:100%;min-width:0}
@media(max-width:480px){ .ginbox-top .gib-new-t{display:none} .gib-new{padding:10px 12px} .gib-new .ic{margin-right:0} }
/* 16px is load-bearing on every officer-app text input: anything smaller triggers mobile focus-zoom
   and the whole layout jumps (the same reason the compose textarea is 16px). */
.gib-search{flex:1;min-width:0;padding:10px 12px;border:1px solid var(--line,#d3dae6);border-radius:12px;font-size:16px;background:#fff}
[data-theme="dark"] .gib-search{background:#141b28;color:#e6edf7;border-color:#2a3648}
/* 44px, not 40: these sit on the SAME row as the Call button and were the last two controls in the
   messenger still under the house --tap floor (the thread header's .g-hbtn next to them is already
   44). Measured at 360x800 — .gib-search is flex:1;min-width:0, so it absorbs the extra 8px and the
   row still fits with the Call button whole. */
.gib-tool{flex:0 0 auto;width:var(--tap,44px);height:var(--tap,44px);min-height:var(--tap,44px);border:1px solid var(--line,#d3dae6);background:#fff;border-radius:12px;color:var(--navy,#22345f);display:inline-flex;align-items:center;justify-content:center;cursor:pointer}
.gib-tool.off{opacity:.38}
[data-theme="dark"] .gib-tool{background:#141b28;border-color:#2a3648;color:#9cc0ff}
.gib-new{flex:0 0 auto;min-height:var(--tap,44px);border:none;background:var(--navy,#22345f);color:#fff;font-weight:700;font-size:13px;padding:10px 14px;border-radius:12px;white-space:nowrap}
/* Filter pills — the console's All / Unread / @Me row, with live counts. */
.gib-filters{display:flex;gap:7px;margin:0 0 6px;overflow-x:auto;scrollbar-width:none;-webkit-overflow-scrolling:touch}
.gib-filters::-webkit-scrollbar{display:none}
.gib-f{flex:0 0 auto;display:inline-flex;align-items:center;gap:5px;border:1px solid var(--line,#d3dae6);background:#fff;color:var(--ink,#33405a);border-radius:20px;padding:6px 13px;font-size:13px;font-weight:700;cursor:pointer}
.gib-f.on{background:var(--navy,#22345f);border-color:var(--navy,#22345f);color:#fff}
.gib-fn{min-width:18px;padding:0 5px;height:18px;border-radius:9px;background:var(--navy,#22345f);color:#fff;font-size:11px;font-weight:800;display:inline-flex;align-items:center;justify-content:center}
.gib-f.on .gib-fn{background:rgba(255,255,255,.26)}
[data-theme="dark"] .gib-f{background:#141b28;border-color:#2a3648;color:#cbd6e6}
[data-theme="dark"] .gib-f.on{background:#1d4a80;border-color:#1d4a80;color:#fff}
[data-theme="dark"] .gib-fn{background:#1d4a80}
/* Section header */
.gib-block{margin-bottom:2px}
/* THE SECTION HEADER, made readable and made a real target. 11px small caps at .05em is the size a
   legal footer is set in, and the whole strip was ~23px tall on a phone — under half the house
   --tap floor, on the control an officer hits most. 12px/.04em keeps the "divider, not a channel"
   character and stops making people squint at it. */
.gib-sec{display:flex;align-items:center;gap:0;width:100%;background:none;border:none;text-align:left;font-size:12px;font-weight:800;letter-spacing:.04em;text-transform:uppercase;color:var(--muted,#697690);margin:8px 2px 2px;padding:6px 4px;min-height:var(--tap,44px);box-sizing:border-box;cursor:pointer;border-radius:10px}
.gib-sec:active{background:rgba(34,52,95,.07)}
[data-theme="dark"] .gib-sec:active{background:rgba(120,170,255,.09)}
.gib-sec:focus-visible{outline:2px solid var(--navy,#22345f);outline-offset:-2px}
/* The label brightens out of --muted whenever the fold is hiding something — Discord's exact
   treatment, and the half of the signal that survives even when the count is a bare dot. */
.gib-sec.has-unread{color:var(--heading,#0b2036)}
[data-theme="dark"] .gib-sec.has-unread{color:#eaf0fa}
/* ONE chevron that ROTATES. Pointing RIGHT is the collapsed rest state, so the expanded state is
   the transform — the direction every tree control agrees on. */
.gib-caret{flex:0 0 auto;width:19px;display:inline-flex;align-items:center;justify-content:center;color:var(--muted,#697690)}
.gib-sec .gib-caret .ic{width:13px;height:13px;margin-right:0;vertical-align:0;transform:rotate(90deg)}
.gib-sec.col .gib-caret .ic{transform:rotate(0deg)}
.gib-sec-l{margin-right:7px}
.gib-sec-n{color:var(--muted,#697690);opacity:.72;font-weight:700}
/* ══ THE SECTION AGGREGATE — visible ONLY while collapsed ════════════════════════════════════════
   Expanded, every per-channel badge is already on screen one row below, so a header badge is the
   same fact printed twice. Collapsed, the header is the only surface left. Colours mirror the row
   badges exactly (.gib-at red = named you or urgent, .gib-n navy = a direct conversation's message
   count) so one colour rule holds at both altitudes. */
.gib-sec-agg{display:none;margin-left:auto;min-width:18px;padding:0 5px;height:18px;border-radius:9px;background:var(--navy,#22345f);color:#fff;font-size:10.5px;font-weight:800;align-items:center;justify-content:center;letter-spacing:0;font-variant-numeric:tabular-nums}
.gib-block.col>.gib-sec>.gib-sec-agg{display:inline-flex}
.gib-sec-agg.ment{background:#e11d2a}
.gib-sec-agg.dot{min-width:9px;width:9px;height:9px;padding:0;border-radius:50%;background:#e11d2a}
/* A PLAIN unread — chatter in a shared room, nobody named — is weight, never a number and never
   red. The quiet navy pip is what the rows under it are already saying. */
.gib-sec-agg.dot.plain{background:var(--navy,#22345f);opacity:.75}
[data-theme="dark"] .gib-sec-agg,[data-theme="dark"] .gib-sec-agg.dot.plain{background:#1d4a80}
[data-theme="dark"] .gib-sec-agg.ment,[data-theme="dark"] .gib-sec-agg.dot{background:#ff5a60;color:#0b1520}
/* ══ THE FOLD, and what survives it ═════════════════════════════════════════════════════════════
   ★ THE ROWS FOLD, NOT THE BODY. Collapsing .gib-sec-body (or the old inline display:none) makes
     the collapsed state necessarily EMPTY — the reported bug. Folding each row instead animates,
     AND lets an UNREAD row stay on screen under a collapsed section the way Discord does, AND
     leaves .gib-sec-body the direct parent of every .ginbox-row, which gibRowResort requires.
   ⚠ 88px, not a round 1000px: a max-height transition spends the gap between the cap and the row's
     real height (66px here — a 46px avatar plus 10px of padding each side) doing nothing visible.
     A loose cap turns the first half of every collapse into dead air. Nothing in a row wraps. */
.gib-sec-body>.ginbox-row{overflow:hidden;max-height:88px}
.gib-block.col>.gib-sec-body>.ginbox-row{max-height:0;opacity:0;padding-top:0;padding-bottom:0;pointer-events:none}
/* MUTED IS ABSENT ON PURPOSE: a muted channel stays hidden when collapsed whatever it is holding —
   Discord's mute rule, and the same one chanBadgeOf applies to the badge. A mention inside a muted
   room still lights the HEADER (the red pill pierces a mute); it just does not force the row back. */
.gib-block.col>.gib-sec-body>.ginbox-row.unread:not(.muted){max-height:88px;opacity:1;padding-top:10px;padding-bottom:10px;pointer-events:auto}
/* DESIGN-SYSTEM §7: --t-med for the fold, --t-fast for the fade, --ease with no overshoot, and the
   whole thing inside no-preference so a person who asked for stillness gets an instant fold. */
@media (prefers-reduced-motion:no-preference){
  .gib-sec-body>.ginbox-row{transition:max-height var(--t-med) var(--ease),opacity var(--t-fast) var(--ease),padding var(--t-med) var(--ease)}
  .gib-sec .gib-caret .ic{transition:transform var(--t-med) var(--ease)}
  .gib-sec{transition:color var(--t-fast) var(--ease),background var(--t-fast) var(--ease)}
}
/* Row */
.ginbox-row{position:relative;display:flex;align-items:center;gap:12px;width:100%;text-align:left;background:transparent;border:none;border-radius:12px;padding:10px 8px;cursor:pointer;-webkit-tap-highlight-color:transparent}
.ginbox-row::after{content:"";position:absolute;left:66px;right:8px;bottom:0;height:1px;background:var(--line,#e6e9ef)}
.gib-sec-body .ginbox-row:last-child::after{display:none}
[data-theme="dark"] .ginbox-row::after{background:#232d3d}
.ginbox-row.unread{background:rgba(34,52,95,.055)}
[data-theme="dark"] .ginbox-row.unread{background:rgba(120,170,255,.07)}
.ginbox-row:active{background:rgba(34,52,95,.1)}
[data-theme="dark"] .ginbox-row:active{background:rgba(120,170,255,.12)}
.gib-av{flex:0 0 auto;width:46px;height:46px;border-radius:50%;background:#5b6b86;color:#fff;font-family:var(--font-display);font-weight:800;font-size:18px;display:flex;align-items:center;justify-content:center;overflow:hidden}
.gib-av.glyph{color:#fff}
.gib-av.glyph .ic{width:22px;height:22px;vertical-align:0}
.gib-av.glyph.disp{background:linear-gradient(160deg,#0a6bb0,#032b49)}
.gib-av.glyph.ai{background:linear-gradient(160deg,#6f5bd0,#3c2f80)}
.gib-main{flex:1;min-width:0}
.gib-l1{display:flex;align-items:baseline;gap:6px}
.gib-l2{display:flex;align-items:center;gap:6px;margin-top:2px}
.gib-name{flex:1;min-width:0;font-weight:700;font-size:15px;color:var(--heading,#0b2036);white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
.ginbox-row.unread .gib-name{font-weight:800}
[data-theme="dark"] .gib-name{color:#eaf0fa}
.gib-pinned{flex:0 0 auto;opacity:.5;line-height:1}
.gib-pinned .ic{width:13px;height:13px;vertical-align:-1px}
.gib-time{flex:0 0 auto;font-size:11.5px;color:var(--muted,#697690);font-variant-numeric:tabular-nums}
.gib-time.hot{color:var(--navy,#22345f);font-weight:800}
[data-theme="dark"] .gib-time.hot{color:#8fbdff}
.gib-prev{flex:1;min-width:0;font-size:13px;color:var(--muted,#697690);white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
.ginbox-row.unread .gib-prev{color:var(--ink,#33405a)}
[data-theme="dark"] .ginbox-row.unread .gib-prev{color:#c3cfe0}
.gib-none{opacity:.7}
.gib-kic{margin-right:4px;opacity:.75}
.gib-kic .ic{width:14px;height:14px;vertical-align:-2px}
.gib-badges{flex:0 0 auto;display:flex;align-items:center;gap:5px}
.gib-at{min-width:19px;height:19px;padding:0 4px;border-radius:10px;background:#e11d2a;color:#fff;font-size:11.5px;font-weight:800;display:inline-flex;align-items:center;justify-content:center}
.gib-n{min-width:20px;height:20px;padding:0 6px;border-radius:10px;background:var(--navy,#22345f);color:#fff;font-size:11.5px;font-weight:800;display:inline-flex;align-items:center;justify-content:center}
[data-theme="dark"] .gib-n{background:#1d4a80}
.gib-more{flex:0 0 auto;border:none;background:none;color:var(--muted,#697690);font-size:19px;line-height:1;padding:6px 2px;cursor:pointer;opacity:.55}
.gib-dot{width:9px;height:9px;border-radius:50%;background:#e11d2a;display:inline-block}
/* The PLAIN-UNREAD pip on an inbox row — the officer half of the console's .chan-dot.plain. Chatter
   in a shared room earns no number (chanBadgeOf still returns kind:'' for it) but bold alone is not
   something anyone notices, which is the report. Navy, matching .gib-sec-agg.dot.plain one altitude
   up, because red is "named you, or urgent" and nothing else. flex:none — .gib-badges is a flex row
   and a 9px pip must not be allowed to shrink next to a count. */
.gib-dot.plain{flex:none;background:var(--navy,#22345f);opacity:.75}
[data-theme="dark"] .gib-dot.plain{background:#1d4a80;opacity:.9}
/* Empty states smile instead of going blank ([[ui-quality-bar]]). */
.gib-empty{text-align:center;color:var(--muted,#697690);font-size:13px;padding:38px 18px;display:flex;flex-direction:column;align-items:center;gap:4px}
.gib-empty .ic{width:30px;height:30px;opacity:.4;margin-bottom:6px}
.gib-empty b{font-size:15px;color:var(--heading,#0b2036);font-weight:800}
[data-theme="dark"] .gib-empty b{color:#eaf0fa}
.gib-empty.ok .ic{color:#1d9e57;opacity:.7}
.chip-at{color:#e11d2a}
/* Thread header extras: the tile carries the channel's own identity, the sub-line goes green while
   someone is typing (WhatsApp's exact placement), and a group header is tappable for the roster. */
.gth2-tile.gl .ic{width:19px;height:19px;vertical-align:0}
.gth2-t.tap{cursor:pointer}
/* The title is a real control whenever the header is tappable — the member rail on a room, and now
   the person sheet on a 1:1 — and it has carried role=button/tabindex all along with nothing to
   show the keyboard. The ring is LIGHT because this bar is navy; the house blue disappears into it.
   The chevron leans toward where it goes, on the house motion tokens, and stands down on request. */
.gth2-t.tap:focus-visible{outline:2px solid rgba(255,255,255,.78);outline-offset:2px;border-radius:8px}
.gth2-t.tap::after{content:"›";margin-left:6px;color:#9dbdd6;font-size:15px;display:inline-block;
  transition:transform var(--t-fast) var(--ease)}
.gth2-t.tap:hover::after{transform:translateX(2px)}
@media (prefers-reduced-motion:reduce){ .gth2-t.tap::after{transition:none} .gth2-t.tap:hover::after{transform:none} }
#gth2-sub.typing{color:#7fe0a9;font-weight:700}
/* "Read by …" line, the console's #msg-receipt. Sits between the thread and the composer so it can
   never cover the input — the invisible-overlay lesson from [[chat-native-feel]]. */
.g-seen{font-size:11px;color:var(--muted,#697690);text-align:right;padding:2px 6px 4px;pointer-events:none}
/* A read tick is blue; an explicit acknowledgement stays the highlighted one. */
.gmeta .gtkw.rd{color:#3ba3ff}
/* Channel member sheet — Discord's rail, phone-sized. */
.gmem-sheet{max-height:78vh;display:flex;flex-direction:column}
.gmem-list{overflow-y:auto;overscroll-behavior:contain;padding-bottom:6px}
.gmem-sec{font-size:11px;font-weight:800;letter-spacing:.05em;text-transform:uppercase;color:var(--muted,#697690);margin:12px 2px 5px}
.gmem{display:flex;align-items:center;gap:11px;padding:7px 2px}
.gmem-av{flex:0 0 auto;width:36px;height:36px;border-radius:50%;background:#5b6b86;color:#fff;font-weight:800;font-size:14px;display:flex;align-items:center;justify-content:center}
.gmem:not(.live) .gmem-av{filter:saturate(.35);opacity:.72}
/* The staff opt-out has to OUT-SPECIFY the dimming rule, not merely follow it. `.gmem-av.st`
   is (0,2,0) and the dimmer above is (0,3,0) — .gmem + :not(.live), which counts its argument,
   + .gmem-av — so the opt-out lost the cascade and every office staffer in the sheet rendered
   desaturated as though they were an off-duty officer (staff rows never carry .live; see
   srow() in guard.js). Qualifying it with the same ancestor takes it to (0,4,0) and it wins.
   This matters more now than it did as a flat initial disc: the generated avatar is the
   colour, so saturate(.35) is the whole point of it being drained away. */
.gmem:not(.live) .gmem-av.st{filter:none;opacity:1}
.gmem-n{flex:1;min-width:0}
.gmem-n b{display:block;font-size:14px;font-weight:700;color:var(--heading,#0b2036);white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
.gmem-you{font-weight:600;color:var(--muted,#697690)}
.gmem-n>i{display:block;font-size:11.5px;color:var(--muted,#697690);font-style:normal;margin-top:1px}
[data-theme="dark"] .gmem-n b{color:#eaf0fa}
.gmem-crown .ic{width:13px;height:13px;vertical-align:-2px;color:#d5a134}
.gmem-dot{flex:0 0 auto;width:10px;height:10px;border-radius:50%;background:#8894a8}
.gmem-dot.on,.gmem-dot.online{background:#1d9e57}
.gmem-dot.away{background:#e0a02c}
.gmem-dot.offline{background:#8894a8;opacity:.5}
.gmem-note{display:flex;gap:8px;align-items:flex-start;font-size:12px;color:var(--muted,#697690);padding:12px 2px 4px;line-height:1.45}
.gmem-note .ic{width:15px;height:15px;flex:0 0 auto;margin-top:1px;opacity:.7}
/* ── A member row that a STAFF viewer can act on ──────────────────────────────────
   Only ever emitted on the supervisor context (see gPersonPolicy in guard.js); an
   officer's sheet has plain divs here and none of this applies. `.gmem` keeps every
   layout rule above — this only undoes the UA button defaults (which include a BLACK
   colour that is unreadable on the dark sheet) and clears the 44px tap floor. */
.gmem-b{width:100%;border:0;background:none;font:inherit;color:var(--heading,#0b2036);
  text-align:left;cursor:pointer;min-height:var(--tap,44px);border-radius:9px;
  transition:background var(--t-fast) var(--ease)}
.gmem-b:active{background:rgba(10,107,176,.10)}
.gmem-b:focus-visible{outline:2px solid var(--blue,#0a6bb0);outline-offset:1px}
[data-theme="dark"] .gmem-b{color:#eaf0fa}
[data-theme="dark"] .gmem-b:active{background:rgba(120,170,235,.14)}
/* ── The person sheet: identity, then the cap-gated action grid ─────────────────── */
.gpf-sheet{max-height:82vh;overflow-y:auto;overscroll-behavior:contain}
.gpf-id{display:flex;align-items:center;gap:12px;padding:6px 2px 14px}
.gpf-id .gpf-av2{width:54px;height:54px;font-size:20px;margin:0;flex:0 0 auto}
.gpf-n{flex:1;min-width:0}
.gpf-n b{display:block;font-size:16px;font-weight:800;color:var(--heading,#0b2036);white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
.gpf-n>i{display:block;font-size:12px;color:var(--muted,#697690);font-style:normal;margin-top:2px}
[data-theme="dark"] .gpf-n b{color:#eaf0fa}
/* ⚠ .gpf-arow, NOT .gpf-acts. This rule used to be spelled .gpf-acts — and so is the officer's OWN
   profile-tab hero row (guard.css:~2007, `display:flex;justify-content:center;flex-wrap:wrap`, used
   by guard.js's .gpf-chip row). Same selector, same specificity, later one wins: the person sheet's
   action row has been shipping as a centre-justified wrapping FLEX row this whole time, not the 2-up
   grid this rule and the console's .mp-acts both intend. No test caught it — the phone assertions
   cover .gpf-a's size, colour, motion and dark rules, never the container. Two different rows, two
   different names now, so neither can silently restyle the other again. */
.gpf-arow{display:grid;grid-template-columns:1fr 1fr;gap:8px;padding-bottom:4px}
.gpf-a{display:flex;align-items:center;gap:9px;min-height:var(--tap,44px);padding:10px 12px;
  border:1px solid var(--line,#e6e9ef);border-radius:11px;background:var(--card,#fff);
  color:var(--heading,#0b2036);font:inherit;font-size:13.5px;font-weight:700;
  text-align:left;cursor:pointer;
  transition:background var(--t-fast) var(--ease),border-color var(--t-fast) var(--ease)}
.gpf-a .ic{width:17px;height:17px;flex:0 0 auto;color:var(--blue,#0a6bb0)}
.gpf-a span{overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
.gpf-a:active{background:rgba(10,107,176,.10);border-color:var(--blue,#0a6bb0)}
.gpf-a:focus-visible{outline:2px solid var(--blue,#0a6bb0);outline-offset:1px}
[data-theme="dark"] .gpf-a{background:#182234;border-color:#2a3852;color:#eaf0fa}
[data-theme="dark"] .gpf-a .ic{color:#8fc0f2}
[data-theme="dark"] .gpf-a:active{background:#22304a;border-color:#3d6ea0}
@media (prefers-reduced-motion:reduce){.gmem-b,.gpf-a{transition:none}}
/* thread header with back button */
.gthread-head{display:flex;align-items:center;gap:8px;padding:2px 0 8px;border-bottom:1px solid var(--line,#e6e9ef);margin-bottom:8px}
.g-back{border:none;background:none;color:var(--navy,#22345f);font-weight:700;font-size:15px;padding:4px 6px 4px 0;cursor:pointer;flex:0 0 auto}
[data-theme="dark"] .g-back{color:#9cc0ff}
.gthread-title{flex:1;font-weight:800;font-size:15px;color:var(--heading,#0b2036);white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
[data-theme="dark"] .gthread-title{color:#eaf0fa}
.gthread-actions{display:flex;gap:6px;flex:0 0 auto}
.gchan-chip.on{background:#032b49;color:#fff;border-color:#032b49}
.gchan-chip.unread:not(.on){box-shadow:0 0 0 2px #e11d2a inset}
.chip-at{color:#e11d2a}
.gchan-none{color:#7a8699;font-size:13px;padding:6px}
.gchat-head{display:flex;gap:8px;margin-bottom:6px}
.g-pinbtn{border:1px solid #d3dae6;background:#fff;border-radius:10px;padding:6px 12px;font-weight:700;color:#0b2036}
.gbubble.prio{border:2px solid #e11d2a}
.gbubble.prio .bm b{color:#c0182a}
.grxbar{display:flex;flex-wrap:wrap;gap:4px;margin-top:5px}
.grx{border:1px solid #d3dae6;background:#fff;border-radius:12px;padding:1px 8px;font-size:13px;line-height:1.6}
.grx.on{background:#e7f0ff;border-color:#8fb3ff}
.grx.add{color:#8894a8;font-weight:700}
.gack{display:block;margin-top:6px;font-weight:800;border-radius:10px;padding:9px;text-align:center;border:none;width:100%}
.gack.btn-copy{background:var(--grad-primary,#0a6bb0);color:#fff;font-size:15px;letter-spacing:.5px}
.gack.done{background:#e6f6ec;color:#1e7a44}
/* Radio bar (officer) */
.rad-bar{position:fixed;left:12px;right:12px;bottom:12px;width:auto;background:#0b1220;color:#fff;border-radius:16px;box-shadow:0 8px 30px rgba(0,0,0,.5);padding:14px;z-index:100000}
.rad-top{display:flex;align-items:center;gap:6px;font-size:12px;margin-bottom:10px}
.rad-live{color:#38d996;font-weight:800}
.rad-mem{margin-left:auto;color:#9fb0c8}
.rad-x{background:transparent;border:none;color:#9fb0c8;font-size:18px;margin-left:6px}
.rad-status{font-size:14px;text-align:center;margin-bottom:10px;color:#9fb0c8;min-height:20px}
.rad-status.me{color:#38d996;font-weight:700}.rad-status.other{color:#ff9d6b;font-weight:700}
.rad-ptt{width:100%;padding:26px 0;border:none;border-radius:14px;background:#032b49;color:#fff;font-size:18px;font-weight:800;user-select:none;-webkit-user-select:none;touch-action:none}
.rad-ptt:active,.rad-ptt.on{background:#e11d2a}
/* Inline icon system (officer) */
.ic{width:18px;height:18px;vertical-align:-4px;flex:0 0 auto}
.gicon .ic{width:22px;height:22px;vertical-align:-5px}
.gib-sec .ic{width:14px;height:14px;vertical-align:-2px;margin-right:5px}
.badge-chip .ic{width:14px;height:14px;vertical-align:-3px}
.gwx-ic .ic{width:24px;height:24px;vertical-align:middle}
.break-t .ic{width:16px;height:16px;vertical-align:-3px;margin-right:3px}
.gai .ic{width:15px;height:15px;vertical-align:-2px;margin-right:3px}
.gaw .ic{width:14px;height:14px;vertical-align:-2px}
.gib-new .ic{width:15px;height:15px;vertical-align:-3px;margin-right:3px}
.gdir-chip .ic{width:15px;height:15px;vertical-align:-3px;margin-right:3px}
.gib-pin .ic{width:16px;height:16px;vertical-align:-3px}
.site .ic{width:15px;height:15px;vertical-align:-2px;margin-right:4px}
.gbtn .ic,.photo-add .ic{margin-right:6px;vertical-align:-3px}
.bm .ic{width:12px;height:12px;vertical-align:-1px}
.po-file .ic,.gatt-file .ic,.course-les a .ic,.ann-kind .ic{width:15px;height:15px;vertical-align:-2px;margin-right:3px}
/* Quoted replies (officer) */
.reply-quote{border-left:3px solid #8fa8d8;background:rgba(0,0,0,.06);padding:3px 8px;margin-bottom:4px;font-size:12px;opacity:.9;border-radius:4px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
.g-reply{display:flex;align-items:center;gap:8px;background:#e7f0fa;border-left:3px solid #0a6bb0;padding:8px 10px;font-size:13px}
.g-reply span{flex:1;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
.g-reply-x{border:none;background:transparent;color:#888;font-size:16px}

/* ============================================================
   MODERN REFRESH 2026 (officer app) — appended overrides
   UNIFIED COMMAND (2026-07): logo navy/blue · red = danger only
   ============================================================ */
@font-face{font-family:'Saira';font-style:normal;font-weight:600;font-display:swap;src:url('fonts/saira-600.woff2') format('woff2')}
@font-face{font-family:'Saira';font-style:normal;font-weight:700;font-display:swap;src:url('fonts/saira-700.woff2') format('woff2')}
@font-face{font-family:'Saira';font-style:normal;font-weight:800;font-display:swap;src:url('fonts/saira-800.woff2') format('woff2')}
@font-face{font-family:'JetBrains Mono';font-style:normal;font-weight:400;font-display:swap;src:url('fonts/jetbrains-mono-400.woff2') format('woff2')}
@font-face{font-family:'JetBrains Mono';font-style:normal;font-weight:700;font-display:swap;src:url('fonts/jetbrains-mono-700.woff2') format('woff2')}
:root{--navy:#032b49;--navy-2:#005080;--red:#e11d2a;--ink:#16222f;--muted:#5f7288;--line:#e4eaf1;--bg:#eaf0f6;--green:#0f9d58;--amber:#e08600;
  /* previously-phantom vars (consumed via fallbacks below) — now real */
  --panel:#f8fafc;--card:#fff;--heading:#0b2036;
  /* Unified Command palette */
  --blue:#0a6bb0;--blue-deep:#005080;--blue-bright:#1a86d0;--blue-light:#4aa3dd;
  --grad-primary:linear-gradient(90deg,#005080,#0a6bb0);
  --grad-header-mobile:linear-gradient(165deg,#032b49,#005080);
  --font-body:system-ui,-apple-system,"Segoe UI",Roboto,Helvetica,Arial,sans-serif;
  --font-display:'Saira','Segoe UI Variable Display','Segoe UI',var(--font-body);
  --font-mono:'JetBrains Mono',ui-monospace,'Cascadia Mono',Menlo,Consolas,monospace;
  /* ---- MOTION — the same three tokens as the console (styles.css :root, DESIGN-SYSTEM.md §7).
     Identical values on purpose: the officer app and the console are one product, and a sheet
     should not open at a different speed depending on which screen you are holding.
       --t-fast 150ms  hover/reveal/colour · --t-med 200ms  something arriving or leaving
       --ease   cubic-bezier(.2,.7,.3,1) — already this file's swipe-settle curve.
     NOT the same as --press-ms (.09s, section 14). Press feedback is deliberately faster than
     the house token: a tap must feel like it landed before the finger lifts, and 150ms there
     reads as a laggy button on a mid-range Android. Two different jobs, two numbers. */
  --t-fast:150ms;--t-med:200ms;--ease:cubic-bezier(.2,.7,.3,1)}
html{-webkit-font-smoothing:antialiased}
.ghead{background:var(--grad-header-mobile,linear-gradient(165deg,#032b49,#005080));padding:16px 18px;box-shadow:0 2px 14px rgba(3,18,34,.16)}
.gmain{padding:18px;padding-bottom:calc(66px + env(safe-area-inset-bottom,0px))}
/* cards: softer depth, larger radius */
.duty-card,.shift-card,.av-row,.note-card,.gbubble,.rw-item{box-shadow:0 1px 2px rgba(18,28,48,.05),0 6px 18px rgba(18,28,48,.05)}
.shift-card{border-radius:16px;padding:15px 17px}
.duty-card{border-radius:20px}
/* buttons: focus + subtle depth. The press treatment used to live here as translateY(1px); it now
   comes from the one house rule in section 14 so every control presses the same way. */
.bigbtn{box-shadow:0 6px 18px rgba(20,28,48,.16)}
.bigbtn.in{background:var(--grad-primary,linear-gradient(90deg,#005080,#0a6bb0));box-shadow:0 6px 16px rgba(0,80,128,.28)}
.bigbtn.out{background:#fff;color:#5f7288;border:1px solid #e0e7ef;box-shadow:0 2px 8px rgba(18,28,48,.08)}
.gbtn{box-shadow:0 2px 8px rgba(20,28,48,.14)}
.gbtn.ghost{box-shadow:none}
input:focus,select:focus,textarea:focus{outline:none;border-color:#1a86d0;box-shadow:0 0 0 3px rgba(26,134,208,.28)}
.glogin input{border-radius:12px}
/* bottom nav: floating glassy bar */
.gnav{background:rgba(255,255,255,.92);backdrop-filter:saturate(1.4) blur(10px);-webkit-backdrop-filter:saturate(1.4) blur(10px);border-top:1px solid var(--line);box-shadow:0 -4px 20px rgba(18,28,48,.06)}
.gnav button.on{color:#0a6bb0;font-weight:800}
.gnav button.on .ic{transform:translateY(-1px)}
/* ---- Cohesive SVG nav icon system (2026) ---- */
.gnav button{position:relative;transition:color var(--t-fast) var(--ease);-webkit-tap-highlight-color:transparent}
.gnav button .ic{font-size:0;display:flex;align-items:center;justify-content:center;height:24px}
.gnav button .ic svg{width:23px;height:23px;transition:transform var(--t-fast) var(--ease)}
.gnav button.on .ic svg{stroke-width:2.2}
.gnav button.on::before{content:"";position:absolute;top:0;left:50%;transform:translateX(-50%);width:26px;height:3px;border-radius:0 0 3px 3px;background:#0a6bb0}
.gnav button:active .ic svg{transform:scale(.9)}
.gnav button[data-badge]::after{content:"";position:absolute;top:6px;right:calc(50% - 15px);width:8px;height:8px;border-radius:50%;background:var(--red);box-shadow:0 0 0 2px #fff}
.section-title{letter-spacing:.08em}
/* ---- Officer pay card + stubs ---- */
.pay-card{background:#fff;border:1px solid var(--line);border-radius:14px;padding:14px;margin:12px 0}
.pay-grid{display:grid;grid-template-columns:1fr 1fr;gap:10px}
.pay-k{font-size:11px;font-weight:700;letter-spacing:.05em;text-transform:uppercase;color:var(--muted,#697690)}
.pay-v{font-family:var(--font-display,inherit);font-size:24px;font-weight:800;color:var(--heading,#0b2036)}
.pay-sub{font-size:12px;color:var(--muted,#697690);margin-top:8px}
.pay-last{font-size:13px;margin-top:8px;padding-top:8px;border-top:1px solid var(--line)}
.pay-note{font-size:11px;color:var(--muted,#98a2b3);margin-top:6px;font-style:italic}
.stub-card{border:1px solid var(--line);border-radius:12px;padding:12px;margin:8px 0}
.stub-top{display:flex;justify-content:space-between;align-items:center;font-size:19px}
.stub-paid{font-size:12px;color:#0d7a45;font-weight:700}
.stub-pend{font-size:12px;color:#e08600;font-weight:700}
.stub-sub{font-size:12px;color:var(--muted,#697690);margin-top:3px}
.stub-line{display:flex;justify-content:space-between;font-size:13px;margin-top:6px}
.stub-line.net{border-top:1px solid var(--line);padding-top:6px;margin-top:8px;font-weight:700}
.sigpad{width:100%;height:120px;border:1px dashed #b9c2d2;border-radius:8px;background:#fff;touch-action:none;display:block}
.ff-help{font-size:12px;color:#5f7288;margin:-2px 0 5px}
/* My hours (self-service timesheet) */
.gh-nav{display:flex;align-items:center;justify-content:space-between;gap:8px;margin:2px 0 10px;font-size:14px}
.gh-list{margin-top:10px}
.gh-row{display:flex;justify-content:space-between;align-items:center;gap:10px;background:var(--card,#fff);border:1px solid var(--line,#e3e7ee);border-radius:10px;padding:9px 12px;margin-bottom:7px;font-size:13.5px}
.gh-row.open{border-left:4px solid #0f9d58}
.gh-t{font-variant-numeric:tabular-nums;color:#3a4356}
.gh-h{text-align:right;min-width:52px}
.gh-open{color:#0d7a45;font-weight:700;font-size:12px}
.gh-done{background:#e4f6ec;color:#0d7a45;border-radius:10px;padding:10px 14px;margin-top:10px;font-size:13.5px;font-weight:600;text-align:center}
/* supervisor coaching notes on duty screen */
.coach-need{background:#fff;border:2px solid #e11d2a;border-radius:14px;padding:13px 15px;margin-bottom:12px}
.coach-need-t{font-weight:800;font-size:14px;color:#b3261e;margin-bottom:6px}
.coach-need-b{font-size:14px;white-space:pre-wrap;margin-bottom:6px}
.coach-need .gbtn{margin-top:8px}
.coach-kudos{background:#e4f6ec;color:#0d7a45;border-radius:12px;padding:11px 14px;margin-bottom:12px;font-size:13.5px}
/* welfare check + pass-downs + patrol runs */
.welfare-card{background:#fff;border:3px solid #e11d2a;border-radius:16px;padding:15px;margin-bottom:12px;text-align:center;animation:wpulse 1.4s ease-in-out infinite}
.welfare-t{font-weight:800;font-size:16px;color:#c0182a}
@keyframes wpulse{0%,100%{box-shadow:0 0 0 0 rgba(225,29,42,.35)}50%{box-shadow:0 0 0 8px rgba(225,29,42,0)}}
.pd-card{background:var(--card,#fff);border:1px solid var(--line,#e3e7ee);border-radius:14px;padding:13px 15px;margin-bottom:12px}
.pd-row{font-size:13.5px;padding:8px 0;border-bottom:1px solid #f0f2f7;white-space:pre-wrap}
.pd-row:last-of-type{border-bottom:none}
.pd-row.bolo{background:#fff5f5;border-radius:8px;padding:8px 10px;border:1px solid #f3c2c0;margin:6px 0}
.pd-bolo{color:#b3261e;font-weight:800;font-size:12px}
.run-card .run-stop{display:flex;align-items:center;gap:8px;font-size:13.5px;padding:7px 0;border-bottom:1px solid #f0f2f7;flex-wrap:wrap}
.run-card .run-stop:last-of-type{border-bottom:none}
.run-card .run-stop.done{color:#0d7a45}
.run-n{display:inline-flex;width:22px;height:22px;border-radius:50%;background:#eef1f6;color:#3a4356;font-size:11px;font-weight:800;align-items:center;justify-content:center;flex:0 0 auto}
.run-stop.done .run-n{background:#e4f6ec;color:#0d7a45}
.po-acked{color:#177a43;font-weight:700;font-size:12.5px;margin-top:8px}

/* ============================================================
   PHASE D 2026 — Officer app redesign (design: Officer App.dc)
   ============================================================ */
@keyframes usgpulse{0%,100%{opacity:1;transform:scale(1)}50%{opacity:.35;transform:scale(.82)}}
@keyframes usgblink{0%,49%{opacity:1}50%,100%{opacity:.25}}
/* --- login: full-bleed dark glass --- */
.glogin{background:linear-gradient(170deg,#032b49 0%,#01192b 70%);flex-direction:column;padding:0 28px}
.glogin form{background:transparent;box-shadow:none;border-radius:0;padding:0;max-width:360px;width:100%;flex:1;display:flex;flex-direction:column;justify-content:center;gap:11px;margin:0 auto}
.glogin .gbrand{text-align:center;margin-bottom:15px}
.glogin .glogo{width:76px;height:76px;filter:drop-shadow(0 6px 18px rgba(0,0,0,.4))}
.glogin .gbrand .u{font-family:var(--font-display);font-weight:800;font-size:30px;color:#fff;letter-spacing:.02em;margin-top:14px;line-height:1}
.glogin .gbrand .s{font-family:var(--font-display);font-weight:600;font-size:11px;letter-spacing:.34em;color:#4aa3dd;margin-top:5px}
.glogin .gbrand .t{display:inline-block;font-family:var(--font-display);font-weight:700;font-size:10px;letter-spacing:.22em;color:#9dbdd6;background:rgba(255,255,255,.08);border:1px solid rgba(255,255,255,.14);border-radius:20px;padding:5px 13px;margin-top:12px}
.glogin input{padding:15px 16px;border-radius:13px;border:1px solid rgba(255,255,255,.16);background:rgba(255,255,255,.07);color:#fff;font-size:15px}
.glogin input::placeholder{color:rgba(255,255,255,.45)}
.glogin input:focus{border-color:#1a86d0;background:rgba(255,255,255,.11);box-shadow:none}
.glogin input[type=password]{letter-spacing:.15em}
.glogin button[type=submit]{padding:16px;border-radius:13px;background:linear-gradient(90deg,#005080,#0a6bb0);font-size:15.5px;font-weight:700;box-shadow:0 8px 22px rgba(0,80,128,.4);margin-top:4px}
.gl-foot{padding-bottom:calc(34px + env(safe-area-inset-bottom,0px));text-align:center;font-size:11px;color:#51667c}
.gl-foot span{display:inline-flex;align-items:center;gap:6px}
.gl-foot i{width:6px;height:6px;border-radius:50%;background:#3fbf7a;display:inline-block}
.gl-foot.off i{background:#8593a5}
/* --- login: phone-code (OTP) sign-in ---------------------------------------------------------
   The login screen is the one surface that is dark in BOTH app themes (.glogin hardcodes its
   gradient), so these rules deliberately use literal glass values rather than theme vars — the
   same way the password form above does. Everything is sized for a 360px phone held one-handed:
   the thing you tap is always the widest, lowest element in its step. */
.gl-alt{margin-top:18px}
.gl-or{display:flex;align-items:center;gap:12px;color:rgba(255,255,255,.38);font-size:11px;font-weight:600;letter-spacing:.18em;text-transform:uppercase}
.gl-or::before,.gl-or::after{content:"";flex:1;height:1px;background:rgba(255,255,255,.14)}
.glogin button.gl-alt-btn{width:100%;margin-top:14px;display:flex;align-items:center;justify-content:center;gap:9px;padding:14px;border-radius:13px;background:rgba(255,255,255,.07);border:1px solid rgba(255,255,255,.18);color:#dbe8f4;font-size:14.5px;font-weight:700;box-shadow:none}
.glogin button.gl-alt-btn:active{background:rgba(255,255,255,.13)}
.gl-alt-btn svg{width:17px;height:17px;flex:none;color:#4aa3dd}
#g-otp-form #gotp-s-phone,#g-otp-form #gotp-s-code{display:flex;flex-direction:column;gap:11px}
.gotp-h{font-family:var(--font-display,inherit);font-weight:800;font-size:20px;color:#fff;text-align:center;letter-spacing:.01em}
.gotp-sub{font-size:13px;line-height:1.55;color:#9dbdd6;text-align:center;margin-top:-4px}
.gotp-sub b{color:#dbe8f4;font-weight:700;white-space:nowrap}
/* Inline "Change number": the negative margin cancels the padding's effect on the sentence it sits
   in, so the touch area grows to a thumb-sized box without moving a single pixel of the line. */
.glogin button.gotp-inline{display:inline-block;width:auto;padding:10px 6px;margin:-10px -6px;background:none;border:none;box-shadow:none;color:#4aa3dd;font-size:13px;font-weight:700;text-decoration:underline;text-underline-offset:2px}
.glogin input.gotp-code{text-align:center;font-size:30px;font-weight:700;letter-spacing:.42em;text-indent:.42em;padding:16px 10px;font-family:ui-monospace,'SF Mono',Menlo,Consolas,monospace}
.glogin input.gotp-code::placeholder{letter-spacing:.42em;color:rgba(255,255,255,.22)}
.gotp-resend{text-align:center}
.glogin button#gotp-resend{width:auto;padding:13px 16px;background:none;border:none;box-shadow:none;color:#4aa3dd;font-size:13.5px;font-weight:700}
.glogin button#gotp-resend[disabled]{color:#617d95;font-weight:600}
.gotp-back{text-align:center;margin-top:6px}
.gotp-back a{color:#8593a5;font-size:13px;text-decoration:none;display:inline-block;padding:8px 10px}
/* --- app header: brand + duty pill + who --- */
.gapp{background:#eef2f7}
.ghead{padding:calc(14px + env(safe-area-inset-top,0px)) 18px 14px;display:flex;align-items:center;gap:9px}
.ghead .gh-ic{width:26px;height:26px;display:block;flex:none}
.ghead .u{font-family:var(--font-display);font-weight:800;font-size:15px;letter-spacing:.02em;line-height:1}
.ghead .u span{display:block;font-weight:600;font-size:7.5px;letter-spacing:.3em;color:#4aa3dd;margin-top:2px}
.gh-pill{font-size:10.5px;font-weight:800;letter-spacing:.08em;padding:4px 10px;border-radius:20px;display:inline-flex;align-items:center;gap:6px;margin-left:4px;white-space:nowrap}
.gh-pill i{width:6px;height:6px;border-radius:50%;display:inline-block}
.gh-pill.on{background:rgba(15,157,88,.18);border:1px solid rgba(63,191,122,.5);color:#7fe0a9}
.gh-pill.on i{background:#3fbf7a;animation:usgpulse 1.6s infinite}
.gh-pill.off{background:rgba(255,255,255,.1);border:1px solid rgba(255,255,255,.22);color:#b9c9d8}
.gh-pill.off i{background:#93a3b5}
.ghead .who{margin-left:auto;text-align:right;line-height:1.2}
.ghead .who #g-name{font-size:12.5px;font-weight:700}
.ghead .who a{font-size:10.5px;color:#9dbdd6}
/* --- mono live clock --- */
.g-clock{text-align:center;font-family:var(--font-mono);font-size:13px;font-weight:700;color:#5f7288;font-variant-numeric:tabular-nums;letter-spacing:.02em;padding:2px 0 0;margin-bottom:12px}
/* --- duty: current-post / upcoming card --- */
.gd-card{background:var(--card,#fff);border:1px solid var(--line,#e4eaf1);border-radius:16px;padding:16px;box-shadow:0 1px 2px rgba(18,28,48,.05),0 8px 22px rgba(18,28,48,.05);margin-bottom:12px}
.gd-kick{font-size:10.5px;font-weight:800;letter-spacing:.1em;color:#8593a5;text-transform:uppercase}
.gd-row{display:flex;align-items:center;gap:8px}
.glive{display:inline-flex;align-items:center;gap:5px;background:#e4f6ec;color:#0d7a45;font-size:11px;font-weight:700;padding:3px 9px;border-radius:20px;margin-left:auto}
.glive i{width:6px;height:6px;border-radius:50%;background:#0f9d58;animation:usgpulse 1.6s infinite}
.gd-upill{background:#fdf0dc;color:#a8660a;font-size:10.5px;font-weight:800;letter-spacing:.04em;padding:3px 9px;border-radius:20px;margin-left:auto;white-space:nowrap}
.gd-title{font-family:var(--font-display);font-weight:700;font-size:19px;color:var(--heading,#0b2036);margin-top:8px}
.gd-meta{font-size:12.5px;color:#5f7288;margin-top:2px}
.gd-prog{display:flex;align-items:center;gap:10px;margin-top:13px}
.gd-prog .t{font-family:var(--font-mono);font-size:11.5px;font-weight:700;color:#0b2036}
.gd-prog .t.end{color:#8593a5}
.gd-track{flex:1;height:6px;border-radius:20px;background:#e9eff5;overflow:hidden}
.gd-track span{display:block;height:100%;border-radius:20px;background:linear-gradient(90deg,#0a6bb0,#4aa3dd)}
.gd-foot{display:flex;align-items:center;justify-content:space-between;gap:10px;margin-top:11px;font-size:12px;color:#5f7288}
.gd-foot .mono{font-family:var(--font-mono);font-weight:700;color:#0b2036}
.gd-ghostbtn{background:#fff;color:#5f7288;border:1px solid #e0e7ef;border-radius:10px;padding:9px 14px;font-size:12.5px;font-weight:700;white-space:nowrap;cursor:pointer}
/* --- welfare v2 (amber) --- */
.welfare-card{background:#fff;border:1.5px solid #f3ddb5;border-radius:16px;padding:14px 16px;text-align:left;animation:none}
.wf-head{display:flex;align-items:center;gap:8px}
.wf-dot{width:8px;height:8px;border-radius:50%;background:#e08600;animation:usgblink 1.1s infinite;flex:none}
.wf-kick{font-size:11px;font-weight:800;letter-spacing:.08em;color:#a8660a}
.wf-by{margin-left:auto;font-family:var(--font-mono);font-size:11px;color:#a8660a}
.wf-body{font-size:13px;color:#3a5064;margin-top:7px}
.wf-btns{display:flex;gap:8px;margin-top:11px}
.wf-ok{flex:1.6;background:#0f9d58;color:#fff;border:none;border-radius:11px;padding:13px;font-size:14px;font-weight:800;cursor:pointer}
.wf-help{flex:1;background:#fff;color:#c0182a;border:1.5px solid #f3c3c3;border-radius:11px;padding:13px;font-size:13px;font-weight:700;cursor:pointer}
/* --- next checkpoint banner --- */
.gd-next{background:#e7f0fa;border:1px solid #cfe3f5;border-radius:16px;padding:13px 15px;display:flex;align-items:center;gap:12px;margin-bottom:12px}
.gd-next .tile{width:38px;height:38px;border-radius:11px;background:#fff;display:flex;align-items:center;justify-content:center;flex:none}
.gd-next .tile svg{width:19px;height:19px;stroke:#0a6bb0;fill:none}
.gd-next b{font-size:13.5px;font-weight:700;color:#0b2036;display:block}
.gd-next small{font-size:11.5px;color:#3a5064;margin-top:1px;display:block}
.gd-next small .mono{font-family:var(--font-mono);font-weight:700}
.gd-scan{margin-left:auto;background:linear-gradient(90deg,#005080,#0a6bb0);color:#fff;border:none;border-radius:10px;padding:11px 14px;font-size:12.5px;font-weight:700;flex:none;cursor:pointer}
/* --- quick-action tiles --- */
.gd-tiles{display:grid;grid-template-columns:1fr 1fr;gap:10px;margin-bottom:12px}
.gd-tile{background:#fff;border:1px solid #e4eaf1;border-radius:14px;padding:14px;display:flex;flex-direction:column;align-items:flex-start;gap:8px;cursor:pointer;font-family:inherit;text-align:left}
.gd-tile .sq{width:36px;height:36px;border-radius:10px;display:flex;align-items:center;justify-content:center}
.gd-tile .sq svg{width:17px;height:17px;fill:none;stroke-width:2}
.gd-tile b{font-size:12.5px;font-weight:700;color:#16222f}
.gd-tile .sq.red{background:#fdeaea}.gd-tile .sq.red svg{stroke:#c0182a}
.gd-tile .sq.grey{background:#eef1f5}.gd-tile .sq.grey svg{stroke:#5f7288}
.gd-tile .sq.blue{background:#e7f0fa}.gd-tile .sq.blue svg{stroke:#0a6bb0}
/* --- bottom nav polish --- */
.gnav{padding-bottom:env(safe-area-inset-bottom,0px)}
.gnav button{color:#8ea0b3;font-size:9.5px;font-weight:700}
.gnav button.on{color:#0a6bb0;font-weight:800}
/* --- shifts: hero header + week strip + cards (design: Shifts) --- */
.gsh-hero{background:var(--grad-header-mobile,linear-gradient(165deg,#032b49,#005080));margin:-18px -18px 12px;padding:18px 18px 16px;border-radius:0 0 18px 18px;color:#fff}
.gsh-trow{display:flex;align-items:center}
.gsh-trow b{font-family:var(--font-display);font-weight:800;font-size:21px;flex:1}
.gsh-trow span{font-family:var(--font-mono);font-size:12px;color:#9dbdd6}
.gsh-strip{display:flex;gap:5px;margin-top:13px}
.gsh-day{flex:1;text-align:center;border-radius:10px;padding:7px 0}
.gsh-day .dw{display:block;font-size:9px;font-weight:800;color:#7d9cb5}
.gsh-day .dn{display:block;font-family:var(--font-display);font-size:14px;font-weight:700;color:#c9dcea}
.gsh-day.today{background:rgba(255,255,255,.16);border:1px solid rgba(255,255,255,.3)}
.gsh-day.today .dw{color:#bcd7ea}
.gsh-day.today .dn{font-weight:800;color:#fff}
.gsh-day i{display:block;width:4px;height:4px;border-radius:50%;background:#4aa3dd;margin:2px auto 0}
.gsh-card{background:var(--card,#fff);border:1px solid var(--line,#e4eaf1);border-radius:15px;padding:14px;position:relative;overflow:hidden;margin-bottom:10px}
.gsh-card.hero{box-shadow:0 1px 2px rgba(18,28,48,.05),0 8px 22px rgba(18,28,48,.05)}
.gsh-accent{position:absolute;left:0;top:0;bottom:0;width:4px;background:linear-gradient(180deg,#0a6bb0,#003050)}
.gsh-eye{display:flex;align-items:center;gap:8px}
.gsh-eye>span:first-child{font-size:10.5px;font-weight:800;letter-spacing:.06em;color:#8593a5}
.gsh-eye>span.blue{color:#0a6bb0}
.gsh-eye .glive{margin-left:auto;font-size:10px;font-weight:800}
.gsh-site{font-family:var(--font-display);font-weight:700;font-size:15px;color:var(--heading,#0b2036);margin-top:5px}
.gsh-card.hero .gsh-site{font-size:16px}
.gsh-meta{font-size:12px;color:#5f7288;margin-top:2px}
.gsh-meta .mono{font-family:var(--font-mono);font-weight:700;color:#0b2036}
.gsh-card.open{border:1px dashed #f0a9b1}
.gsh-bonus{margin-left:auto;background:#fdeaea;color:#c0182a;font-size:9.5px;font-weight:800;padding:3px 8px;border-radius:6px;white-space:nowrap}
.gsh-open-row{display:flex;align-items:center;gap:8px;margin:6px 0 10px}
.gsh-open-row span:first-child{font-size:10.5px;font-weight:800;letter-spacing:.08em;color:#5f7288}
.gsh-open-row .cnt{background:#fdeaea;color:#c0182a;font-family:var(--font-mono);font-weight:700;font-size:10px;padding:2px 7px;border-radius:20px}
.gsh-claim{width:100%;margin-top:11px;background:linear-gradient(90deg,#005080,#0a6bb0);color:#fff;border:none;border-radius:11px;padding:12px;font-size:13px;font-weight:700;cursor:pointer;font-family:inherit}
.gsh-claim.ghost{background:#fff;color:#0a6bb0;border:1.5px solid #0a6bb0;padding:11px}
.gsh-claim[disabled]{opacity:.55;cursor:default}
/* --- multi-guard events on the officer board ---------------------------------------------------
   The pill is the number ("1 of 3 spots left"); the bar is the same fact as a shape, filled with
   what is TAKEN so a nearly-full event looks nearly full at arm's length. A FULL event keeps its
   card and stops being red-dashed: it is not a gap any more, it is simply not available. */
.gsh-spots{margin-left:auto;display:inline-flex;align-items:center;gap:4px;background:#e8f1fa;color:#0a5a94;font-size:9.5px;font-weight:800;padding:3px 8px;border-radius:6px;white-space:nowrap}
.gsh-spots svg{width:11px;height:11px}
.gsh-spots.full{background:#eef1f5;color:#5f7288}
.gsh-card.open.full{border-style:solid;border-color:var(--line,#e4eaf1)}
.gsh-prog{height:5px;border-radius:3px;background:#e8edf3;overflow:hidden;margin-top:10px}
.gsh-prog>span{display:block;height:100%;border-radius:3px;background:linear-gradient(90deg,#005080,#0a6bb0);transition:width .22s cubic-bezier(.22,1,.36,1)}
.gsh-mine{margin-top:11px;display:flex;align-items:center;gap:7px;justify-content:center;background:#e9f6ee;color:#0d7a45;border-radius:11px;padding:11px;font-size:12.5px;font-weight:700}
.gsh-mine svg{width:14px;height:14px}
@media (prefers-reduced-motion:reduce){.gsh-prog>span{transition:none}}
/* --- tour route engine (design: Tour) --- */
.gt-hero{background:var(--grad-header-mobile,linear-gradient(165deg,#032b49,#005080));margin:-18px -18px 12px;padding:18px 18px 16px;border-radius:0 0 18px 18px;color:#fff}
.gt-hero b{font-family:var(--font-display);font-weight:800;font-size:19px;line-height:1.1;display:block}
.gt-hero small{font-size:12px;color:#9dbdd6;margin-top:2px;display:block}
.gt-hero .cnt{font-family:var(--font-mono);font-weight:700;font-size:15px;color:#7fd0ff;flex:none}
.gt-bar{margin-top:13px;height:7px;border-radius:20px;background:rgba(255,255,255,.14);overflow:hidden}
.gt-bar span{display:block;height:100%;border-radius:20px;background:linear-gradient(90deg,#4aa3dd,#7fd0ff)}
.gt-cp{background:#fff;border:1px solid #e4eaf1;border-radius:13px;padding:11px 13px;display:flex;align-items:center;gap:11px;margin-bottom:8px}
.gt-cp b{font-size:13.5px;font-weight:700;color:#5f7288;display:block}
.gt-cp small{font-size:11.5px;color:#a9b6c4;display:block}
.gt-cp.done{background:#f0faf4;border-color:#b7e6ca;opacity:.85}
.gt-cp.done b{color:#16222f}
.gt-cp.done small{color:#8593a5}
.gt-cp.skip{background:#fdf7ec;border-color:#f3ddb5;opacity:.9}
.gt-cp.skip b{color:#16222f}
.gt-cp.skip small{color:#a8660a}
.gt-ic{width:26px;height:26px;border-radius:50%;display:flex;align-items:center;justify-content:center;flex:none}
.gt-ic.ok{background:#0f9d58}
.gt-ic.ok svg{width:13px;height:13px}
.gt-ic.amber{background:#fdf0dc;color:#a8660a;font-weight:800;font-size:13px}
.gt-ic.num{background:#eef1f5;border:2px dashed #c2cedd;font-size:10.5px;font-weight:800;color:#8ea0b3}
.gt-ic.pulse{background:#e7f0fa;border:2px solid #0a6bb0}
.gt-ic.pulse i{width:9px;height:9px;border-radius:50%;background:#0a6bb0;animation:usgpulse 1.4s infinite}
.gt-time{font-family:var(--font-mono);font-size:11px;color:#0d7a45;font-weight:700;flex:none}
.gt-time.amber{color:#a8660a}
.gt-cp.next{display:block;background:#fff;border:2px solid #0a6bb0;border-radius:15px;padding:14px;box-shadow:0 8px 22px rgba(0,80,128,.16);opacity:1}
.gt-cp.next b{font-size:14px;font-weight:800;color:#0b2036}
.gt-cp.next small{font-size:11px;color:#3a5064}
.gt-cp.next small .mono{font-family:var(--font-mono);font-weight:700}
.gt-sla{font-size:10px;font-weight:800;color:#a8660a;background:#fdf0dc;border-radius:99px;padding:1px 7px;margin-left:4px;display:inline-block}
.gt-nextbadge{background:#e7f0fa;color:#0a5a94;font-size:9.5px;font-weight:800;padding:3px 8px;border-radius:6px;letter-spacing:.05em;flex:none}
.gt-scanbtn{width:100%;margin-top:12px;background:linear-gradient(90deg,#005080,#0a6bb0);color:#fff;border:none;border-radius:12px;padding:13px;font-size:14px;font-weight:700;display:flex;align-items:center;justify-content:center;gap:9px;box-shadow:0 6px 16px rgba(0,80,128,.3);cursor:pointer;font-family:inherit}
.gt-scanbtn svg{width:17px;height:17px;flex:none}
.gt-skipbtn{width:100%;margin-top:7px;background:#fff;color:#5f7288;border:1px solid #e0e7ef;border-radius:12px;padding:11px;font-size:12.5px;font-weight:700;cursor:pointer;font-family:inherit}
[data-theme="dark"] .gt-cp{background:#1a2231;border-color:#26303f}
[data-theme="dark"] .gt-cp b{color:#9fb2c4}
[data-theme="dark"] .gt-cp.done{background:#14261c;border-color:#1d5c3a}
[data-theme="dark"] .gt-cp.done b,[data-theme="dark"] .gt-cp.skip b{color:#e6eaf2}
[data-theme="dark"] .gt-cp.skip{background:#261f10;border-color:#5c4a1e}
[data-theme="dark"] .gt-cp.next{background:#1a2231}
[data-theme="dark"] .gt-cp.next b{color:#e6eaf2}
[data-theme="dark"] .gt-ic.num{background:#232f42;border-color:#3a4a5e}
[data-theme="dark"] .gt-ic.pulse{background:#14263a}
[data-theme="dark"] .gt-skipbtn{background:#1a2231;border-color:#26303f;color:#9fb2c4}
/* --- incident report v2 composer (design: Report) --- */
.irc-ov{position:fixed;inset:0;z-index:90;background:#eef2f7;display:flex;flex-direction:column;max-width:480px;margin:0 auto}
.irc-head{background:var(--grad-header-mobile,linear-gradient(165deg,#032b49,#005080));padding:calc(16px + env(safe-area-inset-top,0px)) 18px 16px;color:#fff;display:flex;align-items:center;gap:10px;flex:none}
.irc-head b{font-family:var(--font-display);font-weight:800;font-size:19px;display:block}
.irc-head small{font-size:12px;color:#9dbdd6;margin-top:2px;display:block}
.irc-x{background:none;border:none;padding:4px;cursor:pointer;flex:none}
.irc-x svg{width:20px;height:20px}
.irc-body{flex:1;overflow-y:auto;padding:14px 16px;display:flex;flex-direction:column;gap:13px}
.irc-lbl{font-size:10.5px;font-weight:800;letter-spacing:.08em;color:#5f7288;margin-bottom:7px;text-transform:uppercase}
.irc-types{display:grid;grid-template-columns:1fr 1fr 1fr;gap:7px}
.irc-type{background:#fff;color:#5f7288;border:1px solid #e4eaf1;border-radius:10px;padding:10px 6px;text-align:center;font-size:11.5px;font-weight:600;cursor:pointer;font-family:inherit}
.irc-type.on{background:linear-gradient(90deg,#005080,#0a6bb0);color:#fff;border-color:transparent;font-weight:700;box-shadow:0 4px 12px rgba(0,80,128,.25)}
.irc-sevs{display:flex;gap:3px;background:#e6edf4;border-radius:11px;padding:3px}
.irc-sev{flex:1;border:none;background:transparent;border-radius:9px;padding:11px 4px;font-size:12.5px;font-weight:600;color:#5f7288;cursor:pointer;font-family:inherit}
.irc-sev.on{background:#e08600;color:#fff;font-weight:800;box-shadow:0 3px 8px rgba(224,134,0,.3)}
.irc-loc{background:#fff;border:1px solid #e4eaf1;border-radius:12px;padding:12px 13px;display:flex;align-items:center;gap:11px}
.irc-loc>svg{width:17px;height:17px;flex:none}
.irc-loc b{font-size:13px;font-weight:700;color:#16222f;display:block;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
.irc-loc small{font-size:10.5px;color:#0d7a45;font-weight:700;margin-top:1px;display:block}
.irc-edit{font-size:11.5px;font-weight:700;color:#0a6bb0;background:none;border:none;cursor:pointer;flex:none;font-family:inherit}
.irc-photos{display:grid;grid-template-columns:1fr 1fr 1fr;gap:7px}
.irc-thumb{aspect-ratio:1;border-radius:11px;position:relative;overflow:hidden;background:#1d2b3a}
.irc-thumb img{width:100%;height:100%;object-fit:cover;display:block}
.irc-thumb button{position:absolute;right:6px;top:6px;width:18px;height:18px;border-radius:50%;background:rgba(0,0,0,.5);color:#fff;font-size:11px;border:none;display:flex;align-items:center;justify-content:center;cursor:pointer;padding:0}
.irc-thumb span{position:absolute;left:7px;bottom:6px;font-family:var(--font-mono);font-size:9px;color:rgba(255,255,255,.75)}
.irc-add{aspect-ratio:1;border-radius:11px;border:2px dashed #b9c6d6;display:flex;flex-direction:column;align-items:center;justify-content:center;gap:4px;color:#5f7288;cursor:pointer}
.irc-add svg{width:19px;height:19px}
.irc-add b{font-size:10px;font-weight:700}
.irc-notes{width:100%;box-sizing:border-box;background:#fff;border:1px solid #e4eaf1;border-radius:12px;padding:12px 13px;font-size:13px;color:#16222f;line-height:1.5;min-height:84px;font-family:inherit;margin-bottom:8px}
.irc-dictate{display:inline-flex;align-items:center;gap:7px;background:#fff;color:#0a6bb0;border:1px solid #cfe3f5;border-radius:10px;padding:9px 13px;font-size:12px;font-weight:700;font-family:inherit;cursor:pointer}
.irc-dictate.on{background:#fdeaea;color:#c0182a;border-color:#f3c3c3}
.irc-dictate svg{width:14px;height:14px}
.irc-input{width:100%;box-sizing:border-box;background:#fff;border:1px solid #e4eaf1;border-radius:12px;padding:12px 13px;font-size:13px;font-family:inherit}
.irc-foot{padding:11px 16px calc(16px + env(safe-area-inset-bottom,0px));background:#fff;border-top:1px solid #e4eaf1;display:flex;gap:9px;flex:none}
.irc-draftbtn{flex:1;background:#fff;color:#5f7288;border:1px solid #e0e7ef;border-radius:12px;padding:13px;font-size:13.5px;font-weight:700;font-family:inherit;cursor:pointer}
.irc-submit{flex:2;background:linear-gradient(90deg,#005080,#0a6bb0);color:#fff;border:none;border-radius:12px;padding:13px;font-size:13.5px;font-weight:700;box-shadow:0 6px 16px rgba(0,80,128,.3);font-family:inherit;cursor:pointer}
[data-theme="dark"] .irc-ov{background:#141b28}
[data-theme="dark"] .irc-type,[data-theme="dark"] .irc-loc,[data-theme="dark"] .irc-notes,[data-theme="dark"] .irc-input,[data-theme="dark"] .irc-dictate{background:#1a2231;border-color:#26303f;color:#c7d1de}
[data-theme="dark"] .irc-sevs{background:#232f42}
[data-theme="dark"] .irc-loc b,[data-theme="dark"] .irc-notes{color:#e6eaf2}
[data-theme="dark"] .irc-foot{background:#1a2231;border-top-color:#26303f}
[data-theme="dark"] .irc-draftbtn{background:#1a2231;border-color:#26303f;color:#9fb2c4}
/* --- news feed v2 (design: News) --- */
.gn-hero{background:var(--grad-header-mobile,linear-gradient(165deg,#032b49,#005080));margin:-18px -18px 12px;padding:18px 18px 16px;border-radius:0 0 18px 18px;color:#fff;display:flex;align-items:center}
.gn-hero b{font-family:var(--font-display);font-weight:800;font-size:21px;flex:1}
.gn-new{background:#e11d2a;color:#fff;font-size:10px;font-weight:800;min-width:18px;height:18px;border-radius:9px;display:inline-flex;align-items:center;justify-content:center;padding:0 6px}
.ann-card{border-radius:15px;padding:15px;border:1px solid #e4eaf1}
.ann-card.pinned{border-left:4px solid #e11d2a;box-shadow:0 1px 2px rgba(18,28,48,.05),0 8px 22px rgba(18,28,48,.05)}
.gn-prio{font-size:9.5px;font-weight:800;letter-spacing:.08em;color:#c0182a;background:#fdeaea;padding:3px 8px;border-radius:6px}
.gn-read{margin-left:auto;font-size:10px;color:#0d7a45;font-weight:700;white-space:nowrap}
/* 10px/700 at #0d7a45 measures 2.95:1 on the dark --card. #7fd3a2 is the green ink this file
   already uses in dark (.st.ok, .gh-done, .coach-kudos) — 9.00:1 on #1a2231. Light is untouched. */
[data-theme="dark"] .gn-read{color:#7fd3a2}
.gn-dot{margin-left:auto;width:9px;height:9px;border-radius:50%;background:#e11d2a;flex:none}
.ann-title{font-family:var(--font-display);font-weight:700;font-size:15.5px;color:#0b2036;margin-top:7px}
.ann-body{font-size:12.5px;color:#3a5064;line-height:1.5;margin-top:4px}
.gn-slots{display:flex;gap:6px;margin-top:10px;flex-wrap:wrap}
.gn-slot{flex:1;min-width:70px;background:#e7f0fa;color:#0a5a94;border:none;border-radius:9px;padding:9px;font-size:11.5px;font-weight:700;font-family:inherit;cursor:pointer}
.gn-slot.on{background:linear-gradient(90deg,#005080,#0a6bb0);color:#fff}
.gn-slot:disabled{opacity:.45;cursor:default}
.gn-slot small{font-family:var(--font-mono);font-size:9.5px;font-weight:400;opacity:.8}
.gn-cta{display:block;width:100%;box-sizing:border-box;margin-top:11px;background:linear-gradient(90deg,#005080,#0a6bb0);color:#fff;border:none;border-radius:10px;padding:11px;font-size:12.5px;font-weight:700;text-align:center;text-decoration:none}
[data-theme="dark"] .ann-title{color:#e6eaf2}
[data-theme="dark"] .ann-body{color:#9fb2c4}
[data-theme="dark"] .gn-slot{background:#14263a;color:#4aa3dd}
/* --- to-do unification (design: To-do) --- */
.td-hero{background:var(--grad-header-mobile,linear-gradient(165deg,#032b49,#005080));margin:-18px -18px 12px;padding:18px 18px 16px;border-radius:0 0 18px 18px;color:#fff}
.td-hero b{font-family:var(--font-display);font-weight:800;font-size:21px;flex:1}
.td-hero .cnt{font-family:var(--font-mono);font-size:12px;color:#9dbdd6}
.td-lbl{font-size:10.5px;font-weight:800;letter-spacing:.08em;color:#5f7288;text-transform:uppercase;margin:12px 2px 7px}
.td-row{background:#fff;border:1px solid #e4eaf1;border-radius:14px;padding:13px 14px;display:flex;align-items:center;gap:12px;margin-bottom:8px}
.td-row.urgent{border:1.5px solid #f3ddb5}
.td-row.urgent .td-box{border-color:#e08600}
.td-row.urgent .td-mid small{color:#a8660a;font-weight:700}
.td-row.done{opacity:.7}
.td-row.done .td-mid b{text-decoration:line-through;text-decoration-color:#93a3b5}
.td-box{width:24px;height:24px;border-radius:8px;border:2px solid #c2cedd;flex:none;display:flex;align-items:center;justify-content:center;position:relative}
.td-box input{width:24px;height:24px;opacity:0;position:absolute;inset:0;margin:0;cursor:pointer}
.td-box:has(input:checked),.td-box.on{background:#0f9d58;border-color:#0f9d58}
.td-box.static{border-style:dashed}
.td-check{width:24px;height:24px;border-radius:8px;background:#0f9d58;flex:none;display:flex;align-items:center;justify-content:center}
.td-check svg{width:13px;height:13px}
.td-mid{flex:1;min-width:0}
.td-mid b{font-size:13.5px;font-weight:700;color:#16222f;display:block}
.td-mid small{font-size:11.5px;color:#8593a5;display:block;margin-top:1px}
.td-mid small.ok{color:#0d7a45;font-weight:700}
/* "Done — pending review": ticked, but dispatch has not signed off yet. Amber, not green — green
   would tell the officer it is finished business when somebody still has to confirm it. */
.td-mid small.pend{color:#8a6100;font-weight:700}
/* dispatch sent it back with a reason — this is the only red on the to-do list, and it earns it */
.td-mid small.bad{color:#b3261e;font-weight:700}
[data-theme="dark"] .td-mid small.pend{color:#e0b45e}
[data-theme="dark"] .td-mid small.bad{color:#f0a9b1}
.td-btn{background:#fff;color:#0a6bb0;border:1.5px solid #cfe3f5;border-radius:9px;padding:9px 13px;font-size:11.5px;font-weight:700;font-family:inherit;cursor:pointer;flex:none;white-space:nowrap}
.td-btn.pri{background:linear-gradient(90deg,#005080,#0a6bb0);color:#fff;border:none}
[data-theme="dark"] .td-row{background:#1a2231;border-color:#26303f}
[data-theme="dark"] .td-row.urgent{border-color:#5c4a1e}
[data-theme="dark"] .td-mid b{color:#e6eaf2}
[data-theme="dark"] .td-btn{background:#1a2231;border-color:#26424f}
[data-theme="dark"] .td-lbl{color:#8ea0b3}
/* --- chat v2 (design: Chat) --- */
.gth2{background:var(--grad-header-mobile,linear-gradient(165deg,#032b49,#005080));margin:-18px -18px 0;padding:14px 18px;color:#fff;display:flex;align-items:center;gap:11px}
.gth2-back{background:none;border:none;color:#9dbdd6;font-size:22px;line-height:1;cursor:pointer;flex:none;
  /* 44px tap floor without moving the glyph: the visible arrow stays 22px, the hit area doesn't. */
  min-width:var(--tap,44px);min-height:var(--tap,44px);display:inline-flex;align-items:center;justify-content:flex-start;padding:0 4px 0 0;margin:-8px 0}
.gth2-tile{width:34px;height:34px;border-radius:10px;background:rgba(255,255,255,.14);font-family:var(--font-display);font-weight:800;font-size:15px;display:flex;align-items:center;justify-content:center;flex:none}
.gth2-t{flex:1;min-width:0}
.gth2-t b{font-family:var(--font-display);font-weight:800;font-size:16px;display:block;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
.gth2-t small{font-size:11px;color:#9dbdd6;margin-top:1px;display:block}
.gth2-live{display:inline-flex;align-items:center;gap:5px;background:rgba(15,157,88,.18);border:1px solid rgba(63,191,122,.5);color:#7fe0a9;font-size:9.5px;font-weight:800;letter-spacing:.06em;padding:3px 9px;border-radius:20px;flex:none}
.gth2-live i{width:6px;height:6px;border-radius:50%;background:#3fbf7a;animation:usgpulse 1.6s infinite}
.gth2-acts{margin:0 -18px;padding:8px 18px;background:#fff;border-bottom:1px solid #e4eaf1;display:flex;gap:8px}
.gwrap{display:flex;flex-direction:column;max-width:82%;margin-bottom:11px}
.gwrap.them{align-self:flex-start;align-items:flex-start}
.gwrap.mine{align-self:flex-end;align-items:flex-end}
.bm2{font-size:10px;color:#8593a5;font-weight:700;margin:0 2px 3px}
.bm2 .t{font-family:var(--font-mono);font-weight:400}
.gwrap .gbubble{max-width:100%;margin-bottom:0}
.gwrap.them .gbubble{background:#fff;border:1px solid #e4eaf1;border-radius:13px 13px 13px 4px;padding:10px 13px;font-size:13.5px;color:#16222f;line-height:1.4;box-shadow:0 1px 2px rgba(18,28,48,.06);border-left:none}
.gwrap.mine .gbubble{background:linear-gradient(135deg,#005080,#0a6bb0);color:#fff;border:none;border-radius:13px 13px 4px 13px;padding:10px 13px;font-size:13.5px;line-height:1.4;box-shadow:0 4px 12px rgba(0,80,128,.25)}
/* priority border must survive the wrapper's border override (higher specificity, later in file) */
.gwrap.them .gbubble.prio,.gwrap.mine .gbubble.prio{border:2px solid #e11d2a}
.bm2 b{color:#c0182a}
/* optimistic (instant) send states — sending / queued-offline / failed */
.gwrap.pending .gbubble{opacity:.6}
.gwrap.pending.queued .gbubble{opacity:.78}
.gwrap.pending.failed .gbubble{opacity:1;box-shadow:0 0 0 1.5px #e2564d inset}
/* jump-to-message landing flash (gJumpToMessage). A RING, not a background wash: .gbubble already
   owns its background four ways (mine/them × light/dark) and a tint would fight all four. It sits
   on the WRAP so an inspection or field-report card — which are not .gbubble — light up too.
   Amber, which reads on every one of those, and is not the red the app reserves for danger. */
@keyframes gjump{0%,55%{box-shadow:0 0 0 3px rgba(233,163,25,.9)}100%{box-shadow:0 0 0 3px rgba(233,163,25,0)}}
.gwrap.gjumped{border-radius:14px;animation:gjump 2.4s ease-out}
@media (prefers-reduced-motion:reduce){.gwrap.gjumped{animation:none;box-shadow:0 0 0 3px rgba(233,163,25,.9)}}
.reply-quote[data-gjump]{cursor:pointer}
.gpin-row{cursor:pointer}
.g-pend-meta{font-size:10px;font-weight:600;opacity:.9;margin-top:4px;letter-spacing:.02em}
.gwrap.pending.queued .g-pend-meta{color:#ffd8a6}
.g-retry{background:none;border:none;color:#ffd0cb;font-size:10.5px;font-weight:800;padding:0;text-decoration:underline;cursor:pointer}
/* ── the in-flight attachment, ON the message ────────────────────────────────────────────────
   One row per file with the SAME 3px determinate bar the composer strip already uses, so a clip
   climbing to 100% is visible in the conversation rather than only in a toast that has faded.
   ⚠ The bubble is .pending, and .gwrap.pending .gbubble is opacity:.6 — so these rows inherit the
   dim without a rule of their own, and become fully legible the moment it goes .failed. */
.gatts{display:flex;flex-direction:column;gap:5px;margin:5px 0 2px}
.gatt-r{display:grid;grid-template-columns:minmax(0,1fr) auto;gap:3px 8px;align-items:center;
  background:rgba(0,0,0,.14);border-radius:8px;padding:5px 7px}
.gatt-r.bad{box-shadow:0 0 0 1px #ffd0cb inset}
.gatt-n{font-size:11px;font-weight:600;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
.gatt-s{font-size:10px;opacity:.88;white-space:nowrap}
.gatt-b{grid-column:1/3;height:3px;border-radius:2px;background:rgba(255,255,255,.26);overflow:hidden}
/* scaleX, not width: a transform is composited, a width change relayouts the thread on every frame
   of a ten-minute upload — the same rule the strip's own bar follows. */
.gatt-b i{display:block;width:100%;height:100%;background:var(--ok,#0f9d58);transform:scaleX(0);transform-origin:left}
.gsys{align-self:center;background:#fdeaea;border:1px solid #f3c3c3;color:#c0182a;font-size:10px;font-weight:700;padding:4px 11px;border-radius:20px;text-align:center;margin:2px auto 11px;max-width:90%}
.gsys .t{font-family:var(--font-mono);font-weight:400}
.gvoice{display:inline-flex;align-items:center;gap:9px;background:none;border:none;color:inherit;padding:4px 0;cursor:pointer;font-family:inherit}
.gvoice svg{width:14px;height:14px;flex:none}
.gvoice .wf{display:inline-flex;align-items:flex-end;gap:2px;height:13px}
.gvoice .wf i{width:3px;border-radius:2px;background:rgba(255,255,255,.85);display:inline-block}
.gwrap.them .gvoice .wf i{background:#0a6bb0}
.gvoice .dur{font-family:var(--font-mono);font-size:10.5px}
.gvoice.played .dur{opacity:.6}
/* position+z-index are LOAD-BEARING: the compose bar must stack above the fixed toast (z20) and the
   bottom nav (z10) so its hit area can never be shadowed by them. 16px font = no mobile focus-zoom. */
.g-compose{background:#fff;border-top:1px solid #e4eaf1;padding:11px 16px calc(11px + env(safe-area-inset-bottom,0px));position:relative;z-index:30}
.g-compose input#g-msginput{flex:1;padding:12px 15px;border-radius:12px;border:1px solid #dbe4ee;background:#f8fafc;font-size:16px;color:#16222f}
.g-compose .gicon{width:44px;height:44px;padding:0;border-radius:12px;border:none;background:#eef1f5;color:#5f7288;display:flex;align-items:center;justify-content:center;flex:none}
.g-compose .gsend{width:44px;height:44px;padding:0;border-radius:12px;border:none;background:linear-gradient(90deg,#005080,#0a6bb0);box-shadow:0 4px 12px rgba(0,80,128,.25);display:flex;align-items:center;justify-content:center;flex:none;cursor:pointer}
.g-compose .gsend svg{width:17px;height:17px}
/* PTT bar → design's dark mini-panel */
.rad-bar{background:#01192b !important;border-radius:12px;box-shadow:0 6px 22px rgba(0,0,0,.4)}
/* While a chat thread is open, keep the radio bar ABOVE the compose area — it auto-expands
   (z-index 100000) when anyone transmits and used to land exactly on the text input mid-tap. */
body:has(#gv-messages.on .gchat) .rad-bar{bottom:calc(150px + env(safe-area-inset-bottom,0px)) !important}

/* ---- "Notifications are off" bar (guard.js gNotifWatch, appended to <body>) ----
   THE SECOND body-appended floater on this page, and it has the same problem .rad-bar has: nothing
   inside .gapp or #g-sup can reach it, so its offsets belong here. It shipped with them inline at
   `bottom:8px` and z-index 99990, which put it ON TOP of .gnav (z10), #g-sup .snav (z60) and
   .g-compose (z30) and took their taps — an officer who declined the notification permission could
   not reach the tab bar or the send button, and the bar carried no way to dismiss itself.
   --g-nb-dock is "the height of whatever bottom chrome is on screen, plus an 8px gap". Every number
   below is that chrome's OWN declared height, so if one of them moves this moves with it.

   ★ THE DEFAULT IS THE TALLEST CLEARANCE, AND THE RULES BELOW LOWER IT. Written that way round on
   purpose: every rule here is a :has(), and if a WebView ever drops one the bar falls back to
   covering nothing rather than to covering something. The default clears BOTH the tab bar (.gnav5,
   60px + safe) and the SOS button, which guard.html fixes at `bottom:78px` at 62px across — an
   emergency control is the last thing a notice may sit on. It cannot be gated on directly: gnav()
   shows and hides that button with `style.display`, which no selector can read (it only ever loses
   its .hidden class once, in gWireSos), so the SURFACES it is hidden on are named instead.

   ★ THE DOCK IS DECLARED ON <body>, NOT ON THE BAR. It reads "the height of whatever bottom chrome
   is on screen", which is a fact about the PAGE, and it now has two consumers: this bar, and
   #g-toast, which has to stack ABOVE the bar instead of underneath it (see the .toast rule at the
   end of this block). A custom property set on #g-notifbar is invisible to the toast — they are
   siblings on <body> — so declaring it here is what lets both read one number per surface instead
   of two that can drift apart. The bar's geometry is unchanged: it inherits every value below. */
body{--g-nb-dock:calc(148px + env(safe-area-inset-bottom,0px));   /* #g-sos ends at 78+62 */
  --g-nb-lift:0px;
  /* THE BAND THE BAR ITSELF OCCUPIES, which is the one piece of bottom chrome here whose height is
     content-driven rather than declared. MEASURED in Chrome on this stylesheet with the exact
     markup gNotifWatch builds: 68px at ≥430px wide (the 44px dismiss tap target plus 12px padding
     top and bottom — the floor), 81px at 360–412px (the notice wraps to three lines), 100px at
     320px (four lines). 112px is that worst case with a margin. It is a RESERVATION, not a height —
     nothing is forced to it — and guard.js overrides it with the bar's real offsetHeight the moment
     the bar exists, which is what covers a WebView text-zoom setting (1.2× at 320px measures
     160px). This value is the floor that renders before that lands, and if script never runs. */
  --g-nb-band:112px}
#g-notifbar{position:fixed;left:8px;right:8px;bottom:calc(var(--g-nb-dock) + var(--g-nb-lift));z-index:99990;
  display:flex;align-items:center;gap:10px;background:#7a1420;color:#fff;border-radius:14px;
  padding:12px 14px;box-shadow:0 8px 24px rgba(0,0,0,.4);font-family:-apple-system,Arial,sans-serif}
/* The Chat tab is the one surface gnav() takes the SOS button off (guard.js: `sos.style.display =
   (v === 'messages') ? 'none' : ''`), so here the tab bar is the only thing to clear. Gated on the
   officer shell being the visible one: Field mode and the admin command post hide #g-app but leave
   #gv-messages.on standing in the DOM, and :has() reads the DOM, not what is painted. */
body:has(#g-app:not(.hidden) #gv-messages.on){--g-nb-dock:calc(68px + env(safe-area-inset-bottom,0px))}
/* Inside a thread: above .g-compose, at the SAME measured offset .rad-bar uses for the same reason.
   One class more specific than the rule above, so the inbox and the thread each get their own.
   (Typing mode takes the bar off screen entirely — see section 21d.) */
body:has(#g-app:not(.hidden) #gv-messages.on .gchat){--g-nb-dock:calc(150px + env(safe-area-inset-bottom,0px))}
/* The update banner is at `bottom:72px + safe` and measures ~58px tall (gShowUpdateBanner). Stack on
   top of it rather than over it: at z-index 9500 it would lose, and it carries the only Update
   button there is. This is a LIFT rather than a dock, so it composes with whichever dock the rules
   above chose instead of replacing it. */
body:has(#g-apk-bar){--g-nb-lift:70px}
/* ---- …and the toast stacks on top of the bar, never under it ----
   .toast is fixed at bottom:96px with z-index 20 (section 2). The bar is z-index 99990 and, on the
   Chat tab and in Field mode, docks at 68px — and its band starts at 68px tall, so it covers the
   96px toast line outright and paints over it. Every "Sent", "Saved", "Could not reach dispatch" on
   those two surfaces was landing behind the notice, invisible: the app's only transient feedback,
   silently swallowed by a bar that is on screen for as long as the permission is off.
   The lift follows the dock, so it is right on every surface without repeating a single condition.
   `:not(.g-typing)` because section 21d takes the bar OFF SCREEN while the keyboard is up — :has()
   still matches a display:none element, and with nothing to clear the toast belongs back at 96px. */
body:not(.g-typing):has(#g-notifbar) .toast{bottom:calc(var(--g-nb-dock) + var(--g-nb-lift) + var(--g-nb-band) + 8px)}

/* ---- WhatsApp-style reaction quick-bar (long-press/＋ on a bubble) ----
   The pill is capped to the viewport — unconstrained it runs ~450px and clips its tail at phone
   widths. The emoji ride a scrollable strip INSIDE it; ＋ sits after the strip so the full-sheet
   entry can never scroll out of reach. The strip carries the pill's vertical padding (negative
   margin cancels it in layout) so the :active scale-up isn't clipped by the strip's scroll box. */
.rxq-ov{position:fixed;inset:0;z-index:9000;background:rgba(1,15,28,.25)}
.rxq{position:fixed;z-index:9001;display:flex;align-items:center;gap:2px;background:#fff;border-radius:26px;padding:6px 8px;box-shadow:0 8px 28px rgba(2,20,38,.28);animation:rxqin .14s ease-out;max-width:calc(100vw - 16px)}
.rxq-strip{display:flex;gap:2px;min-width:0;overflow-x:auto;overscroll-behavior-x:contain;scrollbar-width:none;padding:6px 0;margin:-6px 0}
.rxq-strip::-webkit-scrollbar{display:none}
@keyframes rxqin{from{transform:scale(.8);opacity:0}to{transform:scale(1);opacity:1}}
.rxq button{border:none;background:none;font-size:26px;line-height:1;padding:0;min-width:var(--tap,44px);min-height:var(--tap,44px);flex:0 0 auto;display:inline-flex;align-items:center;justify-content:center;border-radius:50%;cursor:pointer}
.rxq button:active{background:#eef3f9;transform:scale(1.25)}
[data-theme="dark"] .rxq{background:#1a2231}
[data-theme="dark"] .rxq button:active{background:#26303f}

/* ---- Modern emoji bottom sheet (compose ＋ full reaction set) ---- */
.emo-ov{position:fixed;inset:0;z-index:9500;background:rgba(1,15,28,.35)}
.emo-sheet{position:fixed;left:50%;transform:translateX(-50%);bottom:0;width:100%;max-width:480px;z-index:9501;background:#fff;border-radius:18px 18px 0 0;box-shadow:0 -8px 30px rgba(2,20,38,.3);display:flex;flex-direction:column;max-height:46vh;animation:emoin .18s ease-out;padding-bottom:env(safe-area-inset-bottom,0px)}
@keyframes emoin{from{transform:translate(-50%,30%);opacity:.4}to{transform:translate(-50%,0);opacity:1}}
.emo-grab{width:38px;height:4px;border-radius:2px;background:#d3dae6;margin:8px auto 2px;flex:none}
.emo-tabs{display:flex;gap:2px;padding:6px 10px 2px;flex:none;overflow-x:auto;scrollbar-width:none}
.emo-tabs button{border:none;background:none;font-size:18px;padding:6px 9px;border-radius:10px;opacity:.55;flex:none}
.emo-tabs button.on{background:#eef3f9;opacity:1}
.emo-grid{flex:1;overflow-y:auto;overscroll-behavior:contain;display:grid;grid-template-columns:repeat(auto-fill,minmax(44px,1fr));padding:6px 10px 12px;gap:2px}
.emo-grid button{border:none;background:none;font-size:27px;line-height:1;padding:8px 0;border-radius:10px;cursor:pointer}
.emo-grid button:active{background:#eef3f9;transform:scale(1.2)}
[data-theme="dark"] .emo-sheet{background:#141b28}
[data-theme="dark"] .emo-tabs button.on,[data-theme="dark"] .emo-grid button:active{background:#26303f}

/* ---- "New messages" pill: shown instead of yanking the scroll while reading history ---- */
.gnewmsg{position:absolute;left:50%;transform:translateX(-50%);bottom:96px;z-index:25;background:linear-gradient(90deg,#005080,#0a6bb0);color:#fff;border:none;border-radius:20px;padding:9px 16px;font-size:13px;font-weight:700;box-shadow:0 6px 18px rgba(0,80,128,.35);display:flex;align-items:center;gap:6px}

/* Reaction chips: real 32px+ touch targets (were ~22px) */
.grx{padding:6px 12px;font-size:14px;border-radius:16px;min-height:32px;position:relative}
/* 44px tap floor via invisible hit-area extension (house ::after trick) — a 44px-tall visible
   pill would bloat reaction rows inside bubbles; the chip stays 32px, the finger gets 44px.
   ⚠ THIS BLEED IS SIZED FOR THE 32px CHIP ABOVE, and section 5 re-styles the THREAD's chips down to
   28px (.gwrap>.grxbar .grx) and 30px (.grx.act), where 32+12 stops being 44. Section 5 carries its
   own higher-specificity ::after for those two — see "44px IN THE THREAD REACTION ROW". This rule
   is still the only extension on every .grx outside a message wrap (the field-report card's own
   .grxbar), so do not delete it, and do not re-point it at the thread. */
.grx::after{content:"";position:absolute;left:-4px;right:-4px;top:-6px;bottom:-6px}
.grx.add{min-width:38px}
.rad-bar .rad-live{color:#38d996;font-weight:800;letter-spacing:.03em;font-size:9.5px}
.rad-bar .rad-label{color:#c3cede;font-size:9.5px}
.rad-bar .rad-mem{color:#9fb0c8;font-size:9.5px}
.rad-bar .rad-ptt{background:#032b49;color:#fff;border:none;border-radius:9px;font-size:12px;font-weight:800;letter-spacing:.03em}
.rad-bar .rad-ptt.on{background:#c0182a}
[data-theme="dark"] .gwrap.them .gbubble{background:#1a2231;border-color:#26303f;color:#c7d1de}
[data-theme="dark"] .g-compose{background:#141b28;border-top-color:#26303f}
[data-theme="dark"] .g-compose input#g-msginput{background:#1a2231;border-color:#26303f;color:#c7d1de}
[data-theme="dark"] .gth2-acts{background:#141b28;border-bottom-color:#26303f}
/* --- dark theme parity --- */
[data-theme="dark"] .gsh-card{background:#1a2231;border-color:#26303f}
[data-theme="dark"] .gsh-card.open{border-color:#5c2a30}
[data-theme="dark"] .gsh-site{color:#e6eaf2}
[data-theme="dark"] .gsh-meta .mono{color:#e6eaf2}
[data-theme="dark"] .gsh-claim.ghost{background:#1a2231}
[data-theme="dark"] .gsh-spots{background:#16354d;color:#7fc0f0}
[data-theme="dark"] .gsh-spots.full{background:#232c3a;color:#93a3b8}
[data-theme="dark"] .gsh-card.open.full{border-color:#26303f}
[data-theme="dark"] .gsh-prog{background:#232c3a}
[data-theme="dark"] .gsh-mine{background:#16321f;color:#67d69a}
[data-theme="dark"] .gd-card,[data-theme="dark"] .gd-tile{background:#1a2231;border-color:#26303f}
[data-theme="dark"] .gd-title,[data-theme="dark"] .gd-tile b{color:#e6eaf2}
[data-theme="dark"] .gd-track{background:#232f42}
[data-theme="dark"] .gd-prog .t,[data-theme="dark"] .gd-foot .mono{color:#e6eaf2}
[data-theme="dark"] .gd-ghostbtn{background:#1a2231;border-color:#26303f;color:#9fb2c4}
[data-theme="dark"] .gd-next{background:#14263a;border-color:#1d3a52}
[data-theme="dark"] .gd-next .tile{background:#0e1e2e}
[data-theme="dark"] .gd-next b{color:#e6eaf2}
[data-theme="dark"] .welfare-card{background:#1a2231;border-color:#5c4a1e}
[data-theme="dark"] .wf-help{background:#1a2231}
[data-theme="dark"] .gd-tile .sq.grey{background:#232f42}
[data-theme="dark"] .gd-tile .sq.blue{background:#14263a}
[data-theme="dark"] .gd-tile .sq.red{background:#3a1a1e}

/* Training tutorials list (officer) */
.gtrn-item{display:flex;align-items:center;gap:11px;width:100%;text-align:left;background:var(--white);border:1px solid var(--line);border-radius:12px;padding:11px 12px;margin-bottom:7px}
.gtrn-item:active{background:#eef1f8}
.gtrn-no{flex:none;width:26px;height:26px;border-radius:50%;background:var(--navy);color:#fff;font-size:12.5px;font-weight:800;display:flex;align-items:center;justify-content:center}
.gtrn-txt{flex:1;min-width:0}
.gtrn-txt b{display:block;font-size:14px;color:var(--navy);line-height:1.3}
.gtrn-txt small{color:var(--muted);font-size:11.5px}
.gtrn-play{flex:none;color:var(--navy);opacity:.7}.gtrn-play svg{width:18px;height:18px}
.gtrn-player video{margin:4px 0 10px}

/* ---- Report a problem sheet (gNewTicket) -------------------------------------------------
   Shipped in APK 1.2.4 using g-sheet / g-sheet-in / g-sheet-h / g-x / field / seg — none of
   which existed in any stylesheet, so the sheet rendered as raw unstyled HTML on every
   officer's phone: a default file input, overlapping labels, no panel. Found while recording
   the support-ticket tutorial. Modelled on .emo-sheet (the app's other bottom sheet) so it
   behaves the same: slides up, respects the home indicator, and has a dark variant. */
.g-sheet{position:fixed;inset:0;z-index:9600;background:rgba(3,20,38,.45);display:flex;align-items:flex-end;justify-content:center}
.g-sheet-in{background:#fff;width:100%;max-width:480px;border-radius:18px 18px 0 0;padding:16px 16px calc(16px + env(safe-area-inset-bottom,0px));box-shadow:0 -8px 30px rgba(2,20,38,.3);max-height:92vh;overflow-y:auto;animation:gsheetin var(--t-med) var(--ease)}
/* A bottom sheet comes from the BOTTOM EDGE, all the way — 24% reads as a card that was already
   half on screen and then jumped. 100% of its own height is the house sheet entrance. */
@keyframes gsheetin{from{transform:translateY(100%)}to{transform:translateY(0)}}
.g-sheet-h{display:flex;align-items:center;justify-content:space-between;margin-bottom:12px}
.g-sheet-h b{font-size:17px;color:var(--heading,#0b2036)}
.g-x{border:0;background:none;font-size:26px;line-height:1;color:var(--muted);padding:0 4px}
.g-sheet .field{margin-bottom:12px}
.g-sheet .field label{display:block;font-size:12.5px;font-weight:700;color:var(--muted);margin-bottom:5px}
.g-sheet .field input[type=text],.g-sheet .field input:not([type]),.g-sheet .field textarea{width:100%;border:1px solid var(--line);border-radius:12px;padding:12px;font-size:15px;background:#fff;color:var(--ink)}
.g-sheet .field input[type=file]{width:100%;font-size:13px;color:var(--muted)}
.g-sheet .seg{display:flex;gap:8px}
.g-sheet .seg button{flex:1;border:1px solid var(--line);background:#fff;color:var(--ink);border-radius:12px;padding:11px 8px;font-size:14px;font-weight:600}
.g-sheet .seg button.on{background:var(--navy);border-color:var(--navy);color:#fff}
[data-theme="dark"] .g-sheet-in{background:#141b28}
[data-theme="dark"] .g-sheet .field input[type=text],[data-theme="dark"] .g-sheet .field input:not([type]),[data-theme="dark"] .g-sheet .field textarea,[data-theme="dark"] .g-sheet .seg button{background:#0e1520;color:#e8eef5;border-color:#22394d}
/* …and the SELECTED one has to survive that. The rule above matches `.g-sheet .seg button` at the
   same specificity as `.g-sheet .seg button.on` three lines up and sits later in the file, so in
   dark theme it repainted the chosen segment back to the unchosen fill: measured live at 360x800,
   `.on` and `:not(.on)` computed to the SAME background, colour and border — the control looked
   like nothing was selected at all. It hit both surfaces that use this idiom (the "Report a
   problem" kind picker, and the #254 equipment condition, where the choice decides whether the
   note field opens and whether the shift is flagged to dispatch).
   #1d4a80 is the lifted navy this stylesheet already uses for a selected chip in dark (.gib-f.on);
   --navy itself is #032b49, which is DARKER than the sheet and is why the light-mode value could
   never simply be reused here. White on #1d4a80 measures 8.87:1. */
[data-theme="dark"] .g-sheet .seg button.on{background:#1d4a80;border-color:#1d4a80;color:#fff}

/* ---- Field report card (a message with attach_kind='fieldreport') ----------------------------
   A record on the wall, not one side of a conversation: it runs the full width of the thread,
   carries its own header, a WhatsApp-style photo album and the people who were tagged. It is
   built ON .gbubble.them so it inherits the thread's own incoming surface in BOTH themes — a
   white slab in a dark chat is the tell that something was bolted on afterwards. */
.gwrap.fr-wrap{max-width:100%;width:100%;align-self:stretch;align-items:stretch}
.gwrap.them .gbubble.fr-card{padding:0;overflow:hidden;white-space:normal;border-radius:14px;border-left:3px solid #0a6bb0;box-shadow:0 1px 2px rgba(18,28,48,.06),0 8px 22px rgba(18,28,48,.07)}
/* Severity lives on the left edge, the way a case file is tabbed. Red is danger-only in this
   design system and an officer asleep on post is exactly what it is reserved for. */
.gwrap.them .gbubble.fr-card.sev-info{border-left-color:#0a6bb0}
.gwrap.them .gbubble.fr-card.sev-concern{border-left-color:#e08600}
.gwrap.them .gbubble.fr-card.sev-serious{border-left-color:#e11d2a}
.fr-card .fr-head{display:flex;align-items:center;gap:7px;padding:11px 13px 0}
.fr-card .fr-head svg{width:15px;height:15px;flex:none;color:#5f7288}
.fr-card .fr-head b{font-family:var(--font-display,inherit);font-weight:700;font-size:13.5px;letter-spacing:.01em}
.fr-card .fr-time{margin-left:auto;font-family:var(--font-mono,ui-monospace,monospace);font-size:11px;color:#5f7288;flex:none}
.fr-sev{font-size:9.5px;font-weight:800;letter-spacing:.06em;text-transform:uppercase;border-radius:99px;padding:3px 8px;line-height:1;flex:none}
.fr-sev.info{background:#e2eefa;color:#0a5a94}
.fr-sev.concern{background:#fdf0dc;color:#a8660a}
.fr-sev.serious{background:#fbe3e5;color:#c0182a}
.fr-card .fr-sub{padding:3px 13px 0;font-size:11.5px;color:#5f7288;font-weight:600}
.fr-card .fr-sub .fr-dot{margin:0 5px;opacity:.55}
.fr-card .fr-sub svg{width:11px;height:11px;vertical-align:-1px}
/* "This was not a client post" — the kind of place, on a neutral outlined chip. Deliberately NOT a
   filled status pill: the severity pill above it owns colour on this card, and an errand carries no
   status. Outlined and grey, it reads as a label rather than a second severity. */
.fr-card .fr-sub .fr-off{display:inline-block;margin-left:7px;border:1px solid #d3dce6;border-radius:99px;padding:1px 7px;font-size:10px;font-weight:800;letter-spacing:.03em;text-transform:uppercase;color:#5f7288;vertical-align:1px}
.fr-card .fr-gone{display:inline-flex;align-items:center;gap:5px;margin-left:6px;color:#a8660a;font-weight:700}
.fr-card .fr-gone svg{width:12px;height:12px}
.fr-card .fr-text{padding:7px 13px 0;font-size:13.5px;line-height:1.45;white-space:pre-wrap;word-break:break-word;overflow-wrap:anywhere}
/* A report longer than a chat message (guard.js gFrFillText) folds instead of burying the thread.
   ⚠ NOTHING IS CUT — the whole body is in the DOM, this only limits the height until it is opened.
   The fade is a MASK, not a gradient in the card's colour: a mask needs no background to match, so
   it is right in both themes and stays right if the bubble is re-skinned. */
.fr-card .fr-text.fr-fold{max-height:17em;overflow:hidden;transition:max-height .3s cubic-bezier(.2,.7,.3,1);
  -webkit-mask-image:linear-gradient(to bottom,#000 calc(100% - 3.4em),transparent);mask-image:linear-gradient(to bottom,#000 calc(100% - 3.4em),transparent)}
/* ★ OPENING RELEASES THE CLIP — IT DOES NOT RAISE IT ────────────────────────────────────────────
   This used to open to max-height:2000em, which at the card's 13.5px is ~27,000px ≈ 1,379 rendered
   lines: a newline-heavy body (a scanner dump, one entry per line) reaches that on line count alone
   and the tail simply vanished — with the button already reading "Show less" and nothing left to
   click. A ceiling of ANY size is still a cut, and the server-side body caps were removed this same
   train (owner order, 2026-08-13), so over-boundary bodies are legal and expected. `max-height:none`
   is the only value that can never amputate a report, and it is what makes the promise six lines up
   ("NOTHING IS CUT") true of the OPEN state as well as the closed one.
   `none` does not interpolate, so the reveal is carried by a keyframe rather than the transition
   above: fr-unfold travels from the folded height up through one screenful and then STOPS — the
   default fill-mode is `none`, so the instant it ends the element is back on max-height:none with
   no ceiling at all. The 100vh in it is a travel distance for 0.3s, never a resting limit.
   overflow stays hidden ON PURPOSE: with no height cap it clips nothing (the box is exactly its
   content), and it is what stops the keyframe from painting the whole report over the button while
   the box is still growing.
   ⚠ KEEP THIS IDENTICAL TO styles.css — one report, two apps, one affordance. */
.fr-card .fr-text.fr-fold.open{max-height:none;-webkit-mask-image:none;mask-image:none;animation:fr-unfold .3s cubic-bezier(.2,.7,.3,1)}
@keyframes fr-unfold{from{max-height:17em}to{max-height:100vh}}
.fr-card .fr-more-btn{display:inline-flex;align-items:center;margin:8px 13px 0;min-height:var(--tap,44px);padding:0 15px;
  border:1px solid #d3dce6;border-radius:99px;background:transparent;color:#0a6bb0;
  font-family:inherit;font-size:13px;font-weight:700;cursor:pointer;transition:background .15s ease}
.fr-card .fr-more-btn:active{background:rgba(10,107,176,.1)}
[data-theme="dark"] .fr-card .fr-more-btn{border-color:#334052;color:#63a8dd}
@media (prefers-reduced-motion:reduce){.fr-card .fr-text.fr-fold{transition:none}
  .fr-card .fr-text.fr-fold.open{animation:none}}
/* Album: 1 runs full width, 2 split the row, 3 give one tall tile plus two stacked, 4+ fall into
   a 2x2 whose last tile carries "+N". Full-bleed and bare — the card's overflow:hidden is what
   rounds the outer corners of the group, so no tile ever gets a frame of its own. */
.fr-mos{display:grid;gap:4px;margin-top:10px}
.fr-mos .fr-t{position:relative;overflow:hidden;cursor:pointer;background:#dbe3ec;min-width:0}
.fr-mos .fr-t img{width:100%;height:100%;object-fit:cover;display:block}
.fr-mos.n1{grid-template-columns:1fr}
.fr-mos.n1 .fr-t{aspect-ratio:4/3}
.fr-mos.n2{grid-template-columns:1fr 1fr}
.fr-mos.n2 .fr-t{aspect-ratio:1/1}
.fr-mos.n3{grid-template-columns:1.62fr 1fr;grid-template-rows:1fr 1fr;aspect-ratio:3/2}
.fr-mos.n3 .fr-t:first-child{grid-row:1/3}
.fr-mos.n4{grid-template-columns:1fr 1fr;grid-template-rows:1fr 1fr;aspect-ratio:1/1}
.fr-mos .fr-more{position:absolute;inset:0;background:rgba(6,16,28,.58);color:#fff;display:flex;align-items:center;justify-content:center;font-family:var(--font-display,inherit);font-weight:800;font-size:21px;letter-spacing:.02em}
.fr-ments{display:flex;flex-wrap:wrap;gap:5px;padding:9px 13px 0}
.fr-ments:empty{display:none}
.fr-ment{background:#e2eefa;color:#0a5a94;font-size:11.5px;font-weight:700;border-radius:99px;padding:3px 9px;max-width:100%;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
.fr-ment.plain{background:transparent;color:#5f7288;font-weight:600;padding-left:2px}
.fr-card .fr-foot{padding:0 13px 10px}
.fr-card .fr-foot .grxbar{margin-top:9px}
[data-theme="dark"] .gwrap.them .gbubble.fr-card{background:#1a2231;border-color:#26303f;color:#c7d1de;box-shadow:0 1px 2px rgba(0,0,0,.32),0 8px 22px rgba(0,0,0,.26)}
[data-theme="dark"] .fr-card .fr-head svg,[data-theme="dark"] .fr-card .fr-sub,[data-theme="dark"] .fr-card .fr-time,[data-theme="dark"] .fr-ment.plain{color:#8b98ab}
[data-theme="dark"] .fr-sev.info{background:#16324c;color:#7fb8e6}
[data-theme="dark"] .fr-sev.concern{background:#3a2c12;color:#e0ab5a}
[data-theme="dark"] .fr-sev.serious{background:#3d1a1e;color:#f08d96}
[data-theme="dark"] .fr-ment{background:#16324c;color:#9fc9ec}
[data-theme="dark"] .fr-card .fr-sub .fr-off{border-color:#334052;color:#8b98ab}
[data-theme="dark"] .fr-mos .fr-t{background:#232c3c}

/* ---- A chat message carrying SEVERAL files ----------------------------------------------------
   The album is .fr-mos's geometry, sat inside a bubble whose padding is already 3px (.gbubble
   .hasimg), so it loses the card's top margin and takes the bubble's inner radius. .fr-x holds the
   photos past the fourth: they exist so the viewer's set is the WHOLE set, and are never drawn. */
.gatt-mos{margin-top:0;border-radius:9px;overflow:hidden}
.gatt-mos .fr-x{display:none}
/* .gvn is in this list because a voice note inside a SET is the pill now, not a bare <audio>. */
.gbubble .gatt-mos+.gatt-file,.gbubble .gatt-mos+.gatt-vid,.gbubble .gatt-mos+.gatt-aud,.gbubble .gatt-mos+.gvn{margin-top:4px}

/* ── A record's album costs the thread what a channel upload costs ──────────────────────────────
   Owner, 2026-08-11: "field report pictures should be same as channel uploads all in one gallery
   type this takes to much space and it's not looking good." A record card deliberately runs the
   FULL row (.fr-wrap / .insp-wrap are 100%, where an ordinary .gwrap is 78%), and its album used to
   run full-bleed inside that — so the very same photos were always bigger coming from a supervisor
   than from anyone else, one near-square picture per card all the way down the thread.
   The album now takes the ordinary bubble's 78% and the card's own 13px gutter, so a report's
   photos and a channel upload's photos are the same object at the same size. The CARD still runs
   full width: it is the pictures that match, not the record.
   ⚠ This rule has to exist because .gatt-mos above zeroes the top margin .fr-mos sets (an album in
   a chat bubble sits against 3px of bubble padding and wants none); inside a record card there is
   no bubble padding to sit against. */
.fr-card .gatt-mos,.insp-card .gatt-mos{margin:10px 13px 0;max-width:78%}
.gbubble .gatt-mos+.gatt-file,.gbubble .gatt-mos+.gatt-vid,.gbubble .gatt-mos+.gatt-aud{margin-top:4px}
.gatt-aud{display:block;width:100%;margin-top:5px}
/* ---- Files waiting in the composer ------------------------------------------------------------
   In NORMAL FLOW directly above the bar, the slot #g-reply and #g-plusrow already use — never an
   overlay, which is how the invisible .toast used to eat taps on this bar. Scrolls sideways rather
   than shrinking its targets. */
.g-pend{display:flex;align-items:center;gap:6px;background:var(--gb-surf);border-top:1px solid var(--line);
  padding:8px;position:relative;z-index:30;overflow-x:auto;-webkit-overflow-scrolling:touch;scrollbar-width:none}
.g-pend::-webkit-scrollbar{display:none}
.g-pt{position:relative;flex:0 0 auto;width:58px;height:58px;border-radius:9px;overflow:hidden;background:var(--gb-field)}
.g-pt img,.g-pt-v{width:100%;height:100%;object-fit:cover;display:block}
/* A staged clip shows its own first frame (preload="metadata" on a local blob costs nothing), so it
   needs one mark saying "this one moves" — otherwise it is indistinguishable from a photo in a
   58px square. A ▶ glyph drawn in CSS: no icon lookup, no extra node to keep in sync. */
.g-pt-play{position:absolute;left:50%;top:50%;transform:translate(-50%,-50%);width:0;height:0;
  border-left:12px solid rgba(255,255,255,.94);border-top:8px solid transparent;border-bottom:8px solid transparent;
  filter:drop-shadow(0 1px 2px rgba(0,0,0,.6));pointer-events:none}
.g-pt.file{width:auto;min-width:92px;max-width:150px;display:flex;flex-direction:column;align-items:center;justify-content:center;gap:3px;padding:6px 8px;background:var(--gb-tint);color:var(--ink)}
.g-pt-ic svg,.g-pt-ic .ic{width:20px;height:20px}
.g-pt-n{font-size:10px;line-height:1.2;max-width:100%;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
/* 26px, not the --tap 44px floor: it sits ON a 58px thumbnail, and a 44px target would cover the
   picture it is meant to let you check before sending. The thumbnail is not itself a target. */
.g-pt-x{position:absolute;top:2px;right:2px;width:26px;height:26px;border:0;border-radius:50%;background:rgba(6,16,28,.72);
  color:#fff;font-size:12px;line-height:1;padding:0;display:flex;align-items:center;justify-content:center;cursor:pointer}
.g-pt-c{flex:0 0 auto;margin-left:auto;padding-left:6px;font-size:11px;font-weight:600;color:var(--muted)}
/* ---- A file riding the LARGE lane -------------------------------------------------------------
   It is not a 58px square: it carries a name AND a live percentage, and squeezing both into the
   photo tile's box gives four legible characters. Wider, and the ONLY tile in the strip that grows.
   The determinate bar is the point of the whole treatment — 300 MB over an Afghan link with a
   static screen is indistinguishable from a frozen app, and a frozen app gets force-quit. */
/* padding-right clears the absolutely-positioned × (26px at right:2px, section above) — without it
   a long filename runs underneath the remove button and both become unreadable. */
.g-pt.big{width:auto;min-width:132px;max-width:190px;height:auto;min-height:58px;
  display:flex;flex-direction:column;align-items:stretch;justify-content:center;gap:4px;
  padding:7px 9px;padding-right:32px;background:var(--gb-tint);color:var(--ink)}
.g-pt.big .g-pt-n{max-width:100%;text-align:left}
/* A large clip still shows its own first frame — at 300 MB "is this the right one?" is worth more
   here than on any other tile — but the media has to be given a height EXPLICITLY. The square
   tile's `.g-pt img,.g-pt-v{height:100%}` resolves against a content-sized column here, which is
   circular: the thumbnail collapses to nothing and the bar slides up under the ✕. The ▶ is then
   pinned to the middle of THAT band (7px padding + 46/2) instead of the middle of the whole tile,
   which now includes the progress block below it. */
.g-pt.big .g-pt-v,.g-pt.big>img{flex:none;height:46px;border-radius:7px}
.g-pt.big .g-pt-play{top:30px}
.g-pt-up{display:flex;flex-direction:column;gap:3px}
/* scaleX on an <i> inside a fixed track, never a width — a width change relayouts the compose bar
   on every frame of a ten-minute upload (DESIGN-SYSTEM §7.3 rule 2). transform-origin:left is what
   makes the bar grow from the left rather than from its middle. */
.g-pt-bar{position:relative;height:3px;border-radius:2px;background:var(--gb-press);overflow:hidden}
.g-pt-bar i{display:block;height:100%;width:100%;transform-origin:left center;transform:scaleX(0);
  border-radius:2px;background:var(--blue-bright,#1a86d0)}
.g-pt-pct{font-family:var(--font-mono);font-size:9.5px;font-weight:700;color:var(--muted);
  font-variant-numeric:tabular-nums;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
/* A failed upload says so in colour as well as in words — the sentence itself can be clipped at
   this width, and the tile has to read as "wrong" before anyone squints at the text. */
.g-pt-up.bad .g-pt-bar i{background:var(--red)}
.g-pt-up.bad .g-pt-pct{color:var(--red)}
[data-theme="dark"] .g-pt.big{background:var(--gb-field)}
/* The one control that can stop a running upload. Deliberately NOT the per-tile ×, which is inert
   during a send by design (see gPendBusy) and could not reach an XHR anyway. It sits at the end of
   the strip beside the counter, and .g-pend.sending's dim must not reach it — a cancel that looks
   half-disabled is the one control here that has to look pressable. */
.g-pt-cn{flex:0 0 auto;margin-left:8px;min-height:var(--tap,44px);padding:0 14px;border:0;
  border-radius:22px;background:var(--gb-press);color:var(--red);font-family:inherit;
  font-size:12.5px;font-weight:700;cursor:pointer}
/* [hidden] must actually hide: the strip is a flex row, and `.g-pend > *` layout would otherwise
   lay this button out at full size — the same trap `.g-compose [hidden]` exists for. */
.g-pt-cn[hidden]{display:none!important}
.g-pt-cn:active{background:var(--red);color:#fff}
@media (prefers-reduced-motion:no-preference){
  /* The bar itself is smoothed so a chunky onprogress tick does not jump; --t-fast, not longer, or
     the paint lags behind the real number and the percentage and the bar disagree on screen. */
  .g-pt-bar i{transition:transform var(--t-fast) linear}
  .g-pt-cn{transition:background var(--t-fast) var(--ease),color var(--t-fast) var(--ease)}
}
/* SENDING. A 40 MB clip is thirty seconds-plus of upload on a weak link, and thirty seconds of a
   dead-looking screen is how a half-sent message gets force-quit. The strip dims, its × buttons go
   inert (dropping a file mid-post would send a set that no longer matches what is on screen) and a
   hairline sweeps underneath — an indeterminate bar, because the one POST reports no progress and
   a fake percentage would be a lie about how long is left.
   prefers-reduced-motion gets the bar without the sweep: still visibly a different state. */
/* ⚠ THE DIM IS ON THE TILES, NOT ON THE STRIP. It used to be `.g-pend.sending{opacity:.62}`, and
   opacity on a parent is a GROUP opacity — it composites the whole subtree, so nothing inside can
   ever declare its way back to full. That was invisible while the strip held only tiles; the moment
   it grew a Cancel control (the only thing on screen able to stop a 300 MB upload) the blanket dim
   made the one button that must look pressable look half-disabled, and no rule on the button could
   undo it. Dimming the tiles instead is the same painting for everything that was already there. */
.g-pend.sending .g-pt{opacity:.62}
.g-pend.sending .g-pt-x{pointer-events:none;opacity:.5}
.g-pend.sending::after{content:'';position:absolute;left:0;right:0;bottom:0;height:2px;
  background:linear-gradient(90deg,transparent,var(--blue-bright,#1a86d0),transparent);
  background-size:44% 100%;background-repeat:no-repeat;animation:gPendSweep 1.15s linear infinite}
@keyframes gPendSweep{from{background-position:-46% 0}to{background-position:146% 0}}
@media (prefers-reduced-motion:reduce){.g-pend.sending::after{animation:none;background:var(--blue-bright,#1a86d0)}}

/* ==============================================================================================
   CHAT SKIN 2026 — the bar is the owner's WhatsApp screenshots (phone, dark theme).
   "this is how the chat should look, clean pro ui… the skin is as important as the code."

   Everything below is appended, so it wins over the earlier chat rules by document order at equal
   specificity. Colours are behind variables (--gb-*) so a bubble fill can be retuned in one line
   instead of hunting through selectors.
   ============================================================================================== */

/* --- 1. Theme tokens ---------------------------------------------------------------------------
   guard.js resolves light/dark/auto and stamps data-theme on <html> BEFORE the app paints, so the
   whole stylesheet only ever needs the explicit [data-theme="dark"] branch. The media query below
   exists purely to colour the single pre-JS frame on a dark phone. */
@media (prefers-color-scheme:dark){html:not([data-theme]){background:#0f1723}}
:root{
  --gb-chat:#e7edf4;                       /* the paper the bubbles float on */
  --gb-them:#ffffff; --gb-them-line:#e2e9f1; --gb-them-ink:#16222f;
  --gb-mine:linear-gradient(135deg,#005080,#0a6bb0); --gb-mine-ink:#ffffff;
  --gb-chip:rgba(255,255,255,.94); --gb-chip-ink:#5b6b80;
  --gb-tick:#0a6bb0;                       /* acknowledged double-tick */
  --gb-rule:rgba(20,40,70,.14);
  --gb-surf:#ffffff;                       /* compose bar / mention list surface */
  --gb-field:#f2f5f9; --gb-field-line:#dde5ee;
  --gb-tint:#e7f0fa;
  --gb-press:rgba(10,30,60,.07);
}
[data-theme="dark"]{
  /* Global tokens first — every component that already reads var(--ink)/--bg/--card/--line follows
     these without needing a rule of its own. */
  --ink:#e6eaf2; --muted:#8b98ab; --line:#26303f; --bg:#0f1723;
  --panel:#161f2c; --card:#1a2231; --heading:#eaf0fa;
  /* navy-as-TEXT only — see the note beside :root at the top of this file. #8fc4f0 is the accent
     ink this stylesheet already settled on for dark (.gh-link, .gh-more, .gtrn-txt b, .tk-reopen);
     measured 8.60:1 on --card #1a2231, against 1.10:1 for the #032b49 it replaces. */
  --navy-ink:#8fc4f0;
  --gb-chat:#0d1420;
  --gb-them:#1a2231; --gb-them-line:#26303f; --gb-them-ink:#dbe3ee;
  /* Deep green own bubble, per the reference. It is a SURFACE here, not a status colour — the
     duty/OK greens elsewhere in the app are #0f9d58 and never this. Swap this one line back to
     var(--grad-primary) if the navy gradient is wanted in dark too. */
  --gb-mine:#0b5744; --gb-mine-ink:#e8f6f0;
  --gb-chip:rgba(30,42,58,.92); --gb-chip-ink:#9aa9bd;
  --gb-tick:#53bdeb;
  --gb-rule:rgba(150,180,220,.16);
  --gb-surf:#141b28;
  --gb-field:#1e2735; --gb-field-line:#2b3648;
  --gb-tint:#16273a;
  --gb-press:rgba(255,255,255,.09);
}

/* --- 2. The thread canvas + doodle pattern -----------------------------------------------------
   A 360px tile of faint line-art (rings, stars, plus marks, chevrons, waves), hand-written and
   inlined as a data-URI so it costs no request and works offline. Nothing crosses a tile edge, so
   the repeat never clips a shape. Two copies: dark strokes for the light paper, light strokes for
   the dark one, both at an opacity where you only notice it when the thread is nearly empty. */
:root{--gb-doodle:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='360' height='360'%3E%3Cg fill='none' stroke='%232c4560' stroke-width='1.5' stroke-linecap='round' stroke-linejoin='round' opacity='.085'%3E%3Ccircle cx='34' cy='44' r='7'/%3E%3Cpath d='M92 28q1.7 7.6 8.2 8.8q-6.5 1.2-8.2 8.8q-1.7-7.6-8.2-8.8q6.5-1.2 8.2-8.8z'/%3E%3Cpath d='M150 46h16M158 38v16'/%3E%3Cpath d='M212 32l9 9-9 9'/%3E%3Crect x='262' y='30' width='19' height='19' rx='3'/%3E%3Ccircle cx='325' cy='42' r='3.5'/%3E%3Cpath d='M20 108q14-17 28 0t28 0'/%3E%3Ccircle cx='112' cy='106' r='4'/%3E%3Cpath d='M152 96l10 18h-20z'/%3E%3Cpath d='M198 94v20M188 104h20'/%3E%3Ccircle cx='252' cy='106' r='9'/%3E%3Cpath d='M302 94q1.7 7.6 8.2 8.8q-6.5 1.2-8.2 8.8q-1.7-7.6-8.2-8.8q6.5-1.2 8.2-8.8z'/%3E%3Cpath d='M30 168l9 9-9 9'/%3E%3Crect x='74' y='163' width='18' height='18' rx='3'/%3E%3Ccircle cx='136' cy='174' r='3.5'/%3E%3Cpath d='M170 184q14-17 28 0'/%3E%3Cpath d='M232 162v20M222 172h20'/%3E%3Ccircle cx='284' cy='173' r='6.5'/%3E%3Cpath d='M326 160q1.7 7.6 8.2 8.8q-6.5 1.2-8.2 8.8q-1.7-7.6-8.2-8.8q6.5-1.2 8.2-8.8z'/%3E%3Ccircle cx='42' cy='242' r='4'/%3E%3Cpath d='M86 232l10 18h-20z'/%3E%3Cpath d='M134 230v20M124 240h20'/%3E%3Cpath d='M178 232l9 9-9 9'/%3E%3Ccircle cx='228' cy='243' r='9'/%3E%3Crect x='272' y='231' width='18' height='18' rx='3'/%3E%3Cpath d='M320 238q10-12 20 0'/%3E%3Cpath d='M28 304q1.7 7.6 8.2 8.8q-6.5 1.2-8.2 8.8q-1.7-7.6-8.2-8.8q6.5-1.2 8.2-8.8z'/%3E%3Ccircle cx='86' cy='315' r='6.5'/%3E%3Cpath d='M128 306l9 9-9 9'/%3E%3Cpath d='M174 304v20M164 314h20'/%3E%3Ccircle cx='224' cy='316' r='3.5'/%3E%3Crect x='262' y='306' width='18' height='18' rx='3'/%3E%3Cpath d='M306 322q14-17 28 0'/%3E%3C/g%3E%3C/svg%3E")}
[data-theme="dark"]{--gb-doodle:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='360' height='360'%3E%3Cg fill='none' stroke='%23b9d2e8' stroke-width='1.5' stroke-linecap='round' stroke-linejoin='round' opacity='.055'%3E%3Ccircle cx='34' cy='44' r='7'/%3E%3Cpath d='M92 28q1.7 7.6 8.2 8.8q-6.5 1.2-8.2 8.8q-1.7-7.6-8.2-8.8q6.5-1.2 8.2-8.8z'/%3E%3Cpath d='M150 46h16M158 38v16'/%3E%3Cpath d='M212 32l9 9-9 9'/%3E%3Crect x='262' y='30' width='19' height='19' rx='3'/%3E%3Ccircle cx='325' cy='42' r='3.5'/%3E%3Cpath d='M20 108q14-17 28 0t28 0'/%3E%3Ccircle cx='112' cy='106' r='4'/%3E%3Cpath d='M152 96l10 18h-20z'/%3E%3Cpath d='M198 94v20M188 104h20'/%3E%3Ccircle cx='252' cy='106' r='9'/%3E%3Cpath d='M302 94q1.7 7.6 8.2 8.8q-6.5 1.2-8.2 8.8q-1.7-7.6-8.2-8.8q6.5-1.2 8.2-8.8z'/%3E%3Cpath d='M30 168l9 9-9 9'/%3E%3Crect x='74' y='163' width='18' height='18' rx='3'/%3E%3Ccircle cx='136' cy='174' r='3.5'/%3E%3Cpath d='M170 184q14-17 28 0'/%3E%3Cpath d='M232 162v20M222 172h20'/%3E%3Ccircle cx='284' cy='173' r='6.5'/%3E%3Cpath d='M326 160q1.7 7.6 8.2 8.8q-6.5 1.2-8.2 8.8q-1.7-7.6-8.2-8.8q6.5-1.2 8.2-8.8z'/%3E%3Ccircle cx='42' cy='242' r='4'/%3E%3Cpath d='M86 232l10 18h-20z'/%3E%3Cpath d='M134 230v20M124 240h20'/%3E%3Cpath d='M178 232l9 9-9 9'/%3E%3Ccircle cx='228' cy='243' r='9'/%3E%3Crect x='272' y='231' width='18' height='18' rx='3'/%3E%3Cpath d='M320 238q10-12 20 0'/%3E%3Cpath d='M28 304q1.7 7.6 8.2 8.8q-6.5 1.2-8.2 8.8q-1.7-7.6-8.2-8.8q6.5-1.2 8.2-8.8z'/%3E%3Ccircle cx='86' cy='315' r='6.5'/%3E%3Cpath d='M128 306l9 9-9 9'/%3E%3Cpath d='M174 304v20M164 314h20'/%3E%3Ccircle cx='224' cy='316' r='3.5'/%3E%3Crect x='262' y='306' width='18' height='18' rx='3'/%3E%3Cpath d='M306 322q14-17 28 0'/%3E%3C/g%3E%3C/svg%3E")}
.g-thread{gap:0;padding:6px 10px 14px;position:relative;background-color:var(--gb-chat);background-image:var(--gb-doodle);background-repeat:repeat;background-size:360px 360px}

/* --- 3. Bubble geometry -----------------------------------------------------------------------
   Radius 14 with the corner nearest the sender notched to 5, ~78% max width, and a run of bubbles
   from one person hugging at 2px while a change of speaker opens up to 10px. Only the FIRST bubble
   of a run is notched — the rest are fully rounded, which is what makes a run read as one block. */
.gwrap{max-width:78%;margin:10px 0 0}
.gwrap.cont{margin-top:2px}
.g-thread>.gwrap:first-child{margin-top:2px}
.gwrap>.gbubble{max-width:100%;margin:0;position:relative;box-shadow:0 1px 1px rgba(11,22,40,.10)}
.gwrap.them>.gbubble{background:var(--gb-them);border:1px solid var(--gb-them-line);color:var(--gb-them-ink);border-radius:5px 14px 14px 14px;padding:7px 11px 7px 11px;font-size:14px;line-height:1.4}
.gwrap.mine>.gbubble{background:var(--gb-mine);border:none;color:var(--gb-mine-ink);border-radius:14px 5px 14px 14px;padding:7px 11px;font-size:14px;line-height:1.4}
.gwrap.cont.them>.gbubble{border-radius:14px}
.gwrap.cont.mine>.gbubble{border-radius:14px}
.gwrap.them>.gbubble.prio,.gwrap.mine>.gbubble.prio{border:1.5px solid #e11d2a}
/* sender name above the bubble: incoming, group channel, first of a run — and nowhere else */
.bm2{font-size:12.5px;font-weight:700;margin:0 0 2px 3px;letter-spacing:.01em}
.gtx{white-space:pre-wrap;word-break:break-word}
/* "(edited)" — the console's .bedited, in this app's type scale. Inherits the bubble's own ink in
   both themes and on both sides of the thread, so there is no second palette to keep in sync; the
   opacity is the whole treatment. Not selectable and never wrapped away from the word it follows. */
.gedited{margin-left:5px;font-size:10.5px;opacity:.6;user-select:none;white-space:nowrap}
.gwrap.mine .gedited{opacity:.72}

/* --- 4. Time + delivery ticks, inside the bubble ---------------------------------------------- */
.gmeta{display:inline-flex;align-items:center;gap:3px;font-family:var(--font-mono);font-size:10px;line-height:1;white-space:nowrap;color:rgba(255,255,255,.68)}
.gwrap.them .gmeta{color:var(--muted)}
.gbubble.meta-abs>.gmeta{position:absolute;right:10px;bottom:6px}
.gbubble.meta-row>.gmeta{display:flex;justify-content:flex-end;margin-top:5px}
.gbubble.meta-over>.gmeta{position:absolute;right:9px;bottom:9px;color:#fff;background:rgba(6,16,28,.5);padding:3px 7px;border-radius:11px}
/* an inline spacer the size of the floated meta, so the last line of text can never run under it.
   Widened when the bubble clock went 12-hour: "23:59" is 5 mono glyphs, "11:59 PM" is 8, and the
   old 58/42 reserved for the short form let the last line run under the time again. */
.gmspace{display:inline-block;width:78px;height:1px}
.gwrap.them .gmspace{width:62px}
.gtkw{display:inline-flex;align-items:center}
.gtkw .gtk{width:15px;height:12px;display:block}
.gtkw.ok{color:var(--gb-tick)}
.gpin .ic{width:11px;height:11px;vertical-align:-1px;opacity:.85}
.gprio{display:flex;align-items:center;gap:4px;width:fit-content;font-size:9.5px;font-weight:800;letter-spacing:.07em;border-radius:99px;padding:3px 8px;margin:1px 0 5px;background:#fbe3e5;color:#c0182a}
.gprio .ic{width:11px;height:11px}
.gwrap.mine .gprio{background:rgba(255,255,255,.22);color:#fff}
[data-theme="dark"] .gwrap.them .gprio{background:#3d1a1e;color:#f08d96}
/* @mention token, same treatment the console gives a bubble body */
.ment{font-weight:700;color:#0a5a94;background:rgba(10,107,176,.13);border-radius:4px;padding:0 3px}
/* ★ ON AN OUTGOING BUBBLE THE PILL IS A SCRIM, NOT A HIGHLIGHT — and that is a contrast fix, not a
   taste one. --gb-mine is a gradient (#005080 → #0a6bb0), so a translucent WHITE pill under white
   ink has to clear AA at the LIGHT end too, and it did not: measured in Chromium at #ffffff on
   #408cc1 = 3.67:1 here, and 3.28:1 for the two ringed chips below at .28 — under the 4.5:1 floor
   for text this size. Darkening the overlay instead of lightening it keeps the idiom (a
   translucent layer wearing the bubble's own ink) and clears both ends of the gradient with room:
   8:1 at #0a6bb0, 11.21:1 at #005080. The console carries the identical change. */
.gwrap.mine .ment{color:#fff;background:rgba(0,0,0,.22)}
[data-theme="dark"] .ment{color:#7fc4f0;background:rgba(127,196,240,.15)}
[data-theme="dark"] .gwrap.mine .ment{color:#bdf0dd;background:rgba(255,255,255,.15)}
/* An @everyone that actually FANNED OUT (server flag at_everyone) — amber, because red on this app
   is SOS and a broadcast is loud, not an emergency. A message that merely contains the word keeps
   the ordinary blue chip above: the styling must never claim a fan-out the server refused. */
.ment.ment-all{color:#8a5a00;background:rgba(232,169,60,.22);box-shadow:inset 0 0 0 1px rgba(190,132,0,.35)}
.gwrap.mine .ment.ment-all{color:#fff;background:rgba(0,0,0,.22);box-shadow:inset 0 0 0 1px rgba(255,255,255,.45)}
[data-theme="dark"] .ment.ment-all{color:#f4c869;background:rgba(244,200,105,.16);box-shadow:inset 0 0 0 1px rgba(244,200,105,.32)}
/* A ROLE ping that really fanned out (server sentinel '@role:<key>'). The console twin of the same
   chip: violet, because blue is a personal @ and amber is @everyone, and it wears the same 1px
   inset ring as .ment-all so the two read as one family — "this actually notified people" — while
   a plain @ stays flat. Loudness sits between them, which is exactly what a desk ping is. */
.ment.ment-role{color:#4a3390;background:rgba(74,51,144,.14);box-shadow:inset 0 0 0 1px rgba(74,51,144,.28)}
/* ⚠ This rule serves BOTH themes. There is no [data-theme="dark"] .gwrap.mine twin for the ringed
   chips, and at (0,4,0) this rule OUT-RANKS the dark them-side rule below at (0,3,0) — so it was
   the DARK own-bubble chip too, measured in Chromium at #ffffff on #4f8678 (rgba(255,255,255,.28)
   over --gb-mine #0b5744) = 4.18:1, under AA in dark as well. The scrim takes it to 11.13:1 there
   and 8:1 in light. (The bare .ment above is the opposite case: [data-theme="dark"] .gwrap.mine
   .ment IS (0,4,0), so the dark twin keeps that one and its look is unchanged.) */
.gwrap.mine .ment.ment-role{color:#fff;background:rgba(0,0,0,.22);box-shadow:inset 0 0 0 1px rgba(255,255,255,.45)}
[data-theme="dark"] .ment.ment-role{color:#c3b4f2;background:rgba(195,180,242,.16);box-shadow:inset 0 0 0 1px rgba(195,180,242,.32)}
/* ---- Forwarded-message quote strip (inside the bubble, above the text). .gbubble is
   white-space:pre-wrap, so this is an inline-block SPAN with display:flex — a block element here
   would inherit the bubble's preserved whitespace and open a blank line under itself. ---- */
.gfwd{display:flex;align-items:center;gap:6px;margin:0 0 5px;padding:3px 7px;border-left:3px solid currentColor;
  background:rgba(10,107,176,.09);border-radius:0 7px 7px 0;font-size:11.5px;color:#4a5c72;white-space:normal}
[data-theme="dark"] .gfwd{color:#9fb3c9}
.gfwd svg{width:13px;height:13px;flex:none;opacity:.8}
.gfwd-tx{flex:1;min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
.gfwd-tx b{color:var(--ink);font-weight:700}
.gfwd-jump{flex:none;border:1px solid var(--line);background:var(--card);color:var(--blue,#0a6bb0);
  font:inherit;font-size:11px;font-weight:700;border-radius:99px;padding:3px 10px;min-height:26px;cursor:pointer}
.gwrap.mine .gfwd{background:rgba(255,255,255,.16);color:rgba(255,255,255,.85)}
.gwrap.mine .gfwd-tx b{color:#fff}
.gwrap.mine .gfwd-jump{background:rgba(255,255,255,.2);border-color:rgba(255,255,255,.4);color:#fff}
[data-theme="dark"] .gwrap.them .gfwd{background:rgba(127,196,240,.1)}
/* --blue is not re-declared for dark, so the light #0a6bb0 label landed on the dark #1a2231 card at
   ~2.5:1 — unreadable at 11px. MEASURED, not guessed: this pair is ~7.4:1. */
[data-theme="dark"] .gfwd-jump{background:#16324c;border-color:#26445f;color:#9fc9ec}
/* Forward picker: the sheet is a list, so it scrolls inside itself rather than growing past the
   safe area on a phone with 30 conversations. */
.gfwd-note{font-size:12px;color:var(--muted);padding:2px 2px 8px;line-height:1.45}
/* The filter box carries NO skin of its own: section 1's by-element form rule ("one form skin, by
   element") already paints every `.g-sheet input` — 46px tall, 12px radius, --gb-field in both
   themes, 16px so neither mobile browser zooms on focus. All this adds is the gap to the list. */
.gfwd-q{margin:0 0 10px}
/* max-height stays in vh on purpose. guard.html ships interactive-widget=resizes-content, so the
   Android keyboard SHRINKS the layout viewport and 52vh re-resolves against what is still visible —
   the list stays above the keyboard instead of behind it. */
.gfwd-list{max-height:52vh;overflow:auto;-webkit-overflow-scrolling:touch}
.gfwd-none{padding:22px 8px;text-align:center;font-size:13.5px;color:var(--muted)}
/* "Jump to message" landing flash — 3s settle, then back to whatever skin the bubble had. */
@keyframes gflash{0%{box-shadow:0 0 0 3px rgba(10,107,176,.55)}100%{box-shadow:0 0 0 3px rgba(10,107,176,0)}}
.gwrap.gflash>.gbubble{animation:gflash 3s ease-out;border-radius:14px}
[data-theme="dark"] .gwrap.gflash>.gbubble{animation:gflash 3s ease-out}
/* …and a message that tags YOU is not the same as a message that tags anyone else. Discord's rule:
   a warm wash plus a solid left edge, so a tag is findable by scrolling past it rather than by
   reading every bubble. Incoming only — .mine is the officer's own navy gradient and a message you
   wrote that names you is not news.
   ⚠ Specificity, checked against every later rule that repaints this bubble: the wash is (0,3,1) so
   it outranks `.gwrap.them>.gbubble` (0,2,1) at ~1116, and the dark copy is (0,4,1) so it outranks
   `[data-theme="dark"] .gwrap.them>.gbubble` (0,3,1) at ~1302 — which sits BELOW it in the file and
   would otherwise win on order alone. The edge is held off .prio deliberately: `.gwrap.them>.gbubble
   .prio` is (0,3,1) too, so an equal-specificity amber rule written later would quietly repaint a
   PRIORITY message's red border. Priority outranks a mention. */
.gwrap.atme.them .gbubble{background:#fff8e8}
.gwrap.atme.them .gbubble:not(.prio){border-color:#f0dcae;border-left:3px solid #e0a12a}
.gwrap.atme.them .gbubble.fr-card:not(.prio){border-left-color:#e0a12a}
[data-theme="dark"] .gwrap.atme.them .gbubble{background:#33291a;color:#e8dcc4}
[data-theme="dark"] .gwrap.atme.them .gbubble:not(.prio){border-color:#5c4a26;border-left-color:#e0a12a}
/* Links in a bubble — WhatsApp's treatment: underlined, tinted to stay legible on BOTH bubble
   colors, and long URLs must break rather than stretch the bubble off-screen. */
.glink{color:#0a6bb0;text-decoration:underline;word-break:break-all;font-weight:600}
/* .glink is both a chat-bubble link and a plain app link (patrol "Improve with AI", "Clear
   signature"). Inside a bubble the two rules below win on specificity and keep their own colours;
   this only catches the app-surface case, which measured 2.95:1 -> 2.84:1 #0a6bb0 on --card. */
[data-theme="dark"] .glink{color:#8fc4f0}
.glink .ic{width:13px;height:13px;vertical-align:-2px;margin-right:3px}
.gwrap.mine .glink{color:#d2f2e5}
[data-theme="dark"] .gwrap.them .glink{color:#53bdeb}
[data-theme="dark"] .gwrap.mine .glink{color:#8fe0c2}
/* A link to a conversation INSIDE this app is not a URL, so it does not read as one: a pill that
   names the destination, sharing the meeting card's tint family because both are in-app doors. It
   carries no href on purpose (see gDeepChipHtml), so the cursor and the tap floor are stated here
   rather than inherited from a real anchor — and the label must not break-all like a raw URL. */
.gdlink{display:inline-flex;align-items:center;gap:5px;max-width:100%;vertical-align:-3px;
  padding:3px 11px;border-radius:99px;background:rgba(10,107,176,.12);color:#0a6bb0;
  font-weight:700;font-size:12.5px;text-decoration:none;word-break:normal;overflow-wrap:anywhere;cursor:pointer;
  transition:background-color var(--t-fast,150ms) var(--ease,ease)}
.gdlink svg{flex:none;width:13px;height:13px}
.gdlink:active{background:rgba(10,107,176,.22)}
.gdlink:focus-visible{outline:2px solid #0a6bb0;outline-offset:2px}
/* On the officer's own bubble. Unconditional here, unlike the console twin: this thread has ONE
   layout and `.gwrap.mine .gbubble` is always the blue gradient, so there is no flat-row state in
   which the fill disappears from under the chip. BLACK-tinted rather than white-tinted for the
   reason the console note gives — white on translucent white over that gradient measures ~3.7:1. */
.gwrap.mine .gdlink{background:rgba(0,0,0,.2);color:#fff}
.gwrap.mine .gdlink:active{background:rgba(0,0,0,.3)}
[data-theme="dark"] .gdlink{background:rgba(83,189,235,.16);color:#8fc4f0}
[data-theme="dark"] .gdlink:active{background:rgba(83,189,235,.26)}
[data-theme="dark"] .gwrap.mine .gdlink{background:rgba(0,0,0,.26);color:#fff}
/* Meeting invite card — a call is an action, so it gets one full-width Join target */
.gmeetcard{display:flex;align-items:center;gap:10px;min-width:210px;margin:2px 0 1px;padding:8px 10px;border-radius:12px;cursor:pointer;background:rgba(10,107,176,.09);white-space:nowrap}
.gmeetcard .gmc-ic{flex:none;width:36px;height:36px;border-radius:50%;display:flex;align-items:center;justify-content:center;background:var(--gb-tint,rgba(10,107,176,.14));color:var(--blue,#0a6bb0)}
.gmeetcard .gmc-ic .ic,.gmeetcard .gmc-ic svg{width:18px;height:18px}
.gmeetcard .gmc-tx{display:flex;flex-direction:column;gap:1px;min-width:0}
.gmeetcard .gmc-tx b{font-size:13.5px;font-weight:700}
.gmeetcard .gmc-tx span{font-size:11.5px;opacity:.75}
.gmeetcard .gmc-join{flex:none;margin-left:auto;font-size:12.5px;font-weight:800;letter-spacing:.02em;border-radius:99px;padding:6px 14px;background:var(--blue,#0a6bb0);color:#fff}
.gwrap.mine .gmeetcard{background:rgba(255,255,255,.14)}
.gwrap.mine .gmeetcard .gmc-ic{background:rgba(255,255,255,.22);color:#fff}
.gwrap.mine .gmeetcard .gmc-join{background:rgba(255,255,255,.92);color:#0b5744}
[data-theme="dark"] .gwrap.them .gmeetcard{background:rgba(83,189,235,.10)}
[data-theme="dark"] .gwrap.them .gmeetcard .gmc-ic{background:rgba(83,189,235,.16);color:#53bdeb}
[data-theme="dark"] .gwrap.them .gmeetcard .gmc-join{background:#53bdeb;color:#0c1521}

/* --- 5. Reactions + per-message actions, OUTSIDE the bubble ------------------------------------
   They used to sit inside it, which crowded every single message with chrome and left nowhere for
   the time to go. Out here the bubble interior is just the message. Scoped to a DIRECT child of
   .gwrap so the field-report card's own .grxbar (inside .fr-foot) is untouched. */
.gwrap>.grxbar{display:flex;flex-wrap:wrap;align-items:center;gap:4px;margin:3px 2px 0}
.gwrap.mine>.grxbar{justify-content:flex-end}
.gwrap>.grxbar .grx{background:var(--gb-them);border:1px solid var(--gb-them-line);color:var(--ink);border-radius:16px;padding:3px 9px;font-size:13px;min-height:28px;box-shadow:0 1px 1px rgba(11,22,40,.08)}
.gwrap>.grxbar .grx.on{background:var(--gb-tint);border-color:#8fb3ff}
.grx.act{border:none!important;background:transparent!important;box-shadow:none!important;color:var(--muted);opacity:.45;width:30px;height:30px;min-height:30px;padding:0;display:inline-flex;align-items:center;justify-content:center;border-radius:50%}
.grx.act svg{width:15px;height:15px}
.grx.act:active{opacity:1;background:var(--gb-press)!important}
[data-theme="dark"] .gwrap>.grxbar .grx.on{background:#26407a;border-color:#4a74c8}

/* ---- 44px IN THE THREAD REACTION ROW -----------------------------------------------------------
   The chips above are RE-STYLED SMALLER than the base .grx they inherit from, and the old hit-area
   bleed was never re-sized with them. .grx (~1259) is min-height:32px and its ::after (~1262) bleeds
   left/right -4px, top/bottom -6px — 32+12 = the 44px floor, exactly. Then this section pulls the
   thread chip down to min-height:28px and .grx.act down to 30x30, and the bleed sized for 32 is
   suddenly sized for nothing.

   MEASURED live, headless Chromium, officer thread at 390x844 and 360x800, elementFromPoint walked
   out from each control's centre until it stops answering:

                              painted      hit box BEFORE   hit box WITH THIS BLOCK
     .grx chip                48.4 x 28.8  52.3 x 36.4      48.9 x 39.1
     .grx.act reply / react   30   x 30    33.9 x 38.1      34.0 x 39.1
     .grx.act ⋯ (last in row) 30   x 30    38.9 x 38.1      44.9 x 39.1

   ⚠ READ THAT LAST COLUMN BEFORE ASSUMING THIS BUYS THE FLOOR BACK. It does not, and the reason is
   arithmetic in the LAYOUT, not in this rule:

     · WIDTH is capped by the row's PITCH. .grx.act is 30px painted in a bar with gap:4px, so the
       buttons repeat every 34px. Two adjacent 44px overlays overlap by 10px and the later sibling
       wins the overlap, so no button with a neighbour on its right can own more than 34px however
       wide its overlay is declared. Only the LAST control in the bar has a free side, which is why
       ⋯ measures 44.9 and reply/react do not. This is not new: the -4/-4 bleed already overlapped,
       and the probes show the right painted edge of every act button ALREADY belonged to its
       right-hand neighbour before this block.
     · HEIGHT is capped by `.gwrap.cont{margin-top:2px}` (~1609): a grouped follow-up message sits
       TWO pixels under the action row, its .gbubble is position:relative and LATER in the DOM, so
       it paints over the bottom of the overlay. Measured reach 22.97 up + 16.17 down = 39.1. On the
       last message in a thread, or under a NON-grouped message (10px), the same overlay takes a
       true 44.

   So this block raises the CEILING to 44 and takes it wherever the layout has room, which is the
   most an invisible overlay can do here. Closing the last 4.9px of height and the 10px of width
   needs the controls to actually BE 44px — the .rxq emoji row's idiom (min-width/min-height on the
   button itself) — which grows every message in every thread by ~14px. That is a density change
   with an owner's name on it, not a hit-area fix, and it is deliberately NOT made here.

   THE FORM. Centred translate, width:100% under min-width, never a flat width:var(--tap): a chip
   reading "👍 12" is well over 44px wide and a fixed-44 overlay would clamp its target to a band in
   the middle and lose the ends. width:100% resolves against the PADDING box, so on the bordered
   chip the overlay is the painted 48.4 minus its 2px of border = 46.4 (the chip's OWN box is
   hit-testable too, so the union still measures 48.9). That clears the floor, and it must NOT be
   "fixed" with a calc() that hardcodes the border width of a rule 480 lines up.

   right:auto / bottom:auto ARE LOAD-BEARING. The older `.grx::after` still matches this same
   pseudo-element and still supplies right:-4px and bottom:-6px; left+width+right and top+height+
   bottom are over-constrained boxes, and without these two resets the geometry rides on which
   declaration the engine chooses to drop rather than on what is written here.

   position:relative is re-stated even though .grx (~1259) already sets it. That is on purpose: this
   block has to survive somebody tidying up the base rule, and an overlay that loses its positioning
   context does not shrink — it flies off and hit-tests over an unrelated part of the thread.

   ⚠ THE OLD .grx::after AT ~1262 STAYS. It is still the only hit-area extension on every .grx
   OUTSIDE a message wrap — the field-report card renders its own .grxbar inside .fr-foot
   (guard.js ~10605 / ~10761) and is out of this fix's scope. Note that `.grx.act::after` below is
   deliberately UNSCOPED, matching the .grx.act rule it pairs with, so the field-report card's react
   button picks the overlay up too; that is one control on an unmeasured surface getting the same
   improvement, not a second design.

   Nothing here paints — no background, no border, no shadow — so there is no dark-theme twin to
   keep in step and the overlay can never draw over the emoji it sits on. */
.gwrap>.grxbar .grx{position:relative}
.gwrap>.grxbar .grx::after{content:"";position:absolute;top:50%;left:50%;right:auto;bottom:auto;
  transform:translate(-50%,-50%);width:100%;min-width:var(--tap,44px);height:var(--tap,44px)}
.grx.act{position:relative}
.grx.act::after{content:"";position:absolute;top:50%;left:50%;right:auto;bottom:auto;
  transform:translate(-50%,-50%);width:100%;min-width:var(--tap,44px);height:var(--tap,44px)}

/* --- 6. Day chips + the one-shot unread divider ------------------------------------------------ */
.gdayrow{display:flex;justify-content:center;margin:14px 0 8px}
.gdaychip{background:var(--gb-chip);color:var(--gb-chip-ink);font-size:10.5px;font-weight:800;letter-spacing:.07em;text-transform:uppercase;padding:5px 13px;border-radius:99px;box-shadow:0 1px 2px rgba(10,20,35,.10)}
.gunread{display:flex;align-items:center;justify-content:center;position:relative;margin:16px 0 8px}
.gunread::before{content:"";position:absolute;left:0;right:0;top:50%;height:1px;background:var(--gb-rule)}
.gunread>span{position:relative;background:var(--gb-chip);color:#c0182a;font-size:9.5px;font-weight:800;letter-spacing:.1em;padding:5px 14px;border-radius:99px;box-shadow:0 1px 2px rgba(10,20,35,.10)}
[data-theme="dark"] .gunread>span{color:#ff9aa2}
.gsys{background:var(--gb-chip);border:none;color:#c0182a;margin:12px auto 8px;box-shadow:0 1px 2px rgba(10,20,35,.10)}
[data-theme="dark"] .gsys{color:#ff9aa2}
.gnewmsg{bottom:calc(86px + env(safe-area-inset-bottom,0px))}

/* ---- "Jump to beginning": the new-messages pill's OWN slot, shared and yielded ----------------
   ⚠ It sits at EXACTLY .gnewmsg's bottom, and that is a constraint, not a preference. The always-on
   radio bar is position:fixed, full width (left:12/right:12) at z-index 100000, and inside a chat
   thread it is pinned to bottom:150px (line 945 above) — .gnewmsg's 86px slot is the band BELOW it.
   A 44px pill anywhere between 120px and 150px does not fit, and one at 138px spends 32 of its 44
   pixels underneath a bar that outranks it by five orders of z-index: invisible, and untappable.
   So the two pills share one slot and this one YIELDS, in CSS rather than in gStartPaint so the
   rule cannot be defeated by paint order — a message arriving while the reader sits still paints
   .gnewmsg with no scroll event to re-run the JS. Yielding is also the right priority on a phone:
   getting back to what just arrived outranks going to read what was said last year.
   Secondary treatment (chip surface, accent ink) for the same reason. --tap is the house 44px
   floor, and the pill IS the tap target here (no ::after trick needed at this size). */
.gstartpill{position:absolute;left:50%;transform:translateX(-50%);z-index:25;
  bottom:calc(86px + env(safe-area-inset-bottom,0px));
  min-height:var(--tap,44px);padding:0 17px;border-radius:22px;
  background:var(--gb-them);border:1px solid var(--gb-them-line);color:var(--navy-ink,#005080);
  font-family:inherit;font-size:13.5px;font-weight:700;
  box-shadow:0 6px 18px rgba(10,20,35,.22);display:inline-flex;align-items:center;gap:6px}
.gstartpill:active{background:var(--gb-press)}
.gchat:has(.gnewmsg) .gstartpill{display:none}
/* The beginning-of-conversation card. Built from what the thread already owns — the .gdaychip
   surface tokens — rather than a second card palette that could drift out of sync in dark. */
.gstart{display:flex;flex-direction:column;align-items:center;text-align:center;gap:5px;flex:none;
  margin:10px 6px 14px;padding:16px 14px;border-radius:16px;background:var(--gb-chip);
  box-shadow:0 1px 3px rgba(10,20,35,.14)}
.gstart b{font-size:15px;font-weight:800;line-height:1.25;color:var(--ink);overflow-wrap:anywhere}
.gstart span{font-size:12.5px;line-height:1.45;color:var(--gb-chip-ink);max-width:36ch}
/* Motion: this base carries no --t-fast/--t-med/--ease in :root, so the house numbers are written
   out — 200ms for something arriving, on the house curve. */
@media (prefers-reduced-motion:no-preference){
  @keyframes gstart-in{from{opacity:0;transform:translateY(-7px)}to{opacity:1;transform:none}}
  .gstart{animation:gstart-in 200ms cubic-bezier(.2,.7,.3,1)}
  .gstartpill{transition:background 150ms cubic-bezier(.2,.7,.3,1),box-shadow 150ms cubic-bezier(.2,.7,.3,1)}
}

/* Optimistic states. 'sending' is silent — the clock in the meta says it — but 'queued offline' and
   'failed' add a line of real words, and that line would sit right under the floated meta, so those
   two states push the meta back into normal flow. */
.g-pend-meta:empty{display:none}
.gwrap.pending.queued>.gbubble>.gmeta,.gwrap.pending.failed>.gbubble>.gmeta{position:static;display:flex;justify-content:flex-end;margin-top:4px}
.gwrap.pending.queued .gmspace,.gwrap.pending.failed .gmspace{display:none}

/* --- 7. Media --------------------------------------------------------------------------------- */
.gatt-img{display:block;width:100%;max-width:100%;max-height:none;height:auto;border-radius:11px;margin:0;cursor:pointer;background:var(--gb-field)}
.gwrap>.gbubble.hasimg{padding:3px;overflow:hidden}
.gbubble.hasimg .gtx{display:block;padding:6px 8px 4px}
.gbubble.hasimg.meta-abs>.gmeta{right:11px;bottom:7px}
.gatt-vid{max-width:100%;width:100%;margin:0;border-radius:11px}
.gatt-file{display:inline-flex;align-items:center;gap:6px;margin:2px 0;text-decoration:none;background:var(--gb-press);border-radius:9px;padding:8px 10px;font-size:13px}

/* --- 8. Voice note pill ------------------------------------------------------------------------
   Bars are the REAL peaks measured while the microphone was open (guard.js vnBars → vnPeaks), sent
   with the note and stored on the row. A note recorded before that shipped has none and falls back
   to a stable pattern hashed from the message id, so it still looks like a waveform. No audio file
   is ever decoded for its samples, in either case — an officer's handset is the worst machine we
   ship to and decodeAudioData on a three-minute note is seconds of jank for a picture.
   26 bars: the count guard.js renders and the count it asks vnBars for are the same number, and
   they have to stay that way. `.gvn-bars i` has a 2px floor and a 2px gap, so 40 bars would need
   158px of a column that is about 120px wide. */
.gvn{display:flex;align-items:center;gap:10px;min-width:206px;max-width:100%;padding:2px 0 1px;white-space:nowrap}
.gvn-pp{flex:none;width:34px;height:34px;border-radius:50%;border:0;padding:0;cursor:pointer;display:flex;align-items:center;justify-content:center;background:rgba(255,255,255,.2);color:#fff}
.gwrap.them .gvn-pp{background:var(--gb-tint);color:var(--blue,#0a6bb0)}
.gvn-pp svg{width:16px;height:16px}
.gvn-mid{flex:1;min-width:0}
.gvn-wave{position:relative;display:flex;align-items:center;height:22px;cursor:pointer;touch-action:manipulation}
.gvn-bars{display:flex;align-items:center;gap:2px;flex:1;height:100%}
.gvn-bars i{flex:1;min-width:2px;max-width:3px;border-radius:2px;background:currentColor;opacity:.32}
.gvn-bars i.on{opacity:1}
.gvn-dot{position:absolute;top:50%;left:0;width:9px;height:9px;margin:-4.5px 0 0 -4.5px;border-radius:50%;background:currentColor;pointer-events:none;opacity:.9}
.gvn-t{display:flex;justify-content:space-between;gap:8px;font-family:var(--font-mono);font-size:9.5px;opacity:.68;margin-top:1px}
.gvn-av{flex:none;width:32px;height:32px;border-radius:50%;display:flex;align-items:center;justify-content:center;font-family:var(--font-display,inherit);font-weight:800;font-size:13px;background:rgba(255,255,255,.2);color:#fff}
.gwrap.them .gvn-av{background:linear-gradient(135deg,#0a6bb0,#003050);color:#fff}
/* THE SAVE KEY, and the only thing the download policy controls. An admin's voice note is rendered
   with no .gvn-dl in the markup at all (guard.js gVnLocked) — so there is no anchor to long-press
   and no ⋯ menu, and the <audio> that plays it is a detached `new Audio()` that never enters the
   DOM. The server refuses download-shaped requests on its own; this is the affordance, not the
   fence. Inherits the bubble's ink, so it is white on `mine` and blue on `them` for free. */
.gvn-dl{position:relative;flex:none;width:30px;height:30px;border-radius:50%;display:flex;align-items:center;justify-content:center;
  color:inherit;opacity:.6;text-decoration:none;-webkit-tap-highlight-color:transparent}
.gvn-dl svg{width:16px;height:16px}
/* 30px painted + 7px of invisible margin all round = the house --tap 44px floor, via the ::after
   trick rather than a bigger button. .gvn's gap is 10px, so the overlay lives in the gutters and
   never steals a tap from the waveform's seek area on either side. */
.gvn-dl::after{content:"";position:absolute;left:-7px;right:-7px;top:-7px;bottom:-7px}
.gvn-dl:active{opacity:1;background:rgba(255,255,255,.18)}
.gwrap.them .gvn-dl:active{background:var(--gb-press)}

/* ---- Recording strip ---------------------------------------------------------------------------
   On screen only while a voice note is being recorded. The level segments are driven from vnMeter's
   readings, and they are the point: a clock counting up next to a dead microphone is exactly how an
   empty note used to get sent with nobody the wiser. `.nometer` is the honest state for an engine
   that hands us no analyser — the clock still runs and no level is claimed that was never measured.
   Same normal-flow slot and surface as #g-pend (never an overlay, which is how an invisible element
   used to eat taps on this bar). */
.gvn-rec{display:flex;align-items:center;gap:10px;background:var(--gb-surf);border-top:1px solid var(--line);
  padding:6px 12px;position:relative;z-index:30}
.gvn-rec-dot{flex:none;width:9px;height:9px;border-radius:50%;background:var(--red,#e11d2a)}
.gvn-rec-lv{flex:1;min-width:0;display:flex;align-items:center;gap:2px;height:18px}
/* Lit segments grow with transform, not height — a height animation relayouts the bar twenty times
   a second on a mid-range Android, and this meter updates every 50ms. NOT transitioned, either:
   --t-fast is 150ms and would smear a live measurement into something that never reaches the
   reading it is meant to be showing. A level meter follows the input or it is decoration. */
.gvn-rec-lv i{flex:1;min-width:2px;height:100%;border-radius:2px;background:var(--muted);opacity:.24;
  transform:scaleY(.3);transform-origin:50% 50%}
.gvn-rec-lv i.on{opacity:1;background:var(--blue-bright,#1a86d0);transform:scaleY(1)}
.gvn-rec.nometer .gvn-rec-lv{display:none}
.gvn-rec.nometer .gvn-rec-t{margin-right:auto}
.gvn-rec-t{flex:none;font-family:var(--font-mono);font-size:12px;font-weight:600;color:var(--ink);font-variant-numeric:tabular-nums}
.gvn-rec-x{flex:none;min-height:var(--tap,44px);padding:0 14px;border:0;border-radius:22px;background:transparent;
  color:var(--muted);font-family:inherit;font-size:13px;font-weight:700;cursor:pointer;-webkit-tap-highlight-color:transparent}
.gvn-rec-x:active{background:var(--gb-press);color:var(--ink)}
/* Motion (house tokens, section 16). The strip ARRIVES, so --t-med; the two press/hover states are
   --t-fast; the red dot's pulse is an indefinite loop and keeps its own duration per §7.1. */
@media (prefers-reduced-motion:no-preference){
  @keyframes gvnRecIn{from{opacity:0;transform:translateY(6px)}to{opacity:1;transform:none}}
  @keyframes gvnRecPulse{0%,100%{opacity:1}50%{opacity:.3}}
  .gvn-rec{animation:gvnRecIn var(--t-med) var(--ease)}
  .gvn-rec-dot{animation:gvnRecPulse 1.1s var(--ease) infinite}
  .gvn-rec-x{transition:background var(--t-fast) var(--ease),color var(--t-fast) var(--ease)}
  .gvn-dl{transition:opacity var(--t-fast) var(--ease),background var(--t-fast) var(--ease)}
  .gvn-pp{transition:background var(--t-fast) var(--ease)}
  /* The playback-position highlight. 150ms on the house curve, replacing a hand-rolled .1s. */
  .gvn-bars i{transition:opacity var(--t-fast) var(--ease)}
}

/* --- 9. Compose bar --------------------------------------------------------------------------
   One rounded field (emoji inside left, paperclip + camera inside right) and a round FAB clear of
   it that is a MIC until there is text and a SEND arrow after. position/z-index stay LOAD-BEARING:
   the bar must outrank the fixed toast (z20) and the bottom nav (z10) so nothing can shadow its
   hit area — that is the bug the .toast pointer-events note above is about. */
.g-compose{display:flex;align-items:flex-end;gap:8px;background:var(--gb-surf);border-top:1px solid var(--line);padding:8px 10px calc(8px + env(safe-area-inset-bottom,0px));position:relative;z-index:30}
.gcx{flex:1;min-width:0;display:flex;align-items:flex-end;gap:1px;background:var(--gb-field);border:1px solid var(--gb-field-line);border-radius:22px;padding:2px}
.gcx textarea#g-msginput{flex:1;min-width:0;border:0;outline:none;background:transparent;resize:none;font-family:inherit;font-size:16px;line-height:1.35;color:var(--ink);padding:11px 4px;max-height:118px;overflow-y:auto}
.gcx textarea#g-msginput::placeholder{color:var(--muted);opacity:.85}
.gcx-b{flex:none;width:38px;height:38px;border:0;background:transparent;color:var(--muted);border-radius:50%;padding:0;display:flex;align-items:center;justify-content:center;cursor:pointer}
.gcx-b svg,.gcx-b .ic{width:21px;height:21px;vertical-align:0}
.gcx-b:active{background:var(--gb-press)}
/* ★ `hidden` MUST actually hide, or the bar silently eats its own text field.
   #g-video and #g-doc are kept in the DOM purely as handler anchors: gAttachSheet() and
   wireGuardAttach() find them by id and fire them with .click() from the attach sheet, so they are
   never meant to be laid out. They carry the `hidden` attribute — but `[hidden]{display:none}` lives
   in the UA stylesheet, and the pre-rebuild rule `.g-compose button{…display:flex…}` (section 2,
   ~line 231) is an AUTHOR rule, so it wins and both buttons rendered as 36x48 boxes with
   flex-shrink:0. MEASURED at 360x800 before this rule: the two ghosts plus their two 8px gaps took
   88px of a 304px content box, and because .gcx is the only flex:1 child it absorbed the whole
   shortfall — textarea#g-msginput came out 39px wide, about four characters.
   Keep this rule below the legacy block, and never "fix" a hidden control by adding a class. */
.g-compose [hidden]{display:none!important}
.gfab{flex:none;width:46px;height:46px;border:0;border-radius:50%;padding:0;display:flex;align-items:center;justify-content:center;cursor:pointer;color:#fff;background:var(--grad-primary,linear-gradient(90deg,#005080,#0a6bb0));box-shadow:0 4px 14px rgba(0,80,128,.3);transition:transform .08s}
.gfab svg,.gfab .ic{width:21px;height:21px;vertical-align:0}
/* :active is the house scale(.97) from section 14 — this used to be a one-off .93. */
.gfab.recording{background:var(--red);animation:gpulse 1s infinite}
[data-theme="dark"] .gfab{box-shadow:0 4px 14px rgba(0,0,0,.45)}

/* --- 10. @-mention type-ahead ------------------------------------------------------------------
   In normal flow directly above the compose bar (same place #g-reply lives), never floating over
   it. An overlay next to this input is exactly how the invisible .toast used to eat taps. */
.g-ment{background:var(--gb-surf);border-top:1px solid var(--line);max-height:38vh;overflow-y:auto;overscroll-behavior:contain;position:relative;z-index:30}
.g-ment-i{display:flex;align-items:center;gap:11px;padding:9px 14px;cursor:pointer}
.g-ment-i.on,.g-ment-i:active{background:var(--gb-press)}
.g-ment-av{flex:none;width:32px;height:32px;border-radius:50%;background:linear-gradient(135deg,#0a6bb0,#003050);color:#fff;font-family:var(--font-display,inherit);font-weight:800;font-size:13px;display:flex;align-items:center;justify-content:center}
.g-ment-tx{min-width:0;flex:1}
.g-ment-tx b{display:block;font-size:14px;font-weight:700;color:var(--heading,#0b2036);white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
/* --slate, not --muted, and the HIGHLIGHTED row is why — the same failure and the same fix as the
   console's .mention-item .ment-sub (styles.css). MEASURED on the live preview: --muted (#5f7288)
   is 4.94:1 on the plain white --gb-surf row but only 4.30:1 once .g-ment-i.on lays --gb-press
   over it, i.e. under the 4.5:1 AA floor on the one row the officer is about to tap or Enter.
   #3a5064 is 7.2:1 on that pressed row and 8.4:1 on white. Named through var(--slate,…) even
   though the officer palette does not declare --slate, exactly as .g-ment-tx b above names
   var(--heading,#0b2036) that this stylesheet does not declare in light either — the literal is
   the shipped value, the token is there for the day the palettes converge.
   ⚠ #3a5064 is a DARK ink and must not paint text on the dark surface, so dark keeps --muted
   (#8b98ab), which already measures 4.54:1 on the pressed row and 5.9:1 on the plain one. */
.g-ment-tx small{display:block;font-size:11.5px;color:var(--slate,#3a5064);white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
/* The role chip: WHO this person is to the officer, in one word. The list is staff only, so the
   chip is the difference between paging dispatch and paging HR at 3am. */
.g-ment-role{flex:none;font-size:10.5px;font-weight:800;letter-spacing:.04em;text-transform:uppercase;
  padding:3px 8px;border-radius:999px;background:var(--gb-tint,rgba(10,107,176,.1));color:var(--blue,#0a6bb0);
  border:1px solid var(--line);white-space:nowrap}
.g-ment-role[data-role="dispatch"],.g-ment-role[data-role="dispatcher"]{background:rgba(10,107,176,.15);color:#0a5a94;border-color:rgba(10,107,176,.3)}
.g-ment-role[data-role="supervisor"]{background:rgba(120,80,190,.14);color:#5b3fa0;border-color:rgba(120,80,190,.28)}
[data-theme="dark"] .g-ment-tx small{color:var(--muted,#8b98ab)}
[data-theme="dark"] .g-ment-role{background:rgba(127,196,240,.12);color:#7fc4f0;border-color:rgba(127,196,240,.22)}
[data-theme="dark"] .g-ment-role[data-role="supervisor"]{background:rgba(178,150,240,.14);color:#c0aaf5;border-color:rgba(178,150,240,.26)}
.g-ment-none{padding:12px 14px;font-size:13px;color:var(--muted)}
.g-reply{background:var(--gb-surf);border-left:3px solid var(--blue,#0a6bb0);color:var(--ink);position:relative;z-index:30}

/* --- 11. Attach sheet (paperclip) — reuses the app's own .g-sheet bottom sheet ----------------- */
.g-atr{display:flex;align-items:center;gap:13px;width:100%;text-align:left;border:0;background:transparent;color:var(--ink);padding:12px 2px;cursor:pointer;font-family:inherit}
.g-atr+.g-atr{border-top:1px solid var(--line)}
.g-atr-ic{flex:none;width:42px;height:42px;border-radius:50%;background:var(--gb-tint);color:var(--blue,#0a6bb0);display:flex;align-items:center;justify-content:center}
.g-atr-ic .ic{width:20px;height:20px;vertical-align:0}
.g-atr b{display:block;font-size:14.5px;font-weight:700}
.g-atr small{display:block;font-size:12px;color:var(--muted);margin-top:1px}
/* A row the OS will refuse. NOT `disabled` and NOT pointer-events:none — the whole point is that it
   stays tappable and EXPLAINS (a permission an officer can turn back on is worse than useless as an
   inert control). Dimmed enough to read as unavailable, and the sub-line carries the reason. See
   gVideoSourceSheet's camera-permission probe. */
.g-atr.off .g-atr-ic{background:var(--gb-field,#eef2f7);color:var(--muted)}
.g-atr.off b{color:var(--muted)}
[data-theme="dark"] .g-atr.off .g-atr-ic{background:rgba(255,255,255,.06)}

/* --- 11b. Per-channel notification level (Discord's channel mute) ------------------------------
   The chooser reuses .g-atr rows so the sheet is one visual language; the only additions are the
   name line under the title and the tick that makes it read as a set of choices rather than three
   independent actions. --tap:44px is the house floor and .g-atr already clears it. */
.g-sheet-sub{font-size:12.5px;color:var(--muted);margin:-6px 0 6px}
.g-lvl{justify-content:flex-start}
.g-lvl>span:nth-child(2){flex:1 1 auto;min-width:0}
.g-lvl-tick{flex:none;width:18px;text-align:center;font-size:15px;font-weight:800;color:var(--blue,#0a6bb0)}
.g-lvl.on b{color:var(--blue,#0a6bb0)}
[data-theme="dark"] .g-lvl-tick,[data-theme="dark"] .g-lvl.on b{color:#7fc0f0}

/* --- 11c. Voice & video sheet (More → Voice & video) -------------------------------------------
   Almost nothing new: the profile chooser is .g-atr/.g-lvl from 11b, the camera switch is the
   .g-thm/.g-thmb pill pair from the Appearance card, the toggles are .ck2, and the level meter is
   the recording strip's own .gvn-rec-lv — reused verbatim so the bars in the test mean exactly what
   the bars mean while recording a voice note. What is left is spacing and one row shape. */
.gvv-test{padding:2px 2px 6px}
/* .gvn-rec-lv is normally a flex CHILD of .gvn-rec (flex:1 does the sizing there). Standing alone
   in the sheet it is a block-level flex container, so it needs its own width and breathing room —
   the segments, their colours and the no-transition rule all still come from 11's own block. */
.gvv-test .gvn-rec-lv{width:100%;margin:2px 0 9px}
.gvv-msg{font-size:12.5px;color:var(--muted);line-height:1.5;margin-bottom:10px}
.gvv-msg b{color:var(--ink)}
.gvv-row{display:flex;align-items:center;gap:12px;padding:8px 2px;min-height:var(--tap,44px)}
.gvv-row-t{flex:1 1 auto;min-width:0;font-size:14.5px;font-weight:700;color:var(--ink)}
.gvv-row-t small{display:block;font-size:12px;font-weight:400;color:var(--muted);margin-top:2px;line-height:1.45}
/* The pill pair is margin-left:auto on the Appearance card, where it is the only thing on its row.
   Here it sits beside a two-line label, so the row's own flex does the pushing. */
.gvv-row .g-thm{margin-left:0}
/* Header bell: quiet grey the moment this conversation is anything but "All messages". */
.g-hbtn.on{color:var(--muted,#66788c);border-color:var(--line,#d5e2ec)}
[data-theme="dark"] .g-hbtn.on{color:#7d90a6}
/* A MUTED row: dimmed, no bold, no count — still listed, still opens, still fully readable. */
.ginbox-row.muted{opacity:.55}
.ginbox-row.muted:active{opacity:1}
.ginbox-row.muted .gib-name{font-weight:600}
.gib-muted{flex:0 0 auto;opacity:.6;line-height:1}
.gib-muted .ic{width:13px;height:13px;vertical-align:-1px}
/* --- 11b. Message action sheet (long-press a bubble) — same .g-sheet/.g-atr chassis ------------
   Only three things are new here: the quoted preview under the title, the Discord row of quick
   reactions above the actions, and a destructive tone for Delete. The rows themselves are .g-atr,
   so the 42px glyph disc, the dark theme and the divider rules all come for free.
   .g-atr's own 12px padding + the 42px disc already clears the --tap 44px floor; the quick-react
   buttons state it explicitly because they are the only bare-emoji targets in the sheet. */
.g-msheet .g-sheet-h{margin-bottom:6px}
.g-msheet-q{font-size:12.5px;line-height:1.5;color:var(--muted);background:var(--gb-press);border-radius:10px;padding:8px 10px;margin-bottom:10px;
  display:-webkit-box;-webkit-line-clamp:2;-webkit-box-orient:vertical;overflow:hidden}
.g-msq{display:flex;align-items:center;gap:2px;overflow-x:auto;-webkit-overflow-scrolling:touch;scrollbar-width:none;padding-bottom:4px;margin-bottom:2px;border-bottom:1px solid var(--line)}
.g-msq::-webkit-scrollbar{display:none}
.g-msq-b{flex:0 0 auto;border:none;background:transparent;font-size:25px;line-height:1;padding:0;border-radius:50%;cursor:pointer;
  min-width:var(--tap,44px);min-height:var(--tap,44px);display:inline-flex;align-items:center;justify-content:center}
.g-msq-b:active{background:var(--gb-press)}
.g-msq-b.more{color:var(--muted)}
.g-msq-b.more svg{width:19px;height:19px}
/* The sheet's glyphs are the raw G_I* svgs, which carry no `.ic` class for .g-atr-ic to size. */
.g-msheet .g-atr-ic svg{width:20px;height:20px}
.g-msheet .g-atr.danger .g-atr-ic{background:rgba(225,29,42,.12);color:var(--red)}
.g-msheet .g-atr.danger b{color:var(--red)}
[data-theme="dark"] .g-msheet .g-atr.danger .g-atr-ic{background:rgba(255,120,130,.15);color:#ff8f98}
[data-theme="dark"] .g-msheet .g-atr.danger b{color:#ff8f98}
/* The other half of the gesture (see gWireLongPress). On a TOUCH device the hold now belongs to
   the action sheet, so the platform's own long-press must not fire first: the callout is off, and
   the bubble text is unselectable — a selection drag raises pointercancel, which would kill the
   hold timer and make the gesture fail intermittently. That trade is only payable because the
   sheet's "Copy text" replaces what selecting-to-copy used to be for; officers never had it.
   Fine pointers (a supervisor on a laptop in Field mode) keep normal text selection — their route
   in is right-click, which the same handler answers. */
@media (pointer:coarse){
  .g-thread .gwrap[data-mid]{-webkit-touch-callout:none}
  .g-thread .gwrap[data-mid]>.gbubble{-webkit-user-select:none;user-select:none}
  /* …except the things you are meant to be able to act on with a normal tap. */
  .g-thread .gwrap[data-mid] a,.g-thread .gwrap[data-mid] input{-webkit-user-select:auto;user-select:auto}
}

/* --- 12. Thread chrome in dark ----------------------------------------------------------------- */
[data-theme="dark"] .gth2-acts{background:var(--gb-surf);border-bottom-color:var(--line)}
[data-theme="dark"] .g-hbtn{background:var(--gb-field);border-color:var(--gb-field-line);color:#bcd7ea}
[data-theme="dark"] .g-hbtn:active{background:#243044}
[data-theme="dark"] .g-hbtn-call{border-color:#1d9e57;color:#5fd39a}
[data-theme="dark"] .g-hbtn-meet{border-color:#4f7bff;color:#93b0ff}
[data-theme="dark"] .gwrap.them>.gbubble{background:var(--gb-them);border-color:var(--gb-them-line);color:var(--gb-them-ink)}
[data-theme="dark"] .gwrap.them>.gbubble.fr-card{background:var(--gb-them);border-color:var(--gb-them-line)}
[data-theme="dark"] .reply-quote{background:rgba(255,255,255,.07);border-left-color:#5f8fd8}
[data-theme="dark"] .gack.done{background:#14261c;color:#7fd3a2}
[data-theme="dark"] .gib-meet{background:#166b3d}

/* ==============================================================================================
   DARK THEME — the rest of the officer app. guard.css already carried a good deal of this, but
   nothing ever switched it on, so the gaps had never been seen. These are the surfaces that were
   still hard-coded white (or reading a var that only had a light value) and would have been
   unreadable the moment data-theme went to dark.
   ============================================================================================== */
[data-theme="dark"] body{background:var(--bg);color:var(--ink)}
[data-theme="dark"] .gapp{background:var(--bg)}
[data-theme="dark"] .duty-card,[data-theme="dark"] .shift-card,[data-theme="dark"] .av-row,[data-theme="dark"] .rep-item,[data-theme="dark"] .cp-card,[data-theme="dark"] .ann-card,[data-theme="dark"] .coach-need,[data-theme="dark"] .stub-card,[data-theme="dark"] .gtrn-item,[data-theme="dark"] .g-appcard{background:var(--card);border-color:var(--line);color:var(--ink)}
[data-theme="dark"] .gtrn-item:active{background:#232f42}
[data-theme="dark"] .gtrn-txt b,[data-theme="dark"] .gtrn-play{color:#8fc4f0}
[data-theme="dark"] .gtrn-no{background:#14263a;color:#9fc9ec}
[data-theme="dark"] .coach-kudos{background:#14261c;color:#7fd3a2}
[data-theme="dark"] .gnav{background:rgba(17,24,36,.94);border-top-color:var(--line)}
[data-theme="dark"] .gnav button[data-badge]::after{box-shadow:0 0 0 2px #111824}
[data-theme="dark"] .gsub button,[data-theme="dark"] .gchan-chip,[data-theme="dark"] .g-pinbtn,[data-theme="dark"] .gd-ghostbtn{background:var(--card);border-color:var(--line);color:#9fb2c4}
[data-theme="dark"] .gbtn.ghost,[data-theme="dark"] .gicon,[data-theme="dark"] .photo-chip{background:#232f42;color:#cfe0ff;border-color:var(--line)}
[data-theme="dark"] .gfield input,[data-theme="dark"] .gfield select,[data-theme="dark"] .gfield textarea,[data-theme="dark"] .note-card input,[data-theme="dark"] .swap-panel select,[data-theme="dark"] .swap-panel input,[data-theme="dark"] .av-add select,[data-theme="dark"] .av-add input,[data-theme="dark"] .ai-card textarea,[data-theme="dark"] .ann-cadd input{background:#0e1520;border-color:#22394d;color:var(--ink)}
[data-theme="dark"] .swap-panel{background:#161f2c}
[data-theme="dark"] .sigpad{background:#0e1520;border-color:#3a4a5e}
[data-theme="dark"] .empty,[data-theme="dark"] .ff-help,[data-theme="dark"] .cp-hint,[data-theme="dark"] .po-line{color:var(--muted)}
[data-theme="dark"] .st.ok{background:#14261c;color:#7fd3a2}
[data-theme="dark"] .st.no{background:#3d1a1e;color:#f08d96}
[data-theme="dark"] .tag.patrol{background:#16324c;color:#9fc9ec}
[data-theme="dark"] .tag.security{background:#3d1a1e;color:#f08d96}
[data-theme="dark"] .tag.parking{background:#3a2c12;color:#e0ab5a}
[data-theme="dark"] .tag.maintenance{background:#16324c;color:#8fc4f0}
[data-theme="dark"] .tasks,[data-theme="dark"] .pd-row,[data-theme="dark"] .run-card .run-stop,[data-theme="dark"] .stub-line.net,[data-theme="dark"] .pay-last{border-color:var(--line)}
[data-theme="dark"] .run-n{background:#232f42;color:#c7d2e2}
[data-theme="dark"] .pd-row.bolo{background:#2a1518;border-color:#5c2a30}
/* .uc-* (the call screen) is deliberately identical in both themes now — see the block near the top
   of this file. These three overrides existed to make the old WHITE modal card readable in dark; on
   the opaque call screen they only fought it (repainting the transparent card and restyling the
   Answer/Decline buttons back to a flat grey), so they are gone rather than re-tuned. */
[data-theme="dark"] .gh-row{background:var(--card);border-color:var(--line)}
[data-theme="dark"] .gh-t{color:#9fb2c4}
[data-theme="dark"] .gh-done{background:#14261c;color:#7fd3a2}
[data-theme="dark"] .gwx-adv{background:#3a2c14;color:#f0c078}
[data-theme="dark"] .ann-card.pinned{border-left-color:#e11d2a}
[data-theme="dark"] .gn-prio{background:#3d1a1e;color:#f08d96}
[data-theme="dark"] .gsh-hero,[data-theme="dark"] .gt-hero,[data-theme="dark"] .gn-hero,[data-theme="dark"] .td-hero,[data-theme="dark"] .gth2,[data-theme="dark"] .ghead{background:linear-gradient(165deg,#0a1b2b,#0d3350)}
[data-theme="dark"] .shift-card .post,[data-theme="dark"] .cp-iv{background:#16324c;color:#9fc9ec}

/* Found by auditing every light background in this file against its dark counterpart rather than by
   looking at screens one at a time — these are the three that were still going to render as a white
   slab, and a pile of light tint-badges that were merely loud on a dark card.
     · .gbubble.them  — the PINNED-MESSAGES sheet builds bubbles outside .gwrap, so the new
       .gwrap.them>.gbubble skin never reached them.
     · .grx           — the field-report card's reaction chips live in .fr-foot, not in .gwrap>.grxbar.
     · .bigbtn.out    — the Clock out button, the largest control on the home screen. */
[data-theme="dark"] .gbubble.them{background:var(--gb-them);border-color:var(--gb-them-line);color:var(--gb-them-ink)}
[data-theme="dark"] .grx{background:var(--gb-them);border-color:var(--gb-them-line);color:var(--gb-them-ink)}
[data-theme="dark"] .bigbtn.out{background:var(--card);color:#9fb2c4;border-color:var(--line)}
[data-theme="dark"] .glive{background:#14261c;color:#7fd3a2}
[data-theme="dark"] .gd-upill,[data-theme="dark"] .gt-sla,[data-theme="dark"] .gt-ic.amber{background:#3a2c12;color:#e0ab5a}
[data-theme="dark"] .gsh-bonus,[data-theme="dark"] .gsh-open-row .cnt,[data-theme="dark"] .irc-dictate.on{background:#3d1a1e;color:#f08d96}
[data-theme="dark"] .gt-nextbadge{background:#16324c;color:#9fc9ec}
[data-theme="dark"] .badge-chip.silver{background:#242c38;color:#c2cbd8}
[data-theme="dark"] .badge-chip.bronze{background:#33241a;color:#d6a273}

/* App-settings block on the Duty home: version + updates, and the appearance picker beside it.
   These used to be inline styles on the element, which no stylesheet could ever override. */
.g-appcard{background:#fff;border:1px solid var(--line,#e4eaf1);border-radius:14px;padding:12px 14px;margin:12px 0;display:flex;align-items:center;gap:12px}
.g-appcard-t{flex:1;min-width:0}
.g-appcard-h{font-weight:800;font-size:14px;color:var(--heading,#0b2036)}
.g-appchk-sub{font-size:12px;color:var(--muted);margin-top:2px}
/* The "Download & install manually" escape hatch on the update card takes a full row of its own:
   two buttons side by side do not fit a 360px phone, and this one is a sentence, not a word. Wrap
   is a no-op for the normal one-button card because .g-appcard-t already has min-width:0. */
.g-appcard{flex-wrap:wrap}
.g-appchk.alt{flex-basis:100%;margin-top:4px;justify-content:center;text-align:center}
.g-thmcard{flex-wrap:wrap}
.g-thm{display:flex;gap:3px;background:var(--gb-field);border-radius:11px;padding:3px;flex:none;margin-left:auto}
.g-thmb{border:0;background:transparent;color:var(--muted);border-radius:9px;padding:8px 11px;font-size:12.5px;font-weight:700;font-family:inherit;cursor:pointer}
.g-thmb.on{background:var(--card,#fff);color:var(--blue,#0a6bb0);box-shadow:0 1px 3px rgba(18,28,48,.16)}
[data-theme="dark"] .g-thmb.on{background:#26344a;color:#8fc4f0}

/* .g-sec / .g-card are emitted by loadUpdates() for the helpdesk block and had NO styles in any
   stylesheet — the "App problems & requests" card rendered as bare unstyled text on every phone. */
.g-sec{font-size:11px;font-weight:800;letter-spacing:.06em;text-transform:uppercase;color:var(--muted);margin:16px 4px 7px}
.g-card{background:var(--card,#fff);border:1px solid var(--line,#e4eaf1);border-radius:14px;padding:13px 15px;margin-bottom:9px;font-size:14px}
.g-card b{font-size:14.5px;color:var(--heading,#0b2036)}
.g-card .meta{font-size:12px;color:var(--muted);margin-top:4px}
.g-card .tag{background:var(--gb-tint);color:var(--blue,#0a6bb0)}

/* ---- Help desk: the desk picker, the ticket rows, and the ticket CONVERSATION ------------------
   Built on what the chat already owns — .g-thread's paper, .gwrap/.gbubble, .gdayrow chips and
   the .g-compose bar — so a support thread looks and behaves like every other thread in the app.
   Only the shell (overlay, header, rows, chips) is new. Every colour goes through the theme vars
   the chat skin defines, so the dark variant is real rather than a white slab in a dark app. */
.tk-desks{display:flex;gap:7px;overflow-x:auto;-webkit-overflow-scrolling:touch;scrollbar-width:none;padding:1px 1px 3px}
.tk-desks::-webkit-scrollbar{display:none}
.tk-desk-c{flex:0 0 auto;display:inline-flex;align-items:center;gap:7px;min-height:44px;padding:0 14px;border-radius:99px;
  border:1px solid var(--line,#e4eaf1);background:var(--card,#fff);color:var(--ink);font-family:inherit;font-size:13px;font-weight:600;cursor:pointer}
.tk-desk-c .ic{width:16px;height:16px;flex:none;color:var(--blue,#0a6bb0)}
/* The dark BASE goes above the .on rules on purpose: [data-theme="dark"] .tk-desk-c and
   .tk-desk-c.on have the same specificity, so placed after it the dark rule would win and the
   selected chip would lose its gradient in dark mode. Measured, not guessed. */
[data-theme="dark"] .tk-desk-c{background:#1a2231;border-color:#26303f;color:#c7d1de}
.tk-desk-c.on{background:linear-gradient(90deg,#005080,#0a6bb0);border-color:transparent;color:#fff;font-weight:700;box-shadow:0 4px 12px rgba(0,80,128,.25)}
.tk-desk-c.on .ic{color:#fff}

/* A ticket row reads like an inbox row: where it landed, where it stands, when it last moved. */
.tk-row{display:block;width:100%;text-align:left;background:var(--card,#fff);border:1px solid var(--line,#e4eaf1);
  border-radius:14px;padding:11px 13px;margin-bottom:9px;font-family:inherit;cursor:pointer;min-height:44px}
.tk-row:active{background:var(--gb-press,#eef1f8)}
.tk-row.unread{border-color:#b9d4ea;box-shadow:0 1px 2px rgba(18,28,48,.06),0 6px 16px rgba(10,80,128,.10)}
.tk-r1{display:flex;align-items:center;gap:6px;margin-bottom:5px}
.tk-desk{display:inline-flex;align-items:center;gap:4px;background:var(--gb-tint);color:var(--blue,#0a6bb0);
  font-size:10.5px;font-weight:800;letter-spacing:.02em;padding:3px 8px;border-radius:99px;max-width:46%;
  white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
.tk-desk .ic{width:12px;height:12px;flex:none}
.tk-when{margin-left:auto;font-family:var(--font-mono);font-size:10.5px;color:var(--muted);flex:none}
.tk-t{font-size:14.5px;font-weight:700;color:var(--heading,#0b2036);line-height:1.35;
  display:-webkit-box;-webkit-line-clamp:2;-webkit-box-orient:vertical;overflow:hidden}
.tk-row.unread .tk-t{font-weight:800}
.tk-r3{display:flex;align-items:center;gap:8px;margin-top:3px}
.tk-prev{flex:1;min-width:0;font-size:12px;color:var(--muted);line-height:1.4;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
.tk-prev b{font-size:12px;color:var(--muted);font-weight:700}
.tk-r3 .gib-dot{flex:none}
/* Status. Red is danger-only in this design system, so "Not planned" is grey, not a rejection stamp. */
.tk-pill{flex:none;font-size:9.5px;font-weight:800;letter-spacing:.06em;text-transform:uppercase;padding:3px 8px;border-radius:99px;line-height:1.35}
.tk-pill.open{background:var(--gb-tint);color:var(--blue,#0a6bb0)}
.tk-pill.planned{background:#e2eefa;color:#0a5a94}
.tk-pill.in_progress{background:#fdf0dc;color:#a8660a}
.tk-pill.done{background:#dff3e6;color:#0d7a45}
.tk-pill.declined{background:#e9edf2;color:#5f7288}
[data-theme="dark"] .tk-row{background:#1a2231;border-color:#26303f}
[data-theme="dark"] .tk-row.unread{border-color:#2c4a66}
[data-theme="dark"] .tk-t{color:#e6eaf2}
[data-theme="dark"] .tk-pill.planned{background:#14263a;color:#8fc4f0}
[data-theme="dark"] .tk-pill.in_progress{background:#33260f;color:#e6b467}
[data-theme="dark"] .tk-pill.done{background:#12301f;color:#6fd39a}
[data-theme="dark"] .tk-pill.declined{background:#232f42;color:#9fb2c4}

/* The conversation itself: a full-screen column above the bottom nav, modelled on .irc-ov (the
   app's other full-screen composer) so the header, safe areas and dark skin all behave the same. */
.tk-ov{position:fixed;inset:0;z-index:95;background:var(--gb-chat);display:flex;flex-direction:column;max-width:480px;margin:0 auto}
.tk-head{gap:6px}
.tk-back{flex:none;width:44px;height:44px;margin-left:-10px;border:0;background:none;color:#cfe2f2;display:flex;align-items:center;justify-content:center;padding:0;cursor:pointer}
.tk-back svg{width:24px;height:24px}
.tk-tile{flex:none;width:34px;height:34px;border-radius:10px;background:rgba(255,255,255,.14);display:flex;align-items:center;justify-content:center;color:#fff}
.tk-tile .ic{width:18px;height:18px}
.tk-ht{flex:1;min-width:0}
.tk-ht b{font-family:var(--font-display);font-weight:800;font-size:15px;display:block;line-height:1.3;
  display:-webkit-box;-webkit-line-clamp:2;-webkit-box-orient:vertical;overflow:hidden}
.tk-ht small{font-size:11px;color:#9dbdd6;margin-top:2px;display:block;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
.tk-pill.onhead{flex:none;align-self:flex-start;background:rgba(255,255,255,.16);color:#e8f2fb}
.tk-thread{flex:1;min-height:0}
/* The composer reuses .g-compose + .gcx + .gfab wholesale; only the textarea rule in the chat is
   id-scoped (#g-msginput), so this is the one line that has to be restated for #tk-msg. */
.tk-compose .gcx textarea#tk-msg{flex:1;min-width:0;border:0;outline:none;background:transparent;resize:none;
  font-family:inherit;font-size:16px;line-height:1.35;color:var(--ink);padding:11px 12px;max-height:118px;overflow-y:auto}
.tk-compose .gcx textarea#tk-msg::placeholder{color:var(--muted);opacity:.85}
/* Closed: readable, not answerable — and one obvious way to raise it again on the same desk. */
.tk-closed{background:var(--gb-surf);border-top:1px solid var(--line);padding:12px 16px calc(12px + env(safe-area-inset-bottom,0px));position:relative;z-index:30}
.tk-closed-n{font-size:12.5px;color:var(--muted);text-align:center;line-height:1.5}
.tk-reopen{display:block;width:100%;margin-top:10px;min-height:46px;border-radius:12px;border:1px solid var(--line);
  background:var(--card,#fff);color:var(--blue,#0a6bb0);font-family:inherit;font-size:13.5px;font-weight:700;cursor:pointer}
.tk-reopen:active{background:var(--gb-press,#eef1f8)}
[data-theme="dark"] .tk-reopen{background:#1a2231;border-color:#26303f;color:#8fc4f0}

/* ==============================================================================================
   5-TAB IA — Home · Chat · Clock · Tasks · More   (owner's Connecteam-mobile benchmark)
   Built on the tokens already in this file (--card/--line/--ink/--muted/--heading/--gb-tint/
   --gb-field/--gb-press), so dark mode follows without a parallel stylesheet. Only the tint
   SQUARES need an explicit dark branch — a pastel that reads as a tint on white reads as a smudge
   on a #1a2231 card. Laid out at 360x800 first: nothing here has a fixed width, every row
   ellipsises, and the tab bar sits in thumb reach with the safe-area inset added under it.
   ============================================================================================== */

/* --- bottom bar: five tabs, the centre one raised (the clock is the most-tapped control) ------- */
.gnav5{height:calc(60px + env(safe-area-inset-bottom,0px));align-items:stretch}
.gnav5 button{flex:1 1 0;min-width:0;padding:9px 0 10px;gap:2px;justify-content:flex-start}
.gnav5 .gnav-mid{position:relative;justify-content:flex-end;padding-bottom:9px}
.gnav5 .gnav-mid .ic{position:absolute;top:-19px;left:50%;transform:translateX(-50%);
  width:52px;height:52px;border-radius:50%;background:var(--grad-primary,linear-gradient(90deg,#005080,#0a6bb0));
  color:#fff;box-shadow:0 6px 18px rgba(0,80,128,.36);border:3px solid var(--card,#fff);transition:transform .12s ease}
.gnav5 .gnav-mid .ic svg{width:25px;height:25px;stroke-width:2.1}
.gnav5 .gnav-mid.on .ic{box-shadow:0 6px 20px rgba(0,80,128,.5)}
.gnav5 .gnav-mid.on::before{display:none}          /* the raised puck IS the active affordance */
.gnav5 .gnav-mid:active .ic{transform:translateX(-50%) scale(.93)}
.gnav5 .gnav-mid:active .ic svg{transform:none}
[data-theme="dark"] .gnav5 .gnav-mid .ic{border-color:#111824}
/* numeric tab badge (Chat / Tasks / More) */
.gnav button .gnb{position:absolute;top:3px;left:calc(50% + 6px);min-width:17px;height:17px;padding:0 4px;
  border-radius:9px;background:var(--red,#e11d2a);color:#fff;font-size:10px;font-weight:800;line-height:17px;
  text-align:center;box-shadow:0 0 0 2px rgba(255,255,255,.92);font-family:var(--font-mono,inherit)}
[data-theme="dark"] .gnav button .gnb{box-shadow:0 0 0 2px #111824}
/* Bottom chrome clearance. This used to be 86px = the 60px bar + the 19px the clock puck rises
   above it + air — which accounted for the NAV and forgot the SOS button, the taller of the two.
   #g-sos (guard.html) is fixed at `bottom:calc(78px + safe-area)` and is 62px across, so its top
   edge sits 140px above the bottom of the viewport; anything the app is willing to lay out above
   86px was landing underneath an opaque red circle.
   MEASURED at 360x800, scrolled to the end, before/after: Clock — last block 58x54px under the
   FAB -> 0; Home — last news card 58x45px -> 0. 152 = 140 + 12px of air, and it also clears the
   puck (79px) with room to spare. If #g-sos ever moves, this number moves with it.
   ⚠ This fixes content at the END of a scroll. It cannot fix a card that is under the FAB at the
   view's RESTING scroll position — see the note on .gclk-pair further down. */
.gmain{padding-bottom:calc(152px + env(safe-area-inset-bottom,0px))}

/* --- shared bits: tint square, chevron, avatar ------------------------------------------------- */
.gh-tile .sq,.gh-row .sq,.gm-tile .sq,.gm-row .sq,.gh-alert .sq,.gclk-big .sq{
  flex:none;display:flex;align-items:center;justify-content:center;border-radius:11px;background:var(--gb-tint);color:var(--blue,#0a6bb0)}
.gh-row .sq,.gm-row .sq,.gh-alert .sq{width:38px;height:38px}
.gh-tile .sq,.gm-tile .sq,.gclk-big .sq{width:40px;height:40px}
.gh-tile .sq .ic,.gh-row .sq .ic,.gm-tile .sq .ic,.gm-row .sq .ic,.gh-alert .sq .ic,.gclk-big .sq .ic{width:19px;height:19px;vertical-align:0}
.sq.red{background:#fdeaea;color:#c0182a}
.sq.green{background:#e4f6ec;color:#0d7a45}
.sq.amber{background:#fdf0dc;color:#a8660a}
.sq.navy{background:#e8edf5;color:#032b49}
.sq.grey{background:#eef1f5;color:#5f7288}
[data-theme="dark"] .sq.red{background:#3d1a1e;color:#f08d96}
[data-theme="dark"] .sq.green{background:#14261c;color:#7fd3a2}
[data-theme="dark"] .sq.amber{background:#3a2c12;color:#e0ab5a}
[data-theme="dark"] .sq.navy{background:#16273a;color:#9fc9ec}
[data-theme="dark"] .sq.grey{background:#232f42;color:#a7b6c8}
.gh-row .ch,.gm-row .ch,.gm-me .ch,.gh-alert .ch{flex:none;color:var(--muted);font-size:20px;line-height:1;opacity:.65}
.gav,.ghg-av,.gm-av,.gpf-av2{flex:none;border-radius:50%;overflow:hidden;background:var(--grad-primary,linear-gradient(90deg,#005080,#0a6bb0));
  color:#fff;display:flex;align-items:center;justify-content:center;font-family:var(--font-display,inherit);font-weight:800}
.gav img,.ghg-av img,.gm-av img,.gpf-av2 img{width:100%;height:100%;object-fit:cover;display:block}
.ghg-av{width:44px;height:44px;font-size:17px}
.gm-av{width:46px;height:46px;font-size:18px}
.gpf-av2{width:84px;height:84px;font-size:31px;margin:0 auto;border:3px solid var(--card,#fff);box-shadow:0 6px 20px rgba(18,28,48,.16)}

/* --- HOME: greeting header -------------------------------------------------------------------- */
.gh-greet{display:flex;align-items:center;gap:12px;margin-bottom:14px}
.ghg-tx{flex:1;min-width:0}
.ghg-tx b{display:block;font-family:var(--font-display,inherit);font-weight:800;font-size:19px;line-height:1.2;
  color:var(--heading,#0b2036);white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
.ghg-tx small{display:block;font-size:12px;color:var(--muted);margin-top:2px}
.ghg-pill{flex:none;display:inline-flex;align-items:center;gap:5px;font-size:9.5px;font-weight:800;letter-spacing:.07em;
  padding:5px 9px;border-radius:20px;white-space:nowrap}
.ghg-pill i{width:6px;height:6px;border-radius:50%;display:inline-block}
.ghg-pill.on{background:#e4f6ec;color:#0d7a45}.ghg-pill.on i{background:#0f9d58;animation:usgpulse 1.6s infinite}
.ghg-pill.off{background:var(--gb-field);color:var(--muted)}.ghg-pill.off i{background:#93a3b5}
[data-theme="dark"] .ghg-pill.on{background:#14261c;color:#7fd3a2}

/* --- HOME: the one thing that needs an answer first -------------------------------------------- */
/* color:var(--ink) on every one of these <button>s is load-bearing, not tidiness: a button with no
   color rule inherits the UA's black, which on a #1a2231 dark card is invisible the moment any
   child stops setting its own colour. Same reason .gh-alert names its border colour per tone —
   `border:1.5px solid` alone resolves to currentColor. */
.gh-alert{display:flex;align-items:center;gap:11px;width:100%;text-align:left;border-radius:15px;padding:13px 14px;
  margin-bottom:12px;cursor:pointer;font-family:inherit;border:1.5px solid var(--line);background:var(--card,#fff);color:var(--ink)}
.gh-alert .tx{flex:1;min-width:0}
.gh-alert .tx b{display:block;font-size:14px;font-weight:800;color:var(--heading,#0b2036)}
.gh-alert .tx small{display:block;font-size:11.5px;color:var(--muted);margin-top:2px}
.gh-alert.red{border-color:#f3c3c3}
.gh-alert.red .sq{background:#fdeaea;color:#c0182a}
.gh-alert.amber{border-color:#f3ddb5}
.gh-alert.amber .sq{background:#fdf0dc;color:#a8660a}
[data-theme="dark"] .gh-alert.red{border-color:#5c2a30}[data-theme="dark"] .gh-alert.red .sq{background:#3d1a1e;color:#f08d96}
[data-theme="dark"] .gh-alert.amber{border-color:#5a4520}[data-theme="dark"] .gh-alert.amber .sq{background:#3a2c12;color:#e0ab5a}

/* --- HOME: shift card extras (the card itself reuses .gd-card from the Clock screen) ------------ */
.gd-card.gd-quiet .gd-meta{line-height:1.55;margin-top:6px}
.gh-link{display:block;width:100%;margin-top:9px;border:0;background:none;color:var(--blue,#0a6bb0);
  font-family:inherit;font-size:12.5px;font-weight:700;padding:6px 0 0;cursor:pointer;text-align:center}
.gh-link:active{opacity:.6}
[data-theme="dark"] .gh-link{color:#8fc4f0}

/* --- HOME: quick-access tiles (Connecteam's tile row; 3-up so it never scrolls sideways) -------- */
.gh-tiles{display:grid;grid-template-columns:repeat(3,1fr);gap:9px;margin-bottom:6px}
.gh-tile{position:relative;background:var(--card,#fff);border:1px solid var(--line);border-radius:14px;color:var(--ink);
  padding:12px 6px 10px;display:flex;flex-direction:column;align-items:center;gap:7px;cursor:pointer;font-family:inherit}
.gh-tile:active{background:var(--gb-press)}
.gh-tile .tl{font-size:11.5px;font-weight:700;color:var(--ink);text-align:center;line-height:1.2}
.gh-tdot{position:absolute;top:9px;right:11px;width:8px;height:8px;border-radius:50%;background:var(--red,#e11d2a)}

/* --- HOME + MORE: navigation rows -------------------------------------------------------------- */
.gh-row,.gm-row{display:flex;align-items:center;gap:12px;width:100%;text-align:left;background:var(--card,#fff);
  border:1px solid var(--line);border-radius:14px;padding:12px 14px;margin-bottom:8px;cursor:pointer;
  font-family:inherit;text-decoration:none;color:var(--ink)}
.gh-row:active,.gm-row:active,.gm-me:active{background:var(--gb-press)}
.gh-row .tx,.gm-row .tx{flex:1;min-width:0}
.gh-row .tx b,.gm-row .tx b{display:block;font-size:14px;font-weight:700;color:var(--heading,#0b2036);
  white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
.gh-row .tx small,.gm-row .tx small{display:block;font-size:11.5px;color:var(--muted);margin-top:2px;
  white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
.gh-bdg{flex:none;min-width:22px;height:22px;padding:0 7px;border-radius:11px;background:var(--red,#e11d2a);color:#fff;
  font-family:var(--font-mono,inherit);font-size:11px;font-weight:700;line-height:22px;text-align:center}

/* --- HOME: announcement preview + friendly empty states ---------------------------------------- */
.gh-ann{display:block;width:100%;text-align:left;background:var(--card,#fff);border:1px solid var(--line);color:var(--ink);
  border-radius:15px;padding:13px 15px;margin-bottom:9px;cursor:pointer;font-family:inherit}
.gh-ann.unread{border-color:#bcd8ef;box-shadow:0 1px 2px rgba(18,28,48,.05),0 6px 18px rgba(18,28,48,.05)}
[data-theme="dark"] .gh-ann.unread{border-color:#2c4a68}
.gh-annh{display:flex;align-items:center;gap:8px;margin-bottom:5px}
.gh-anndt{margin-left:auto;font-size:11px;color:var(--muted)}
.gh-ann b{display:block;font-size:14.5px;font-weight:700;color:var(--heading,#0b2036);line-height:1.3}
.gh-ann p{margin:4px 0 0;font-size:13px;color:var(--muted);line-height:1.5}
.gh-more{display:inline-block;margin-top:8px;font-size:12.5px;font-weight:700;color:var(--blue,#0a6bb0)}
[data-theme="dark"] .gh-more{color:#8fc4f0}
.gh-empty{background:var(--card,#fff);border:1px dashed var(--line);border-radius:15px;padding:24px 18px;text-align:center}
.gh-empty .ic{width:26px;height:26px;color:var(--muted);opacity:.6;margin-bottom:8px}
.gh-empty b{display:block;font-size:14px;font-weight:700;color:var(--heading,#0b2036)}
.gh-empty small{display:block;font-size:12.5px;color:var(--muted);margin-top:4px;line-height:1.5}

/* --- MORE ------------------------------------------------------------------------------------- */
.gm-me{display:flex;align-items:center;gap:13px;width:100%;text-align:left;
  background:var(--grad-header-mobile,linear-gradient(165deg,#032b49,#005080));
  border:0;border-radius:16px;padding:15px;margin-bottom:4px;cursor:pointer;font-family:inherit;color:#fff}
.gm-me .tx{flex:1;min-width:0}
.gm-me .tx b{display:block;font-family:var(--font-display,inherit);font-weight:800;font-size:17px;
  white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
.gm-me .tx small{display:block;font-size:12px;color:#9dbdd6;margin-top:2px}
.gm-me .ch{color:#9dbdd6;opacity:1}
.gm-me .gm-av{border:2px solid rgba(255,255,255,.35);background:rgba(255,255,255,.14)}
.gm-grid{display:grid;grid-template-columns:repeat(3,1fr);gap:9px;margin-bottom:4px}
.gm-tile{background:var(--card,#fff);border:1px solid var(--line);border-radius:14px;padding:13px 6px 11px;color:var(--ink);
  display:flex;flex-direction:column;align-items:center;gap:8px;cursor:pointer;font-family:inherit}
.gm-tile:active{background:var(--gb-press)}
.gm-tile b{font-size:11.5px;font-weight:700;color:var(--ink);text-align:center;line-height:1.25}
.gm-out{display:block;width:100%;margin-top:14px;min-height:48px;border-radius:13px;border:1px solid #f3c3c3;
  background:var(--card,#fff);color:#c0182a;font-family:inherit;font-size:14.5px;font-weight:800;cursor:pointer}
.gm-out:active{background:#fdeaea}
[data-theme="dark"] .gm-out{border-color:#5c2a30;color:#f08d96}
[data-theme="dark"] .gm-out:active{background:#2a1518}
.gm-foot{text-align:center;font-size:11px;color:var(--muted);margin:12px 0 4px;opacity:.8}

/* --- PROFILE ---------------------------------------------------------------------------------- */
.gpf-hero{background:var(--card,#fff);border:1px solid var(--line);border-radius:18px;padding:20px 16px 16px;
  text-align:center;margin-bottom:12px}
.gpf-nm{font-family:var(--font-display,inherit);font-weight:800;font-size:20px;color:var(--heading,#0b2036);margin-top:11px}
.gpf-rl{font-size:12.5px;color:var(--muted);margin-top:3px}
.gpf-acts{display:flex;gap:8px;justify-content:center;margin-top:13px;flex-wrap:wrap}
.gpf-chip{display:inline-flex;align-items:center;gap:6px;background:var(--gb-field);border:1px solid var(--line);
  border-radius:20px;padding:8px 14px;font-size:12.5px;font-weight:700;color:var(--ink);cursor:pointer;font-family:inherit}
.gpf-chip .ic{width:15px;height:15px;vertical-align:0}
.gpf-chip:active{background:var(--gb-press)}

/* --- CLOCK tab header + the two big tiles under the quick actions ------------------------------ */
.gclk-head{display:flex;align-items:center;gap:10px;margin-bottom:10px}
.gclk-t{flex:1;min-width:0}
.gclk-k{display:block;font-size:10.5px;font-weight:800;letter-spacing:.09em;text-transform:uppercase;color:var(--muted)}
.gclk-v{display:block;font-family:var(--font-mono,inherit);font-size:27px;font-weight:700;color:var(--heading,#0b2036);
  line-height:1.15;margin-top:2px;font-variant-numeric:tabular-nums}
.gclk-st{flex:none;display:inline-flex;align-items:center;gap:6px;font-size:11px;font-weight:700;padding:6px 11px;border-radius:20px}
.gclk-st i{width:7px;height:7px;border-radius:50%;display:inline-block}
.gclk-st.on{background:#e4f6ec;color:#0d7a45}.gclk-st.on i{background:#0f9d58;animation:usgpulse 1.6s infinite}
.gclk-st.off{background:var(--gb-field);color:var(--muted)}.gclk-st.off i{background:#93a3b5}
[data-theme="dark"] .gclk-st.on{background:#14261c;color:#7fd3a2}
/* ⚠ THE SOS FAB AND THIS GRID — measured, so nobody re-derives it.
   #g-sos is `position:fixed;right:14px;bottom:calc(78px + safe-area)` at 62x62 (declared in
   guard.html), i.e. a viewport rect of x 284..346, y 660..722 at 360x800. The right cell of this
   grid (#gclk-req "My requests") lays out at x 185..342, y 597..711 at scroll 0, so the FAB covers
   58x51px of it — including the last 22x15px of the subtitle "Time off & availability".
   Raising .gmain's bottom clearance (done, see the 152px note above) fixes the END of the scroll
   but NOT this, because padding below the content cannot move content that is above it upward.
   Moving the FAB per view (the .rad-bar precedent further up) does not fix it either — measured
   both directions: bottom:150px puts the FAB at y 588..650 and the overlap grows to 53px; the only
   position that clears y=711 is bottom<=27px, which puts a 62px opaque circle on top of the "More"
   tab (nav tabs at 360px are 72px each, More = x 288..360). There is no vertical band that is free
   of content at every scroll offset, because the FAB is viewport-fixed over a single scrolling
   column that is only 360px wide.
   What would actually fix it is a design decision, not a CSS one: dock SOS into the tab bar as
   fixed chrome (then this clearance covers it completely), or shrink it and inset it. Left as-is
   deliberately — it is a safety control and resizing it is the owner's call. */
.gclk-pair{display:grid;grid-template-columns:1fr 1fr;gap:10px;margin-bottom:12px}
.gclk-big{background:var(--card,#fff);border:1px solid var(--line);border-radius:14px;padding:14px;color:var(--ink);
  display:flex;flex-direction:column;align-items:flex-start;gap:8px;cursor:pointer;font-family:inherit;text-align:left}
.gclk-big:active{background:var(--gb-press)}
.gclk-big b{font-size:13px;font-weight:700;color:var(--heading,#0b2036)}
.gclk-big small{font-size:11px;color:var(--muted);line-height:1.35}

/* --- 360px: the narrowest phone in the fleet. Nothing may push the page sideways. -------------- */
@media (max-width:370px){
  .gh-tiles,.gm-grid{gap:7px}
  .gh-tile,.gm-tile{padding:11px 3px 9px}
  .gh-tile .tl,.gm-tile b{font-size:10.5px}
  .ghg-tx b{font-size:17.5px}
  .gclk-v{font-size:24px}
  .gnav5 button{font-size:9px}
}

/* ==================================================================================================
   HOUSE CONTROL LAYER (2026 modernization pass — officer app)
   ---------------------------------------------------------------------------------------------
   Why this exists, and why it is at the END of the file rather than woven into the sections above:
   before this pass the app's dark theme was an ENUMERATED allow-list — one long selector listing
   every input that had been remembered (`.gfield input, .note-card input, .swap-panel select, …`).
   Anything added later, or under a selector nobody thought of, kept its light-mode `background:#fff`
   and rendered as a white box inside a dark card. Measured at 360x800 in dark mode before this
   block: `.note-card textarea` (order gear), `select#pt-kind` (time off), `select#gr-rep` (reminder)
   were all rgb(255,255,255). And `appearance:none` appeared ZERO times in the whole stylesheet, so
   every <select> in the app rendered as a raw OS dropdown.
   So the fix is by ELEMENT and by TOKEN, not by remembering a class:
     • every control the officer can type into or pick from inherits one skin from --gb-field /
       --gb-field-line / --ink, which both themes already define, so dark comes free forever;
     • the exclusion list is short and explicit (chat composer, login, the incident composer, which
       all own their skin), instead of an inclusion list that silently rots.
   Second job of this block: TOUCH TARGETS. Measured before: .gbtn 39px, .gbtn.sm 30px, sub-tabs
   38px, .ann-react 34px, .gn-slot 34px, .g-thmb 33px, .gai-chip 30px, .gpf-chip 35px,
   .gd-ghostbtn 37px, .g-x 26px, .td-box 24px. The bar is 44. Where a control must stay visually
   small (a chip, a checkbox), the BOX stays small and the HIT AREA is grown with a ::after overlay
   so the thumb target is 44 without the design changing.
   ================================================================================================== */

/* --- 1. one form skin, by element ---------------------------------------------------------------
   Scope: the app shell and every overlay it opens. Excluded on purpose:
     .glogin/#g-otp-form  — the login card owns its own look (white card on the navy gradient)
     #gv-messages/.g-compose — the chat composer was rebuilt this train and is HANDS OFF
     .irc-* (incident composer) — already a designed dark-first surface
     [type=checkbox]/[type=radio] — native semantics, styled separately below */
.gapp input:not([type=checkbox]):not([type=radio]):not([type=file]):not([type=range]),
.gapp select, .gapp textarea,
.note-card input:not([type=checkbox]):not([type=radio]):not([type=file]),
.note-card select, .note-card textarea,
.g-modal input:not([type=checkbox]):not([type=radio]):not([type=file]),
.g-modal select, .g-modal textarea,
.g-sheet input:not([type=checkbox]):not([type=radio]):not([type=file]),
.g-sheet select, .g-sheet textarea{
  width:100%;box-sizing:border-box;min-height:46px;padding:12px 14px;
  border:1px solid var(--gb-field-line,#d8e0ea);border-radius:12px;
  background:var(--gb-field,#f4f7fb);color:var(--ink);
  font-family:inherit;font-size:16px;line-height:1.35;   /* 16px = iOS/Android never zooms on focus */
  /* border + shadow only. `background` was in this list and made a theme switch ANIMATE every field
     on screen; in a throttled WebView the animation does not run and the control can sit on the
     wrong colour (reproduced while measuring this file). Focus feedback is what needs the ease. */
  transition:border-color .15s,box-shadow .15s}
/* the two surfaces that own their own control skin */
.glogin input,#g-otp-form input,
#gv-messages input,#gv-messages textarea,.g-compose textarea,.g-compose input,
.irc-ov input,.irc-ov select,.irc-ov textarea,
.gapp input[type=checkbox],.gapp input[type=radio]{min-height:auto}
/* ⚠ This line used to read `background:#fff;color:#16222f;border:1px solid #e4eaf1` — the
   PRE-Phase-D login, when the form really was a white card. Phase D (line ~560) made the login
   full-bleed dark glass on the navy gradient and dropped the card (`.glogin form{background:
   transparent}`), but this rule lives LATER in the file at identical specificity, so it silently
   won and painted two opaque white slabs onto the gradient. Measured live before the fix:
   #gl-user and #gl-pass both computed background rgb(255,255,255), color rgb(22,34,47).
   These values are now the same glass as line ~560, so 560 and this line agree instead of fight. */
.glogin input,#g-otp-form input{width:100%;background:rgba(255,255,255,.07);color:#fff;
  border:1px solid rgba(255,255,255,.16);font-size:16px}
/* --- autofill: the one UA paint that `background` cannot reach -----------------------------------
   Chrome/Samsung Internet paint an autofilled field from the UA sheet; only an inset box-shadow
   covers it, and only -webkit-text-fill-color moves the text. Without this the login showed a
   stark WHITE field with dark text the moment the password manager filled it — and only the field
   it filled, which is why the owner's two screenshots disagreed about WHICH field went white.
   #132e42 is the measured composite of `rgba(255,255,255,.07)` over the login gradient at the
   field band (gradient rgb(2,30,51) at t≈0.5), so the autofilled state matches the rest state.
   The 9999s transition is the standard trick: Chrome re-applies the paint on blur/refill and the
   transition simply never completes. */
.glogin input:-webkit-autofill,.glogin input:-webkit-autofill:hover,
.glogin input:-webkit-autofill:focus,.glogin input:-webkit-autofill:active,
#g-otp-form input:-webkit-autofill,#g-otp-form input:-webkit-autofill:hover,
#g-otp-form input:-webkit-autofill:focus,#g-otp-form input:-webkit-autofill:active{
  -webkit-box-shadow:0 0 0 1000px #132e42 inset;box-shadow:0 0 0 1000px #132e42 inset;
  -webkit-text-fill-color:#fff;caret-color:#fff;
  transition:background-color 9999s ease-in-out 0s}
#gv-messages textarea,.g-compose textarea{width:auto;padding:9px 12px;border-radius:20px}
.irc-ov .irc-input,.irc-ov .irc-notes{background:var(--gb-field,#f4f7fb)}
.gapp textarea,.note-card textarea,.g-modal textarea,.g-sheet textarea{min-height:92px;resize:vertical;line-height:1.5}
.gapp input:focus,.gapp select:focus,.gapp textarea:focus,
.note-card input:focus,.note-card select:focus,.note-card textarea:focus,
.g-modal input:focus,.g-modal select:focus,.g-modal textarea:focus,
.g-sheet input:focus,.g-sheet select:focus,.g-sheet textarea:focus{
  outline:none;border-color:var(--blue-bright,#1a86d0);box-shadow:0 0 0 3px rgba(26,134,208,.22)}
.gapp ::placeholder,.note-card ::placeholder,.g-modal ::placeholder,.g-sheet ::placeholder{color:var(--muted);opacity:.85}
/* --- 1b. autofill, by element and by token -------------------------------------------------------
   Same doctrine as the block above: reach the control by ELEMENT and paint from --gb-field/--ink,
   so this covers every field the app has now and every field it grows later, in both themes, with
   no allow-list to rot. It is needed because an autofilled input is painted by the USER AGENT:
   `background` is ignored, and the only declaration that beats it is an inset box-shadow big enough
   to fill the box. `-webkit-text-fill-color` is likewise the only thing that moves autofilled text.
   Password managers fill by heuristic — a field called "phone", "email" or "name" is fair game — so
   the officer's profile, directory and helpdesk fields are all reachable, not just the login.
   The login and the OTP form are excluded here and carry their own copy up at the .glogin block,
   because that surface is dark in BOTH themes and must not read --gb-field. */
.gapp input:-webkit-autofill,.gapp input:-webkit-autofill:hover,
.gapp input:-webkit-autofill:focus,.gapp input:-webkit-autofill:active,
.note-card input:-webkit-autofill,.g-modal input:-webkit-autofill,.g-sheet input:-webkit-autofill{
  -webkit-box-shadow:0 0 0 1000px var(--gb-field,#f4f7fb) inset;
  box-shadow:0 0 0 1000px var(--gb-field,#f4f7fb) inset;
  -webkit-text-fill-color:var(--ink);caret-color:var(--ink);
  transition:background-color 9999s ease-in-out 0s}
/* focus must still show its ring: re-state the focus shadow AFTER the fill so the two compose
   rather than the fill swallowing the ring on an autofilled field the officer then taps. */
.gapp input:-webkit-autofill:focus,.note-card input:-webkit-autofill:focus,
.g-modal input:-webkit-autofill:focus,.g-sheet input:-webkit-autofill:focus{
  -webkit-box-shadow:0 0 0 1000px var(--gb-field,#f4f7fb) inset,0 0 0 3px rgba(26,134,208,.22);
  box-shadow:0 0 0 1000px var(--gb-field,#f4f7fb) inset,0 0 0 3px rgba(26,134,208,.22)}

/* --- 2. <select>: never the raw OS dropdown -----------------------------------------------------
   appearance:none + our own chevron. The chevron is a data-URI so it costs no request offline, and
   it is re-declared for dark because a stroke colour cannot come from a CSS variable inside a
   background-image URL. */
.gapp select,.note-card select,.g-modal select,.g-sheet select{
  appearance:none;-webkit-appearance:none;-moz-appearance:none;padding-right:40px;cursor:pointer;
  background-image:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='20' height='20' viewBox='0 0 24 24' fill='none' stroke='%235f7288' stroke-width='2.2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M6 9l6 6 6-6'/%3E%3C/svg%3E");
  background-repeat:no-repeat;background-position:right 13px center;background-size:18px}
[data-theme="dark"] .gapp select,[data-theme="dark"] .note-card select,
[data-theme="dark"] .g-modal select,[data-theme="dark"] .g-sheet select{
  background-image:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='20' height='20' viewBox='0 0 24 24' fill='none' stroke='%238b98ab' stroke-width='2.2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M6 9l6 6 6-6'/%3E%3C/svg%3E")}
/* The OPTION list and the date/time picker are drawn by the OS, not by this stylesheet: without
   color-scheme they stay a white popup in dark mode on Android WebView.
   ⚠ ORDER IS LOAD-BEARING. `:root` and `[data-theme="dark"]` have IDENTICAL specificity (0,1,0) and
   both match <html>, so whichever is written LAST wins. With them the other way round the dark theme
   computed `color-scheme: light` — measured — and shipped exactly the white picker this rule exists
   to prevent. Light first, dark second. */
:root{color-scheme:light}
[data-theme="dark"]{color-scheme:dark}

/* --- 3. file inputs: a real button, not the UA's grey slab ------------------------------------- */
.gapp input[type=file]:not([hidden]),.note-card input[type=file]:not([hidden]),
.g-modal input[type=file]:not([hidden]),.g-sheet input[type=file]:not([hidden]){
  width:100%;box-sizing:border-box;font-size:13px;color:var(--muted);
  border:1px dashed var(--gb-field-line,#d8e0ea);border-radius:12px;padding:10px 12px;background:transparent}
.gapp input[type=file]::file-selector-button,.note-card input[type=file]::file-selector-button,
.g-modal input[type=file]::file-selector-button,.g-sheet input[type=file]::file-selector-button{
  margin-right:10px;min-height:36px;padding:0 14px;border:0;border-radius:9px;
  background:var(--gb-tint,#e8f1fa);color:var(--blue,#0a6bb0);font-family:inherit;font-size:13px;font-weight:700;cursor:pointer}

/* --- 4. labels: one size, both themes ----------------------------------------------------------
   `.field` and `.il` had NO rule outside `.g-sheet` — the order-gear and reminder sheets were
   rendering raw 16px UA labels crammed against their inputs. */
.field,.gfield{margin-bottom:13px;min-width:0}
/* two fields side by side. `min-width:0` on the children is the load-bearing part: a flex item
   defaults to min-width:auto, and a date/time <input> has a wide intrinsic minimum, so without it
   the pair pushes 16px past a 360px screen (measured on the time-off sheet). */
.grow2{display:flex;gap:10px}
.grow2>.field{flex:1;min-width:0;margin-bottom:13px}
.grow2>.field>input,.grow2>.field>select{max-width:100%}
.field>label,.gfield>label,.note-card .il,label.il{
  display:block;font-size:12.5px;font-weight:700;color:var(--muted);margin:0 0 6px;letter-spacing:.01em}
.field>label .meta,.gfield>label .meta{font-weight:400;text-transform:none;letter-spacing:0}
.ff-help{font-size:11.5px;color:var(--muted);margin:-2px 0 6px;line-height:1.45}

/* --- 5. tap targets: 44px, without redesigning the chips ---------------------------------------
   `--tap` names the number so it is one edit if the bar ever moves. Small chips keep their painted
   size and grow an invisible ::after to 44px; that is why they get position:relative. */
:root{--tap:44px}
.gbtn,.bigbtn,.gsub button,.gd-ghostbtn,.gt-scanbtn,.gt-skipbtn,.gclk-big,.gsh-claim,
.td-btn,.irc-submit,.irc-draftbtn,.wf-ok,.wf-help,.gpf-chip,.g-thmb,.gn-slot,.ann-react,.ann-cbtn{
  min-height:var(--tap);display:inline-flex;align-items:center;justify-content:center;gap:6px}
.gbtn.wfull,.bigbtn,.gsh-claim,.td-btn.wfull{display:flex;width:100%}
.gclk-big{display:flex;flex-direction:column;align-items:flex-start;justify-content:center}
.gsub button{flex:1}
.gbtn.sm{min-height:40px;padding:8px 13px;position:relative}
.gbtn.sm::after{content:"";position:absolute;left:0;right:0;top:50%;height:var(--tap);transform:translateY(-50%)}
.ann-react,.ann-cbtn{padding:0 14px;min-width:var(--tap)}
.gai-chip{min-height:38px;display:inline-flex;align-items:center;position:relative;padding:8px 13px}
.gai-chip::after{content:"";position:absolute;left:0;right:0;top:50%;height:var(--tap);transform:translateY(-50%)}
.g-x{min-width:var(--tap);min-height:var(--tap);display:inline-flex;align-items:center;justify-content:center}
.gh-link{min-height:var(--tap)}
/* the to-do checkbox: a 26px painted box with a 44px thumb target.
   The target is a ::after on the LABEL rather than an oversized <input>: an input grown past its
   parent gets clipped out of hit-testing by the row (measured — elementFromPoint 8px above the box
   returned `.td-row`, not the input), whereas a click anywhere on a <label> toggles the control it
   contains, so the overlay works without touching the markup. */
.td-box{width:26px;height:26px;position:relative}
.td-box::after{content:"";position:absolute;left:50%;top:50%;width:var(--tap);height:var(--tap);transform:translate(-50%,-50%)}
.td-box input{width:26px;height:26px;position:absolute;inset:0;margin:0;opacity:0;cursor:pointer}
.ck2,.ck,.tk{display:flex;align-items:center;gap:9px;min-height:var(--tap);font-size:14px;cursor:pointer}
.ck2 input,.ck input,.tk input{width:22px;height:22px;flex:none;accent-color:var(--blue,#0a6bb0)}

/* --- 6. empty states: a designed card, never a line of grey text -------------------------------
   `.empty` was `text-align:center;color:muted` — a bare sentence floating on the page background.
   It now paints the same dashed card as `.gh-empty`, so an officer with nothing scheduled sees a
   deliberate screen instead of a screen that looks like it failed to load. */
.empty{background:var(--card,#fff);border:1px dashed var(--line);border-radius:15px;
  padding:26px 18px;text-align:center;color:var(--muted);font-size:13.5px;line-height:1.6}
.empty b{display:block;font-size:14px;font-weight:700;color:var(--heading,#0b2036);margin-bottom:4px}
.empty .ic{width:26px;height:26px;color:var(--muted);opacity:.55;margin-bottom:9px}
.empty .gbtn{margin-top:14px;max-width:230px;margin-left:auto;margin-right:auto}

/* --- 7. skeletons: the screen has a SHAPE before the data lands --------------------------------
   Home fires ~12 parallel requests; Clock and Shifts several more. On a patrol road that is a
   visible blank. A skeleton is the difference between "loading" and "broken". */
@keyframes gskshine{0%{background-position:-220px 0}100%{background-position:260px 0}}
.gsk{border-radius:10px;background:var(--gb-field,#eef2f7);
  background-image:linear-gradient(90deg,transparent 0,var(--gb-press,rgba(255,255,255,.55)) 45%,transparent 90%);
  background-repeat:no-repeat;background-size:220px 100%;animation:gskshine 1.15s linear infinite}
.gsk-card{background:var(--card,#fff);border:1px solid var(--line);border-radius:16px;padding:15px 16px;margin-bottom:11px}
.gsk-row{display:flex;align-items:center;gap:12px;margin-bottom:11px}
.gsk-av{width:44px;height:44px;border-radius:50%;flex:none}
.gsk-l{height:12px}.gsk-l.t{height:15px}
.gsk-tiles{display:grid;grid-template-columns:repeat(3,1fr);gap:9px;margin-bottom:12px}
.gsk-tile{height:82px;border-radius:14px}
@media (prefers-reduced-motion:reduce){.gsk{animation:none}}

/* --- 8. segmented control (availability type, and anywhere a 2-3 way pick beats a dropdown) ----- */
.gseg{display:flex;gap:0;background:var(--gb-field,#eef2f7);border:1px solid var(--gb-field-line,#d8e0ea);
  border-radius:12px;padding:3px;margin-bottom:12px}
.gseg button{flex:1;min-width:0;min-height:40px;border:0;border-radius:9px;background:transparent;color:var(--muted);
  font-family:inherit;font-size:13px;font-weight:700;cursor:pointer;transition:background var(--t-fast) var(--ease),color var(--t-fast) var(--ease)}
.gseg button.on{background:var(--card,#fff);color:var(--blue,#0a6bb0);box-shadow:0 1px 3px rgba(18,28,48,.14)}

/* --- 9. house confirm sheet (replaces window.confirm) -------------------------------------------
   window.confirm paints the BROWSER's dialog: system font, "localhost:3942 says", and on Android
   WebView it is a grey slab with no relation to the app. Every destructive/irreversible answer in
   this app now goes through gConfirm() and this sheet. */
.gcf-ov{position:fixed;inset:0;z-index:10050;background:rgba(6,12,22,.6);display:flex;align-items:flex-end;
  justify-content:center;animation:gcffade .16s ease}
@keyframes gcffade{from{opacity:0}to{opacity:1}}
@keyframes gcfrise{from{transform:translateY(18px)}to{transform:translateY(0)}}
.gcf-card{background:var(--card,#fff);width:100%;max-width:560px;border-radius:20px 20px 0 0;padding:22px 20px
  calc(20px + env(safe-area-inset-bottom,0px));animation:gcfrise .18s ease;box-shadow:0 -6px 26px rgba(0,0,0,.28)}
.gcf-ic{width:46px;height:46px;border-radius:14px;display:flex;align-items:center;justify-content:center;
  background:var(--gb-tint);color:var(--blue,#0a6bb0);margin-bottom:12px}
.gcf-ic.warn{background:#fdf0dc;color:#a8660a}.gcf-ic.danger{background:#fdeaea;color:#c0182a}
[data-theme="dark"] .gcf-ic.warn{background:#3a2c12;color:#e0ab5a}
[data-theme="dark"] .gcf-ic.danger{background:#3d1a1e;color:#f08d96}
.gcf-ic .ic{width:23px;height:23px}
.gcf-t{font-family:var(--font-display,inherit);font-weight:800;font-size:18px;color:var(--heading,#0b2036);line-height:1.25}
.gcf-b{font-size:13.5px;color:var(--muted);line-height:1.6;margin-top:7px}
.gcf-btns{display:flex;gap:10px;margin-top:18px}
.gcf-btns button{flex:1;min-height:var(--tap);border:0;border-radius:13px;font-family:inherit;font-size:15px;
  font-weight:700;cursor:pointer}
.gcf-no{background:var(--gb-field,#eef1f8);color:var(--ink)}
.gcf-yes{background:var(--grad-primary,linear-gradient(90deg,#005080,#0a6bb0));color:#fff}
.gcf-yes.danger{background:linear-gradient(90deg,#c0182a,#e11d2a)}
/* gPrompt's field (replaces window.prompt's one-line browser box). The sheet mounts on <body>, so it
   sits OUTSIDE .gapp and the by-element form skin at line ~1724 (".gapp input, .g-sheet input, …")
   never reaches it. This IS that skin, painted from the same --gb-field/--gb-field-line/--ink
   tokens, so dark mode comes free and the field is indistinguishable from every other field in the
   app. 46px matches those controls and clears the 44px tap floor; 16px is the size below which
   iOS and Android zoom the page on focus. */
.gcf-l{display:block;margin-top:16px;font-size:12.5px;font-weight:700;color:var(--muted)}
.gcf-in{width:100%;box-sizing:border-box;min-height:46px;margin-top:6px;padding:12px 14px;
  border:1px solid var(--gb-field-line,#d8e0ea);border-radius:12px;
  background:var(--gb-field,#f4f7fb);color:var(--ink);
  font-family:inherit;font-size:16px;line-height:1.35;
  transition:border-color .15s,box-shadow .15s}
.gcf-card > .gcf-in{margin-top:16px}      /* unlabelled: the label's own margin is what spaces it */
/* gPrompt({multiline:true}) — editing a message that already has line breaks. Same field skin, room
   for four lines, and the browser's own drag handle removed: at 360px it would sit under the OK
   button, and the fixed height is already generous for what this asks anyone to type. */
.gcf-ta{min-height:104px;resize:none;white-space:pre-wrap;overflow-y:auto}
.gcf-in:focus{outline:none;border-color:var(--blue-bright,#1a86d0);box-shadow:0 0 0 3px rgba(26,134,208,.22)}
.gcf-in::placeholder{color:var(--muted);opacity:.85}
/* Same chassis, one field taller: the "Create task from this message" composer (supervisor.js) is a
   FORM on this sheet, not a single question, so it needs a select skin and a two-up row. Kept here
   rather than in supervisor.css because that file is scoped under #g-sup and this sheet mounts on
   <body> — the trap that renders a body-appended overlay unstyled. */
.gcf-sel{appearance:none;-webkit-appearance:none;padding-right:38px;cursor:pointer;
  background-image:url("data:image/svg+xml;charset=utf-8,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%235f7288' stroke-width='2.2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M6 9l6 6 6-6'/%3E%3C/svg%3E");
  background-repeat:no-repeat;background-position:right 12px center;background-size:16px}
[data-theme="dark"] .gcf-sel{background-image:url("data:image/svg+xml;charset=utf-8,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%239fb0c8' stroke-width='2.2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M6 9l6 6 6-6'/%3E%3C/svg%3E")}
.gcf-row{display:flex;gap:10px}
.gcf-row .gcf-l{flex:1;min-width:0}
/* SCOPED to the form sheet, never to .gcf-card at large: gConfirm and gPrompt are short by design and
   giving every one of them a scroll container would change how a two-line question animates in. A
   composer holding a whole quoted message is the only sheet tall enough to need it, and it scrolls
   ITSELF so "Create task" can never end up below the fold on a 640px phone. */
.gcf-card.gcf-form{max-height:min(88vh,760px);overflow-y:auto;overscroll-behavior:contain}
.gcf-card.gcf-form:focus{outline:none}   /* it takes focus so the dialog is entered; it is not a control */
.gcf-form .gcf-ta{min-height:88px}
.gcf-hint{margin-top:12px;font-size:11.5px;color:var(--muted);line-height:1.5}

/* --- 10. TIMESHEET (My hours) — Connecteam's officer self-timesheet ------------------------------
   Was: a flat 3-column `.gh-row` list that also collided with Home's navigation-row class. Now the
   benchmark anatomy: a range pager with a Today chip, the totals EQUATION strip, and day cards with
   a big date numeral, today marked by a blue left edge. */
.gts-pager{display:flex;align-items:center;gap:8px;margin-bottom:12px}
.gts-pager .nav{flex:none;width:var(--tap);height:var(--tap);border-radius:12px;border:1px solid var(--line);
  background:var(--card,#fff);color:var(--ink);font-size:19px;line-height:1;cursor:pointer;
  display:flex;align-items:center;justify-content:center}
.gts-pager .nav:disabled{opacity:.35;cursor:default}
.gts-pager .rng{flex:1;min-width:0;text-align:center;font-size:13.5px;font-weight:700;color:var(--heading,#0b2036)}
.gts-pager .rng small{display:block;font-size:11px;font-weight:400;color:var(--muted);margin-top:1px}
.gts-today{flex:none;min-height:36px;padding:0 13px;border-radius:18px;border:1px solid var(--line);
  background:var(--card,#fff);color:var(--blue,#0a6bb0);font-family:inherit;font-size:12px;font-weight:700;cursor:pointer}
.gts-sum{background:var(--card,#fff);border:1px solid var(--line);border-radius:16px;padding:15px 16px;margin-bottom:12px}
.gts-eq{display:flex;align-items:baseline;flex-wrap:wrap;gap:7px;font-size:13px;color:var(--muted)}
.gts-eq b{font-family:var(--font-mono,inherit);font-size:19px;font-weight:700;color:var(--heading,#0b2036)}
.gts-eq .op{font-size:15px;color:var(--muted);opacity:.7}
.gts-eq .tot b{color:var(--blue,#0a6bb0)}
.gts-meta{display:flex;gap:14px;flex-wrap:wrap;margin-top:11px;padding-top:11px;border-top:1px solid var(--line)}
.gts-meta div{font-size:11.5px;color:var(--muted)}
.gts-meta div b{display:block;font-family:var(--font-mono,inherit);font-size:14px;color:var(--heading,#0b2036);margin-top:2px}
.gts-day{display:flex;gap:13px;background:var(--card,#fff);border:1px solid var(--line);border-radius:15px;
  padding:13px 15px;margin-bottom:9px;position:relative;overflow:hidden}
.gts-day.today{border-color:#bcd8ef}
.gts-day.today::before{content:"";position:absolute;left:0;top:0;bottom:0;width:4px;background:var(--blue-bright,#1a86d0)}
[data-theme="dark"] .gts-day.today{border-color:#2c4a68}
.gts-day.open{border-color:#b7e2c9}
[data-theme="dark"] .gts-day.open{border-color:#1d5c3a}
.gts-num{flex:none;width:44px;text-align:center}
.gts-num b{display:block;font-family:var(--font-display,inherit);font-size:23px;font-weight:800;line-height:1;
  color:var(--heading,#0b2036)}
.gts-num small{display:block;font-size:10px;font-weight:700;letter-spacing:.06em;color:var(--muted);margin-top:3px}
.gts-mid{flex:1;min-width:0}
.gts-mid b{display:block;font-size:13.5px;font-weight:700;color:var(--heading,#0b2036);
  white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
.gts-mid .t{display:block;font-family:var(--font-mono,inherit);font-size:12px;color:var(--muted);margin-top:3px}
.gts-tags{display:flex;gap:5px;flex-wrap:wrap;margin-top:6px}
.gts-tag{display:inline-flex;align-items:center;gap:4px;font-size:10px;font-weight:700;letter-spacing:.03em;
  padding:3px 8px;border-radius:11px;background:var(--gb-field);color:var(--muted)}
.gts-tag .ic{width:11px;height:11px;vertical-align:0}
.gts-tag.ok{background:#e4f6ec;color:#0d7a45}
.gts-tag.live{background:#e7f2fb;color:#0a6bb0}
[data-theme="dark"] .gts-tag.ok{background:#14261c;color:#7fd3a2}
[data-theme="dark"] .gts-tag.live{background:#16273a;color:#9fc9ec}
.gts-h{flex:none;text-align:right}
.gts-h b{font-family:var(--font-mono,inherit);font-size:16px;font-weight:700;color:var(--heading,#0b2036)}
.gts-h small{display:block;font-size:10.5px;color:var(--muted);margin-top:2px}
.gts-done{display:flex;align-items:center;justify-content:center;gap:8px;background:#e4f6ec;color:#0d7a45;
  border-radius:14px;padding:14px;margin-top:12px;font-size:13px;font-weight:700;line-height:1.45;text-align:left}
.gts-done .ic{width:19px;height:19px;flex:none}
[data-theme="dark"] .gts-done{background:#14261c;color:#7fd3a2}

/* --- 11. GEAR: a picker of real cards, and orders that read as status ---------------------------- */
.ggear-list{display:flex;flex-direction:column;gap:8px;margin-bottom:4px}
.ggear-item{display:flex;align-items:center;gap:12px;width:100%;text-align:left;background:var(--card,#fff);
  border:1.5px solid var(--line);border-radius:14px;padding:11px 13px;color:var(--ink);font-family:inherit;
  cursor:pointer;min-height:var(--tap)}
.ggear-item.on{border-color:var(--blue-bright,#1a86d0);background:var(--gb-tint)}
.ggear-item .sq{flex:none;width:38px;height:38px;border-radius:11px;background:var(--gb-field);
  color:var(--blue,#0a6bb0);display:flex;align-items:center;justify-content:center}
.ggear-item .sq .ic{width:19px;height:19px;vertical-align:0}
.ggear-item .tx{flex:1;min-width:0}
.ggear-item .tx b{display:block;font-size:13.5px;font-weight:700;color:var(--heading,#0b2036);
  white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
.ggear-item .tx small{display:block;font-size:11.5px;color:var(--muted);margin-top:2px}
.ggear-item .pr{flex:none;font-family:var(--font-mono,inherit);font-size:13px;font-weight:700;color:var(--heading,#0b2036)}
.ggear-item.out{opacity:.55}
.ggear-ord{display:flex;align-items:center;gap:11px;background:var(--card,#fff);border:1px solid var(--line);
  border-radius:14px;padding:11px 13px;margin-bottom:8px}
.ggear-ord .tx{flex:1;min-width:0}
.ggear-ord .tx b{display:block;font-size:13px;font-weight:700;color:var(--heading,#0b2036)}
.ggear-ord .tx small{display:block;font-size:11.5px;color:var(--muted);margin-top:2px;line-height:1.45}
.gst{flex:none;display:inline-flex;align-items:center;gap:5px;font-size:10.5px;font-weight:800;letter-spacing:.04em;
  padding:5px 10px;border-radius:12px;background:var(--gb-field);color:var(--muted);white-space:nowrap}
.gst .ic{width:12px;height:12px;vertical-align:0}
.gst.ok{background:#e4f6ec;color:#0d7a45}
.gst.no{background:#fdeaea;color:#c0182a}
.gst.wait{background:#fdf0dc;color:#a8660a}
.gst.info{background:#e7f2fb;color:#0a6bb0}
[data-theme="dark"] .gst.ok{background:#14261c;color:#7fd3a2}
[data-theme="dark"] .gst.no{background:#3d1a1e;color:#f08d96}
[data-theme="dark"] .gst.wait{background:#3a2c12;color:#e0ab5a}
[data-theme="dark"] .gst.info{background:#16273a;color:#9fc9ec}

/* --- 12. odds and ends the sweep turned up -------------------------------------------------------
   .rbtn's icon slot was sized for a text glyph (font-size:26px) and now holds an SVG.
   .av-row / .rep-item / patrol-form card lose their hard-coded #fff via tokens above; the two that
   set it inline are fixed in the markup. */
/* the incident composer's own chips: designed before the 44px bar, measured at 17–38px */
.irc-type,.irc-sev,.irc-dictate{min-height:42px;display:inline-flex;align-items:center;justify-content:center;gap:6px}
.irc-x{min-width:var(--tap);min-height:var(--tap);display:flex;align-items:center;justify-content:center}
.irc-edit,.glink{min-height:var(--tap);display:inline-flex;align-items:center;padding:0 4px}
.irc-loc{align-items:center}
.rbtn{min-height:96px}
.rbtn .ic{width:27px;height:27px;font-size:0;display:flex;align-items:center;justify-content:center}
.rbtn .ic svg{width:27px;height:27px}
.av-row{background:var(--card,#fff);border:1px solid var(--line);min-height:52px}
.gcard-form{background:var(--card,#fff);border:1px solid var(--line);border-radius:16px;padding:16px;margin-top:14px}
.gsec-note{font-size:12px;color:var(--muted);line-height:1.55;margin:-2px 0 10px}
/* an <a> that looks like a row must not inherit the UA's underlined blue */
.gm-row[href],.gm-row[href]:visited{text-decoration:none;color:var(--ink)}

/* --- 13. directory rows ------------------------------------------------------------------------- */
.gdir-row{display:flex;align-items:center;gap:11px;background:var(--card,#fff);border:1px solid var(--line);
  border-radius:14px;padding:10px 12px;margin-bottom:8px}
.gdir-av{flex:none;width:40px;height:40px;border-radius:50%;display:flex;align-items:center;justify-content:center;
  background:var(--grad-primary,linear-gradient(90deg,#005080,#0a6bb0));color:#fff;
  font-family:var(--font-display,inherit);font-weight:800;font-size:16px}
.gdir-row .tx{flex:1;min-width:0}
.gdir-row .tx b{display:block;font-size:14px;font-weight:700;color:var(--heading,#0b2036);
  white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
.gdir-row .tx small{display:block;font-size:11.5px;color:var(--muted);margin-top:2px;
  white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
.gdir-act{flex:none;width:var(--tap);height:var(--tap);border-radius:13px;border:1px solid var(--line);
  background:var(--gb-field);color:var(--muted);display:flex;align-items:center;justify-content:center;
  text-decoration:none;cursor:pointer}
.gdir-act .ic{width:19px;height:19px;vertical-align:0}
.gdir-act.pri{background:var(--gb-tint);border-color:transparent;color:var(--blue,#0a6bb0)}
.gdir-act:active{background:var(--gb-press)}

/* --- 14. REACTIVE CONTROLS: press, working, done ------------------------------------------------
   The standing rule this section exists to satisfy: "make the apps' buttons reactive — if it's done
   an action it should visually show." Three moments, one vocabulary.

   This layer is deliberately UNSCOPED. supervisor.html is a redirect to guard.html, so the officer
   app and the #g-sup supervisor module are the same document and load both stylesheets; anything
   defined here is already in scope for supervisor markup. supervisor.css only has to re-state the
   handful of rules its own #g-sup-scoped selectors would otherwise out-specify.

   1. PRESS — :active, instantly, on every control. ONE treatment everywhere: shrink to
      --press-scale and shift by --press-dim. Splitting it into a motion half (scale) and a colour
      half (filter) is the point: under prefers-reduced-motion the scale drops but THE DIM STAYS,
      because a reduced-motion user still has to see that their tap registered. Killing both would
      re-break the rule for exactly the people who can least afford a dead-feeling control.
      Dark theme inverts the dim to a lift — darkening a #1a2231 button by 7% is invisible on an
      OLED phone at night, lightening it by 18% is not.
   2. WORKING — .is-busy, driven by gBusy() in guard.js. The helper pins the button's MEASURED width
      before swapping the label out, so "Clock in" -> spinner cannot reflow the row underneath the
      officer's thumb mid-tap.
   3. DONE — .is-done (green, check + past-tense label) or .is-err (red, re-armed as "Try again").
      Never a silent return to idle.
   is-busy / is-done are the same class names the inspection Submit already shipped with in
   supervisor.js; this generalises that button's vocabulary rather than inventing a second one. */
:root{--press-scale:.97;--press-dim:brightness(.93);--hover-dim:brightness(1.05);--press-ms:.09s}
[data-theme="dark"]{--press-dim:brightness(1.18);--hover-dim:brightness(1.12)}

.gbtn,.bigbtn,.rbtn,.gsave,.gsub button,.gd-ghostbtn,.gt-scanbtn,.gt-skipbtn,.gclk-big,.gsh-claim,
.td-btn,.irc-submit,.irc-draftbtn,.wf-ok,.wf-help,.gpf-chip,.ann-react,.ann-cbtn,.cp-btn,
.g-hbtn,.gdir-act,.gai-chip,.gfab,.btn-copy,.gl-alt-btn,.g-x{
  transition:transform var(--press-ms) ease-out,filter var(--t-fast) var(--ease),box-shadow var(--t-fast) var(--ease);cursor:pointer}
.gbtn:active,.bigbtn:active,.rbtn:active,.gsave:active,.gsub button:active,.gd-ghostbtn:active,
.gt-scanbtn:active,.gt-skipbtn:active,.gclk-big:active,.gsh-claim:active,.td-btn:active,
.irc-submit:active,.irc-draftbtn:active,.wf-ok:active,.wf-help:active,.gpf-chip:active,
.ann-react:active,.ann-cbtn:active,.cp-btn:active,.g-hbtn:active,.gdir-act:active,
.gai-chip:active,.gfab:active,.btn-copy:active,.gl-alt-btn:active,.g-x:active{
  transform:scale(var(--press-scale));filter:var(--press-dim)}
/* Hover is a bonus for the Windows desktop shell and a docked tablet; on a phone (hover:none) it
   must not exist at all, or the pressed style sticks after the finger lifts. */
@media (hover:hover){
  .gbtn:hover,.bigbtn:hover,.rbtn:hover,.gd-ghostbtn:hover,.gt-scanbtn:hover,.gt-skipbtn:hover,
  .gclk-big:hover,.gsh-claim:hover,.td-btn:hover,.irc-submit:hover,.irc-draftbtn:hover,
  .wf-ok:hover,.wf-help:hover,.cp-btn:hover,.gfab:hover{filter:var(--hover-dim)}
}
/* A control that is working or finished is not pressable — never let it wobble under a second tap. */
.is-busy,.is-done,.is-err{transform:none!important;filter:none!important}
.is-busy{cursor:progress}
/* Deliberately NO generic `button:disabled{opacity:…}` here. `button:disabled:not(.is-busy)…` scores
   (0,0,4,1) and would silently out-specify the two shipped one-offs, .gn-slot:disabled (.45) and
   .gts-pager .nav:disabled (.35). gBusy() gives a working button its own colour and spinner, so a
   blanket fade buys nothing and would quietly restyle controls this rule has no business touching. */
.is-busy:disabled,.is-done:disabled,.is-err:disabled{opacity:1}

/* The spinner. Same 14px/2px geometry as #g-sup .in-spin so the two apps read as one product. */
.gspin{width:14px;height:14px;flex:none;display:inline-block;border:2px solid rgba(255,255,255,.35);
  border-top-color:#fff;border-radius:50%;animation:gspinrot .7s linear infinite}
@keyframes gspinrot{to{transform:rotate(360deg)}}
/* White-on-white is an invisible spinner: ghost/outline buttons get an ink-coloured ring instead. */
.gbtn.ghost .gspin,.bigbtn.out .gspin,.gd-ghostbtn .gspin,.gt-skipbtn .gspin,.wf-help .gspin,
.irc-draftbtn .gspin,.gai-chip .gspin,.gpf-chip .gspin,.gdir-act .gspin{
  border-color:rgba(20,40,70,.20);border-top-color:var(--navy-ink)}
[data-theme="dark"] .gbtn.ghost .gspin,[data-theme="dark"] .bigbtn.out .gspin,
[data-theme="dark"] .gd-ghostbtn .gspin,[data-theme="dark"] .gt-skipbtn .gspin,
[data-theme="dark"] .wf-help .gspin,[data-theme="dark"] .irc-draftbtn .gspin{
  border-color:rgba(200,220,245,.24);border-top-color:var(--navy-ink)}

/* The two settled states. Both are opaque fills so they read at a glance from arm's length, which
   is how an officer actually looks at the phone after tapping Clock in. */
.is-done{background:var(--green)!important;border-color:transparent!important;color:#fff!important}
.is-err{background:var(--red)!important;border-color:transparent!important;color:#fff!important}
.is-done .ic,.is-err .ic,.is-done svg,.is-err svg{width:15px;height:15px;flex:none}
/* Not every control in the list above is a flex box already (section 5 only covers the tap-target
   set), and the busy/done label is always icon + text, so guarantee the pairing lays out. */
.is-busy,.is-done,.is-err{display:inline-flex;align-items:center;justify-content:center;gap:7px}
.gbtn.wfull.is-busy,.gbtn.wfull.is-done,.gbtn.wfull.is-err,.bigbtn.is-busy,.bigbtn.is-done,
.bigbtn.is-err,.gsh-claim.is-busy,.gsh-claim.is-done,.gsh-claim.is-err,.td-btn.wfull.is-busy,
.td-btn.wfull.is-done,.td-btn.wfull.is-err{display:flex}

@media (prefers-reduced-motion:reduce){
  .gbtn:active,.bigbtn:active,.rbtn:active,.gsave:active,.gsub button:active,.gd-ghostbtn:active,
  .gt-scanbtn:active,.gt-skipbtn:active,.gclk-big:active,.gsh-claim:active,.td-btn:active,
  .irc-submit:active,.irc-draftbtn:active,.wf-ok:active,.wf-help:active,.gpf-chip:active,
  .ann-react:active,.ann-cbtn:active,.cp-btn:active,.g-hbtn:active,.gdir-act:active,
  .gai-chip:active,.gfab:active,.btn-copy:active,.gl-alt-btn:active,.g-x:active{transform:none}
  .gspin{animation:none}
}

/* --- 15. NARRATIVE FIELDS: the app's long-form writing surfaces ---------------------------------
   Field report from a working supervisor, 2026-08: *"The space to post report is relatively
   compressed and small. There's a need to expand the space to accommodate lengthy reports."*
   He is right, and it was never one box. MEASURED at 360x800 before this section, every long-form
   textarea in the officer + supervisor app:
     #irc-narr  incident narrative      328x104   (13% of the viewport, 13px type)
     #fr-text   supervisor field report 332x155   (19%)
     #rp-narr   supervisor file-report  332x104   #an-body announcement 332x104
     #in-notes  inspection notes        332x84    #in-failnotes failure notes 304x64  (8%!)
     #scan-note 320x122 · #tk-body 328x116 · #skip-reason/#p-narr/#pd-body/#ff-*  ~98
     #gc-reason/#sq-* 92
   An incident narrative is 15-25 lines of prose typed one-handed at 3am. Four visible lines is a
   letterbox: you cannot re-read the sentence you just wrote, so you cannot edit it, so the report
   that reaches the client is worse. The benchmark set for this app (WhatsApp / Connecteam /
   Discord) all do the same two things — open bigger than one line of intent, and GROW with the
   text until a sane ceiling, then scroll inside.
   So the fix is a class, not a per-box height:
     .g-narr      every long-form box in the app — opens at ~6 lines, grows to 420px
     .g-narr-lg   the three DEDICATED report screens (officer incident composer, supervisor field
                  report, supervisor file-a-report) — opens at 40% of the viewport, grows to ~68%
   and one helper, gGrowField() in guard.js, which reads its ceiling back out of THIS file so the
   numbers live in exactly one place.

   ⚠ WHY THE SELECTOR LIST IS SO LONG. Three older rules already claim these elements at higher
   specificity, and guard.css loads BEFORE supervisor.css so a tie loses:
       .gapp textarea                (0,1,1)  min-height:92px;resize:vertical   [section 1]
       #g-sup .field textarea        (1,1,1)  supervisor.css:157
       #g-sup .fr-sheet textarea     (1,1,1)  supervisor.css:226  min-height:104px
   `textarea.g-narr` (0,1,1) beats the first only because it is later in this file; the #g-sup pair
   need a two-class selector (1,2,0) — class count outranks element count — to be reached at all.
   Add a new narrative surface OUTSIDE #g-sup .field / #g-sup .fr-sheet and it needs a line here.

   ⚠ dvh, not vh. guard.html ships `interactive-widget=resizes-content`, so the Android keyboard
   SHRINKS the layout viewport instead of covering it. In dvh the box shrinks with it and the sticky
   submit (.irc-foot / #g-sup .fr-foot) stays where the thumb expects it; a fixed px minimum would
   push it off the screen the moment the keyboard opened. ------------------------------------- */
.g-narr{--narr-min:180px;--narr-max:min(56dvh,420px)}
/* declared AFTER .g-narr on purpose: same specificity, so the modifier has to come second to win.
   Used together — class="g-narr g-narr-lg", the same shape as .gbtn.sm. */
.g-narr-lg{--narr-min:clamp(220px,40dvh,340px);--narr-max:min(68dvh,560px)}
textarea.g-narr,
.gapp textarea.g-narr,.note-card textarea.g-narr,.g-modal textarea.g-narr,.g-sheet textarea.g-narr,
.irc-ov textarea.g-narr,
#g-sup .field .g-narr,#g-sup .fr-sheet .g-narr{
  box-sizing:border-box;width:100%;display:block;
  min-height:var(--narr-min);max-height:var(--narr-max);height:auto;
  padding:12px 14px;                 /* the house control padding, section 1 */
  /* 16px is load-bearing, not taste: anything smaller and mobile Chrome/Safari zoom-on-focus jumps
     the whole layout mid-sentence. #irc-narr was 13px and #fr-text 15px before this. */
  font-family:inherit;font-size:16px;line-height:1.55;
  /* the box sizes itself now — a drag handle would fight gGrowField on the next keystroke, and a
     phone has no way to grab one anyway. */
  resize:none;overflow-y:auto}
/* A narrative box carries its own label, so the gap under it is the sheet's rhythm, not the box's. */
.irc-ov textarea.g-narr{margin-bottom:8px}

/* ==============================================================================================
   gPhotoPicker — the two-path photo attach control (guard.js gPhotoPicker)
   ----------------------------------------------------------------------------------------------
   UNSCOPED on purpose. The supervisor Inspect form lives inside #g-sup, but this control is about
   to be mounted on officer-app surfaces (#254 equipment photos) that are not — a `#g-sup .gph`
   selector would render those unstyled, which is the same trap that bit the body-appended overlays.
   Everything paints from tokens so both themes follow without a second rule set.

   The two paths must be tellable apart WITHOUT reading: the live tile carries a red rule and "On
   post", the uploaded tile an amber one and "Uploaded". That is the whole integrity argument of the
   feature expressed in 4px of colour, so it is not decoration and must not be tuned away.
   ============================================================================================== */
.gph{display:block}
.gph-grid{display:flex;flex-wrap:wrap;gap:8px}
.gph-grid:empty{display:none}
.gph-grid:not(:empty){margin-bottom:10px}
.gph-t{position:relative;display:block;width:104px;border-radius:12px;overflow:hidden;
  border:1px solid var(--line);background:var(--card,#fff);
  /* the provenance rule — red = taken here, amber = came out of the gallery */
  box-shadow:inset 4px 0 0 var(--red,#e11d2a)}
.gph-t.up{box-shadow:inset 4px 0 0 var(--amber,#e08600)}
.gph-t img{width:104px;height:88px;object-fit:cover;display:block}
.gph-chip{position:absolute;left:8px;top:6px;padding:2px 7px;border-radius:99px;
  font-size:9.5px;font-weight:800;letter-spacing:.04em;text-transform:uppercase;
  background:var(--red,#e11d2a);color:#fff;box-shadow:0 1px 4px rgba(0,0,0,.35)}
.gph-t.up .gph-chip{background:var(--amber,#e08600);color:#1e1400}
.gph-when{display:block;padding:5px 7px 6px;font-family:var(--font-mono,ui-monospace,monospace);
  font-size:9.5px;line-height:1.25;color:var(--muted);word-break:break-word}
/* "No capture data" is a FACT about the file, not a warning to be soothed away — it reads as muted
   italic rather than an alarm, but it is never omitted and never replaced with the current time. */
.gph-when.none{font-style:italic;opacity:.85}
.gph-rm{position:absolute;top:-1px;right:-1px;width:28px;height:28px;border:0;border-bottom-left-radius:12px;
  background:rgba(0,0,0,.55);color:#fff;font-size:16px;line-height:1;cursor:pointer;padding:0}
/* 44px tap floor via the house ::after hit-area trick — the visible × stays 28px so it does not
   cover the photo it is meant to let you check. */
.gph-rm::after{content:"";position:absolute;left:50%;top:50%;width:var(--tap,44px);height:var(--tap,44px);transform:translate(-50%,-50%)}
.gph-t.pend{display:flex;align-items:center;justify-content:center;width:104px;height:118px;
  box-shadow:none;border-style:dashed;background:var(--panel,transparent)}
.gph-t.pend i{width:20px;height:20px;border-radius:50%;border:2px solid var(--line);
  border-top-color:var(--muted);display:block;animation:gph-spin .8s linear infinite}
@keyframes gph-spin{to{transform:rotate(360deg)}}
/* Two peer actions, not a primary and a footnote: at 360px they stack rather than shrinking below
   the tap floor, which is where a 2-up row of two-word buttons ends up on the narrowest phone. */
.gph-acts{display:flex;flex-wrap:wrap;gap:8px}
.gph-btn{flex:1 1 148px;min-height:var(--tap,44px);display:inline-flex;align-items:center;justify-content:center;gap:7px;
  padding:11px 12px;border-radius:13px;border:1px solid var(--line);background:var(--card,#fff);
  color:var(--ink);font-family:inherit;font-size:14px;font-weight:700;cursor:pointer}
.gph-btn svg.ic{width:16px;height:16px;flex:none}
.gph-btn:active{transform:translateY(1px)}
.gph-btn:disabled{opacity:.45;cursor:default}
.gph-btn:focus-visible{outline:2px solid var(--navy-2,#005080);outline-offset:2px}
.gph-hint{margin:8px 0 0;font-size:11.5px;line-height:1.45;color:var(--muted)}
.gph-hint:empty{display:none}
/* Submit blocked for want of a live capture: the control says where to look rather than the toast
   alone, which a thumb over the screen hides. */
.gph.want .gph-live{border-color:var(--red,#e11d2a);box-shadow:0 0 0 3px rgba(225,29,42,.18)}
[data-theme="dark"] .gph-t{background:var(--card)}
[data-theme="dark"] .gph-btn{background:var(--card);border-color:var(--line)}
/* The amber chip deliberately does NOT soften in dark. Measured live at 360x800: darkening it to
   #c47500 with white ink gave 3.56:1, and this is 9.5px/800 text — nowhere near the 18.66px that
   would let the 3:1 large-text bar apply. Keeping the light-mode pair (#e08600 with near-black ink)
   measures 6.56:1 in both themes, and the chip is the label that tells a reader this photo is not
   proof of presence, so it is the last thing that may be hard to read. */
[data-theme="dark"] .gph-rm{background:rgba(0,0,0,.72)}

/* ==============================================================================================
   #254 — end-of-shift EQUIPMENT CHECK sheet (guard.js gEquipSheet)
   ----------------------------------------------------------------------------------------------
   Mounted in the house .g-sheet bottom sheet, one <section> per physical item, each with its own
   gPhotoPicker. The sheet is deliberately the TALLEST thing in the officer app — at 360x800 with
   three issued items it scrolls — so every item is drawn as a self-contained card with its own
   number ("2 / 3"): the officer has to be able to tell at a glance how much is left, and a run of
   undifferentiated fields on a scrolling sheet is where people give up and call dispatch instead.
   All from tokens, so both themes follow.
   ============================================================================================== */
.geq-sheet{max-width:520px}
.geq-lead{margin:-4px 0 14px;font-size:13px;line-height:1.5;color:var(--muted)}
.geq-lead b{color:var(--ink)}
.geq-item{border:1px solid var(--line);border-radius:14px;padding:12px;margin-bottom:12px;background:var(--panel,transparent)}
.geq-h{display:flex;align-items:center;gap:10px;margin-bottom:10px}
.geq-ic{flex:none;width:34px;height:34px;border-radius:10px;display:flex;align-items:center;justify-content:center;
  background:var(--navy);color:#fff}
.geq-ic svg{width:17px;height:17px}
.geq-t{flex:1 1 auto;min-width:0;display:block}
.geq-t b{display:block;font-size:14.5px;line-height:1.3;color:var(--heading,var(--ink));word-break:break-word}
.geq-t small{display:block;font-size:11px;font-weight:700;letter-spacing:.04em;text-transform:uppercase;color:var(--muted)}
.geq-tag{font-family:var(--font-mono,ui-monospace,monospace);font-size:11.5px;font-weight:600;color:var(--muted)}
/* The counter is a progress cue, not a label — mono so "2 / 3" and "11 / 12" hold the same width. */
.geq-n{flex:none;font-family:var(--font-mono,ui-monospace,monospace);font-size:11px;color:var(--muted)}
.geq-chg{display:flex;align-items:center;gap:10px;margin-bottom:10px}
.geq-chg[hidden]{display:none}
.geq-chg label{flex:1 1 auto;font-size:12.5px;font-weight:700;color:var(--muted);margin:0}
/* The % sits INSIDE the field's border rather than beside it: "80" floating next to a stray % sign
   reads as two controls, and this is the one number the dispatcher actually asked for. */
.geq-pct{flex:none;display:inline-flex;align-items:center;gap:2px;border:1px solid var(--line);border-radius:12px;
  padding-right:10px;background:var(--card,#fff)}
.geq-pct input{width:84px;min-height:var(--tap,44px);border:0;background:transparent;padding:0 4px 0 12px;
  font-family:inherit;font-size:15px;font-weight:700;color:var(--ink);text-align:right}
.geq-pct input:focus{outline:none}
.geq-pct:focus-within{border-color:var(--navy-2,#005080);box-shadow:0 0 0 3px rgba(0,80,128,.16)}
.geq-pct i{font-style:normal;font-size:13px;font-weight:700;color:var(--muted)}
.geq-cond{margin-bottom:11px}
.geq-cond button{min-height:var(--tap,44px)}
/* Damaged/Missing are not errors to be shouted, but they are not the neutral choice either —
   selecting one is what opens the note field the whole flag path depends on. */
.geq-cond button[data-c="damaged"].on{background:var(--amber,#e08600);border-color:var(--amber,#e08600);color:#1e1400}
.geq-cond button[data-c="missing"].on{background:var(--red,#e11d2a);border-color:var(--red,#e11d2a);color:#fff}
.geq-item .geq-note{margin-top:10px;margin-bottom:0}
.geq-item .geq-note[hidden]{display:none}
[data-theme="dark"] .geq-item{background:rgba(255,255,255,.02)}
[data-theme="dark"] .geq-pct{background:#0e1520;border-color:#22394d}

/* ══ 21. THE DISCORD COMPOSER — ＋ tray and typing mode ══════════════════════════════════════════
   Owner: "match the messaging tab to discord — a hidden menu, so there is more space for typing."
   The screenshot behind that: keyboard up, and the officer can see two lines of conversation.

   MEASURED at 360x800 with a thread open, BEFORE this section:
     .ghead 58.6 + .gth2 69 + .gth2-acts 61 + .gnav 60 = 248.6px of permanent chrome,
     leaving #g-thread 388px — and that is before the keyboard takes its ~300.
   Discord in the same place shows one slim header line and nothing else.

   ── 21a. The legacy rule that was eating the composer's whole skin ────────────────────────────
   Section 2's pre-rebuild `.g-compose button{background:var(--navy);color:#fff;border-radius:12px;
   height:48px;padding:0 18px}` is (0,1,1). Every control in the rebuilt bar — `.gcx-b`, `.gfab` —
   is (0,1,0), so the LEGACY rule won every one of those properties no matter how far down the file
   the modern one sits. MEASURED at both 360 and 375: #g-emoji / the paperclip / the camera came out
   38x48 painted rgb(3,43,73) with border-radius 12px, and .gfab 46x48 flat navy with radius 12px —
   i.e. the round transparent chips and the round gradient FAB the rebuild designed were never on
   screen; the bar was a row of navy blocks. (The earlier note in section 9 recorded this as "38x48
   ovals"; it is not an oval, the radius is overridden too.)
   Restating them here at (0,2,0) is the smallest fix that cannot break an unseen consumer of the
   legacy block — nothing is deleted. Do NOT "fix" this by adding another bare `.gcx-b` rule; it
   will lose the same way. */
.g-compose .gcx-b{width:38px;height:38px;padding:0;border-radius:50%;background:transparent;color:var(--muted);font-weight:400;box-shadow:none;position:relative}
.g-compose .gcx-b:active{background:var(--gb-press)}
/* ★ TAP FLOOR. Restoring the designed 38px height costs the accidental 44 the legacy `height:48px`
   was giving these chips, so the house ::after overlay (section 14's trick) has to put it back: the
   key stays 38px painted, the finger gets 44x44. .gcx does not clip — the overlay really is
   hit-testable, PROVED with elementFromPoint 4px above the painted box at 360 and 375. */
.g-compose .gcx-b::after{content:"";position:absolute;left:-3px;right:-3px;top:-3px;bottom:-3px}
.g-compose .gfab{width:46px;height:46px;padding:0;border-radius:50%;color:#fff;background:var(--grad-primary,linear-gradient(90deg,#005080,#0a6bb0));box-shadow:0 4px 14px rgba(0,80,128,.3)}
/* …and the recording state has to be re-stated AFTER the line above, or a running voice note would
   lose its red. Same specificity + later would already do it; +1 class makes it unambiguous. */
.g-compose .gfab.recording{background:var(--red)}
[data-theme="dark"] .g-compose .gfab{box-shadow:0 4px 14px rgba(0,0,0,.45)}

/* ── 21b. The ＋ key ─────────────────────────────────────────────────────────────────────────────
   One control at the head of the field owns Camera / Photo / Video / Document. It rotates 45° into
   a close glyph while the tray is open, which is why ICON.plus is a bare cross and not a "+" glyph
   — a rotated font character would land off-centre. */
.gcx-plus svg{transition:transform .16s ease}
.g-compose .gcx-plus.on{background:var(--gb-press);color:var(--blue,#0a6bb0)}
/* --blue (#0a6bb0) is the LIGHT accent; on the dark bar it measured rgb(10,107,176) over #141b28,
   about 2.8:1. The dark surfaces already use #53bdeb (that is what .g-pr-ic switches to). */
[data-theme="dark"] .g-compose .gcx-plus.on{color:#53bdeb}
.gcx-plus.on svg{transform:rotate(45deg)}
/* ── 21c. The tray ──────────────────────────────────────────────────────────────────────────────
   In NORMAL FLOW directly above the bar (where #g-reply and #g-ment already live), never floating
   over it. Scrolls sideways rather than shrinking its targets, the same choice .gth2-acts makes. */
.g-plusrow{display:flex;gap:4px;background:var(--gb-surf);border-top:1px solid var(--line);
  padding:8px 8px 4px;position:relative;z-index:30;overflow-x:auto;-webkit-overflow-scrolling:touch;scrollbar-width:none}
.g-plusrow::-webkit-scrollbar{display:none}
/* touch-action:pan-x — a horizontal drag STARTING ON A TILE must pan the row (the browser then
   pointercancels the tile, which the JS reads as not-a-tap). Without it some WebViews treat the
   button as a plain tap target and the tray only scrolls from the 4px gaps. */
.g-pr{flex:0 0 auto;width:74px;min-height:var(--tap,44px);border:0;background:transparent;padding:4px 0 6px;cursor:pointer;touch-action:pan-x;
  font-family:inherit;color:var(--ink);display:flex;flex-direction:column;align-items:center;gap:5px}
.g-pr-ic{flex:none;width:46px;height:46px;border-radius:50%;background:var(--gb-tint);color:var(--blue,#0a6bb0);
  display:flex;align-items:center;justify-content:center}
.g-pr-ic svg,.g-pr-ic .ic{width:21px;height:21px;vertical-align:0}
/* The Mention key is the one glyph in this tray that is a CHARACTER, not an svg — an @ drawn as a
   path reads as a spiral at 21px. Sized to match the optical weight of the icons beside it. */
.g-pr-at{font-size:23px;font-weight:700;line-height:1;font-family:var(--font-display,inherit)}
/* .g-pr-t, not `.g-pr span` — the icon IS a span, and `.g-pr span` (0,2,1) beat `.g-pr-ic` (0,1,0),
   which painted the glyph muted-grey instead of the accent. MEASURED before the class existed:
   .g-pr-ic color rgb(95,114,136) in light. */
.g-pr-t{font-size:11px;font-weight:600;color:var(--muted);line-height:1.2}
.g-pr:active .g-pr-ic{background:var(--gb-press)}
[data-theme="dark"] .g-pr-ic{background:rgba(83,189,235,.14);color:#53bdeb}

/* ── 21d. Typing mode ───────────────────────────────────────────────────────────────────────────
   ONE body class, set by gTypingWire() in guard.js. Everything here is display/padding only — no
   transitions on the chrome, deliberately: a transitioned property is exactly what goes stale in a
   non-compositing preview pane, and a 250ms fade on a 40px header reads as lag on a real phone. */
body.g-typing .ghead{display:none}
body.g-typing .gnav{display:none}
/* .gmain reserves the bottom-nav strip; with the nav gone that reserve is dead space under the
   compose bar (which applies env(safe-area-inset-bottom) itself). Horizontal padding is NOT
   touched — .gth2 bleeds to the edge with margin:-18px -18px 0 and would come unstuck. */
body.g-typing .gmain{padding-bottom:0}
/* The hero collapses to Discord's one slim line: back chevron, name, LIVE dot. The tile, the
   sub-line and the whole round-button action row stand down. The sub-line is kept ONLY when it is
   carrying "…is typing" — gSubPaint() already stamps .typing on it for exactly that, so this reuses
   a signal that exists rather than inventing a second one. */
body.g-typing .gth2{padding:calc(6px + env(safe-area-inset-top,0px)) 14px 6px;gap:8px}
body.g-typing .gth2-tile{display:none}
body.g-typing .gth2-t b{font-size:14.5px}
body.g-typing .gth2-t small:not(.typing){display:none}
body.g-typing .gth2-t.tap::after{display:none}
body.g-typing .gth2-acts{display:none}
/* The radio bar and its minimized FAB float OVER the chat (z-index 100000/100009) and would land on
   the text field. They stand down with the rest of the chrome and come straight back on blur —
   the radio SESSION is untouched, this is paint only. */
body.g-typing .rad-bar,body.g-typing .rad-fab{display:none}
/* Same treatment, same reason, for the "Notifications are off" bar: it is docked above the compose
   bar while a thread is open, and with the keyboard up that space belongs to the thread. It is a
   standing state, not an event — it comes straight back on blur and nothing is lost by hiding it. */
body.g-typing #g-notifbar{display:none}
/* gnav() already hides #g-sos on the Chat tab; this covers every other surface a composer can open
   on top of (the ticket overlay, Field mode) so nothing overlaps the send button. */
body.g-typing .g-sos{display:none}
/* Help-desk ticket overlay: same treatment, its own header. */
body.g-typing .tk-head{padding-top:calc(6px + env(safe-area-inset-top,0px));padding-bottom:6px}
body.g-typing .tk-tile,body.g-typing .tk-ht small,body.g-typing .tk-pill.onhead{display:none}
body.g-typing .tk-ht b{-webkit-line-clamp:1;font-size:14.5px}
/* Field mode (supervisor) runs the same messenger inside its own shell. */
body.g-typing #g-sup .shead{display:none}
body.g-typing #g-sup .snav{display:none}
/* supervisor.css reserves 56px under the composer for .snav; with the nav gone that is a dead band
   between the bar and the bottom of the screen. */
body.g-typing #g-sup #sv-chat.on .g-compose{padding-bottom:calc(8px + env(safe-area-inset-bottom,0px))}
body.g-typing #g-sup #sv-chat.on .gnewmsg{bottom:calc(78px + env(safe-area-inset-bottom,0px))}
/* With the keyboard up the reader is writing, not browsing history — and the pill would be sitting
   over the thread they have left to look at. It comes straight back on blur. */
body.g-typing .gstartpill{display:none}

/* ══ Message rich text — the officer/supervisor side of Discord-style markdown ══════════════════
   Same tag set as the console (public/msgmd.js is shared); the colours are the officer app's. Two
   things to know before touching it:
   — .gtx is white-space:pre-wrap and msgmd.js emits ZERO literal newlines outside a <pre>, so a
     quote or a list sits flush instead of gaining a phantom blank line above it;
   — .gtx is a SPAN, so these blocks are block boxes inside an inline parent. They are given an
     explicit display:block and no top margin on the first child, which is what keeps a message
     that opens with a code block from pushing off the top of its own bubble. */
.md-code{font-family:var(--font-mono);font-size:.87em;background:rgba(11,32,54,.08);border:1px solid rgba(11,32,54,.10);border-radius:4px;padding:.1em .34em;white-space:pre-wrap;word-break:break-word}
.md-codeblock{display:block;margin:5px 0;border-radius:8px;overflow:hidden;border:1px solid rgba(11,32,54,.12);background:rgba(11,32,54,.05)}
.md-lang{display:block;font-family:var(--font-mono);font-size:10px;font-weight:700;letter-spacing:.06em;text-transform:uppercase;color:var(--muted,#5f7288);padding:5px 10px 0}
.md-pre{display:block;margin:0;padding:8px 10px;font-family:var(--font-mono);font-size:12.5px;line-height:1.5;white-space:pre;overflow-x:auto;-webkit-overflow-scrolling:touch}
.md-pre code{font:inherit;background:none;border:0;padding:0;white-space:inherit}
.md-quote{display:block;margin:3px 0;padding:1px 0 1px 10px;border-left:3px solid var(--blue-light,#4aa3dd)}
.md-list{display:block;margin:3px 0;padding-left:21px}
.md-list li{display:list-item;margin:1px 0}
.md-codeblock:first-child,.md-quote:first-child,.md-list:first-child{margin-top:0}
/* Spoiler: blacked out and unselectable until it is tapped. The reveal is a delegated listener in
   msgmd.js — no inline handler is ever emitted into a message body. Padding keeps the tap target
   honest on a phone without changing the line box. */
.md-spoiler{background:rgba(11,32,54,.86);color:transparent;border-radius:4px;padding:1px 4px;cursor:pointer;-webkit-user-select:none;user-select:none;-webkit-tap-highlight-color:transparent;transition:background .13s ease,color .13s ease}
.md-spoiler>*{visibility:hidden}
.md-spoiler.on{background:rgba(11,32,54,.09);color:inherit;cursor:auto;-webkit-user-select:auto;user-select:auto}
.md-spoiler.on>*{visibility:visible}
.md-spoiler:focus-visible{outline:2px solid var(--blue-bright,#1a86d0);outline-offset:1px}
/* an emoji-only message renders big, the way Discord does */
.md-jumbo{font-size:2.5em;line-height:1.2;display:inline-block}
.md-link{color:var(--blue,#0a6bb0);text-decoration:underline;word-break:break-all;font-weight:600}
/* on the officer's own gradient bubble */
.gwrap.mine .md-code,.gwrap.mine .md-codeblock{background:rgba(255,255,255,.17);border-color:rgba(255,255,255,.25)}
.gwrap.mine .md-lang{color:rgba(255,255,255,.72)}
.gwrap.mine .md-quote{border-left-color:rgba(255,255,255,.6)}
.gwrap.mine .md-spoiler{background:rgba(4,14,26,.7)}
.gwrap.mine .md-spoiler.on{background:rgba(255,255,255,.2)}
.gwrap.mine .md-link,.gwrap.mine .glink{color:#dcecff}
[data-theme="dark"] .md-code{background:rgba(140,180,225,.14);border-color:rgba(140,180,225,.20)}
[data-theme="dark"] .md-codeblock{background:rgba(6,14,24,.55);border-color:rgba(140,180,225,.18)}
[data-theme="dark"] .md-quote{border-left-color:#4a7fb5}
[data-theme="dark"] .md-spoiler{background:rgba(4,12,22,.92)}
[data-theme="dark"] .md-spoiler.on{background:rgba(140,180,225,.16)}
[data-theme="dark"] .md-link{color:#7fc4f0}

/* ══ 22. SWIPE BETWEEN TABS — the affordance ════════════════════════════════════════════════════
   The gesture itself is gSwipeWire() in guard.js; everything here is paint. Two signals, both of
   which exist so the officer knows WHERE the swipe is going before they commit to it:
     · the page follows the finger by up to 30px (JS writes the inline transform),
     · and the tab it is heading for lights up in the bar.
   The peek is deliberately the SAME 26x3 underline and the same #0a6bb0 the active tab already
   wears (sections 9 and 12, light and dark both), at 45% — a preview of the state you are about to
   land in reads instantly; a new colour would just be a second thing to learn. */
/* While a drag is live the view is pushed sideways past the shell's edge. .gapp only clips itself
   on the Chat tab, so without this the document grows a horizontal scroll and the whole page rubber
   -bands sideways on Android. Transient — the class lives only for the length of the gesture. */
body.gsw-drag{overflow-x:hidden}
/* Settle only; the drag itself is direct manipulation and must never be transitioned, or the page
   lags the finger. The class is added at release and taken off 260ms later.
   ALL THREE SHELL SECTION CLASSES. .sview was already given .gsw-settle by the shipped JS and had
   no rule to match, so Field mode's pull SNAPPED back; .aview is the #g-adm command post, which is
   not on this branch yet and must not arrive with a missing rule. --t-med/--ease are the house
   motion tokens where the base defines them, and the literals are the same 200ms house curve. */
.gview.gsw-settle,.sview.gsw-settle,.aview.gsw-settle{transition:transform var(--t-med,200ms) var(--ease,cubic-bezier(.2,.7,.3,1))}
@media (prefers-reduced-motion:reduce){.gview.gsw-settle,.sview.gsw-settle,.aview.gsw-settle{transition:none}}
/* The ARRIVING screen, after a committed swipe or a system back: it slides the short way in, from
   the side the finger came from. Opt-IN motion (no-preference), CSS keyframes only — no rAF loop
   and nothing measured per frame, which is the whole reason this is a class and not a JS tween. */
@media (prefers-reduced-motion:no-preference){
  .gview.gsw-in-l,.sview.gsw-in-l,.aview.gsw-in-l{animation:gswinl var(--t-med,200ms) var(--ease,cubic-bezier(.2,.7,.3,1))}
  .gview.gsw-in-r,.sview.gsw-in-r,.aview.gsw-in-r{animation:gswinr var(--t-med,200ms) var(--ease,cubic-bezier(.2,.7,.3,1))}
}
@keyframes gswinl{from{transform:translateX(-16px);opacity:.55}to{transform:none;opacity:1}}
@keyframes gswinr{from{transform:translateX(16px);opacity:.55}to{transform:none;opacity:1}}
.gnav button.gnav-peek{color:#0a6bb0}
.gnav button.gnav-peek::before{content:"";position:absolute;top:0;left:50%;transform:translateX(-50%);
  width:26px;height:3px;border-radius:0 0 3px 3px;background:#0a6bb0;opacity:.45}
/* The centre Clock tab wears a raised puck instead of an underline (.gnav5 .gnav-mid.on::before is
   display:none for exactly that reason), so its peek has to be the puck growing, not a bar that
   would appear from nowhere behind it. */
.gnav5 .gnav-mid.gnav-peek::before{display:none}
.gnav5 .gnav-mid.gnav-peek .ic{transform:translateX(-50%) scale(1.06)}

/* The quote inside an OUTGOING bubble sits on the blue fill — inherit the bubble's light text
   instead of darkening it (console twin fixed the same day, owner: "hard to read when replying"). */
.gbubble.me .reply-quote{background:rgba(255,255,255,.16);border-left-color:rgba(255,255,255,.6);color:inherit}

/* ═══════════════════════════════════════════════════════════════════════════════════════════════
   16. MOTION — the officer app's half of the house motion system.
   THE LAW: unified-platform/DESIGN-SYSTEM.md §7. Tokens: :root at the top of this file.
   ═══════════════════════════════════════════════════════════════════════════════════════════════
   SHELL (settled on the integration branch claude/int-1323): guard.css CHANGED in this branch, so the officer
     shell needed a purge or every already-installed app would keep serving the old stylesheet from
     cache and none of this motion would ship. This branch deliberately bumped nothing itself — two
     branches racing that constant is a merge conflict with a silent-failure payload — and the
     integration branch performed the one bump, v106 → v107, carrying this pass and the generated
     -identity reskin together. The reasoning is on that constant's line in sw.js.

   Same three tokens, same values, as the console — a sheet must not open at a different speed
   depending on which screen you are holding.

   FOUR RULES, identical to styles.css → MOTION SYSTEM:
     1. inside @media (prefers-reduced-motion:no-preference);
     2. opacity / transform / box-shadow / background only — NEVER top/left/height. On this app
        that is not a style preference: these are mid-range Androids on Afghan networks, and a
        height transition is a layout pass every frame;
     3. CSS only, no JS animation loops;
     4. enter via @keyframes so the failure mode is "no animation", not "invisible control".

   THREE THINGS THIS SECTION DELIBERATELY DOES NOT TOUCH, and each has a reason:
     · THE TYPING COLLAPSE (section 21d, body.g-typing) — .gcx / .gnav / .gth2 heights and paddings
       are MEASURED, and the collapse is a display/padding swap by design. A transition on any of
       those is a layout animation on the one surface where the keyboard is already fighting for
       every pixel. Not tokenised, not animated, not touched.
     · TAB CONTENT (.gview.on) — no cross-fade. The app already has a purpose-built DIRECTIONAL
       swipe between tabs (.gsw-settle, tokenised above); a fade layered on top would fight it and
       would fire on every gnav() call as well, giving one action two competing motions.
     · --press-ms (.09s, section 14) stays sub-token. A tap has to feel landed before the finger
       lifts; --t-fast there reads as a laggy button. Different job, different number.
   ═══════════════════════════════════════════════════════════════════════════════════════════════ */
@keyframes gmo-fade{from{opacity:0}to{opacity:1}}

/* ⚠⚠ THE SPACING ON THE NEXT LINE IS LOAD-BEARING. DO NOT NORMALISE IT. ⚠⚠
   Same pin as styles.css, same reason, and it is the officer half of the same pair. This file also
   carries TWO motion blocks — this house one and the messenger one at the end — and
   msg-dynamism-ui.test.js separates them by ONE character: it looks for the at-rule written WITH A
   SPACE after the colon, so this opener must stay colon-tight and no comment above the messenger
   block may spell it the spaced way. Give this one a space and that test silently reads the wrong
   block and then complains that '.g-ment' lost its rule. motion-block-discrimination in that same
   file fails loudly if this drifts.

   ⚠ CORRECTION, claude/int-1323: the pin first said "exactly one spaced spelling per file". THIS
   FILE NOW HAS TWO and that is correct — the messenger block at section 16's end, and the search
   section's own block further down (claude/msg-parity), which must stay spaced because its suite
   matches on that form. So the rule is ORDER: the messenger block must remain the FIRST spaced one,
   because that is the one motionBlock() returns. Below it, another spaced block is harmless. */
@media (prefers-reduced-motion:no-preference){
  /* Bottom sheets: the panel rises (gsheetin, above) and the scrim behind it fades in with it,
     instead of the hard grey cut that used to land a frame before the sheet arrived.
     Inherited by every .g-sheet in the officer app — the attach/paperclip sheet, the house confirm
     sheet, the availability and note sheets, and the supervisor overlay's sheets (#g-sup shares
     this stylesheet's document, so it inherits the tokens and this rule for free). */
  /* ⚠ .g-sheet IS DECLARED TWICE IN THIS FILE, and that is a known, deliberate state as of the
     claude/int-1323 integration — not something to "clean up" without reading both. The messenger
     dynamism batch reached the same scrim independently (its base did not carry this section) and
     its MOTION LAYER at the very end of this file re-declares .g-sheet with its own gm-veil/.18s.
     Source order gives that one the win, and the two are the same effect — an opacity fade of the
     same backdrop — so the rendered result is identical either way. This declaration is kept
     because it is the house layer's only officer-app rule and it is what makes the token vocabulary
     real here; that one is kept because msg-dynamism-ui.test.js asserts .g-sheet has a rule inside
     ITS guarded block. Collapsing them to a single owner is a call for whoever ships this train. */
  .g-sheet{animation:gmo-fade var(--t-fast) var(--ease)}
}

@media (prefers-reduced-motion:reduce){
  /* gsheetin had no stillness guard of its own; the sheet still appears, it just does not travel. */
  .g-sheet-in{animation:none}
  .g-sheet{animation:none}
}

/* ══ LINK PREVIEW CARD — officer app ═══════════════════════════════════════════════════════════
   The phone twin of the console's .lpc. It sits BETWEEN .gbubble and .grxbar inside .gwrap (see
   gBubble / gApplyPreview), not inside the bubble — .gbubble is white-space:pre-wrap and its
   timestamp placement is measured against its own contents, so a block card in there would move
   the ticks. Out here it inherits .gwrap's alignment (flex-start for them, flex-end for mine) and
   touches none of that.

   ★ NO min-height and NO placeholder. The unfurl lands a second or two after the bubble is already
   painted, and most messages never get a card at all — a reserved box would be a grey hole under
   every link in the thread. Nothing is drawn until there is something to draw.
   ★ --tap does not apply: this is a LINK, not a control in a control row. It is a 300px-wide
   target that is already far larger than the 44px floor. */
.glpc{display:block;max-width:100%;width:100%;margin:5px 0 0;padding:8px 10px;border-radius:11px;
  border:1px solid var(--gb-them-line,#e2e9f1);border-left:3px solid var(--navy-2,#005080);
  background:var(--gb-them,#fff);color:var(--ink,#16222f);text-decoration:none;overflow:hidden;
  -webkit-tap-highlight-color:transparent}
.glpc:active{background:var(--gb-tint,#e7f0fa)}
.glpc-dom{display:block;font-size:11px;font-weight:700;color:var(--muted,#5f7288);
  white-space:nowrap;overflow:hidden;text-overflow:ellipsis;margin-bottom:2px}
.glpc-t{font-size:13.5px;font-weight:700;color:var(--navy-2,#005080);line-height:1.3;
  display:-webkit-box;-webkit-line-clamp:2;-webkit-box-orient:vertical;overflow:hidden}
.glpc-d{font-size:12.5px;color:var(--ink,#16222f);opacity:.8;line-height:1.4;margin-top:3px;
  display:-webkit-box;-webkit-line-clamp:3;-webkit-box-orient:vertical;overflow:hidden}
/* max-height in vh, not px: the same card must not eat half a 360x640 phone. */
.glpc-img{display:block;margin-top:7px;width:100%;max-height:26vh;object-fit:cover;border-radius:8px;
  background:var(--gb-them-line,#e2e9f1)}
/* On an own-message bubble the wrap is right-aligned but the CARD keeps the neutral surface: the
   bubble's blue gradient is on .gbubble only, so there is nothing here to fight and a second blue
   panel beside it would read as a second message. */
.gwrap.mine .glpc{border-left-color:var(--navy-2,#005080)}
[data-theme="dark"] .glpc{background:var(--gb-them,#1a2231);border-color:var(--gb-them-line,#26303f)}
[data-theme="dark"] .glpc-t{color:#7fc4f0}
[data-theme="dark"] .glpc:active{background:var(--gb-tint,#16273a)}

/* ══ MOTION LAYER — officer app (2026-08-11) ═══════════════════════════════════════════════════
   Same doctrine as the console's block in styles.css, and the same three rules: opt-in via
   prefers-reduced-motion:no-preference (this is NEW motion, so under `reduce` it simply does not
   exist), CSS only — never a JS animation loop, because this runs on the cheapest Android in the
   fleet — and transform/opacity only, never a layout property, because the thread's scroll
   position is load-bearing on a phone.

   ⚠ .g-sheet-in and .rxq ALREADY animate (gsheetin / rxqin, unconditionally). They are deliberately
   left alone: retro-fitting them into this block would change shipped behaviour under `reduce`,
   which is a separate decision from adding new motion, and they are referenced by name in the
   officer test suite. This block only adds what had none. */
@media (prefers-reduced-motion: no-preference){
  /* The @-suggestion panel opens in normal flow directly above the compose bar (never an overlay —
     see gMentPanel), so it grows downward out of that slot rather than fading in place. */
  .g-ment{animation:gm-rise .16s cubic-bezier(.16,.84,.44,1)}
  @keyframes gm-rise{from{opacity:0;transform:translateY(6px)}to{opacity:1;transform:none}}
  /* The sheet BACKDROP. .g-sheet-in (the panel) already slides; the scrim behind it appeared as a
     hard cut, which is what made the whole gesture read as a jump rather than a lift. */
  .g-sheet{animation:gm-veil .18s ease-out}
  @keyframes gm-veil{from{opacity:0}to{opacity:1}}
  /* A card that lands on a message already on screen. Opacity only — a card that MOVED would drag
     the thread's scroll with it, and gApplyPreview only chases the bottom when the reader was
     already there. */
  .glpc{animation:gm-fade .18s ease-out}
  @keyframes gm-fade{from{opacity:0}to{opacity:1}}
}

/* ══ 23. MESSAGE SEARCH + JUMP TO A DATE ════════════════════════════════════════════════════════
   Two surfaces, one rule: everything sits IN FLOW. The chip row, the suggestion list, the "search
   everything" bar and the results all follow the search field down the column rather than floating
   over it — the same reasoning the @mention picker is built on (.g-ment, section 11): an overlay
   next to a text field on a phone is how a control ends up eating the taps meant for the thing
   underneath it, and an in-flow panel cannot cover anything.

   Colour comes entirely from the tokens already in this file (--gb-tint / --gb-field /
   --gb-field-line / --gb-press / --gb-chip-ink / --line / --ink / --muted / --heading / --card), so
   dark mode follows without a parallel stylesheet. The two places that need a FILLED navy get the
   lifted #1d4a80 in dark, exactly like .gib-f.on — --navy itself is #032b49, darker than the dark
   surface, which is why it can never simply be reused there. */

/* --- search mode: which half of the inbox is on screen ----------------------------------------
   A class on the container, never [hidden]: .gib-filters carries display:flex from this stylesheet,
   which outranks the user agent [hidden]{display:none} rule, so the filter row would have stayed on
   screen over the results. Empty containers collapse on :empty for the same reason — no attribute
   to keep in sync with the markup. */
.ginbox.gms-on #gib-filters,.ginbox.gms-on #gib-list{display:none}
.gms-chips:empty,.gms-sugg:empty,.gms-bar:empty,.gms-results:empty{display:none}

/* --- committed filters, as chips ---------------------------------------------------------------
   The painted chip is 32px because a row of 44px pills would push the conversation list off a
   360x800 screen; the REMOVE control carries the house 44px target as an invisible ::after instead
   (section 5 idiom — that is why the chip is position:relative). */
.gms-chips{display:flex;flex-wrap:wrap;align-items:center;gap:6px;margin:0 0 8px}
.gms-chip{display:inline-flex;align-items:center;min-height:32px;max-width:100%;min-width:0;
  border:1px solid var(--gb-field-line,#d8e0ea);background:var(--gb-tint,#e7f0fa);border-radius:999px;
  font-size:12.5px;line-height:1.2;padding-left:10px;overflow:hidden}
.gms-chip-k{flex:none;font-weight:800;letter-spacing:.02em;color:var(--blue,#0a6bb0)}
.gms-chip-v{min-width:0;margin-left:5px;font-weight:600;color:var(--ink);white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
.gms-chip-x{flex:none;position:relative;border:0;background:none;color:var(--muted);
  font-size:19px;line-height:1;width:30px;height:30px;margin-left:2px;padding:0;cursor:pointer;
  display:inline-flex;align-items:center;justify-content:center;border-radius:50%;
  transition:background .15s cubic-bezier(.2,.7,.3,1),color .15s cubic-bezier(.2,.7,.3,1)}
.gms-chip-x::after{content:"";position:absolute;left:50%;top:50%;width:var(--tap,44px);height:var(--tap,44px);transform:translate(-50%,-50%)}
.gms-chip-x:active{background:var(--gb-press,rgba(10,30,60,.07));color:var(--ink)}
.gms-chip-clr{position:relative;border:0;background:none;color:var(--muted);font-family:inherit;
  font-size:12.5px;font-weight:700;padding:0 6px;min-height:32px;cursor:pointer;text-decoration:underline;text-underline-offset:3px}
.gms-chip-clr::after{content:"";position:absolute;left:0;right:0;top:50%;height:var(--tap,44px);transform:translateY(-50%)}
[data-theme="dark"] .gms-chip{background:var(--gb-tint,#16273a);border-color:var(--gb-field-line,#2b3648)}
[data-theme="dark"] .gms-chip-k{color:#7fc4f0}

/* --- inline autocomplete ------------------------------------------------------------------------
   Capped at 8 rows and ~40vh: this opens between the search field and the conversation list, and a
   list that fills the screen would hide the very thing the officer is deciding between. */
.gms-sugg{background:var(--gb-surf,#fff);border:1px solid var(--line);border-radius:14px;
  margin:0 0 8px;overflow-x:hidden;overflow-y:auto;max-height:40vh;overscroll-behavior:contain;
  box-shadow:0 6px 18px rgba(11,22,40,.09)}
.gms-sug-h{display:flex;align-items:baseline;gap:8px;padding:9px 13px 7px;font-size:10.5px;font-weight:800;
  letter-spacing:.08em;text-transform:uppercase;color:var(--gb-chip-ink,#5b6b80);border-bottom:1px solid var(--line)}
.gms-sug-h small{font-size:11px;font-weight:600;letter-spacing:0;text-transform:none;color:var(--muted)}
.gms-sug-i{display:flex;flex-direction:column;align-items:flex-start;justify-content:center;gap:1px;
  width:100%;min-height:var(--tap,44px);border:0;border-bottom:1px solid var(--line);background:none;
  font-family:inherit;text-align:left;padding:8px 13px;cursor:pointer;
  transition:background .15s cubic-bezier(.2,.7,.3,1)}
.gms-sug-i:last-child{border-bottom:0}
.gms-sug-i b{font-size:14px;font-weight:700;color:var(--ink);line-height:1.25;
  max-width:100%;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
.gms-sug-i small{font-size:11.5px;color:var(--muted);line-height:1.3;
  max-width:100%;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
.gms-sug-i.on,.gms-sug-i:active{background:var(--gb-press,rgba(10,30,60,.07))}
.gms-sug-none{padding:12px 13px;font-size:12.5px;line-height:1.5;color:var(--muted)}
[data-theme="dark"] .gms-sugg{background:var(--gb-surf,#141b28);box-shadow:0 6px 18px rgba(0,0,0,.4)}

/* --- "search all messages" ---------------------------------------------------------------------
   The discovery point. Without it the ONLY way into the server search is the keyboard search key,
   which several Android keyboards draw as a plain return arrow. */
.gms-bar{margin:0 0 9px}
.gms-go{display:flex;align-items:center;gap:9px;width:100%;min-height:var(--tap,44px);
  border:1px solid var(--gb-field-line,#d8e0ea);background:var(--gb-tint,#e7f0fa);color:var(--blue,#0a6bb0);
  border-radius:12px;padding:9px 13px;font-family:inherit;font-size:13.5px;font-weight:700;text-align:left;cursor:pointer;
  transition:background .15s cubic-bezier(.2,.7,.3,1),border-color .15s cubic-bezier(.2,.7,.3,1),opacity .15s cubic-bezier(.2,.7,.3,1)}
.gms-go svg{width:18px;height:18px;flex:none}
.gms-go span{min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
.gms-go:active{background:var(--gb-press,rgba(10,30,60,.07))}
.gms-go:disabled{opacity:.6;cursor:default;color:var(--muted);background:var(--gb-field,#f2f5f9)}
.gms-hint{margin:6px 2px 0;font-size:11.5px;line-height:1.55;color:var(--muted)}
.gms-hint b{font-family:var(--font-mono,ui-monospace,monospace);font-weight:700;color:var(--ink);font-size:11px}
[data-theme="dark"] .gms-go{background:var(--gb-tint,#16273a);border-color:var(--gb-field-line,#2b3648);color:#7fc4f0}
[data-theme="dark"] .gms-go:disabled{background:var(--gb-field,#1e2735);color:var(--muted)}

/* --- results ------------------------------------------------------------------------------------ */
.gms-head{display:flex;align-items:center;gap:10px;margin:0 0 10px;padding-bottom:9px;border-bottom:1px solid var(--line)}
.gms-h-t{flex:1;min-width:0;font-family:var(--font-display,inherit);font-size:15px;font-weight:800;color:var(--heading,#0b2036);
  overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
.gms-x{flex:none;display:inline-flex;align-items:center;gap:6px;min-height:var(--tap,44px);
  border:1px solid var(--line);background:var(--card,#fff);color:var(--ink);border-radius:12px;
  padding:0 12px;font-family:inherit;font-size:13px;font-weight:700;cursor:pointer;
  transition:background .15s cubic-bezier(.2,.7,.3,1)}
.gms-x svg,.gms-x .ic{width:15px;height:15px;flex:none}
.gms-x:active{background:var(--gb-press,rgba(10,30,60,.07))}

.gms-grp{margin:0 0 14px}
.gms-g{display:flex;align-items:center;gap:8px;margin:0 0 5px;padding:0 2px;min-width:0}
.gms-g .gib-av{width:26px;height:26px;font-size:11px;border-radius:8px}
.gms-g .gib-av.glyph .ic{width:14px;height:14px}
.gms-g-n{min-width:0;font-size:12.5px;font-weight:800;color:var(--heading,#0b2036);
  overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
.gms-g-tag{flex:none;border:1px solid var(--line);border-radius:999px;padding:1px 7px;
  font-size:9.5px;font-weight:800;letter-spacing:.05em;text-transform:uppercase;color:var(--muted)}
.gms-g-c{flex:none;margin-left:auto;font-size:11px;font-weight:800;color:var(--gb-chip-ink,#5b6b80);
  background:var(--gb-field,#f2f5f9);border-radius:999px;padding:2px 8px;font-variant-numeric:tabular-nums}
[data-theme="dark"] .gms-g-c{background:var(--gb-field,#1e2735)}

/* One hit. The thumbnail is 52px on the RIGHT so every card text starts on the same left edge — a
   photo-led left rail would make the column of names ragged, and the name is what is scanned. */
.gms-hit{position:relative;display:flex;align-items:flex-start;gap:10px;width:100%;min-width:0;
  border:1px solid var(--line);background:var(--card,#fff);border-radius:14px;
  padding:10px 11px;margin:0 0 7px;font-family:inherit;text-align:left;cursor:pointer;
  -webkit-tap-highlight-color:transparent;
  transition:background .15s cubic-bezier(.2,.7,.3,1),border-color .15s cubic-bezier(.2,.7,.3,1),transform .15s cubic-bezier(.2,.7,.3,1)}
.gms-hit:active{background:var(--gb-press,rgba(10,30,60,.07));transform:scale(.99)}
.gms-hit-m{flex:1;min-width:0;display:flex;flex-direction:column;gap:3px}
.gms-l1{display:flex;align-items:baseline;gap:6px;min-width:0}
.gms-who{min-width:0;font-size:13px;font-weight:800;color:var(--heading,#0b2036);
  overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
.gms-pin{flex:none;color:var(--muted);opacity:.8}
.gms-pin .ic,.gms-pin svg{width:12px;height:12px;vertical-align:-1px}
.gms-when{flex:none;margin-left:auto;font-size:11px;color:var(--muted);font-variant-numeric:tabular-nums;white-space:nowrap}
/* Three lines, then ellipsis: the snippet is already centred on the match server-side, so a fourth
   line adds height without adding the answer. */
.gms-tx{font-size:13px;line-height:1.45;color:var(--ink);word-break:break-word;
  display:-webkit-box;-webkit-line-clamp:3;line-clamp:3;-webkit-box-orient:vertical;overflow:hidden}
.gms-kic{margin-right:4px;opacity:.75;color:var(--muted)}
.gms-kic .ic,.gms-kic svg{width:13px;height:13px;vertical-align:-2px}
.gms-att{color:var(--muted);font-style:italic}
.gms-mk{background:rgba(233,163,25,.30);color:inherit;border-radius:3px;padding:0 1px}
[data-theme="dark"] .gms-mk{background:rgba(233,163,25,.34)}
.gms-th{flex:none;position:relative;width:52px;height:52px;border-radius:10px;overflow:hidden;background:var(--gb-field,#f2f5f9)}
.gms-th img{width:100%;height:100%;object-fit:cover;display:block}
/* A video hit shows its poster frame; the play triangle says it is a clip without a second row of text. */
.gms-th.vid::after{content:"";position:absolute;left:50%;top:50%;transform:translate(-50%,-50%);
  border-style:solid;border-width:7px 0 7px 11px;border-color:transparent transparent transparent #fff;
  filter:drop-shadow(0 1px 2px rgba(0,0,0,.6))}

.gms-page{display:flex;align-items:center;gap:10px;margin:4px 0 2px}
.gms-pg{flex:none;min-height:var(--tap,44px);border:1px solid var(--line);background:var(--card,#fff);
  color:var(--ink);border-radius:12px;padding:0 14px;font-family:inherit;font-size:13px;font-weight:700;cursor:pointer;
  transition:background .15s cubic-bezier(.2,.7,.3,1),opacity .15s cubic-bezier(.2,.7,.3,1)}
.gms-pg:active{background:var(--gb-press,rgba(10,30,60,.07))}
.gms-pg:disabled{opacity:.4;cursor:default}
.gms-pg-n{flex:1;text-align:center;font-size:12px;color:var(--muted);font-variant-numeric:tabular-nums}
.gms-note{display:block;font-size:11.5px;line-height:1.55;color:var(--muted)}
.gms-note.foot{margin:10px 2px 4px;text-align:center;opacity:.85}
.gms-tips{display:block;font-size:11.5px;line-height:1.7;color:var(--muted);margin-top:6px}
.gms-tips b{font-family:var(--font-mono,ui-monospace,monospace);font-weight:700;color:var(--ink);font-size:11px}
/* .gsk carries the shimmer AND its own prefers-reduced-motion opt-out (section 7) — reused here so
   the loading state reads as the same app, not a second skeleton with a second timing. */
.gms-skel{display:flex;flex-direction:column;gap:8px}
.gms-skel i{display:block;height:62px;border-radius:14px}

/* --- jump to a date ----------------------------------------------------------------------------
   The field is the phone OWN <input type=date>: one tap to the OS wheel, already skinned by the
   sheet form rules (section 1) and already dark-aware through :root{color-scheme} (section 2), so
   there is no calendar to build, translate or keep in step with the company timezone.
   The failure line stays INSIDE the sheet on purpose: a retention miss is the one answer the officer
   has to act on ("try a more recent day"), and a toast is gone in 1.8s. */
.gjd-sub{font-size:12.5px;line-height:1.5;color:var(--muted);margin:-4px 0 12px}
.gjd-quick{display:flex;flex-wrap:wrap;gap:8px;margin:0 0 12px}
.gjd-q{flex:1 1 auto;min-width:96px;min-height:var(--tap,44px);border:1px solid var(--line);
  background:var(--card,#fff);color:var(--ink);border-radius:12px;padding:0 12px;
  font-family:inherit;font-size:13px;font-weight:700;cursor:pointer;
  transition:background .15s cubic-bezier(.2,.7,.3,1),border-color .15s cubic-bezier(.2,.7,.3,1),color .15s cubic-bezier(.2,.7,.3,1)}
.gjd-q:active{background:var(--gb-press,rgba(10,30,60,.07))}
.gjd-q.on{background:var(--navy);border-color:var(--navy);color:#fff}
[data-theme="dark"] .gjd-q{background:#0e1520;border-color:#22394d;color:#e8eef5}
[data-theme="dark"] .gjd-q.on{background:#1d4a80;border-color:#1d4a80;color:#fff}
.gjd-msg{border:1px solid var(--amber,#e08600);background:rgba(224,134,0,.10);color:var(--ink);
  border-radius:12px;padding:10px 12px;margin:0 0 12px;font-size:12.5px;line-height:1.55;
  animation:gjdmsg .2s cubic-bezier(.2,.7,.3,1)}
@keyframes gjdmsg{from{opacity:0;transform:translateY(-4px)}to{opacity:1;transform:none}}
@media (prefers-reduced-motion:reduce){
  .gjd-msg{animation:none}
  .gms-hit:active{transform:none}
  .gms-chip-x,.gms-sug-i,.gms-go,.gms-x,.gms-hit,.gms-pg,.gjd-q{transition:none}
}

/* ══ 24. MESSENGER PARITY — double-tap react · grouped action sheet · header presence ·
      back-badge · the Filters sheet ══════════════════════════════════════════════════════════════
   Five surfaces the phone was missing next to the console, all built out of tokens already in this
   file (--gb-tint / --gb-field / --gb-field-line / --gb-press / --line / --ink / --muted /
   --heading / --navy / --blue / --green), so both themes follow with no parallel stylesheet, and
   every target clears the --tap 44px floor.
   MOTION: transform + opacity only, and every animation is inside the
   prefers-reduced-motion:no-preference block at the end of this section. Nothing here animates a
   layout property — these are mid-range Androids and the thread's scroll position is load-bearing. */

/* --- 24a. Double-tap to react: the glyph that confirms it landed -------------------------------
   Parented to #g-thread (position:relative, section 15) so it rides the thread's own scroll, and
   pointer-events:none so the 760ms it is alive cannot swallow the next tap. Under `reduce` it
   simply appears and is removed — still a confirmation, with no flight. */
.gdbl-pop{position:absolute;z-index:5;transform:translate(-50%,-50%);pointer-events:none;
  font-size:34px;line-height:1;opacity:0;filter:drop-shadow(0 3px 8px rgba(2,20,38,.35))}

/* --- 24b. Message action sheet: Discord's grouped blocks --------------------------------------
   The rows are still .g-atr (42px disc, dark theme, --tap floor all inherited). What is new is the
   BLOCK: related actions share a rounded surface, and the gap between blocks is what makes a nine-
   row sheet scannable on a 360px screen. The divider rule is re-scoped to inside a block, so the
   hairline separates siblings and never runs across the gap between two groups. */
.g-msheet-g{background:var(--gb-field,#f2f5f9);border:1px solid var(--gb-field-line,#dde5ee);
  border-radius:14px;padding:0 12px;margin-top:8px;overflow:hidden}
.g-msheet-g .g-atr{padding:11px 0}
.g-msheet-g .g-atr+.g-atr{border-top:1px solid var(--gb-field-line,#dde5ee)}
.g-msheet-g .g-atr:active{background:var(--gb-press)}
[data-theme="dark"] .g-msheet-g{background:var(--gb-field,#1e2735);border-color:var(--gb-field-line,#2b3648)}
[data-theme="dark"] .g-msheet-g .g-atr+.g-atr{border-top-color:var(--gb-field-line,#2b3648)}

/* --- 24c. Thread header: "N online" ------------------------------------------------------------
   The second line became a ROW so the pill can sit beside the sub-text instead of under it — the
   header is 34px of vertical budget and a third line would have cost the thread a message. #gth2-sub
   keeps min-width:0 + ellipsis so a long channel description yields to the pill rather than pushing
   it off the edge. The header is always the dark navy gradient in BOTH themes (.gth2), so these
   colours are fixed against that surface and correctly have no [data-theme="dark"] twin. */
.gth2-subrow{display:flex;align-items:center;gap:7px;min-width:0;margin-top:1px}
.gth2-subrow #gth2-sub{margin-top:0;min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
.gth2-on{flex:none;display:inline-flex;align-items:center;gap:5px;font-size:10px;font-weight:800;
  letter-spacing:.03em;color:#7fe0a9;background:rgba(15,157,88,.18);border:1px solid rgba(63,191,122,.45);
  border-radius:20px;padding:2px 8px;white-space:nowrap}
.gth2-on i{width:6px;height:6px;border-radius:50%;background:#3fbf7a;flex:none}
/* Nobody online is an ANSWER, not an error — it goes quiet grey rather than disappearing, so the
   officer can tell "no one is here" from "we do not know". */
.gth2-on.none{color:#9dbdd6;background:rgba(255,255,255,.09);border-color:rgba(255,255,255,.16)}
.gth2-on.none i{background:#7f93a8}

/* --- 24d. The back control's unread badge ------------------------------------------------------
   Rides ON the back chevron rather than beside it: .gth2-back is already a 44px target and the
   badge must not widen the header or move the channel tile. Same red as every other unread count. */
.gth2-back{position:relative}
/* To the RIGHT of the chevron, inside the button's own 44px box: the header gradient is tenant-
   brandable, so the badge deliberately carries no ring in the header's colour — it would be the one
   thing in this file that breaks when a tenant changes their navy. */
.gth2-bk-n{position:absolute;top:3px;right:1px;min-width:17px;height:17px;padding:0 4px;
  border-radius:9px;background:var(--red,#e11d2a);color:#fff;font-size:10px;font-weight:800;
  line-height:17px;text-align:center;pointer-events:none}

/* --- 24e. The Filters button on the inbox ------------------------------------------------------ */
.gib-filt{position:relative}
.gib-filt.on{background:var(--gb-tint,#e7f0fa);border-color:var(--blue,#0a6bb0);color:var(--blue,#0a6bb0)}
[data-theme="dark"] .gib-filt.on{background:#1d4a80;border-color:#1d4a80;color:#fff}
.gib-filt svg{width:19px;height:19px}
.gib-filt-n{position:absolute;top:-5px;right:-5px;min-width:17px;height:17px;padding:0 4px;
  border-radius:9px;background:var(--blue,#0a6bb0);color:#fff;font-size:10px;font-weight:800;
  line-height:17px;text-align:center;pointer-events:none}
[data-theme="dark"] .gib-filt-n{background:#1d4a80}

/* --- 24f. The Filters sheet -------------------------------------------------------------------
   .g-sheet chassis (backdrop, 92vh cap, safe-area padding, the gsheetin slide) — only the contents
   below are new. Every tappable thing in here is a .gmf-chip or a .seg button, and both clear the
   --tap 44px floor without needing a min-height on the sheet itself. */
/* --- 24g. CATEGORIES — collapsed by default (guard.js §gms-groups) ------------------------------
   The sheet used to print six open sections and every channel chip in one column: 870px of content
   against the 92vh (747px) cap. Now it is a quick row plus collapsed headers, so the whole panel is
   under the fold on a 375x812 phone and Search is reachable without scrolling a form. */
.gmf-quick{display:flex;flex-wrap:wrap;gap:7px;padding:12px 0 14px;border-bottom:1px solid var(--line)}
.gmf .gmf-sec{border-bottom:1px solid var(--line)}
/* The whole header row is the target, not the caret. */
.gmf .gmf-sec-b{display:flex;align-items:center;gap:10px;width:100%;min-height:var(--tap,44px);
  padding:4px 0;border:0;background:none;font-family:inherit;text-align:left;cursor:pointer;color:var(--ink)}
.gmf .gmf-h{font-size:11.5px;font-weight:800;letter-spacing:.05em;text-transform:uppercase;
  color:var(--muted);display:flex;align-items:baseline;gap:8px;flex:0 0 auto}
.gmf .gmf-h small{font-size:11px;font-weight:600;letter-spacing:0;text-transform:none;opacity:.85}
/* Badge + summary: what a CLOSED category is holding, so no filter is ever switched on and invisible. */
.gmf-hd{display:flex;align-items:center;gap:7px;flex:1 1 auto;min-width:0;justify-content:flex-end}
.gmf-n{min-width:19px;height:19px;padding:0 5px;border-radius:10px;background:var(--blue,#0a6bb0);
  color:#fff;font-size:11px;font-weight:800;line-height:19px;text-align:center;flex:0 0 auto}
[data-theme="dark"] .gmf-n{background:#1d4a80}
.gmf-sum{font-size:12.5px;font-weight:600;color:var(--muted);white-space:nowrap;overflow:hidden;
  text-overflow:ellipsis;min-width:0}
.gmf-cv{width:18px;height:18px;flex:0 0 auto;color:var(--muted)}
.gmf-sec.open>.gmf-sec-b .gmf-cv{transform:rotate(180deg)}
/* Closed is display:none, not height:0 — the sheet must not reserve space for what it is hiding, and
   a collapsed body must stay out of the tab order. */
.gmf-sec:not(.open)>.gmf-body{display:none}
.gmf-sec>.gmf-body{padding:2px 0 14px}
.gmf-row{display:flex;flex-wrap:wrap;gap:7px}
.gmf-none{font-size:13px;color:var(--muted)}
/* --- The type-ahead inside a category (FROM / MENTIONS / IN) ----------------------------------- */
.gmf-q{width:100%;min-height:var(--tap,44px);border:1px solid var(--gb-field-line,#dde5ee);
  background:var(--gb-field,#f2f5f9);color:var(--ink);border-radius:12px;padding:0 14px;
  font-family:inherit;font-size:15px;margin-bottom:9px}
[data-theme="dark"] .gmf-q{background:var(--gb-field,#1e2735);border-color:var(--gb-field-line,#2b3648);color:#e8eef5}
.gmf-pl{display:flex;flex-direction:column;gap:2px;margin-top:2px}
.gmf-pi{display:flex;align-items:center;gap:8px;width:100%;min-height:var(--tap,44px);
  border:0;background:none;border-radius:10px;padding:6px 10px;font-family:inherit;text-align:left;
  cursor:pointer;color:var(--ink)}
.gmf-pi b{font-size:14.5px;font-weight:650;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
.gmf-pi small{font-size:12px;color:var(--muted);white-space:nowrap}
.gmf-pi:active{background:var(--gb-press)}
[data-theme="dark"] .gmf-pi{color:#e8eef5}
[data-theme="dark"] .gmf-pi:active{background:#1e2735}
.gmf-chosen{margin-bottom:9px}
.gmf-x{margin-left:7px;font-size:16px;line-height:1;opacity:.85}
/* A filter chip. 44px tall on purpose: these are the sheet's primary targets and a 32px chip in a
   wrapped grid is the classic phone mis-tap. */
.gmf-chip{min-height:var(--tap,44px);max-width:100%;border:1px solid var(--gb-field-line,#dde5ee);
  background:var(--gb-field,#f2f5f9);color:var(--ink);border-radius:22px;padding:0 15px;
  font-family:inherit;font-size:13.5px;font-weight:600;cursor:pointer;
  white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
.gmf-chip.on{background:var(--navy);border-color:var(--navy);color:#fff}
.gmf-chip:active{background:var(--gb-press)}
.gmf-chip.on:active{background:var(--navy);opacity:.85}
[data-theme="dark"] .gmf-chip{background:var(--gb-field,#1e2735);border-color:var(--gb-field-line,#2b3648);color:#e8eef5}
/* --navy is #032b49 — DARKER than the dark surface — so a filled chip has to use the lifted navy
   there, exactly like .gib-f.on and the jump-to-date quick picks already do. */
[data-theme="dark"] .gmf-chip.on{background:#1d4a80;border-color:#1d4a80;color:#fff}
.gmf-date{display:flex;align-items:center;gap:8px;margin-bottom:8px}
.gmf-date input[type=date]{flex:1;min-width:0}
.gmf .seg.gmf-seg button{min-height:var(--tap,44px)}
/* The live chip strip: the same filters the search field shows, so the sheet and the box can never
   look like they disagree. It reuses .gms-chip wholesale — same markup, same remove target. */
.gmf-live{display:flex;flex-wrap:wrap;gap:6px;padding:10px;border-radius:12px;
  background:var(--gb-tint,#e7f0fa);margin-bottom:2px}
[data-theme="dark"] .gmf-live{background:#16273a}
/* STICKY, and measured: at 375x812 the sheet's content is 864px against a 92vh (747px) cap, so
   Search would sit below the fold and the officer would have to scroll a form to run it. Full-bleed
   negative margins so the bar's background covers the rows sliding under it; the sheet's own bottom
   padding (16px + the safe area) stays below it, which is why bottom:0 lands correctly on a phone
   with a home indicator. */
.gmf-foot{display:flex;align-items:center;justify-content:space-between;gap:10px;
  position:sticky;bottom:0;z-index:2;background:#fff;
  margin:18px -16px 0;padding:14px 16px 6px;border-top:1px solid var(--line)}
[data-theme="dark"] .gmf-foot{background:#141b28}
.gmf-clr{border:0;background:none;color:var(--muted);font-family:inherit;font-size:13.5px;
  font-weight:700;min-height:var(--tap,44px);padding:0 4px;cursor:pointer}
.gmf-clr[disabled]{opacity:.4}
.gmf-foot .gbtn{min-height:var(--tap,44px);padding:0 26px}
.gmf-foot .gbtn[disabled]{opacity:.45}

@media (prefers-reduced-motion: no-preference){
  /* The reaction landing: up and away, transform + opacity only. */
  .gdbl-pop{animation:gdblpop .62s cubic-bezier(.2,.7,.3,1) forwards}
  @keyframes gdblpop{
    0%{opacity:0;transform:translate(-50%,-50%) scale(.4)}
    28%{opacity:1;transform:translate(-50%,-64%) scale(1.18)}
    55%{opacity:1;transform:translate(-50%,-84%) scale(1)}
    100%{opacity:0;transform:translate(-50%,-124%) scale(.92)}
  }
  .gmf-chip{transition:background var(--t-fast) var(--ease),border-color var(--t-fast) var(--ease),color var(--t-fast) var(--ease)}
  .gib-filt{transition:background var(--t-fast) var(--ease),border-color var(--t-fast) var(--ease),color var(--t-fast) var(--ease)}
  /* Opening a category. The caret turns and the body arrives — transform and opacity only, never the
     height: an animated collapse is a layout animation, which this section does not do. */
  .gmf-cv{transition:transform var(--t-fast) var(--ease)}
  .gmf-pi{transition:background var(--t-fast) var(--ease)}
  .gmf-sec.open>.gmf-body{animation:gmfsecin var(--t-fast) var(--ease)}
  @keyframes gmfsecin{from{opacity:0;transform:translateY(-4px)}to{opacity:1;transform:none}}
  .gth2-on{transition:opacity var(--t-med) var(--ease)}
  .g-msheet-g .g-atr{transition:background var(--t-fast) var(--ease)}
}

/* ══ 23. THE TIME WIDGET — .gtmi (guard.js gTmInput / gWireTm) ══════════════════════════════════
   OWNER RULE: no native <input type="time"> anywhere a human picks a time, because that control
   renders from the DEVICE locale — a 24-hour spinner on one officer's phone, a 12-hour wheel on the
   next. This is a plain type="text" box, so it already inherits the house control skin from the
   HOUSE CONTROL LAYER above (`.gapp input:not(...)`, 46px / 12px 14px / 16px, both themes) wherever
   it is mounted; everything here is the SMALL delta on top of that skin. Deliberately no width, no
   background and no border in the base rule: the widget appears inside .gfield, inside .grow2, and
   inside the gPrompt sheet as `.gtmi.gcf-in`, and each of those already owns its own box.

   Why this block sits at the very END of the file: `.gtmi.bad` (0,2,0) has to outrank `.gcf-in:focus`
   (0,2,0, line ~2159). Equal specificity means the later rule wins, so a field the officer is still
   focused on keeps its red edge instead of flashing back to blue while it is still wrong. */
/* max(), not a bare var(--tap): 46px is the height every other control in the house skin (and .gcf-in
   in the sheet) already computes to, and a bare 44 here would win on source order and make the time
   field 2px shorter than the field beside it. The token still names the FLOOR, so if the tap bar ever
   moves above 46 this follows it. */
.gtmi{min-height:max(46px,var(--tap,44px));font-variant-numeric:tabular-nums}
/* The invalid state is a BORDER, not a background: an officer types this one-handed on a bright post
   and a tinted fill reads as "disabled" at arm's length. Paired with setCustomValidity() in
   gWireTm(), so the reason is spoken as well as painted. */
.gtmi.bad,.gtmi.bad:focus{border-color:#e11d2a;box-shadow:0 0 0 3px rgba(225,29,42,.20)}
[data-theme="dark"] .gtmi.bad,[data-theme="dark"] .gtmi.bad:focus{border-color:#f0707c;box-shadow:0 0 0 3px rgba(240,112,124,.24)}
.gtmi:disabled{opacity:.55}
/* The shorthand is only worth having if the officer knows it exists — one line, under the field(s),
   in the same voice as .ff-help. */
.gtm-hint{font-size:11.5px;color:var(--muted);line-height:1.45;margin:-6px 0 2px}
@media (prefers-reduced-motion:reduce){.gtmi{transition:none}}
/* ---- CASH ADVANCE on the officer to-do ----------------------------------------------------------
   The recipient's side: how much, where it went, and the receipt once the payer files it. The amount
   and the handle are tap-to-copy for the same reason as the console — a pasted $cashtag is a
   correct $cashtag. Sized for a 44px tap target on a phone. */
.td-row.adv{border-left:3px solid #2f9e5c}
.td-adv{display:flex;flex-wrap:wrap;gap:6px;margin-top:6px}
/* ⚠ EVERY RULE HERE IS NAMED TWICE, FOR button AND FOR `.td-adv>span`. The officer's advance chips
   are NOT buttons any more (owner rule 2026-08-14, "the officer should not be able to have copied
   cash advance values" — gAdvCard's opts.copy): the amount and the $cashtag still render, with the
   same text and the same geometry, as plain spans carrying the SAME classes. A rule written only
   against `button` would leave the officer — the reader this card exists for — looking at unstyled
   text where the money used to be, which is the exact failure the specificity note below describes.
   The child combinator matters: `.td-adv-h` wraps an inner <span> for the ellipsis (see below), and
   a bare `.td-adv span` would style that inner one too. */
.td-adv button,.td-adv>span{min-height:var(--tap,44px);display:inline-flex;align-items:center;border-radius:9px;border:1.5px solid #cfe3f5;background:#fff;font-family:inherit;font-weight:700;padding:0 12px}
.td-adv button{cursor:pointer}
/* MEASURED at 375x812: `.td-adv button` above is (0,1,1) and beat the plain `.td-adv-amt` class on
   COLOUR, so the amount rendered in body text and lost the one signal that says "this is money" —
   the background only survived because it carried !important. Every rule below is written at
   button-level specificity instead, so the two themes are decided by the cascade rather than by a
   race between !important flags. (`.td-adv>span.td-adv-amt` is (0,2,1) — a shade higher again, and
   it only ever matches the chips this file's own gAdvCard emits, so nothing else can be caught.) */
.td-adv button.td-adv-amt,.td-adv>span.td-adv-amt{font-size:16px;font-weight:800;color:#0d7a45;border-color:#bfe0cb;background:#f2faf5;font-variant-numeric:tabular-nums}
.td-adv button.td-adv-h,.td-adv>span.td-adv-h{font-size:12.5px;color:#16222f;max-width:100%;overflow:hidden}
.td-adv-h span{overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
.td-adv button.td-adv-r{font-size:12.5px;color:#0a6bb0}
[data-theme="dark"] .td-adv button,[data-theme="dark"] .td-adv>span{background:#1a2231;border-color:#26424f;color:#e6eaf2}
[data-theme="dark"] .td-adv button.td-adv-amt,[data-theme="dark"] .td-adv>span.td-adv-amt{background:#16301f;border-color:#2a5238;color:#8fd6a8}
[data-theme="dark"] .td-adv button.td-adv-r{color:#7fbdf0}
/* ── GAS: THE SENTENCE THAT STOPS IT READING AS A DEBT ──────────────────────────────────────────
   An officer who is told money came to them assumes, correctly on every other money surface in this
   app, that they will be paying it back. Gas is the one case where that is wrong, so the card says
   so in words rather than relying on a colour nobody has been taught.
   ⚠ IT IS A <span> INSIDE .td-adv, so it inherits the (0,1,1) `.td-adv>span` rule above — including
   min-height:44px, which would make a full-width sentence a 44px-tall tap target that is not
   tappable. Named at (0,2,1) to unset that, exactly the way `.td-adv>span.td-adv-amt` overrides the
   same base rule (see the specificity note beside it). */
.td-adv>span.td-gasfree{min-height:0;flex:1 0 100%;justify-content:flex-start;padding:6px 10px;font-size:12px;font-weight:700;color:#12626f;background:#e6f3f6;border-color:#b3dbe4;border-radius:9px}
/* The money chip on a gas card is teal, not the advance's green: still clearly money, unmistakably
   not the same thing as money that is owed back. */
.td-adv.gas button.td-adv-amt,.td-adv.gas>span.td-adv-amt{color:#12626f;background:#e6f3f6;border-color:#b3dbe4}
[data-theme="dark"] .td-adv>span.td-gasfree{color:#7fd0de;background:#0f2a30;border-color:#1f4d57}
[data-theme="dark"] .td-adv.gas button.td-adv-amt,[data-theme="dark"] .td-adv.gas>span.td-adv-amt{color:#7fd0de;background:#0f2a30;border-color:#1f4d57}

/* ══ 24. THE FILE CARD — .gatt-file (guard.js gFileCardHtml) ═══════════════════════════════════
   ⚠ WHY THIS BLOCK IS AT THE END OF THE FILE. `.gatt-file` is declared THREE times above — line
   ~338 (the original underlined link), ~595 (`.gatt-file .ic` sizing, shared with .po-file) and
   ~1536 (the 2026 chat skin's inline-flex pill). All three are single-class rules, so they beat
   nothing and are beaten by nothing except document order. The card is a two-target ROW, not a
   pill, so every one of those has to be re-stated here rather than nudged in place — and moving
   this block up the file silently gives the row back to the pill.

   Geometry: an icon square, a text column that is the only thing allowed to shrink, and a download
   target on the right. `min-width:0` on .gaf-main and .gaf-tx is what makes the ellipsis work at
   all — a flex child refuses to shrink under its content without it, and the name would push the
   download button out of the bubble instead of truncating. MEASURED at 320px: the bubble's content
   box is 258px, of which the icon takes 38 and the download 44, leaving 168 for the name. */
.gatt-file{display:flex;align-items:stretch;gap:2px;width:100%;max-width:100%;margin:3px 0;
  background:var(--gb-press);border-radius:11px;overflow:hidden;text-decoration:none}
.gatt-file .gaf-main{flex:1;min-width:0;display:flex;align-items:center;gap:10px;
  min-height:var(--tap,44px);padding:8px 4px 8px 10px;color:inherit;text-decoration:none}
/* The glyph square. .gatt-file .ic (line ~595) sizes every icon in here to 15px with a 3px right
   margin for the old inline link; (0,3,0) beats that (0,2,0) so the card's own icon is 20px and
   owns its spacing through the flex gap instead. */
.gatt-file .gaf-ic{flex:none;width:38px;height:38px;border-radius:10px;display:flex;
  align-items:center;justify-content:center;background:var(--gb-tint);color:var(--blue,#0a6bb0)}
.gatt-file .gaf-ic .ic{width:20px;height:20px;margin:0;vertical-align:0}
.gatt-file .gaf-tx{flex:1;min-width:0;display:flex;flex-direction:column;justify-content:center;gap:2px}
/* ONE line with an ellipsis, never a wrap: a 90-character filename would otherwise turn a card into
   a paragraph and push the whole thread about. The extension is repeated in .gaf-m below precisely
   so the one thing the truncation eats is never the thing that says what the file IS. */
.gatt-file .gaf-n{font-size:13.5px;font-weight:700;line-height:1.3;color:inherit;
  white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
.gatt-file .gaf-m{font-size:11.5px;line-height:1.25;opacity:.72;white-space:nowrap;
  overflow:hidden;text-overflow:ellipsis;font-variant-numeric:tabular-nums}
/* A full --tap square, and its own control: the officer who wants the file rather than the preview
   must not have to find a menu. Separated from the card body by a hairline so it reads as a second
   target instead of decoration on the first. */
.gatt-file .gaf-dl{flex:none;width:var(--tap,44px);min-height:var(--tap,44px);
  display:flex;align-items:center;justify-content:center;color:inherit;text-decoration:none;
  border-left:1px solid var(--gb-rule);opacity:.75}
.gatt-file .gaf-dl svg{width:19px;height:19px}
.gatt-file .gaf-main:active,.gatt-file .gaf-dl:active{background:var(--gb-press)}
.gatt-file .gaf-dl:active{opacity:1}
/* On MY bubble the fill is the navy gradient, so the card's own surfaces have to be drawn out of
   white at low alpha — --gb-press/--gb-tint are tuned against the page, not against that gradient,
   and both vanish on it. Same reason `.gbubble.mine .gatt-file{color:#fff}` (line ~339) exists. */
.gwrap.mine .gatt-file{background:rgba(255,255,255,.16);color:#fff}
.gwrap.mine .gatt-file .gaf-ic{background:rgba(255,255,255,.20);color:#fff}
.gwrap.mine .gatt-file .gaf-dl{border-left-color:rgba(255,255,255,.22)}
.gwrap.mine .gatt-file .gaf-main:active,.gwrap.mine .gatt-file .gaf-dl:active{background:rgba(255,255,255,.12)}
/* A set puts the cards under the photo mosaic; line ~1232 already gives the first one 4px, this
   keeps the rest from touching each other. */
.gbubble .gatt-file+.gatt-file{margin-top:0}
@media (prefers-reduced-motion:no-preference){
  .gatt-file .gaf-main,.gatt-file .gaf-dl{transition:background var(--t-fast) var(--ease),opacity var(--t-fast) var(--ease)}
}

/* ══ 25. THE PDF SHEET — .gdoc-ov (guard.js gDocPeek) ══════════════════════════════════════════
   The .g-modal chassis is a bottom sheet (line ~257: fixed, flex, align-items:flex-end) and
   .note-card already paints the surface in both themes. Only the column and the frame are new.
   The card is capped at 92vh and made a flex column so the title, the hint and the button row keep
   their intrinsic height and the FRAME is the only thing that absorbs the rest — a percentage
   height on the iframe inside an auto-height card collapses it to nothing. */
.gdoc-card{max-height:92vh;display:flex;flex-direction:column;gap:10px;
  padding-bottom:calc(20px + env(safe-area-inset-bottom,0px))}
.gdoc-t{font-family:var(--font-display,inherit);font-weight:800;font-size:15px;line-height:1.3;
  color:var(--ink);overflow-wrap:anywhere}
.gdoc-f{flex:1 1 auto;min-height:46vh;width:100%;border:0;border-radius:12px;background:var(--gb-field)}
.gdoc-hint{flex:none;font-size:11.5px;line-height:1.45;color:var(--muted)}
.gdoc-btns{flex:none;display:flex;gap:8px;justify-content:flex-end;flex-wrap:wrap}
/* .gbtn is already on the section-14 press list and clears the tap floor at its own padding; only
   the anchor needs the layout a <button> gets for free. */
.gdoc-btns .gbtn{min-height:var(--tap,44px);display:inline-flex;align-items:center;justify-content:center}
@media (prefers-reduced-motion:no-preference){
  @keyframes gdocin{from{opacity:0;transform:translateY(14px)}to{opacity:1;transform:none}}
  .gdoc-ov .gdoc-card{animation:gdocin var(--t-med) var(--ease)}
}

/* --- 25. TASK CARDS + TASK DETAIL SHEET (both lanes) --------------------------------------------
   Field supervisor Sam, on My tasks: "the font size … is relatively small … a base body font size
   of 16px with clear sans-serif characters … line length between 50 and 75 characters … line
   spacing 1.4x to 1.6x. Or, better still, the content of 'My Tasks' should be reorganized and
   spaced out."

   HIS NUMBERS, AND WHERE EACH ONE LANDS:
     16px base   → .gtk-main (the card body) and .gtk-sheet. Set on the card rather than on the
                   lane's row, because BOTH lane chassis (#g-sup .gear-row at 13.5px, .td-mid b at
                   13.5px / small at 11.5px) are shared with other lists — Directory, Posts, the
                   mention picker, forms, training and reminders all ride them, and widening the
                   chassis would silently re-type six screens nobody asked about.
     1.4–1.6     → 1.5 on prose and meta values, 1.35 on the title (a 17px semibold heading at 1.5
                   reads as two loose lines on a 360px phone; the ratio is for running text).
     50–75 chars → max-width:68ch on .gtk-prose p. ch is the right unit here: it tracks the font
                   actually rendering, so the measure holds when the OS font scale moves.
   UNSCOPED ON PURPOSE. This is the one card that renders in three places — the officer's To-do tab
   (#gv-forms), the supervisor's My tasks (inside #g-sup) and the detail sheet (appended to <body>,
   where nothing in supervisor.css can reach it). One definition, or the sheet paints naked. */
.gtk-main{
  font-size:16px;line-height:1.5;color:var(--ink,#16222f);
  display:block;width:100%;text-align:left;min-width:0;
  border-radius:12px;cursor:pointer;-webkit-tap-highlight-color:transparent}
.gtk-main:focus-visible{outline:2px solid var(--navy-2,#005080);outline-offset:3px}
/* The title. 17px/700 against the 16px body is a deliberate small step: the card is a paragraph of
   related facts, not a headline over unrelated ones, and a big jump here re-creates the "dense blob
   with one shouty line" the old row already was. */
.gtk-t{font-size:17px;font-weight:700;line-height:1.35;color:var(--heading,#0b2036);margin:0 0 8px;overflow-wrap:anywhere}

/* CHIPS. Overdue is the only filled one, and it is first — Sam's list is read at arm's length in a
   patrol vehicle, so the single fact that changes what you do next gets the only saturated colour on
   the card. Everything else is an outline chip that reads as reference, not alarm. */
.gtk-chips{display:flex;flex-wrap:wrap;gap:6px;margin:0 0 9px}
.gtk-chip{display:inline-flex;align-items:center;font-size:12.5px;font-weight:700;line-height:1;
  padding:6px 10px;border-radius:999px;background:var(--surface-2,#eef2f8);color:var(--muted,#5f7288);
  border:1px solid transparent;white-space:nowrap}
.gtk-chip.bad{background:#b3261e;color:#fff;border-color:#b3261e;letter-spacing:.02em}
.gtk-chip.hi{background:#fdeaea;color:#a3231b;border-color:#f3c9c5}
.gtk-chip.low{background:transparent;color:var(--muted,#5f7288);border-color:var(--line,#e4eaf1)}

/* STRUCTURED META — the labelled rows the formatter produces out of the pasted "Post name : … Post
   address : …" blob. A two-column grid, label left, value right: the label column is what turns a
   run-on line into something scannable, which is the whole of Sam's "reorganized and spaced out".
   minmax(0,1fr) on the value column, or a long unbroken address forces the grid wider than the
   phone and takes the page's horizontal scroll with it. */
.gtk-meta{display:grid;grid-template-columns:auto minmax(0,1fr);gap:4px 12px;margin:0 0 9px;font-size:15px;line-height:1.5}
.gtk-meta dt{font-weight:700;color:var(--muted,#5f7288);font-size:13px;white-space:nowrap;padding-top:2px}
.gtk-meta dd{margin:0;min-width:0;overflow-wrap:anywhere;color:var(--ink,#16222f)}
.gtk-none{color:var(--muted,#5f7288);font-style:normal;opacity:.7}
/* A meta value that is a real destination (address / phone / email). The 44px floor is measured on
   the LINK, not on its row: it is the thing a thumb aims at, and it sits inside a grid cell that is
   otherwise only as tall as one line of 15px text. */
.gtk-lnk{display:inline-flex;align-items:center;min-height:var(--tap,44px);
  color:var(--navy-2,#005080);font-weight:600;text-decoration:underline;text-underline-offset:2px}

/* PROSE — anything the formatter did not recognise, rendered as what it is. 68ch keeps Sam's 50-75
   measure on a tablet and on the Windows shell; on a 360px phone the viewport is the smaller
   constraint and the rule simply never binds. */
.gtk-prose{font-size:16px;line-height:1.5;color:var(--ink,#16222f)}
.gtk-prose p{margin:0 0 8px;max-width:68ch;overflow-wrap:anywhere}
.gtk-prose p:last-child{margin-bottom:0}
/* The fold is an AFFORDANCE, never a cut — every character is in the DOM in both states, exactly as
   the field-report card's .fr-fold works. max-height + the mask is all that changes. */
.gtk-prose.gtk-fold{max-height:96px;overflow:hidden;
  -webkit-mask-image:linear-gradient(180deg,#000 58%,transparent 100%);
  mask-image:linear-gradient(180deg,#000 58%,transparent 100%)}
.gtk-prose.gtk-fold.gtk-open{max-height:none;-webkit-mask-image:none;mask-image:none}
.gtk-more{display:inline-flex;align-items:center;min-height:var(--tap,44px);padding:0 2px;margin-top:2px;
  border:0;background:none;font-family:inherit;font-size:14px;font-weight:700;
  color:var(--navy-2,#005080);cursor:pointer}

/* ── THE LANE CHASSIS ────────────────────────────────────────────────────────────────────────────
   OFFICER LANE (#gv-forms). .td-row is centred and 13px-padded because most of its rows are one
   line — a form, a course, a reminder. A task card is now several, so the checkbox has to move to
   the top or it floats beside the middle of a paragraph. (0,2,0) beats plain .td-row (0,1,0); the
   supervisor lane's equivalent cannot be written here and lives in supervisor.css — see there. */
.td-row.gtk-row{align-items:flex-start;padding:15px 15px;gap:13px;margin-bottom:11px}
.td-row.gtk-row .td-box{margin-top:2px}
.td-row.gtk-row .td-mid{display:flex;flex-direction:column;min-width:0}
/* The officer lane's "Waiting to be sent" / advance chips still ride .td-mid small; keep them
   legible next to a 16px card rather than at the 11.5px the one-line rows use. */
.td-row.gtk-row .td-mid>small{font-size:13px;line-height:1.45;margin-top:4px}

/* ── THE SHEET ───────────────────────────────────────────────────────────────────────────────────
   Chassis, motion and dismissal are all inherited from .g-sheet / .g-sheet-in (section 11): the
   backdrop, the 92vh cap, the safe-area padding and the slide-up (@keyframes gsheetin) are already
   there, and .g-sheet + .g-x are already in GBK_OVERLAY / GBK_CLOSER, so the right-swipe and the
   Android back key put this away with no new code. Only the CONTENTS are new. */
.gtk-sheet{font-size:16px;line-height:1.5}
.gtk-s-t{font-size:20px;font-weight:800;line-height:1.3;color:var(--heading,#0b2036);margin:0 0 10px;overflow-wrap:anywhere}
.gtk-s-lbl{font-size:11px;font-weight:800;letter-spacing:.08em;text-transform:uppercase;
  color:var(--muted,#5f7288);margin:16px 0 7px}
.gtk-s-none{font-size:15px;line-height:1.5;color:var(--muted,#5f7288)}
.gtk-s-by{font-size:14px;line-height:1.5;color:var(--muted,#5f7288);margin-top:14px}
/* THE LANE'S NOTE — "To pay · <name>" on an advance, "Sent back by X: reason" on work that came
   back. Each lane hands over the SAME fragment its card renders, and those carry their own chassis's
   classes: the officer's <small class="bad"> is dressed by .td-mid small.bad, and Field mode's
   .gear-tag rule is scoped under #g-sup. This sheet is appended to <body>, so NEITHER selector can
   reach it — without the rules below the reason loses its red and Field mode's tag renders at the
   sheet's 16px. Sized here rather than in the lanes so one sheet reads as one thing. */
.gtk-s-note{display:flex;flex-direction:column;gap:5px;margin-top:14px;font-size:14px;line-height:1.45}
.gtk-s-note .gear-tag,.gtk-s-note small{font-size:14px;line-height:1.45}
.gtk-s-note small.bad{color:#b3261e;font-weight:700}
.gtk-s-acts{display:flex;flex-wrap:wrap;gap:9px;margin-top:18px;
  padding-top:15px;border-top:1px solid var(--line,#e4eaf1)}
/* Every action in the sheet foot clears the house tap floor, whatever the lane put there. */
.gtk-s-acts>button,.gtk-s-acts>a{min-height:var(--tap,44px);display:inline-flex;align-items:center;justify-content:center}
/* HISTORY, inline. A rail rather than cards: it is a sequence, and the vertical line is what says so
   at a glance on a 360px screen where three stacked boxes just read as more list. */
.gtk-hist{border-left:2px solid var(--line,#e4eaf1);padding-left:13px}
.gtk-h-row{margin-bottom:12px}
.gtk-h-row:last-child{margin-bottom:0}
.gtk-h-row>b{display:block;font-size:15px;line-height:1.45;color:var(--ink,#16222f)}
.gtk-h-by{display:block;font-size:13px;line-height:1.45;color:var(--muted,#5f7288)}
.gtk-h-ch{font-size:13.5px;line-height:1.5;margin-top:3px;color:var(--ink,#16222f)}

/* ── DARK ────────────────────────────────────────────────────────────────────────────────────────
   The tokens carry most of it (--ink/--muted/--line/--heading are all redefined under
   [data-theme="dark"] at the top of this file), so only the hand-mixed chip colours need a branch —
   the same rule the rest of this stylesheet follows. */
[data-theme="dark"] .gtk-chip{background:#1f2a3a;color:#9fb0c4}
[data-theme="dark"] .gtk-chip.bad{background:#c5372c;color:#fff;border-color:#c5372c}
[data-theme="dark"] .gtk-chip.hi{background:#3a1f1c;color:#f0a89f;border-color:#5c2f28}
[data-theme="dark"] .gtk-chip.low{background:transparent;color:#8b98ab;border-color:#26303f}
[data-theme="dark"] .gtk-lnk,[data-theme="dark"] .gtk-more{color:#8fc4f0}
/* The same lift .td-mid small.bad takes in dark — #b3261e on a dark sheet is unreadable. */
[data-theme="dark"] .gtk-s-note small.bad{color:#f0a9b1}
[data-theme="dark"] .gtk-prose.gtk-fold{
  -webkit-mask-image:linear-gradient(180deg,#000 58%,transparent 100%);
  mask-image:linear-gradient(180deg,#000 58%,transparent 100%)}

/* ── MOTION ──────────────────────────────────────────────────────────────────────────────────────
   ⚠ COLON-TIGHT, DELIBERATELY. The spelling with a SPACE after the colon is reserved:
   msg-dynamism-ui.test.js finds the messenger motion block by plain indexOf on that spaced opener
   and asserts the FIRST such block in this file is the messenger's. An opener written the spaced way
   here would hijack every assertion in that suite and then report a missing .g-ment rule — a bug
   report pointing at the wrong file entirely. Tight opener, no ambiguity, and the spaced form is not
   written out anywhere in this section (not even in prose) so it cannot be counted by accident.
   The card press is transform+filter on the house tokens (section 14's vocabulary), so it feels like
   every other pressable surface in the app rather than a second idea about what a tap looks like. */
.gtk-main{transition:transform var(--press-ms,.09s) ease-out,filter var(--t-fast) var(--ease)}
.gtk-main:active{transform:scale(var(--press-scale,.97));filter:var(--press-dim,brightness(.93))}
/* ⚠ THE FOLD DELIBERATELY DOES NOT ANIMATE, and that is the house motion rule rather than an
   omission: transform and opacity only, never a layout property. An easing on the fold's height cap
   was written here first and officer-msg-parity.test.js rejected it — that suite slices from its own
   section header to the END OF THE FILE, so everything below it, including this section, is held to
   the messenger's motion contract. It is right to: easing a height inside a scrolling list is a
   scroll-position bug waiting to happen, and this list is read one-handed in a moving vehicle.
   The mask already softens the folded edge, so the reveal reads as intentional without moving
   anything underneath it. (The field-report fold does animate, via a keyframe rather than an easing
   because a `none` cap cannot interpolate — but it sits above that slice and predates the rule.)
   ⚠ AND MIND THE PROSE, not just the rules. That scanner does not strip comments and its value
   pattern runs across newlines, so a sentence here that names a property immediately after the word
   it is describing can be read as a declaration and fail the file for a rule it does not contain.
   This paragraph is written to avoid that; keep it that way. */
@media (prefers-reduced-motion:reduce){
  .gtk-main{transition:none}
  /* The DIM stays under `reduce` — it is the only feedback left that the tap registered, and
     dropping it as well leaves the card feeling dead rather than calm. Only the scale goes. */
  .gtk-main:active{transform:none}
}

/* ══ 1.3.134 · THE COMPANY CALENDAR, MOBILE ═════════════════════════════════════════════════════
   Tokens only — var(--card)/--ink/--line/--muted/--bg — so the dark theme is inherited rather than
   re-declared. The one hard-coded colour on this whole screen is the per-item accent, and it is not
   hard-coded here: it arrives as --gcal-k from the server's `color` on each item, which is what
   keeps a payday green on the phone and green on the desktop.
   Every control carries the house var(--tap) floor (section 5). The month cells get it from their
   grid track rather than a min-height, which is why they have no rule of their own for it. */

/* ---- view switcher + filters ---- */
.gcal{padding:2px 0 10px}
.gcal-views{display:flex;gap:6px;padding:2px 2px 8px}
.gcal-v{flex:1;min-height:var(--tap);padding:8px 6px;border:1px solid var(--line);border-radius:11px;
  background:var(--card);color:var(--muted);font-family:inherit;font-weight:800;font-size:13px}
.gcal-v.on{background:var(--navy);border-color:var(--navy);color:#fff}
.gcal-step{display:flex;align-items:center;gap:8px;padding:0 2px 8px}
.gcal-stepb{min-width:var(--tap);min-height:var(--tap);border:1px solid var(--line);border-radius:11px;
  background:var(--card);color:var(--ink);font-size:20px;font-family:inherit;line-height:1}
.gcal-today{flex:1;min-height:var(--tap);border:1px solid var(--line);border-radius:11px;background:var(--card);
  color:var(--navy-ink);font-family:inherit;font-weight:800;font-size:13px}
/* The filter row scrolls sideways rather than wrapping to two lines: seven chips will not fit across
   360px, and a second row pushes the first item of the agenda below the fold on every open. */
.gcal-fils{display:flex;gap:6px;overflow-x:auto;padding:0 2px 10px;scrollbar-width:none;-webkit-overflow-scrolling:touch}
.gcal-fils::-webkit-scrollbar{display:none}
.gcal-fil{flex:0 0 auto;min-height:36px;padding:7px 13px;border:1px solid var(--line);border-radius:999px;
  background:var(--card);color:var(--muted);font-family:inherit;font-weight:700;font-size:12.5px;position:relative}
/* Painted at 36px, tappable at 44 — the section-5 idiom for a chip whose visual size is the design. */
.gcal-fil::after{content:"";position:absolute;left:0;right:0;top:50%;transform:translateY(-50%);height:var(--tap)}
.gcal-fil.on{background:var(--navy);border-color:var(--navy);color:#fff}
.gcal-new{margin:2px 2px 10px}

/* ---- day headers + rows ---- */
.gcal-dhead{display:flex;align-items:center;gap:8px;padding:12px 4px 6px;position:sticky;top:0;z-index:2;
  background:var(--bg);font-family:var(--font-display);font-weight:800;font-size:13px;color:var(--muted);
  text-transform:uppercase;letter-spacing:.04em}
.gcal-dhead.today{color:var(--navy-ink)}
.gcal-dhead b{flex:1;min-width:0}
.gcal-dhead span{font-weight:700;font-size:11.5px;opacity:.75}
/* ★ A ROW IS A GRID OF TWO COLUMNS — time gutter, then everything else — AND NOT A FLEX LINE.
   ⚠ THIS IS THE 1.3.134 FIELD-TODAY DEFECT, AND IT IS WORTH THE PARAGRAPH. `.gcal-row` shipped as
   `display:flex` with THREE children: .gcal-when, .gcal-main and .gcal-acts. `.gcal-acts` was styled
   `padding-left:71px` — i.e. indented past the 62px gutter + 9px gap, which is only meaningful for a
   block sitting on its OWN line — but nothing ever put it there. With no `flex-wrap`, the actions
   stayed on the SAME line as the card and took 163px of a 362px row (71px of dead indent plus a
   92px "Mark done"), leaving the card 115px and its title column 62px. At 360px CSS the title column
   measured 32px and "Needs Training" rendered as `Nee / ds / Trai / ning`, one fragment per line,
   with the button stranded to the right. Measured on the field Today board: a 63-char task title
   drew a 440px-tall row at 360px and 235px at 390px. It is 138px at both now.
   The grid is the fix: column 2 is `minmax(0,1fr)` so the card takes ALL the width the gutter leaves,
   and the actions are placed explicitly into column 2 of the next row — attached under the card they
   act on, left edge aligned with it, rather than floating beside it.
   ⚠ `.gcal-acts` MUST carry `grid-column:2`. Auto-placement would put the third item at row 2 COLUMN
   1 — under the time, half in the gutter — which looks like a different bug and is the same one. */
.gcal-row{display:grid;grid-template-columns:62px minmax(0,1fr);align-items:start;column-gap:9px;
  padding:3px 2px 8px}
.gcal-row.done .gcal-tx b{text-decoration:line-through;opacity:.6}
/* A fixed time gutter is what makes a scanned list readable: the titles all start at the same x, so
   the eye runs down one edge instead of a ragged one. 62px holds the widest label this renderer can
   produce — "12:30 PM" at 12px/700 tabular — so it never has to grow and steal the card's width. */
.gcal-when{grid-column:1;padding-top:13px;text-align:right;font-size:12px;font-weight:700;color:var(--muted);
  font-variant-numeric:tabular-nums}
.gcal-allday{color:var(--line);font-style:normal;font-size:15px}
.gcal-main{grid-column:2;min-width:0;display:flex;align-items:flex-start;gap:9px;min-height:var(--tap);
  padding:10px 11px;border:1px solid var(--line);border-left:3px solid var(--gcal-k,var(--blue));
  border-radius:12px;background:var(--card);text-align:left}
.gcal-main[data-calopen]{cursor:pointer}
.gcal-ic{flex:0 0 auto;width:18px;height:18px;color:var(--gcal-k,var(--blue));margin-top:1px}
.gcal-ic svg{width:18px;height:18px;display:block}
.gcal-tx{flex:1;min-width:0}
/* ⚠ `overflow-wrap:break-word`, NEVER `anywhere` — the two are not synonyms and the difference is
   the whole defect above. `anywhere` counts a soft break when the browser computes MIN-CONTENT, so a
   title's minimum width becomes one character and any greedy sibling can squeeze the column to 32px
   and shred the words. `break-word` breaks a long token exactly the same way at paint time but keeps
   min-content at the longest WORD, so the column can never collapse below something readable.
   The 2-line clamp is the second guard: a 63-char task title used to run to TEN lines at 390px and
   TWENTY at 360px. Two lines then an ellipsis keeps every row the same scannable height, and the
   full title is one tap away in the detail sheet, which wraps it in full.
   `display:block` first, `-webkit-box` second: a WebView too old for the clamp drops the second
   declaration and keeps a plain, unclamped, still-readable block. */
.gcal-tx b{display:block;display:-webkit-box;-webkit-box-orient:vertical;-webkit-line-clamp:2;
  line-clamp:2;overflow:hidden;font-size:14px;font-weight:800;color:var(--ink);line-height:1.35;
  overflow-wrap:break-word}
.gcal-tx small{display:block;margin-top:2px;font-size:11.5px;color:var(--muted);line-height:1.4}
/* Attached under the card, not indented past it: 5px of gap reads as "these belong together" against
   the 11px that now separates one row from the next. */
.gcal-acts{display:flex;flex-wrap:wrap;gap:6px;padding:5px 0 0}
.gcal-row .gcal-acts{grid-column:2}
.gcal-act{min-height:var(--tap);padding:8px 13px;border-radius:10px;border:1px solid var(--line);
  background:var(--card);color:var(--ink);font-family:inherit;font-weight:700;font-size:12.5px}
.gcal-act.pri{background:var(--navy);border-color:var(--navy);color:#fff}
.gcal-act.danger{color:var(--red);border-color:var(--red)}
/* ⚠ THE PRIMARY ACTION NEEDS A DARK SKIN, and it needs one now that the button sits on the PAGE
   under the card rather than tucked beside it. `--navy` is #032b49 against a #0f1723 dark page:
   1.26:1, so the button's shape simply is not there and only the white label floats in space. This
   is the officer app's existing dark "on" fill (.gib-f.on / .gib-fn / .gib-sec-agg), not a new
   colour — same token, so the strip reads as the rest of the app rather than as a new component. */
[data-theme="dark"] .gcal-act.pri{background:#1d4a80;border-color:#1d4a80}
.gcal-freeday{padding:8px 2px 8px 71px;font-size:12.5px;color:var(--muted)}
.gcal-empty{padding:30px 18px;text-align:center;line-height:1.7}
.gcal-empty svg{width:34px;height:34px;color:var(--line);margin-bottom:8px}
.gcal-empty b{display:block;font-size:15px;color:var(--ink);margin-bottom:4px}

/* ---- offline banner ---- */
/* ⚠ color-mix() SECOND, WITH A REAL BACKGROUND FIRST. The fleet includes Android WebViews older than
   Chrome 111, where a color-mix() value is invalid and the whole declaration is dropped — leaving a
   transparent card. The plain background is the floor; the mix is the upgrade. */
.gcal-off{display:flex;align-items:flex-start;gap:10px;margin:2px 2px 10px;padding:11px 13px;
  border:1px solid var(--amber);border-radius:12px;background:var(--card);
  background:color-mix(in srgb,var(--amber) 9%,var(--card))}
.gcal-off svg{width:19px;height:19px;flex:0 0 auto;color:var(--amber);margin-top:1px}
.gcal-off b{display:block;font-size:13.5px;color:var(--ink)}
.gcal-off small{display:block;margin-top:2px;font-size:11.5px;color:var(--muted);line-height:1.45}

/* ---- month: dot density ---- */
.gcal-mtitle{padding:2px 4px 8px;font-family:var(--font-display);font-weight:800;font-size:16px;color:var(--ink)}
.gcal-mh{display:grid;grid-template-columns:repeat(7,1fr);gap:3px;padding:0 2px 4px}
.gcal-mh span{text-align:center;font-size:11px;font-weight:800;color:var(--muted)}
.gcal-mg{display:grid;grid-template-columns:repeat(7,1fr);gap:3px;padding:0 2px}
.gcal-md{aspect-ratio:1;min-height:var(--tap);display:flex;flex-direction:column;align-items:center;
  justify-content:flex-start;gap:3px;padding:5px 2px 0;border:1px solid transparent;border-radius:10px;
  background:var(--card);font-family:inherit}
.gcal-md.pad{background:none;pointer-events:none}
.gcal-md .d{font-size:13px;font-weight:700;color:var(--ink);line-height:1}
.gcal-md.today{border-color:var(--navy);background:var(--card);
  background:color-mix(in srgb,var(--navy) 8%,var(--card))}
.gcal-md.today .d{color:var(--navy-ink);font-weight:800}
.gcal-md .dots{display:flex;flex-wrap:wrap;align-items:center;justify-content:center;gap:2px;line-height:0}
.gcal-md .dots i{width:5px;height:5px;border-radius:50%;display:block}
.gcal-md .dots em{font-size:9px;font-style:normal;font-weight:800;color:var(--muted);line-height:1}
.gcal-mhint{padding:10px 4px 2px;text-align:center;font-size:11.5px;color:var(--muted)}

/* ---- "Next up" on Home ---- */
.gcal-nextsec{display:flex;align-items:center;gap:8px}
.gcal-seeall{margin-left:auto;min-height:32px;padding:4px 8px;border:none;background:none;
  color:var(--navy-ink);font-family:inherit;font-weight:800;font-size:12.5px;position:relative}
.gcal-seeall::after{content:"";position:absolute;inset:50% 0 auto;transform:translateY(-50%);height:var(--tap)}
.gcal-next{display:flex;flex-direction:column;gap:7px}
.gcal-nx{min-height:var(--tap);padding:11px 13px;border:1px solid var(--line);
  border-left:3px solid var(--gcal-k,var(--blue));border-radius:12px;background:var(--card);
  text-align:left;font-family:inherit}
.gcal-nx-k{display:block;font-size:10.5px;font-weight:800;color:var(--muted);text-transform:uppercase;
  letter-spacing:.05em;margin-bottom:3px}
.gcal-nx b{display:block;font-size:14px;font-weight:800;color:var(--ink);line-height:1.35;overflow-wrap:anywhere}
.gcal-nx small{display:block;margin-top:2px;font-size:11.5px;color:var(--muted)}

/* ---- sheets: detail, pick, create ---- */
.gcal-sh{padding:4px 2px 8px}
.gcal-sh-t{font-family:var(--font-display);font-weight:800;font-size:17px;color:var(--ink);line-height:1.3;
  overflow-wrap:anywhere}
.gcal-sh-m{margin-top:5px;font-size:13px;color:var(--muted)}
.gcal-sh .gcal-acts{padding:12px 0 0}
.gcal-pick{display:flex;flex-direction:column;gap:7px;padding:4px 2px 8px}
.gcal-po{min-height:var(--tap);padding:12px 14px;border:1px solid var(--line);border-radius:12px;
  background:var(--card);color:var(--ink);font-family:inherit;font-weight:700;font-size:14px;text-align:left}
.gcal-form{display:flex;flex-direction:column;gap:11px;padding:4px 2px 10px}
.gcal-form label{display:block;font-size:12px;font-weight:800;color:var(--muted)}
.gcal-form label small{font-weight:600;text-transform:none}
.gcal-form input,.gcal-form select{width:100%;margin-top:5px;min-height:var(--tap);padding:11px 12px;
  border:1px solid var(--gb-field-line,var(--line));border-radius:11px;background:var(--gb-field,var(--card));
  color:var(--ink);font-family:inherit;font-size:15px}
.gcal-q{width:100%;min-height:var(--tap);padding:11px 12px;border:1px solid var(--gb-field-line,var(--line));
  border-radius:11px;background:var(--gb-field,var(--card));color:var(--ink);font-family:inherit;font-size:15px}
.gcal-plist{max-height:210px;overflow-y:auto;margin-top:7px;border:1px solid var(--line);border-radius:11px}
.gcal-pi{display:block;width:100%;min-height:var(--tap);padding:11px 13px;border:none;
  border-bottom:1px solid var(--line);background:var(--card);color:var(--ink);font-family:inherit;
  font-size:14px;text-align:left}
.gcal-pi:last-child{border-bottom:none}
.gcal-pnone{padding:14px;font-size:13px;color:var(--muted);text-align:center}
.gcal-chips{display:flex;flex-wrap:wrap;gap:6px}
.gcal-chip{display:inline-flex;align-items:center;gap:5px;padding:6px 6px 6px 11px;border-radius:999px;
  background:var(--navy);color:#fff;font-size:12.5px;font-weight:700}
.gcal-chip button{min-width:26px;min-height:26px;border:none;border-radius:50%;background:rgba(255,255,255,.18);
  color:#fff;font-family:inherit;font-size:14px;line-height:1;position:relative}
.gcal-chip button::after{content:"";position:absolute;left:50%;top:50%;transform:translate(-50%,-50%);
  width:var(--tap);height:var(--tap)}

/* ---- Work: Agenda | Tasks ---- */
.gwk-tabs{display:flex;gap:6px;padding:2px 2px 10px}
.gwk-t{flex:1;min-height:var(--tap);padding:9px 6px;border:1px solid var(--line);border-radius:11px;
  background:var(--card);color:var(--muted);font-family:inherit;font-weight:800;font-size:14px}
.gwk-t.on{background:var(--navy);border-color:var(--navy);color:#fff}
.gwk-pane.hidden{display:none}

/* ---- motion (DESIGN-SYSTEM §7): press feedback only, and only where a press means something ---- */
@media (prefers-reduced-motion:no-preference){
  .gcal-main[data-calopen],.gcal-nx,.gcal-md,.gcal-po,.gcal-act,.gcal-v,.gcal-fil,.gwk-t{
    transition:transform var(--press-ms,.09s) var(--ease),background var(--t-fast) var(--ease),
      border-color var(--t-fast) var(--ease),color var(--t-fast) var(--ease)}
  .gcal-main[data-calopen]:active,.gcal-nx:active,.gcal-md:active,.gcal-po:active{transform:scale(.985)}
}
/* The dim survives `reduce` — it is the only remaining signal that the tap landed. Only scale goes. */
.gcal-main[data-calopen]:active,.gcal-nx:active,.gcal-md:active,.gcal-po:active{filter:var(--press-dim,brightness(.97))}
@media (prefers-reduced-motion:reduce){
  .gcal-main[data-calopen]:active,.gcal-nx:active,.gcal-md:active,.gcal-po:active{transform:none}
}
