  /* ══ Breakline: crew-warm, feed-first. All theming via CSS custom properties.
     Light is the default; dark applies under prefers-color-scheme OR [data-theme=dark];
     [data-theme=light] forces light even under a dark system. ══ */
  :root, #startup {
    --bg:#f7f2ea; --surface:#fffdf9; --surface-2:#f1eade; --surface-3:#e9e0d0;
    --border:#e7ddca; --hair:#efe7d8;
    --ink:#241d16; --text:#241d16; --muted:#8a7c68;
    --accent:#2c2420; --accent-ink:#fdf8ef;
    --basil:#4f9a54; --mozz:#d8a02c; --pepp:#c8452f;
    /* Pizza mark (startup loader). Derived from --mozz rather than hard-coded so a
       single declaration tracks whichever palette block is live: var() inside a
       custom property is substituted where it is USED, so these resolve against the
       active theme's --mozz. Declared once here; no theme block redeclares them. */
    --crust:color-mix(in srgb, var(--mozz) 55%, #7a4718); --cheese:var(--mozz);
    --ok:#4f9a54; --warn:#cf9426; --bad:#c8452f;
    --announce-bg:#fbeecb; --announce-line:#e6c778; --announce-ink:#7a5a12;
    --ring-track:#ece2d1;
    --skel-base:#e7dcc9; --skel-shine:rgba(255,255,255,.6);
    --radius:16px; --radius-sm:11px;
    --shadow:0 1px 2px rgba(60,42,20,.05), 0 6px 20px -12px rgba(60,42,20,.18);
    --font: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, system-ui, sans-serif;
    --font-round: ui-rounded, "SF Pro Rounded", "Nunito", var(--font);
  }
  @media (prefers-color-scheme: dark) {
    :root:not([data-theme="light"]) {
      --bg:#0e1523; --surface:#151d2e; --surface-2:#1c2638; --surface-3:#243044;
      --border:#31405a; --hair:#243048;
      --ink:#e9eef7; --text:#e9eef7; --muted:#93a3bd;
      --accent:#e9eef7; --accent-ink:#101827;
      --basil:#6cc274; --mozz:#e9c05a; --pepp:#e56f55;
      --ok:#6cc274; --warn:#e9c05a; --bad:#e56f55;
      --announce-bg:#26281c; --announce-line:#6a5c2e; --announce-ink:#eccf7e;
      --ring-track:#223048;
      --skel-base:#232f44; --skel-shine:rgba(255,255,255,.07);
      --shadow:0 1px 2px rgba(0,0,0,.3), 0 8px 24px -14px rgba(0,0,0,.6);
    }
  }
  :root[data-theme="dark"] {
    --bg:#0e1523; --surface:#151d2e; --surface-2:#1c2638; --surface-3:#243044;
    --border:#31405a; --hair:#243048;
    --ink:#e9eef7; --text:#e9eef7; --muted:#93a3bd;
    --accent:#e9eef7; --accent-ink:#101827;
    --basil:#6cc274; --mozz:#e9c05a; --pepp:#e56f55;
    --ok:#6cc274; --warn:#e9c05a; --bad:#e56f55;
    --announce-bg:#26281c; --announce-line:#6a5c2e; --announce-ink:#eccf7e;
    --ring-track:#223048;
    --skel-base:#232f44; --skel-shine:rgba(255,255,255,.07);
    --shadow:0 1px 2px rgba(0,0,0,.3), 0 8px 24px -14px rgba(0,0,0,.6);
  }

  * { box-sizing: border-box; -webkit-tap-highlight-color: transparent; }
  html, body { margin: 0; height: 100%; }
  body {
    background: var(--bg); color: var(--text);
    font: 15px/1.45 var(--font); display: flex; flex-direction: column; min-height: 100dvh;
    -webkit-font-smoothing: antialiased;
  }
  .ic { width: 1.25em; height: 1.25em; display: inline-block; vertical-align: -0.2em; flex: none; }
  h1,h2,h3 { font-family: var(--font-round); letter-spacing: -.01em; }

  /* ── App shell ── */
  header {
    background: var(--surface); border-bottom: 1px solid var(--border);
    padding: max(env(safe-area-inset-top), 9px) 14px 9px;
    display: flex; align-items: center; gap: 10px; min-height: 54px;
    position: sticky; top: 0; z-index: 15;
  }
  header .brand { display: flex; align-items: center; gap: 9px; }
  header .logo {
    width: 30px; height: 30px; border-radius: 9px; background: var(--accent); color: var(--accent-ink);
    display: grid; place-items: center; flex: none;
  }
  header .logo svg { width: 18px; height: 18px; }
  header .word { font-family: var(--font-round); font-weight: 800; font-size: 16px; letter-spacing: -.02em; }
  header .word small { display: block; font-size: 9.5px; font-weight: 600; letter-spacing: .14em;
    text-transform: uppercase; color: var(--muted); margin-top: -2px; }
  header .spacer { flex: 1; }
  .topnav { display: none; gap: 2px; }
  /* Desktop nav rail — hidden here, shown by desktop.css at >=1180px. The base
     state lives in THIS file and not that one on purpose: buildNav() fills the
     rail on every platform, so if the hiding rule shipped in the desktop sheet
     and that sheet failed to load, a phone would paint a full-height column of
     nav rows above the feed and shove nav.tabbar off the bottom of the screen. */
  .railnav { display: none; }
  .topnav button {
    background: none; border: none; color: var(--muted); font: inherit; font-weight: 600;
    padding: 8px 13px; border-radius: 999px; cursor: pointer; display: flex; align-items: center; gap: 7px;
  }
  .topnav button .ic { width: 18px; height: 18px; }
  .topnav button.active { color: var(--ink); background: var(--surface-2); }
  .iconbtn {
    background: var(--surface-2); border: 1px solid var(--border); color: var(--ink);
    width: 38px; height: 38px; border-radius: 11px; display: grid; place-items: center;
    cursor: pointer; position: relative; flex: none;
  }
  .iconbtn svg { width: 19px; height: 19px; }
  .iconbtn.avatar-btn { border: none; padding: 0; overflow: hidden; }
  header select#org-switcher {
    background: var(--surface-2); color: var(--ink); border: 1px solid var(--border);
    border-radius: 10px; padding: 7px 9px; font-size: 12.5px; max-width: 140px;
  }
  .dot-badge {
    position: absolute; top: -5px; right: -5px; min-width: 17px; height: 17px; padding: 0 4px;
    border-radius: 999px; background: var(--pepp); color: #fff; font-size: 10.5px; font-weight: 800;
    display: grid; place-items: center; border: 2px solid var(--surface);
  }

  /* ── The tab bar must not ride the overscroll ──
     Reported from a phone, 2026-08-02: on Feed → Activity, a downward swipe while
     already at the top made the bottom tab bar flash away for a split second.

     `main` is the app's scroller and the document root is NOT scrollable (measured:
     root scrollHeight == clientHeight). With overscroll-behavior at its `auto`
     default, a drag at scrollTop 0 CHAINS out of `main` to the root, and Android
     Chromium draws that as the stretch overscroll — a transform on the root
     scroller's visual contents. `nav.tabbar` is position:fixed, which makes it part
     of those contents, so it gets translated off the bottom edge and springs back.
     Nothing in JS was involved, which is why it looked like a phantom.

     `contain` stops the chain without deadening the gesture: `main`'s own contents
     still stretch, which is the native feel. Same declaration, same reason, as
     `.modal` and `.mention-pop` already carry. NOT `none` — that would also kill
     the local effect and make the top of every list feel stuck. */
  main { flex: 1; overflow-y: auto; overscroll-behavior-y: contain; }
  .wrap { max-width: 660px; margin: 0 auto; padding: 16px 14px calc(90px + env(safe-area-inset-bottom)); }

  /* Bottom tab bar (mobile) */
  nav.tabbar {
    position: fixed; bottom: 0; left: 0; right: 0; background: var(--surface);
    border-top: 1px solid var(--border); display: flex; padding-bottom: env(safe-area-inset-bottom); z-index: 15;
  }
  nav.tabbar button {
    flex: 1; background: none; border: none; color: var(--muted); padding: 8px 2px 9px; cursor: pointer;
    font-size: 10.5px; font-weight: 600; display: flex; flex-direction: column; align-items: center; gap: 3px; position: relative;
  }
  nav.tabbar button svg { width: 23px; height: 23px; }
  nav.tabbar button.active { color: var(--ink); }
  nav.tabbar button.active svg { color: var(--accent); }
  nav.tabbar .dot-badge { top: 3px; right: 50%; margin-right: -22px; border-color: var(--surface); }

  /* Compose FAB (mobile) */
  .fab {
    position: fixed; right: 16px; bottom: calc(74px + env(safe-area-inset-bottom)); z-index: 16;
    width: 56px; height: 56px; border-radius: 18px; border: none; cursor: pointer;
    background: var(--accent); color: var(--accent-ink); display: grid; place-items: center;
    box-shadow: 0 6px 18px -6px rgba(60,42,20,.5);
  }
  .fab svg { width: 26px; height: 26px; }

  /* ── Cards & primitives ── */
  .card {
    background: var(--surface); border: 1px solid var(--border); border-radius: var(--radius);
    padding: 15px; box-shadow: var(--shadow);
    /* Space with BOTH top and bottom margins: adjacent vertical margins collapse
       in block flow, so card→card stays 13px (no doubling), but a card now always
       separates from whatever precedes it — a button, a wrapper div, or another
       card. Prevents the "cards touching after something is added" class of bug. */
    margin-top: 13px; margin-bottom: 13px;
  }
  h2.section { font-family: var(--font); font-size: 11.5px; text-transform: uppercase; letter-spacing: .09em;
    color: var(--muted); font-weight: 700; margin: 2px 2px 11px; }
  label { display: block; font-size: 12px; color: var(--muted); font-weight: 600; margin: 12px 0 5px; }
  input, select, textarea {
    width: 100%; background: var(--surface-2); color: var(--text); border: 1px solid var(--border);
    /* 16px floor: iOS Safari/WKWebView auto-zooms into any focused control whose
       font is under 16px and can leave the page stuck zoomed after the keyboard
       closes (clipped right edge / tab bar). Keep every focusable control >=16px. */
    border-radius: var(--radius-sm); padding: 11px 12px; font-size: 16px; font-family: inherit;
  }
  input:focus, select:focus, textarea:focus { outline: 2px solid var(--accent); outline-offset: -1px; border-color: transparent; }
  button.btn {
    background: var(--accent); color: var(--accent-ink); border: none; border-radius: var(--radius-sm);
    padding: 12px 15px; font-size: 15px; font-weight: 700; font-family: inherit; width: 100%; margin-top: 12px; cursor: pointer;
  }
  button.btn.secondary { background: var(--surface-2); color: var(--ink); border: 1px solid var(--border); }
  button.btn:active { transform: translateY(1px); }
  button.chip {
    background: var(--surface-2); color: var(--ink); border: 1px solid var(--border);
    border-radius: 999px; padding: 7px 12px; font-size: 12.5px; font-weight: 600; margin: 0 6px 6px 0; cursor: pointer;
  }
  button.chip.on { background: var(--accent); color: var(--accent-ink); border-color: var(--accent); }
  .row { display: flex; gap: 8px; align-items: center; }
  .grow { flex: 1; min-width: 0; }
  .muted { color: var(--muted); }
  .empty { text-align: center; color: var(--muted); padding: 34px 12px; }
  .err { color: var(--bad); font-size: 13px; margin-top: 8px; min-height: 16px; }

  .person { display: flex; align-items: center; gap: 12px; padding: 12px 0; border-bottom: 1px solid var(--hair); }
  .person:last-child { border-bottom: none; }
  .avatar {
    width: 42px; height: 42px; border-radius: 50%; display: grid; place-items: center;
    color: #fff; font-weight: 800; font-size: 15px; flex: none; font-family: var(--font-round);
  }
  /* The avatar for an app-authored post (appAvatarHtml). It carries no inline
     background, so these win over .avatar's without !important. A rounded square
     rather than a circle: it should read as the app's mark, not as one more
     face. */
  .app-avatar { background: var(--accent); color: var(--accent-ink); border-radius: 32%; }
  .app-avatar .ic { width: 58%; height: 58%; }

  /* ── #F14: a photo inside the circle ──
     One rule for every avatar SHAPE in the app — .avatar, .sc-av (schedule
     chips), .rbub (read-receipt bubbles) and .dm-av (the Chats inbox row) — so a
     surface cannot gain a photo without gaining the correct crop with it.

     The image is absolutely positioned OVER the initials rather than replacing
     them, which is what makes the fallback free: until hydratePhotos() attaches
     the authenticated blob there is no src, so the initials show through. A
     photo that is removed, forbidden or simply offline lands in exactly the
     same state.

     "Nothing paints" was the claim here and it was WRONG on the one engine that
     matters. An <img> with no src is not an empty element: WebKit draws its
     missing-image glyph inside it, and .av-img covers the whole circle, so the
     glyph sat ON TOP of the initials this rule was relying on. Cole's iPhone
     recording caught it on post photos and the placeholder rule below was added
     for `.photo-grid` — avatars were left behind, still asserting a fallback
     they did not have, until a crew member asked why she was a question mark.
     The rule below now covers .av-img too; this comment is the reason it must
     keep covering it.

     object-fit: cover is the belt; the server already center-crops to a square
     (images.AVATAR_EDGE), so this only has to survive a non-square legacy
     original being served as the fallback variant. border-radius: inherit keeps
     the 11px-radius header button square-ish and every circle round without a
     second rule per shape. */
  .has-photo { position: relative; overflow: hidden; }
  .av-img {
    position: absolute; inset: 0; width: 100%; height: 100%;
    object-fit: cover; border-radius: inherit; display: block;
    /* A coworker's face inside a tap-to-open row must not offer iOS's native
       "Save Image" sheet on long-press, and must not be draggable out of the
       app. Same suppression [data-rx] and .imgviewer-img already carry. */
    -webkit-touch-callout: none; -webkit-user-drag: none; user-select: none;
    pointer-events: none;
  }
  .person .name { font-weight: 700; }
  .person .sub { font-size: 12.5px; color: var(--muted); }
  .tag {
    display: inline-block; font-size: 10.5px; font-weight: 700; padding: 2px 8px; border-radius: 999px;
    background: var(--surface-2); border: 1px solid var(--border); color: var(--muted); margin-left: 6px; vertical-align: middle;
  }
  .tag.role { color: #fff; border: none; }
  /* ── #F8: "no shifts linked" is a fault, not a label ──
     Amber is the THIRD channel here, never the only one. The warning triangle and the
     wording carry the meaning on their own, because a yellow-vs-grey chip is invisible
     to a red-green colourblind reader and washes out on a phone screen under a store's
     fluorescent lights. Built from var(--warn) so it re-derives in each theme rather
     than pinning one hex that reads as a warning on cream and disappears on dark. */
  .tag.tag-warn {
    display: inline-flex; align-items: center; gap: 4px; font-weight: 800;
    background: color-mix(in srgb, var(--warn) 20%, transparent);
    border-color: color-mix(in srgb, var(--warn) 55%, transparent);
    /* Not plain var(--warn): amber text on an amber tint measured ~2.5:1 in the light
       theme, under the 4.5:1 AA floor for 10.5px bold — in the one theme this is meant
       to stand out in. Pulling it toward --ink darkens it on cream (~4.5:1) and
       LIGHTENS it on dark, because --ink inverts per theme, so one declaration fixes
       both instead of a second hard-coded amber. */
    color: color-mix(in srgb, var(--warn) 60%, var(--ink));
  }
  .tag.tag-warn svg { width: 12px; height: 12px; flex: none; }
  /* ── D18: the two OFF states are not the same news ──
     "suspended" is a temporary access decision; "deleting" is a 30-day countdown
     to permanent erasure. Rendered as two identical grey pills they were only
     distinguishable by reading two similar small words, next to the Crew and
     Unassigned pills that look the same again — and the plain .tag measured
     ~4.2:1 in light, under the AA floor for 10.5px bold. So the same treatment
     #F8 established: tint plus weight, colour pulled toward --ink so ONE
     declaration works in both themes, and the WORDS still carry the meaning on
     their own for anyone who cannot see the tint. */
  .tag.tag-state {
    font-weight: 800;
    background: color-mix(in srgb, var(--muted) 16%, transparent);
    border-color: color-mix(in srgb, var(--muted) 50%, transparent);
    color: color-mix(in srgb, var(--muted) 45%, var(--ink));
  }
  .tag.tag-state.tag-deleting {
    background: color-mix(in srgb, var(--bad) 18%, transparent);
    border-color: color-mix(in srgb, var(--bad) 55%, transparent);
    color: color-mix(in srgb, var(--bad) 55%, var(--ink));
  }
  /* The healthy state gets quieter in the same change, so the CONTRAST does the work
     instead of the warning having to shout over an equally loud tick. Quieted by
     DROPPING THE PILL rather than by fading the text: an opacity here cost the label
     ~2:1 of contrast to say something a missing background already says.
     2026-08-26: the ROSTER no longer draws a healthy tick at all — a hundred of them
     beside one amber pill was not a contrast, and the absence is reported once in the
     attention strip instead. This treatment stays as the shared quiet-tag shape; it
     is currently drawn by nothing. */
  .tag.tag-quiet {
    display: inline-flex; align-items: center; gap: 4px;
    background: none; border-color: transparent; padding-left: 0;
  }
  .tag.tag-quiet svg { width: 11px; height: 11px; flex: none; }

  /* ── Placeholders / empty tabs ── */
  .placeholder { text-align: center; color: var(--muted); padding: 60px 22px; }
  .placeholder .pic {
    width: 74px; height: 74px; margin: 0 auto 16px; border-radius: 22px; background: var(--surface-2);
    border: 1px solid var(--border); display: grid; place-items: center; color: var(--muted);
  }
  .placeholder .pic svg { width: 34px; height: 34px; }
  .placeholder h2 { margin: 0 0 6px; color: var(--ink); font-size: 19px; }
  .placeholder p { margin: 0 auto; max-width: 300px; font-size: 13.5px; }

  /* ── Kept-alive tab panes: transparent wrappers; only #view is visible ── */
  .tabpane { display: block; }

  /* ── Skeleton loaders (first-visit shimmer; all via theme vars, light+dark) ── */
  .skel { display: inline-block; position: relative; overflow: hidden; background: var(--skel-base); border-radius: 8px; }
  .skel::after {
    content: ""; position: absolute; inset: 0; transform: translateX(-100%);
    background: linear-gradient(90deg, transparent, var(--skel-shine), transparent);
    animation: skel-shimmer 1.25s ease-in-out infinite;
  }
  @keyframes skel-shimmer { 100% { transform: translateX(100%); } }
  @media (prefers-reduced-motion: reduce) { .skel::after { animation: none; } }
  .skel-card {
    background: var(--surface); border: 1px solid var(--border); border-radius: var(--radius);
    padding: 14px; margin-bottom: 12px; box-shadow: var(--shadow);
  }
  .skel-row { display: flex; align-items: center; gap: 11px; margin-bottom: 12px; }
  .skel-lines { display: flex; flex-direction: column; gap: 7px; flex: 1; }
  .skel-line { height: 11px; border-radius: 6px; }
  .skel-line + .skel-line { margin-top: 7px; }
  .skel-card > .skel-line { display: block; }
  .skel-av { width: 40px; height: 40px; border-radius: 12px; flex: none; }
  .skel-ic { width: 40px; height: 40px; border-radius: 11px; flex: none; }
  .skel-bar { display: block; width: 100%; height: 42px; border-radius: 12px; margin-bottom: 12px; }
  .skel-photo { display: block; width: 100%; height: 150px; border-radius: 12px; margin-top: 10px; }
  .skel-chips { display: flex; gap: 8px; margin-bottom: 14px; flex-wrap: wrap; }
  .skel-chip { width: 92px; height: 30px; border-radius: 999px; flex: none; }
  .w15 { width: 15%; } .w20 { width: 20%; } .w30 { width: 30%; } .w40 { width: 40%; }
  .w60 { width: 60%; } .w70 { width: 70%; } .w80 { width: 80%; } .w90 { width: 90%; }

  /* ── Modal (bottom sheet on mobile, centered on desktop) ── */
  .modal-wrap { position: fixed; inset: 0; background: rgba(30,20,8,.5); display: none; align-items: flex-end; z-index: 30; }
  .modal-wrap.show { display: flex; }
  /* 88dvh, not 88vh: vh measures the LARGE viewport and ignores the keyboard, so a
     tall sheet could be capped at 88% of the whole screen while only half of it was
     actually on screen — the bottom of the sheet, buttons included, sat behind the
     keyboard. dvh follows the visible viewport, which is what a sheet has to fit
     inside. Identical to 88vh whenever no keyboard is up. */
  .modal {
    background: var(--surface); width: 100%; max-height: 88dvh; overflow-y: auto;
    border-radius: 22px 22px 0 0; padding: 8px 17px calc(22px + env(safe-area-inset-bottom));
    overscroll-behavior: contain;   /* no rubber-band that would hijack the drag on iOS */
  }
  /* Interactive grab handle (replaces the old ::before pill). touch-action:none is
     the key: the browser won't start a scroll/overscroll from it, so the drag-to-
     dismiss gesture works on iOS/WKWebView where preventDefault on pointer events
     does NOT stop native scrolling. Full-width for an easy hit target. */
  .modal::before { display: none; }
  .sheet-grab {
    touch-action: none; cursor: grab; margin: -2px -17px 6px; padding: 9px 0 11px;
    display: grid; place-items: center;
  }
  .sheet-grab::after { content: ""; width: 40px; height: 5px; border-radius: 999px; background: var(--border); }
  .modal h3 { margin: 2px 0 8px; font-size: 19px; }
  .integration-intro { margin: -2px 0 14px; font-size: 13px; }
  .integration-list { display: grid; gap: 9px; }
  .integration-card { border: 1px solid var(--border); border-radius: 13px; padding: 11px 12px; }
  .integration-card.is-revoked { opacity: .72; }
  .integration-card-head { display: flex; align-items: center; justify-content: space-between; gap: 10px; }
  .integration-status { color: var(--muted); font-size: 12px; white-space: nowrap; }
  .integration-meta { font-size: 12px; line-height: 1.45; margin-top: 5px; overflow-wrap: anywhere; }
  .integration-revoke { margin-top: 9px; padding: 7px 11px; font-size: 12px; }
  .integration-divider { height: 1px; background: var(--border); margin: 17px 0 14px; }
  .integration-help { font-size: 12px; margin: -5px 0 8px; }
  .integration-check { display: flex; align-items: center; gap: 8px; margin: 5px 0; }
  .integration-check input { width: auto; }
  #integration-secret { width: 100%; min-height: 72px; resize: none; font-family: var(--font-mono, monospace); font-size: 12px; overflow-wrap: anywhere; }
  .toast {
    position: fixed; bottom: calc(90px + env(safe-area-inset-bottom)); left: 50%; transform: translateX(-50%);
    background: var(--ink); color: var(--bg); padding: 11px 18px; border-radius: 18px; font-size: 13px; font-weight: 600;
    z-index: 50; opacity: 0; transition: opacity .2s; pointer-events: none; box-shadow: var(--shadow);
    /* Shrink-to-fit at left:50% caps the width at viewport−50% (~195px on a
       390px screen), so a long message wrapped to five lines and covered the
       card behind it. A real width budget instead, and a radius that still looks
       right once a message takes two lines rather than one. */
    width: max-content; max-width: min(88vw, 420px); text-align: center;
  }
  .toast.show { opacity: 1; }

  /* ── Feed ── */
  .storeswitch { display: flex; align-items: center; gap: 9px; margin-bottom: 14px; }
  .storeswitch .store-ic { width: 34px; height: 34px; border-radius: 10px; background: var(--surface-2);
    border: 1px solid var(--border); display: grid; place-items: center; color: var(--muted); flex: none; }
  .storeswitch .store-ic svg { width: 18px; height: 18px; }
  .storeswitch b { font-family: var(--font-round); font-size: 17px; }
  .storeswitch select { width: auto; flex: 1; }

  /* Channel chip rail — pinned channel switcher at the top of the feed. */
  .chan-rail { display: flex; gap: 8px; overflow-x: auto; padding: 1px 2px 11px; margin-bottom: 4px;
    scrollbar-width: none; -webkit-overflow-scrolling: touch; }
  .chan-rail::-webkit-scrollbar { display: none; }
  .chan-chip { flex: 0 0 auto; display: inline-flex; align-items: center; gap: 6px; position: relative;
    background: var(--surface-2); color: var(--ink); border: 1px solid var(--border);
    border-radius: 999px; padding: 7px 13px; font-size: 12.5px; font-weight: 600; cursor: pointer; white-space: nowrap; }
  .chan-chip svg { width: 15px; height: 15px; color: var(--muted); }
  .chan-chip.on { background: var(--accent); color: var(--accent-ink); border-color: var(--accent); }
  .chan-chip.on svg { color: var(--accent-ink); }
  .chan-chip .cc-count { min-width: 17px; height: 17px; padding: 0 4px; border-radius: 999px;
    background: var(--pepp); color: #fff; font-size: 10.5px; font-weight: 800; display: grid; place-items: center; }
  .chan-chip.on .cc-count { background: var(--accent-ink); color: var(--accent); }
  .chan-chip .cc-dot { width: 8px; height: 8px; border-radius: 50%; background: var(--pepp); }

  /* The progress ring produced by ring() in app.js — every checklist card draws
     one. It sat inside the Today rail's comment block until that block was
     deleted (2026-08-13); nothing but adjacency ever connected them, and this
     rule is what stops a ring from being squashed by the flex row around it. */
  .ring { flex: none; }

  /* Feed posts */
  .feed-list { display: flex; flex-direction: column; gap: 13px; }
  .post {
    background: var(--surface); border: 1px solid var(--border); border-radius: var(--radius);
    padding: 14px 15px; box-shadow: var(--shadow);
  }
  /* ── Read receipts: "Seen by X of Y" line (gm+ only) + nudge sheet ── */
  .seenby { margin-top: 11px; }
  .seenby-btn {
    display: inline-flex; align-items: center; gap: 6px; background: var(--surface-2);
    border: 1px solid var(--border); color: var(--muted); border-radius: 999px;
    padding: 5px 11px; font: inherit; font-size: 12.5px; font-weight: 600; cursor: pointer;
    font-variant-numeric: tabular-nums;
  }
  .seenby-btn:hover { color: var(--ink); }
  .seenby-btn svg { width: 14px; height: 14px; }
  .seenby-btn b { color: var(--ink); font-weight: 700; }
  .seen-list { margin: 4px 0 2px; }
  .seen-list .person { padding: 10px 0; }
  .seen-list .person .grow { min-width: 0; }
  .seen-sub { font-size: 11px; text-transform: uppercase; letter-spacing: .08em; color: var(--muted);
    font-weight: 700; margin: 14px 2px 2px; }
  .seen-check { color: var(--ok); flex: none; }
  .seen-check svg { width: 18px; height: 18px; }
  .nudge-btn {
    background: var(--accent); color: var(--accent-ink); border: none; border-radius: 999px;
    padding: 6px 13px; font: inherit; font-size: 12.5px; font-weight: 700; cursor: pointer; flex: none;
  }
  .nudge-btn:disabled { cursor: default; opacity: 1; }
  .nudge-btn.done { background: color-mix(in srgb, var(--ok) 18%, var(--surface-2)); color: var(--ok);
    border: 1px solid color-mix(in srgb, var(--ok) 40%, transparent); }
  .nudge-btn.recent { background: var(--surface-2); color: var(--muted); border: 1px solid var(--border); }
  /* ── Break Room read bubbles: stacked avatars under the newest message ── */
  /* The height is RESERVED from the first paint, and the row no longer collapses
     when empty. Both of those are the fix for a jump, not styling.
     This row is filled by a separate /reads request that lands well after the
     messages are on screen. While it collapsed to nothing, the thread was scrolled
     to a "bottom" that did not yet include it — so when the bubbles arrived the
     content grew underneath the fold and the re-pin dragged every message up by
     32px, a second or so after opening. Whether that happened at all depended on
     whether anyone had read the newest message, which is why it seemed to pick and
     choose between chats.
     Reserving the box means filling it changes no layout: the first scroll-to-bottom
     is already the true bottom, and the bubbles fade into space that was always
     theirs. The cost is a constant 24px under the last message in a thread nobody
     has read yet — breathing room above the composer, and it never moves. */
  .readrow {
    display: flex; justify-content: flex-end; align-items: center;
    margin: 2px 2px 6px; min-height: 24px;
  }
  .readrow-btn {
    display: inline-flex; align-items: center; gap: 7px; background: none; border: none;
    padding: 2px 0; cursor: pointer; font: inherit;
  }
  .rbub-stack { display: inline-flex; flex-direction: row-reverse; }
  .rbub {
    width: 20px; height: 20px; border-radius: 50%; display: grid; place-items: center;
    color: #fff; font-weight: 800; font-size: 9px; font-family: var(--font-round);
    border: 2px solid var(--surface); margin-left: -7px; flex: none;
    /* These bubbles overlap by 7px, and .has-photo adds position:relative — so
       without this, a bubble paints in front of its neighbour purely because
       that person happens to have uploaded a photo. Positioning ALL of them puts
       every bubble in the same stacking regime, so the overlap is decided by
       read order (via the stack's row-reverse) as it always was. */
    position: relative;
  }
  /* A read stack parked on the message somebody stopped at, rather than at the
     bottom of the thread. Same bubbles as .readrow uses; only the placement is
     different, so the two can never drift apart visually.
     Right-aligned and pulled up tight against the message it belongs to: any
     real gap and it reads as a receipt for the NEXT message down, which is the
     one thing it must never say. `align-self` rather than `float` because .msg
     is a flex column, and a float would be ignored. */
  .rbub-at {
    display: flex; align-self: flex-end; align-items: center;
    margin: -2px 2px 2px; cursor: pointer; flex-direction: row-reverse;
  }
  .rbub-at:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; border-radius: 999px; }
  /* Half a step smaller than the bottom row's. The bottom stack is a
     conclusion — everyone is current — and these are waypoints; sizing them the
     same made a thread read as though it had six Seen rows in it. */
  .rbub-at .rbub { width: 17px; height: 17px; font-size: 8px; margin-left: -6px; }
  .rbub:first-child { margin-left: 0; }
  .rbub.more { background: var(--surface-2); color: var(--muted); font-size: 9px; }
  .rr-label { font-size: 11px; font-weight: 700; color: var(--muted); letter-spacing: .02em; }
  /* ── Message Details sheet: Seen / Unseen / Reactions tabs ── */
  .msgd-tabs { display: flex; gap: 6px; margin: 4px 0 6px; }
  .msgd-tab {
    flex: 1; display: inline-flex; align-items: center; justify-content: center; gap: 6px;
    background: var(--surface-2); border: 1px solid var(--border); color: var(--muted);
    border-radius: 999px; padding: 8px 10px; font: inherit; font-size: 13px; font-weight: 700; cursor: pointer;
  }
  .msgd-tab.on { background: var(--accent); border-color: transparent; color: var(--accent-ink); }
  .msgd-n {
    font-size: 11px; font-weight: 800; font-variant-numeric: tabular-nums;
    background: color-mix(in srgb, currentColor 16%, transparent); border-radius: 999px; padding: 0 6px; min-width: 18px; text-align: center;
  }
  .msgd-body { min-height: 90px; }
  .msgd-empty { text-align: center; color: var(--muted); font-size: 13.5px; padding: 30px 10px; }
  .msgd-body .rx-emoji { font-size: 20px; flex: none; line-height: 1; }
  .post .ph { display: flex; align-items: center; gap: 10px; margin-bottom: 9px; }
  .post .ph .avatar { width: 40px; height: 40px; font-size: 14px; }
  .post .ph .pn { font-weight: 700; font-size: 14px; }
  .post .ph .pt { font-size: 11.5px; color: var(--muted); }
  .post .ph .in { font-size: 11.5px; color: var(--muted); font-weight: 500; }
  .post .bubble { white-space: pre-wrap; word-break: break-word; font-size: 15px; line-height: 1.5; }
  .photo-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(120px, 1fr)); gap: 6px; margin-top: 11px; }
  .photo-grid.single { grid-template-columns: 1fr; }
  /* A posted photo is NEVER cropped. `contain` is the belt for every tile: a
     multi-photo collage keeps uniform tiles, so a picture whose shape differs
     from the tile letterboxes against --surface-2 instead of losing its edges.
     The aspect-ratio is the box the tile keeps; only the `.natural` case below
     trades it for the picture's own ratio. Reserving a height at all matters
     beyond the layout jump — hydratePhotos() decides what to fetch from each
     image's rendered rect, and a stack of zero-height <img> would all read as
     on-screen and fetch at once. */
  .photo-grid img {
    width: 100%; aspect-ratio: 4/3; object-fit: contain; border-radius: var(--radius-sm);
    background: var(--surface-2); border: 1px solid var(--border);
  }
  /* Until it hydrates, an authenticated photo is an <img> with no src — and
     that is NOT a neutral empty box. WebKit draws the missing-image glyph and
     the alt text inside the frame we reserved, so a cold open flashes something
     that looks broken for the few frames before the bytes arrive. Replacing the
     element's content with a transparent pixel leaves the reserved box and its
     background exactly as they are and paints nothing inside it. Presentational
     only: the src attribute is untouched, so hydration, the scope purge and
     every test that asks whether a photo has actually arrived are unaffected,
     and the rule stops matching the moment src lands. Hiding the alt TEXT alone
     does not do it — the glyph is drawn by the element, not by the text. */
  .photo-grid img[data-src]:not([src]), .av-img[data-src]:not([src]) {
    content: url("data:image/gif;base64,R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7");
  }
  /* A single photo in the FEED gets no bars at all: the box takes the picture's own
     ratio, and --photo-cap bounds it in BOTH directions — the height directly, the
     width through the same ratio — so a tall phone photo shrinks to fit rather than
     being letterboxed or owning the whole screen. margin-inline centers it once
     that width cap binds. --photo-ar is the same ratio as a bare number because
     calc() cannot multiply by a `w / h` value. Only `.natural` grids opt in: the
     ratio arrives on load, and see renderPost() for why the thread must not reflow. */
  .photo-grid.single.natural { --photo-cap: min(70vh, 520px); }
  .photo-grid.single.natural img {
    aspect-ratio: var(--photo-ratio, 4/3);
    max-height: var(--photo-cap);
    max-width: calc(var(--photo-ar, 1.3333) * var(--photo-cap));
    margin-inline: auto;
  }
  .rx { display: flex; flex-wrap: wrap; gap: 7px; margin-top: 12px; }
  .rx button {
    background: var(--surface-2); border: 1px solid var(--border); color: var(--ink);
    border-radius: 999px; padding: 5px 11px; font-size: 14px; cursor: pointer; line-height: 1.1;
  }
  .rx button.on { background: color-mix(in srgb, var(--accent) 14%, var(--surface-2)); border-color: var(--accent); font-weight: 700; }
  .iclink { background: none; border: none; color: var(--muted); cursor: pointer; padding: 4px; border-radius: 8px; display: grid; place-items: center; }
  .iclink svg { width: 17px; height: 17px; }

  /* ── Thread (channel / DM) view ── */
  .feed-head { display: flex; align-items: center; gap: 10px; margin-bottom: 12px; }
  .feed-head .back {
    background: var(--surface-2); border: 1px solid var(--border); color: var(--ink);
    width: 38px; height: 38px; border-radius: 11px; display: grid; place-items: center; cursor: pointer; flex: none;
  }
  .feed-head .back svg { width: 20px; height: 20px; }
  .feed-head .ft { font-family: var(--font-round); font-weight: 800; font-size: 18px; flex: 1; }
  .thread .msg { padding: 11px 0; border-bottom: 1px solid var(--hair); }
  .thread .msg:last-child { border-bottom: none; }
  .thread .msg .ph { display: flex; align-items: center; gap: 9px; margin-bottom: 6px; }
  .thread .msg .ph .avatar { width: 32px; height: 32px; font-size: 12px; }
  .thread .msg .ph .pn { font-weight: 700; font-size: 13px; }
  .thread .msg .ph .pt { font-size: 11px; color: var(--muted); }
  .thread .msg .bubble { white-space: pre-wrap; word-break: break-word; }
  .composer {
    position: sticky; bottom: 0; background: var(--surface); border-top: 1px solid var(--border);
    padding: 10px 0 4px; margin-top: 8px;
  }
  .composer textarea { min-height: 44px; resize: vertical; }
  .composer .crow { display: flex; gap: 8px; align-items: flex-end; margin-top: 8px; }
  .composer .crow button.btn { margin-top: 0; width: auto; padding: 11px 18px; display: flex; align-items: center; gap: 7px; }
  .composer .crow button.btn svg { width: 18px; height: 18px; }
  .composer .pick {
    background: var(--surface-2); border: 1px solid var(--border); color: var(--ink);
    border-radius: var(--radius-sm); padding: 10px 12px; cursor: pointer; display: grid; place-items: center;
  }
  .composer .pick svg { width: 20px; height: 20px; }
  .composer .previews, .previews { display: flex; gap: 6px; flex-wrap: wrap; margin-top: 8px; }
  /* `contain`, not `cover`: the chip is a preview of what will be posted, and the
     post no longer crops, so a cropped chip would promise the wrong picture. */
  .composer .previews img, .previews img {
    width: 54px; height: 54px; object-fit: contain; border-radius: 9px; cursor: pointer;
    background: var(--surface-2);
  }

  /* ── @mention chips (subtle role-tint) ── */
  .mchip {
    display: inline; font-weight: 700; color: var(--ment-ink, var(--accent));
    background: color-mix(in srgb, var(--ment-tint, var(--accent)) 15%, transparent);
    border-radius: 5px; padding: 0 3px; cursor: pointer;
  }
  /* ── mention autocomplete list ──
     FIXED and full-bleed, sitting directly on top of the composer rather than
     floating beside it. In a chat thread the composer is already at the bottom of
     the screen, so a dropdown anchored BELOW its textarea opens behind the
     keyboard: the list was rendering the whole time, just off-screen, which reads
     as "@ does nothing". positionMentionPop() computes `bottom` from the field's
     own rect so this lands on its real top edge in both keyboard behaviours.
     Height is bounded there too — this scrolls inside whatever room is left. */
  .mention-pop {
    position: fixed; left: 0; right: 0; z-index: 60; background: var(--surface);
    border-top: 1px solid var(--border); border-bottom: 1px solid var(--border);
    box-shadow: 0 -14px 34px -24px #000;
    overflow-y: auto; -webkit-overflow-scrolling: touch;
    /* Reaching the end of this list must not start scrolling the conversation
       underneath it — the whole point is a list you can flick through. */
    overscroll-behavior: contain;
  }
  .mention-pop .mrow {
    display: flex; align-items: center; gap: 11px; padding: 11px 16px; cursor: pointer;
    font-size: 15px; font-weight: 600; border-bottom: 1px solid var(--hair);
  }
  .mention-pop .mrow:last-child { border-bottom: 0; }
  .mention-pop .mrow.sel, .mention-pop .mrow:hover { background: var(--surface-2); }
  .mention-pop .mrow .avatar { width: 34px; height: 34px; font-size: 12px; flex: none; }

  /* ── reply affordance + reply thread ── */
  .replybar { display: flex; align-items: center; gap: 8px; margin-top: 10px; }
  .replybar button {
    background: none; border: none; color: var(--muted); font: inherit; font-weight: 600; font-size: 12.5px;
    cursor: pointer; display: inline-flex; align-items: center; gap: 5px; padding: 3px 4px; border-radius: 7px;
  }
  .replybar button:hover { color: var(--ink); background: var(--surface-2); }
  .replybar button svg { width: 15px; height: 15px; }
  .replies {
    margin-top: 10px; padding-left: 13px; border-left: 2px solid var(--hair);
    display: flex; flex-direction: column; gap: 10px;
  }
  .replies .msg { padding: 6px 0; border: none; }
  .reply-cmp { margin-top: 9px; display: flex; gap: 7px; align-items: flex-end; }
  .reply-cmp textarea { min-height: 38px; flex: 1; min-width: 0; }
  /* The base button.btn is width:100%/margin-top:12px (full-width form button).
     In the reply row it must be a compact pill that sizes to its icon, or it
     eats the whole row and crushes the textarea. Mirror the feed composer's
     .crow reset. */
  .reply-cmp button.btn { width: auto; flex: none; margin-top: 0; padding: 10px 16px; }

  /* ── notifications drawer ── */
  .notif-list { display: flex; flex-direction: column; }
  .notif-row {
    display: flex; align-items: flex-start; gap: 11px; padding: 12px 4px; border-bottom: 1px solid var(--hair);
    cursor: pointer; text-align: left; background: none; border-left: none; border-right: none; border-top: none;
    width: 100%; color: var(--ink); font: inherit;
  }
  .notif-row:last-child { border-bottom: none; }
  .notif-row.unread { background: color-mix(in srgb, var(--accent) 7%, transparent); }
  .notif-row .nic {
    width: 34px; height: 34px; border-radius: 10px; display: grid; place-items: center; flex: none;
    background: var(--surface-2); color: var(--muted);
  }
  .notif-row .nic svg { width: 17px; height: 17px; }
  .notif-row .ngrow { flex: 1; min-width: 0; }
  .notif-row .ntitle { font-size: 13.5px; font-weight: 700; }
  .notif-row .nsnip { font-size: 12.5px; color: var(--muted); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
  .notif-row .nt { font-size: 11px; color: var(--muted); flex: none; font-variant-numeric: tabular-nums; }

  /* Inbox rows */
  .chat-item { display: flex; align-items: center; gap: 12px; padding: 12px 2px; border-bottom: 1px solid var(--hair); cursor: pointer; }
  .chat-item:last-child { border-bottom: none; }
  .chat-item .ci-ico {
    width: 42px; height: 42px; border-radius: 13px; display: grid; place-items: center;
    background: var(--surface-2); color: var(--muted); flex: none;
  }
  .chat-item .ci-ico svg { width: 20px; height: 20px; }
  .chat-item .ci-name { font-weight: 700; }
  .chat-item .ci-sub { font-size: 12.5px; color: var(--muted); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; max-width: 60vw; }
  .badge {
    display: inline-grid; place-items: center; min-width: 20px; height: 20px; padding: 0 6px;
    border-radius: 999px; background: var(--pepp); color: #fff; font-size: 11.5px; font-weight: 800;
  }

  /* ── Checklists (Phase 2b) ── */
  .cl-subnav { display: flex; gap: 6px; margin-bottom: 14px; }
  .cl-subnav button { flex: 1; padding: 9px 4px; font-size: 13px; margin-top: 0; }
  .cl-pickrow { display: flex; gap: 8px; margin-bottom: 14px; align-items: center; }
  .cl-pickrow select, .cl-pickrow input { padding: 9px 10px; font-size: 14px; }
  .cl-card {
    background: var(--surface); border: 1px solid var(--border); border-radius: var(--radius);
    padding: 14px 15px; margin-bottom: 11px; cursor: pointer; box-shadow: var(--shadow);
  }
  .cl-card .top { display: flex; align-items: center; gap: 10px; }
  .cl-card .nm { font-weight: 700; flex: 1; font-size: 15px; }
  .cl-win { font-size: 12px; color: var(--muted); margin-top: 4px; display: flex; align-items: center; gap: 5px; }
  .cl-win svg { width: 13px; height: 13px; flex: none; }
  .cl-state { font-size: 11px; font-weight: 800; padding: 3px 10px; border-radius: 999px; white-space: nowrap; }
  .cl-state.done { background: color-mix(in srgb, var(--ok) 20%, transparent); color: var(--ok); }
  .cl-state.late { background: color-mix(in srgb, var(--warn) 22%, transparent); color: var(--warn); }
  .cl-state.partial { background: color-mix(in srgb, var(--warn) 22%, transparent); color: var(--warn); }
  .cl-state.missed { background: color-mix(in srgb, var(--bad) 18%, transparent); color: var(--bad); }
  .cl-state.pending { background: var(--surface-2); color: var(--muted); border: 1px solid var(--border); }
  .cl-bar { height: 7px; background: var(--surface-2); border-radius: 999px; overflow: hidden; margin-top: 11px; }
  .cl-bar > span { display: block; height: 100%; background: var(--ok); border-radius: 999px; transition: width .25s; }
  .cl-item { display: flex; align-items: flex-start; gap: 12px; padding: 14px 0; border-bottom: 1px solid var(--hair); }
  .cl-item:last-child { border-bottom: none; }
  .cl-check {
    width: 28px; height: 28px; border-radius: 9px; border: 2px solid var(--border); background: var(--surface-2);
    display: grid; place-items: center; flex: none; color: transparent; cursor: pointer;
  }
  .cl-check svg { width: 17px; height: 17px; }
  .cl-check.on { background: var(--ok); border-color: var(--ok); color: #fff; }
  .cl-item .il { flex: 1; min-width: 0; }
  .cl-item .il .lab { font-weight: 600; }
  .cl-item .il .meta { font-size: 11.5px; color: var(--muted); margin-top: 3px; }
  .cl-item .il .req { color: var(--pepp); font-weight: 700; }
  .cl-item .il .opt { color: var(--muted); font-weight: 600; }
  .cl-item input.clv, .cl-item textarea.clv { margin-top: 8px; }
  .cl-thumb { width: 48px; height: 48px; border-radius: 9px; object-fit: cover; border: 1px solid var(--border); background: var(--surface-2); }
  .cl-grid { width: 100%; border-collapse: separate; border-spacing: 3px; font-size: 11px; }
  .cl-grid td, .cl-grid th { padding: 5px 3px; text-align: center; }
  .cl-grid th { color: var(--muted); font-weight: 700; }
  .cl-grid td.st { text-align: left; font-weight: 700; white-space: nowrap; padding-left: 2px; }
  .cl-cell { display: inline-grid; place-items: center; min-width: 34px; height: 28px; border-radius: 8px; font-weight: 800; }
  .cl-cell.g { background: color-mix(in srgb, var(--ok) 18%, transparent); color: var(--ok); }
  .cl-cell.o { background: color-mix(in srgb, var(--warn) 20%, transparent); color: var(--warn); }
  .cl-cell.r { background: color-mix(in srgb, var(--bad) 16%, transparent); color: var(--bad); }
  .cl-cell.n { color: var(--muted); background: var(--surface-2); }
  .cl-wd { display: flex; flex-wrap: wrap; gap: 6px; margin-top: 4px; }
  .cl-ie { border: 1px solid var(--border); border-radius: var(--radius-sm); padding: 11px; margin-bottom: 8px; background: var(--surface-2); }
  .cl-ie .row { margin-top: 7px; }

  /* Compliance snapshot (sidebar) */
  .snap-row { display: flex; align-items: center; gap: 9px; padding: 9px 0; border-bottom: 1px solid var(--hair); }
  .snap-row:last-child { border-bottom: none; }
  .snap-row .st { font-weight: 700; flex: 1; }
  .snap-dots { display: flex; gap: 3px; }
  .snap-dots i { width: 9px; height: 9px; border-radius: 3px; background: var(--surface-2); display: inline-block; }
  .snap-dots i.g { background: var(--ok); } .snap-dots i.o { background: var(--warn); } .snap-dots i.r { background: var(--bad); }

  /* ── Schedule (Phase 3) ── */
  .sched-nav { display: flex; align-items: center; gap: 8px; margin-bottom: 14px; }
  .sched-nav .navbtn { background: var(--surface-2); border: 1px solid var(--border); color: var(--ink);
    width: 38px; height: 38px; border-radius: 11px; display: grid; place-items: center; cursor: pointer; flex: none; }
  .sched-nav .navbtn svg { width: 19px; height: 19px; }
  .sched-nav .wk { flex: 1; text-align: center; }
  .sched-nav .wk b { font-family: var(--font-round); font-size: 16px; }
  .sched-nav .wk .sub { font-size: 11.5px; color: var(--muted); font-variant-numeric: tabular-nums; }
  .sched-toolbar { display: flex; flex-wrap: wrap; gap: 8px; align-items: center; margin-bottom: 14px; }
  .sched-toolbar select { width: auto; flex: 1; min-width: 130px; }
  .sched-toolbar .tbtn { margin-top: 0; width: auto; padding: 10px 13px; display: inline-flex; align-items: center;
    gap: 6px; font-size: 13px; white-space: nowrap; }
  .sched-toolbar .tbtn svg { width: 16px; height: 16px; }
  .pub-badge { display: inline-flex; align-items: center; gap: 5px; font-size: 11px; font-weight: 800;
    padding: 3px 10px; border-radius: 999px; white-space: nowrap; }
  .pub-badge svg { width: 12px; height: 12px; }
  .pub-badge.published { background: color-mix(in srgb, var(--ok) 20%, transparent); color: var(--ok); }
  .pub-badge.draft { background: color-mix(in srgb, var(--warn) 22%, transparent); color: var(--warn); }
  .week-hours { font-variant-numeric: tabular-nums; font-weight: 700; }

  .sched-grid { display: grid; grid-template-columns: 1fr; gap: 11px; }
  .sched-day { background: var(--surface); border: 1px solid var(--border); border-radius: var(--radius);
    padding: 12px; box-shadow: var(--shadow); }
  .sched-day.today { border-color: var(--accent); }
  .sched-day-head { display: flex; align-items: baseline; gap: 8px; margin-bottom: 9px; }
  .sched-day-head .dl { font-family: var(--font-round); font-weight: 800; font-size: 14px; }
  .sched-day-head .dd { font-size: 11px; color: var(--muted); font-variant-numeric: tabular-nums; }
  .sched-day-head .dh { margin-left: auto; font-size: 11px; color: var(--muted);
    font-variant-numeric: tabular-nums; font-weight: 700; }
  .shift-chip { display: flex; align-items: center; gap: 9px; padding: 9px 11px; border-radius: var(--radius-sm);
    background: var(--surface-2); border: 1px solid var(--border); border-left: 4px solid var(--chip-color, var(--muted));
    margin-bottom: 7px; cursor: pointer; }
  .shift-chip:last-of-type { margin-bottom: 0; }
  .shift-chip .who { font-weight: 700; font-size: 13.5px; flex: 1; min-width: 0; overflow: hidden;
    text-overflow: ellipsis; white-space: nowrap; }
  .shift-chip .tm { font-size: 12px; color: var(--muted); font-variant-numeric: tabular-nums; white-space: nowrap; }
  .shift-chip .rc { font-size: 10px; font-weight: 800; padding: 1px 7px; border-radius: 999px; color: #fff; white-space: nowrap; }
  .shift-chip.draft { border-style: dashed; background: transparent; }
  .shift-chip.open .who { color: var(--muted); font-style: italic; }
  /* Picked-up shift: a marketplace claim that landed here — dashed accent frame +
     accent tint, distinct from a solid WAAG/scheduled chip and a warn-dashed draft. */
  .shift-chip.pickedup { border-style: dashed; border-color: var(--accent);
    background: color-mix(in srgb, var(--accent) 8%, var(--surface-2)); --chip-color: var(--accent); }
  .pickup-pill { font-size: 9.5px; font-weight: 800; text-transform: uppercase; letter-spacing: .05em;
    color: var(--accent); background: color-mix(in srgb, var(--accent) 15%, transparent);
    border: 1px solid var(--accent); padding: 1px 6px; border-radius: 999px; white-space: nowrap; }
  .draft-pill { font-size: 9.5px; font-weight: 800; text-transform: uppercase; letter-spacing: .05em; color: var(--warn);
    background: color-mix(in srgb, var(--warn) 18%, transparent); padding: 1px 6px; border-radius: 999px; white-space: nowrap; }
  /* ── Edit mode on an imported week (gm+, owner-switched) ──────────────────
     Every state here has to survive being read at arm's length on a phone in a
     store, so each one carries a colour AND a shape: an editable chip lifts, a
     pending change is dashed, and one marked for removal is struck through. Colour
     alone would collapse for a colour-blind manager into "the chips look the same". */
  .shift-chip.editable { box-shadow: 0 1px 0 var(--border); }
  .shift-chip.editable:active { transform: scale(.985); }
  .shift-chip.pending-edit { border-style: dashed; border-color: var(--warn);
    background: color-mix(in srgb, var(--warn) 8%, var(--surface-2)); }
  .shift-chip.pending-remove { border-style: dashed; border-color: var(--bad);
    background: color-mix(in srgb, var(--bad) 8%, var(--surface-2)); opacity: .72; }
  .shift-chip.pending-remove .who, .shift-chip.pending-remove .tm { text-decoration: line-through; }
  .shift-chip.pending { border-style: dashed; border-color: var(--warn); background: transparent;
    --chip-color: var(--warn); }
  .pending-pill { font-size: 9.5px; font-weight: 800; text-transform: uppercase; letter-spacing: .05em;
    color: var(--warn); background: color-mix(in srgb, var(--warn) 15%, transparent);
    border: 1px solid var(--warn); padding: 1px 6px; border-radius: 999px; white-space: nowrap; }
  /* Says the POS did not print this shift. Quiet on purpose — it is a fact about the
     row, not a warning about it, and it appears on a crew member's own schedule. */
  .edit-pill { font-size: 9.5px; font-weight: 800; text-transform: uppercase; letter-spacing: .05em;
    padding: 1px 6px; border-radius: 999px; white-space: nowrap; color: var(--muted);
    border: 1px solid var(--border); background: var(--surface); }
  .sched-edit-bar { display: flex; align-items: center; gap: 12px; flex-wrap: wrap;
    background: var(--surface); border: 1px solid var(--border); border-radius: var(--radius);
    padding: 11px 13px; margin: 0 2px 11px; box-shadow: var(--shadow); }
  .sched-edit-bar.has-changes { border-color: var(--warn);
    background: color-mix(in srgb, var(--warn) 7%, var(--surface)); }
  .sched-edit-bar .seb-text { display: flex; flex-direction: column; gap: 2px; min-width: 0; flex: 1; }
  .sched-edit-bar .seb-text b { font-family: var(--font-round); font-size: 14px; }
  .sched-edit-bar .seb-text .muted { font-size: 11.5px; }
  .sched-edit-bar .seb-actions { display: flex; gap: 8px; align-items: center; }
  .sched-edit-bar .seb-actions .btn { width: auto; margin: 0; padding: 9px 14px; white-space: nowrap; }
  .sched-edit-orphan { margin: 0 2px 11px; }
  .addshift { width: 100%; margin-top: 7px; background: none; border: 1px dashed var(--border); color: var(--muted);
    border-radius: var(--radius-sm); padding: 8px; font: inherit; font-size: 12.5px; font-weight: 600; cursor: pointer;
    display: flex; align-items: center; justify-content: center; gap: 5px; }
  .addshift svg { width: 15px; height: 15px; }

  /* Open-shift rail (crew) */
  .os-rail { display: flex; gap: 11px; overflow-x: auto; padding: 2px 2px 6px; scrollbar-width: none; }
  .os-rail::-webkit-scrollbar { display: none; }
  .os-card { flex: 0 0 auto; width: 184px; background: var(--surface); border: 1px solid var(--border);
    border-left: 4px solid var(--chip-color, var(--muted)); border-radius: var(--radius); padding: 13px; box-shadow: var(--shadow); }
  .os-card .osd { font-family: var(--font-round); font-weight: 800; display: flex; align-items: center; gap: 7px; }
  .os-card .ost { font-size: 12px; color: var(--muted); font-variant-numeric: tabular-nums; margin-top: 5px;
    display: flex; align-items: center; gap: 5px; }
  .os-card .ost svg { width: 13px; height: 13px; }
  .os-card .btn { margin-top: 11px; padding: 9px; display: flex; align-items: center; justify-content: center; gap: 6px; }
  .os-card .btn svg { width: 16px; height: 16px; }

  /* My-week rows (crew) */
  .mw-row { display: flex; align-items: center; gap: 12px; padding: 12px 0; border-bottom: 1px solid var(--hair);
    border-left: 4px solid var(--chip-color, var(--muted)); padding-left: 11px; border-radius: 3px; }
  .mw-row:last-child { border-bottom: 1px solid var(--hair); }
  .mw-day { display: flex; align-items: baseline; gap: 8px; margin: 16px 2px 8px; }
  .mw-day .wd { font-family: var(--font-round); font-weight: 800; font-size: 15px; }
  .mw-day .dt { font-size: 12px; color: var(--muted); font-variant-numeric: tabular-nums; }
  .mw-row .grow .tm { font-weight: 700; font-variant-numeric: tabular-nums; }
  .mw-row .grow .mt { font-size: 12px; color: var(--muted); margin-top: 2px; }

  /* Availability editor (crew) */
  .av-row { display: flex; align-items: center; gap: 10px; padding: 11px 0; border-bottom: 1px solid var(--hair); }
  .av-row:last-child { border-bottom: none; }
  .avd { font-weight: 800; font-family: var(--font-round); }
  .av-row .avd { width: 40px; flex: none; }
  .av-toggle { border: 1px solid var(--border); background: var(--surface-2); color: var(--muted); border-radius: 999px;
    padding: 6px 12px; font-size: 12px; font-weight: 700; cursor: pointer; flex: none; }
  .av-toggle.on { background: color-mix(in srgb, var(--ok) 16%, var(--surface-2)); border-color: var(--ok); color: var(--ok); }
  .av-row .times { display: flex; gap: 6px; flex: 1; align-items: center; }
  .av-row .times input { padding: 8px 9px; font-size: 13px; font-variant-numeric: tabular-nums; }
  .av-row .times .dash { color: var(--muted); }
  .av-row.off .times { opacity: .35; pointer-events: none; }

  /* Own availability card: a one-line summary carries the week; the seven-row
     breakdown, prefs and exceptions stay folded until asked for. */
  .av-sum { font-size: 13.5px; font-weight: 700; line-height: 1.5; margin: 7px 0 0;
    font-variant-numeric: tabular-nums; overflow-wrap: anywhere; }
  .av-details { margin-top: 6px; }
  .av-details > summary { list-style: none; cursor: pointer; display: flex; align-items: center; gap: 7px;
    min-height: 40px; font-size: 12.5px; font-weight: 800; color: var(--muted); }
  .av-details > summary::-webkit-details-marker { display: none; }
  .av-details > summary::after { content: ""; width: 7px; height: 7px; flex: none;
    border-right: 2px solid currentColor; border-bottom: 2px solid currentColor;
    transform: translateY(-2px) rotate(45deg); transition: transform .15s ease; }
  .av-details[open] > summary::after { transform: translateY(1px) rotate(225deg); }
  .av-details[open] > summary { border-bottom: 1px solid var(--hair); }

  /* Time-off */
  .toff-item { display: flex; align-items: center; gap: 11px; padding: 13px 0; border-bottom: 1px solid var(--hair); }
  .toff-item:last-child { border-bottom: none; }
  .toff-item .grow .dr { font-weight: 700; font-variant-numeric: tabular-nums; }
  .toff-item .grow .rs { font-size: 12.5px; color: var(--muted); margin-top: 3px; }
  .toff-item .grow .who { font-size: 12.5px; color: var(--muted); margin-top: 2px; }
  .toff-acts { display: flex; gap: 7px; flex: none; }
  .toff-acts button { margin: 0; padding: 8px 12px; font-size: 12.5px; width: auto; }
  .btn.approve { background: var(--ok); color: #fff; }
  .btn.deny { background: var(--surface-2); color: var(--bad); border: 1px solid color-mix(in srgb, var(--bad) 45%, transparent); }
  .cl-state.approved { background: color-mix(in srgb, var(--ok) 20%, transparent); color: var(--ok); }
  .cl-state.denied { background: color-mix(in srgb, var(--bad) 18%, transparent); color: var(--bad); }
  .cl-state.canceled { background: var(--surface-2); color: var(--muted); border: 1px solid var(--border); }
  .warn-banner { background: color-mix(in srgb, var(--warn) 14%, transparent);
    border: 1px solid color-mix(in srgb, var(--warn) 40%, transparent); color: var(--warn); border-radius: var(--radius-sm);
    padding: 10px 12px; font-size: 12.5px; font-weight: 600; margin-top: 11px; display: flex; gap: 8px; align-items: flex-start; }
  .warn-banner svg { width: 16px; height: 16px; flex: none; margin-top: 1px; }
  .sched-subnav { display: flex; gap: 6px; margin-bottom: 14px; }
  .sched-subnav button { flex: 1; padding: 9px 4px; font-size: 13px; margin-top: 0; }
  /* Blackout dates banner */
  .blk-banner { border-left: 3px solid var(--warn); }
  .blk-row { display: flex; align-items: center; gap: 10px; padding: 8px 0; border-bottom: 1px solid var(--hair); }
  .blk-row:last-child { border-bottom: none; }
  .blk-dot { width: 9px; height: 9px; border-radius: 50%; background: var(--warn); flex: none; }

  /* Imported (WAAG) schedule: provenance line, open/close pills */
  .sched-prov { display: flex; align-items: flex-start; gap: 7px; flex-wrap: wrap; font-size: 12px;
    color: var(--muted); font-variant-numeric: tabular-nums; margin: -4px 2px 12px; }
  /* Load-bearing, not tidiness: `display: flex` above is an AUTHOR rule and the
     `hidden` attribute is only a UA rule, so author wins and a hidden .sched-prov
     would render at full height. The cache notice is painted hidden and revealed
     only when a refresh actually fails, so without this line it would never be
     hidden at all and the layout shift it exists to avoid would come straight back. */
  .sched-prov[hidden] { display: none; }
  .sched-prov svg { width: 14px; height: 14px; flex: none; margin-top: 1px; }
  .sched-prov b { color: var(--ink); font-weight: 700; }
  .oc-pill { font-size: 9.5px; font-weight: 800; text-transform: uppercase; letter-spacing: .04em;
    padding: 1px 6px; border-radius: 999px; white-space: nowrap; }
  .oc-pill.open { color: var(--ok); background: color-mix(in srgb, var(--ok) 16%, transparent); }
  .oc-pill.close { color: var(--accent); background: color-mix(in srgb, var(--accent) 16%, transparent); }
  .nd-pill { font-size: 9px; font-weight: 800; color: var(--muted); margin-left: 3px; vertical-align: middle; }

  /* Show/hide password toggle (login, gate, PIN modal). */
  .pw-wrap { position: relative; }
  .pw-wrap input { padding-right: 46px; }
  .pw-eye { position: absolute; right: 5px; top: 50%; transform: translateY(-50%); background: none;
    border: none; color: var(--muted); cursor: pointer; padding: 7px; border-radius: 9px; display: grid; place-items: center; }
  .pw-eye:hover { color: var(--ink); }
  .pw-eye svg { width: 20px; height: 20px; }

  /* Availability onboarding gate — full-screen blocking step. */
  /* #F17: the first-run screen is the SAME surface as the availability gate one
     step later, so it shares every rule rather than looking like a different
     product. Selecting both is the point — each of these was id-scoped to
     #availgate, so #firstrun inherited none of it: no safe-area inset (content
     under the iPhone notch), no padding, no scroll container, and `.tag`
     falling through to the generic chip/pill rule instead of the muted
     subtitle. */
  #availgate, #firstrun { min-height: 100dvh; display: flex; flex-direction: column; align-items: center; justify-content: flex-start;
    padding: calc(28px + env(safe-area-inset-top)) 16px 40px; overflow-y: auto; }
  #availgate .brandbig, #firstrun .brandbig {
    display: flex; flex-direction: column; align-items: center; gap: 10px;
    text-align: center; margin-bottom: 14px; }
  #availgate .brandbig .logo, #firstrun .brandbig .logo {
    width: 56px; height: 56px; border-radius: 18px; background: var(--accent);
    color: var(--accent-ink); display: grid; place-items: center; }
  #availgate .brandbig .logo svg, #firstrun .brandbig .logo svg { width: 31px; height: 31px; }
  #availgate .brandbig h1, #firstrun .brandbig h1 { font-size: 25px; margin: 0; }
  /* The generic `.tag` is a chip. Inside a brand block it is a subtitle, and it
     needs the same reset #login already applies. */
  #availgate .brandbig .tag, #firstrun .brandbig .tag {
    background: none; border: none; padding: 0; margin: 0; color: var(--muted);
    font-size: 12.5px; font-weight: 600; letter-spacing: .04em; }
  #availgate .card, #firstrun .card { width: 100%; max-width: 460px; }
  #availgate .ag-lead, #firstrun .ag-lead { font-size: 13px; margin: -2px 0 12px; }

  /* Role swatch (Team → job roles): color only, no hex text. */
  .role-swatch { width: 30px; height: 30px; border-radius: 9px; flex: none; border: 1px solid var(--border); }

  /* Give-away / swap volunteer cards (crew "up for grabs" rail). */
  .grab-card .btn { margin-top: 11px; padding: 9px; display: flex; align-items: center; justify-content: center; gap: 6px; }
  .grab-card .gst { font-size: 12px; color: var(--muted); font-variant-numeric: tabular-nums; margin-top: 5px;
    display: flex; align-items: center; gap: 6px; }
  .grab-card .gst svg { width: 13px; height: 13px; }
  .grab-wait { margin-top: 11px; display: flex; align-items: center; justify-content: center; gap: 6px;
    font-size: 12px; font-weight: 700; color: var(--muted); background: var(--surface-2);
    border: 1px solid var(--border); border-radius: var(--radius-sm); padding: 9px; }
  .grab-wait svg { width: 14px; height: 14px; }

  /* Coverage / up-for-grabs marketplace (Job B). */
  .mkt-kind { display: inline-block; font-size: 9.5px; font-weight: 800; text-transform: uppercase;
    letter-spacing: .05em; padding: 1px 6px; border-radius: 999px; vertical-align: middle;
    border: 1px solid var(--border); color: var(--muted); background: var(--surface-2); }
  .mkt-kind.grab { color: var(--accent); border-color: var(--accent); background: transparent; }
  .mkt-kind.cov { color: var(--text); }
  .mkt-kind.pickedup { color: var(--accent); border-color: var(--accent);
    background: color-mix(in srgb, var(--accent) 14%, transparent); }
  /* Give-aways sit in the same rail as coverage posts but are not first-come — this
     line is what tells a crew member the GM still has to sign off. Muted and small:
     it explains the card, it doesn't compete with the action. */
  .mkt-approve { color: var(--muted); font-style: italic; }
  /* The two mechanisms on this rail look identical and behave oppositely — one is
     yours on tap, one is a request. The kind badge says WHAT the shift is; this
     says what pressing the button will actually do. Filled rather than outlined so
     it reads before the body copy does. */
  .mkt-flow-row { margin-top: 6px; }
  .mkt-flow { display: inline-flex; align-items: center; gap: 3px;
    font-size: 9.5px; font-weight: 800; text-transform: uppercase; letter-spacing: .05em;
    padding: 1px 6px; border-radius: 999px; vertical-align: middle; border: 1px solid transparent; }
  .mkt-flow svg { width: 10px; height: 10px; stroke-width: 2.2; }
  .mkt-flow.instant { color: var(--ok);
    border-color: color-mix(in srgb, var(--ok) 45%, transparent);
    background: color-mix(in srgb, var(--ok) 12%, transparent); }
  .mkt-flow.approve { color: var(--muted); border-color: var(--border);
    background: var(--surface-2); }
  .os-card-actions { display: flex; gap: 8px; margin-top: 11px; }
  .os-card-actions .btn { margin-top: 0; flex: 1; padding: 8px; font-size: 12.5px; }
  .os-card-actions .btn svg { width: 14px; height: 14px; }
  .mw-row .grabbtn { margin-top: 0; width: auto; flex: none; padding: 7px 11px; font-size: 12px;
    display: inline-flex; align-items: center; gap: 5px; }
  .mw-row .grabbtn svg { width: 13px; height: 13px; }
  .mw-row.grabbing { --chip-color: var(--accent); }
  .mw-row.pickedup { --chip-color: var(--accent); }
  /* An away row is INFORMATION, not an action or a claim, so it stays muted while
     a real pickup keeps the accent. The two used to render identically, which is
     how a mis-linked shift came to look like one the person had claimed. */
  .mw-row.away { --chip-color: var(--muted); }
  .mkt-kind.away { color: var(--muted); border-color: var(--border);
    background: var(--surface-2); }
  .cov-row { display: flex; gap: 11px; }
  .cov-row > div { flex: 1; }
  .cov-toggle { display: flex; align-items: center; gap: 9px; margin-top: 14px; font-size: 14px; color: var(--text); }

  /* Swap requests (gm+/manager) + generic approval rows. */
  .swap-item { display: flex; align-items: flex-start; gap: 11px; padding: 13px 0; border-bottom: 1px solid var(--hair); }
  .swap-item:last-child { border-bottom: none; }
  .swap-item .grow .sw-t { font-weight: 700; font-variant-numeric: tabular-nums; }
  .swap-item .grow .sw-s { font-size: 12.5px; color: var(--muted); margin-top: 3px; }
  .swap-acts { display: flex; flex-direction: column; gap: 6px; flex: none; align-items: flex-end; }
  .swap-acts .toff-acts { flex-direction: row; }
  .approval-hint { display: inline-flex; align-items: center; gap: 5px; font-size: 11px; font-weight: 700;
    color: var(--muted); background: var(--surface-2); border: 1px solid var(--border);
    border-radius: 999px; padding: 4px 10px; white-space: nowrap; }
  .approval-hint svg { width: 13px; height: 13px; }
  .kind-pill { font-size: 9.5px; font-weight: 800; text-transform: uppercase; letter-spacing: .04em;
    padding: 2px 7px; border-radius: 999px; color: var(--accent);
    background: color-mix(in srgb, var(--accent) 16%, transparent); margin-left: 6px; vertical-align: middle; }

  /* Manager availability overview (people × days). Mobile-first: per-person cards. */
  .avg-head { display: none; }
  .avg-row { background: var(--surface); border: 1px solid var(--border); border-radius: var(--radius);
    padding: 13px; box-shadow: var(--shadow); margin-bottom: 11px; }
  .avg-name { display: flex; align-items: center; gap: 9px; font-family: var(--font-round); font-weight: 800;
    font-size: 14.5px; margin-bottom: 6px; }
  .avg-name .nm { flex: 1; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
  .avg-c { display: flex; align-items: center; justify-content: space-between; gap: 10px; padding: 8px 0;
    border-bottom: 1px solid var(--hair); font-variant-numeric: tabular-nums; }
  .avg-c:last-child { border-bottom: none; }
  .avg-c .cd { font-size: 12.5px; font-weight: 700; color: var(--muted); }
  .avg-c .val { font-size: 13px; font-weight: 700; font-variant-numeric: tabular-nums; }
  .avg-c .val.win { color: var(--ink); }
  .avg-c .val.muted { color: var(--muted); font-weight: 600; }
  .avg-c .val.unset { color: var(--muted); opacity: .5; }
  .avg-hint { display: flex; align-items: center; gap: 6px; font-size: 12px; margin: 4px 2px 0; }
  .avg-hint svg { width: 13px; height: 13px; flex: none; }

  /* By person / by day switch — the same rows, read the other direction. */
  .avseg { display: inline-flex; gap: 3px; flex: none; padding: 3px;
    background: var(--surface-2); border: 1px solid var(--border); border-radius: 999px; }
  .avseg .avseg-b { width: auto; margin: 0; flex: none; border: none; background: transparent;
    color: var(--muted); border-radius: 999px; padding: 8px 13px; min-height: 38px; cursor: pointer;
    font-family: var(--font-round); font-weight: 800; font-size: 12.5px;
    display: inline-flex; align-items: center; gap: 5px; white-space: nowrap; }
  .avseg .avseg-b svg { width: 14px; height: 14px; flex: none; }
  .avseg .avseg-b.on { background: var(--surface); color: var(--ink); box-shadow: var(--shadow); }

  /* Availability by day: one card per weekday, everyone who can work it. */
  .avd-days { display: grid; gap: 11px; }
  .avd-day { background: var(--surface); border: 1px solid var(--border); border-radius: var(--radius);
    padding: 13px; box-shadow: var(--shadow); }
  .avd-day.today { border-color: var(--accent); }
  .avd-head { display: flex; align-items: baseline; gap: 8px; margin-bottom: 4px; }
  .avd-head .dw { font-family: var(--font-round); font-weight: 800; font-size: 15px; flex: 1; }
  .avd-head .ct { font-size: 11.5px; font-weight: 800; color: var(--muted); white-space: nowrap; }
  .avd-p { display: flex; align-items: center; gap: 9px; padding: 8px 0;
    border-bottom: 1px solid var(--hair); font-variant-numeric: tabular-nums; }
  .avd-p:last-of-type { border-bottom: none; }
  .avd-p .nm { font-size: 13.5px; font-weight: 700; min-width: 0;
    overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
  .avd-p .val { font-size: 13px; font-weight: 800; flex: none; }
  .avd-day .empty { padding: 12px 0; }
  .avd-off { font-size: 12px; line-height: 1.4; margin-top: 8px; padding-top: 9px;
    border-top: 1px solid var(--hair); overflow-wrap: anywhere; }

  @media (min-width: 980px) {
    /* minmax(0, 1fr), not 1fr. A bare `1fr` is `minmax(auto, 1fr)`, and that auto
       minimum is the COLUMN'S CONTENT — a shift chip with a name in it — so the
       seven columns refused to shrink below ~1277px total and the week simply
       ran off the right-hand edge with no scrollbar to reach it. Sunday was
       unreachable on any window under ~1500px. Zeroing the minimum lets the
       columns share whatever width there is, which is what `1fr` was meant to
       say. (Pre-existing, and it is why the page scrolled sideways in the
       980-1179 band; the desktop rail made it worse by taking 236px, which is
       how it was finally noticed — Cole, 2026-08-13.) */
    .sched-grid { grid-template-columns: repeat(7, minmax(0, 1fr)); gap: 9px; align-items: start; }
    .sched-day { padding: 10px; }
    .sched-day-head { flex-wrap: wrap; }
    .sched-day-head .dh { margin-left: 0; width: 100%; }
    .shift-chip { flex-wrap: wrap; gap: 4px 6px; padding: 7px 8px; }
    .shift-chip .tm { width: 100%; }
    .addshift { padding: 6px; }

    /* Availability overview → true people × days matrix. */
    .avg-head { display: grid; grid-template-columns: minmax(130px, 1.2fr) repeat(7, 1fr); gap: 6px;
      padding: 0 6px 8px; align-items: end; }
    .avg-head .avg-name-h { font-size: 11px; font-weight: 800; letter-spacing: .04em; text-transform: uppercase; color: var(--muted); }
    .avg-head .avg-h { text-align: center; display: flex; flex-direction: column; gap: 1px; }
    .avg-head .avg-h .wd { font-family: var(--font-round); font-weight: 800; font-size: 12.5px; }
    .avg-head .avg-h.today .wd { color: var(--accent); }
    .avg-row { display: grid; grid-template-columns: minmax(130px, 1.2fr) repeat(7, 1fr); gap: 6px;
      align-items: stretch; padding: 8px 6px; margin-bottom: 6px; box-shadow: none; }
    .avg-name { margin-bottom: 0; font-size: 13.5px; }
    .avg-c { flex-direction: column; align-items: center; justify-content: center; text-align: center;
      gap: 0; padding: 6px 4px; border-bottom: none; background: var(--surface-2); border-radius: 9px; min-height: 40px; }
    .avg-c .cd { display: none; }
    .avg-c .val { font-size: 12px; }
    .avd-days { grid-template-columns: repeat(2, 1fr); gap: 12px; }
  }

  /* ── Crew Social concept: warm stories rail + conversational feed ── */
  /* #startup rides BOTH light palette rules on purpose (Cole, 2026-07-25): the
     loading screen is light no matter which theme the app is in. Adding the
     selector to the rules that already define light means there is no second copy
     of these values to drift — and because custom properties declared on #startup
     beat the ones inherited from a dark :root, the whole subtree flips light
     without touching anything else. --crust/--cheese are derived from --mozz in
     the base rule above, which #startup also matches, so the pizza re-derives
     from the LIGHT --mozz rather than inheriting a dark crust. */
  :root, #startup {
    --bg:#fffaf3; --surface:#fffdf9; --surface-2:#f7eee2; --surface-3:#f1e2d0;
    --border:#eadcc9; --hair:#f1e6d8; --ink:#2d2520; --text:#2d2520; --muted:#806f62;
    --accent:#ef6f61; --accent-ink:#fffaf3; --basil:#53b587; --mozz:#f2c85b; --pepp:#ef6f61;
    --ok:#3e9d76; --warn:#c4871f; --bad:#d75549; --chip-color:#ef6f61;
    --ment-ink:#c24b41; --ment-tint:#ef6f61;
    --announce-bg:#fff1c9; --announce-line:#e8c96c; --announce-ink:#71551b;
    --radius:22px; --radius-sm:14px; --font-round:"Nunito", "Avenir Next Rounded", ui-rounded, system-ui, sans-serif;
    --shadow:0 2px 4px rgba(74,47,27,.04), 0 12px 28px -18px rgba(74,47,27,.25);
  }
  :root[data-theme="dark"] {
    --bg:#171b25; --surface:#202634; --surface-2:#2a3141; --surface-3:#333c4d;
    --border:#3a4659; --hair:#30394a; --ink:#f7f0e8; --text:#f7f0e8; --muted:#a9a6a4;
    --accent:#f58a78; --accent-ink:#271c1d; --basil:#72c89b; --mozz:#f2cf72; --pepp:#f58a78;
    --ok:#72c89b; --warn:#f2cf72; --bad:#f58a78; --chip-color:#f58a78;
    --ment-ink:#ffb1a3; --ment-tint:#f58a78;
    --announce-bg:#3a3425; --announce-line:#716038; --announce-ink:#f5d781;
    --shadow:0 2px 5px rgba(0,0,0,.25), 0 14px 30px -20px rgba(0,0,0,.6);
  }
  @media (prefers-color-scheme: dark) {
    :root:not([data-theme="light"]) { --bg:#171b25; --surface:#202634; --surface-2:#2a3141; --surface-3:#333c4d;
      --border:#3a4659; --hair:#30394a; --ink:#f7f0e8; --text:#f7f0e8; --muted:#a9a6a4;
      --accent:#f58a78; --accent-ink:#271c1d; --basil:#72c89b; --mozz:#f2cf72; --pepp:#f58a78;
      --ok:#72c89b; --warn:#f2cf72; --bad:#f58a78; --chip-color:#f58a78; --ment-ink:#ffb1a3; --ment-tint:#f58a78;
      --announce-bg:#3a3425; --announce-line:#716038; --announce-ink:#f5d781; }
  }
  .storeswitch { margin-bottom:10px; }
  .storeswitch .store-ic { background:var(--accent); color:var(--accent-ink); border:0; border-radius:13px; }
  .storeswitch b { font-size:18px; }
  .chan-rail { gap:14px; padding:7px 6px 15px; margin:-3px -6px 3px; }
  .chan-chip { flex-direction:column; gap:5px; background:transparent; border:0; padding:0 1px 2px; border-radius:0; font-size:11.5px; min-width:58px; color:var(--muted); }
  .chan-chip:hover, .chan-chip.on { background:transparent; color:var(--ink); }
  .chan-chip.on .story-avatar { box-shadow:0 0 0 3px var(--surface), 0 0 0 5px var(--accent); transform:translateY(-1px); }
  .story-avatar { width:52px; height:52px; border-radius:50%; display:grid; place-items:center; background:var(--basil); color:#fff; font-size:12px; font-weight:900; letter-spacing:-.02em; border:3px solid var(--surface); box-shadow:0 0 0 2px var(--border); }
  .story-everyone { background:var(--accent); color:#fff; }
  .story-announcements { background:var(--mozz); color:#684d13; }
  .story-crew, .story-custom, .story-role { background:var(--basil); color:#fff; }
  .story-name { max-width:72px; overflow:hidden; text-overflow:ellipsis; white-space:nowrap; font-weight:700; }
  .chan-chip .cc-count { position:absolute; top:0; right:0; min-width:19px; height:19px; padding:0 4px; border:2px solid var(--surface); background:var(--accent); }
  .chan-chip .cc-dot { position:absolute; top:1px; right:4px; width:11px; height:11px; border:2px solid var(--surface); background:var(--accent); }
  .feed-list { gap:15px; }
  .post { border-radius:24px; padding:17px 16px 15px; border-color:var(--border); box-shadow:var(--shadow); }
  .post .ph { gap:11px; margin-bottom:12px; }
  .post .ph .avatar { width:44px; height:44px; font-size:15px; border-radius:50%; }
  /* Beats the 50% above — same three-class specificity, and it has to sit after
     it. The app mark stays a squircle at this larger size. */
  .post .ph .app-avatar { border-radius:15px; }
  .post .ph .pn { font-family:var(--font-round); font-size:16px; font-weight:900; }
  .post .ph .pt { font-size:12px; }
  .post .bubble { line-height:1.58; }
  .photo-grid { gap:5px; margin-top:14px; }
  .photo-grid img { border-radius:16px; }
  .photo-grid.photos-2, .photo-grid.photos-4 { grid-template-columns:repeat(2,1fr); }
  .photo-grid.photos-3 { grid-template-columns:repeat(2,1fr); }
  .photo-grid.photos-3 img:first-child { grid-row:span 2; aspect-ratio:1/1; }
  .rx { gap:7px; margin-top:14px; }
  .rx button { min-height:36px; padding:6px 11px; font-size:18px; background:var(--surface-2); border-color:transparent; }
  .rx button.on { background:color-mix(in srgb, var(--accent) 18%, var(--surface-2)); border-color:var(--accent); }
  .rx button.rx-add { font-size:21px; line-height:1; color:var(--muted); padding:4px 13px; border:1px dashed var(--border); }
  .empty { font-family:var(--font-round); font-size:16px; font-weight:800; color:var(--ink); padding:42px 12px; }
  #login .brandbig h1 { font-family:var(--font-round); font-weight:900; }
  #login .brandbig .logo { background:var(--accent); box-shadow:0 10px 22px -12px var(--accent); }

  /* ── Login ── */
  /* Paints its own background from its own (always light) --bg. body's background
     comes from :root's --bg, which is dark for a dark-theme user, so the screen
     would still be dark behind a light mark without this. #startup is a direct
     child of body with flex:1 and every other top-level surface hidden while it
     shows, so this covers the viewport. */
  #startup { flex:1; display:flex; flex-direction:column; align-items:center; justify-content:center; gap:12px; padding:24px; text-align:center; background:var(--bg); color:var(--text); }
  /* The startup mark is a spinning pizza. The shadow lives on the non-rotating
     wrapper, so it stays put under the disc instead of orbiting with it. */
  #startup .startup-logo { width:68px; height:68px; filter:drop-shadow(0 9px 15px color-mix(in srgb, var(--crust) 38%, transparent)); }
  #startup .startup-logo svg { width:100%; height:100%; display:block; animation:pizza-spin 2.6s linear infinite; }
  @keyframes pizza-spin { to { transform: rotate(360deg); } }
  /* A stalled boot reveals #startup-retry. Stop the spin there: a loader that keeps
     turning under "Couldn't connect" promises progress that is not happening. Pure
     CSS, so it needs no cooperation from the scripts that set that state; browsers
     without :has() simply drop this rule and keep spinning. */
  #startup:has(#startup-retry:not(.hidden)) .startup-logo svg { animation:none; }
  @media (prefers-reduced-motion: reduce) { #startup .startup-logo svg { animation:none; } }
  /* Fills go through classes, not fill="var(--…)" presentation attributes — var()
     in a presentation attribute is not honored by WebKit, which is the engine both
     native shells run on. */
  #startup .pz-crust { fill:var(--crust); }
  #startup .pz-cheese { fill:var(--cheese); }
  #startup .pz-pepp { fill:var(--pepp); }
  #startup .pz-basil { fill:var(--basil); }
  #startup .startup-name { font-family:var(--font-round); font-size:27px; font-weight:900; }
  #startup .startup-status { color:var(--muted); font-size:13px; font-weight:600; }
  #startup #startup-retry { width:auto; margin-top:6px; }
  #login { flex: 1; display: flex; flex-direction: column; justify-content: center; padding: 24px; gap: 4px; max-width: 420px; margin: 0 auto; width: 100%; }
  #login .brandbig { display: flex; flex-direction: column; align-items: center; gap: 12px; margin-bottom: 22px; }
  #login .brandbig .logo { width: 62px; height: 62px; border-radius: 19px; background: var(--accent); color: var(--accent-ink); display: grid; place-items: center; }
  #login .brandbig .logo svg { width: 34px; height: 34px; }
  #login .brandbig h1 { font-size: 27px; margin: 0; }
  #login .brandbig .tag { background: none; border: none; padding: 0; color: var(--muted); font-size: 12.5px; font-weight: 600; letter-spacing: .04em; margin: 0; }
  .hidden { display: none !important; }

  /* ── Desktop (≥980px): centered feed column + right sidebar; top nav; no tabbar/FAB ── */
  @media (min-width: 980px) {
    nav.tabbar, .fab { display: none; }
    .topnav { display: flex; }
    .wrap { max-width: 1040px; padding: 22px 22px 40px; }
    .wrap.feed-wrap { max-width: 1040px; }
    .modal-wrap { align-items: center; }
    .modal { max-width: 460px; margin: 0 auto; border-radius: 20px; padding-top: 16px; }
    .modal::before { display: none; }
    .sheet-grab { display: none; }   /* centered dialog on desktop — no drag handle */
    .chat-item .ci-sub { max-width: 340px; }
    .toast { bottom: 28px; }
  }

/* Native iOS shell: every focusable control >=16px so WKWebView never auto-zooms
   on focus (a stuck zoom clips the right edge + tab bar). Web keeps compact sizes. */
html.native-ios input, html.native-ios select, html.native-ios textarea { font-size: 16px !important; }

/* ── Platform (app-owner) dashboard ─────────────────────────────────────────
   Cross-org KPIs as stat tiles + a readable org table with inline quota edit.
   Theme-var only (light + both dark blocks inherit); stacks under 700px. */
  .plat-kpis { display: grid; grid-template-columns: repeat(3, 1fr); gap: 10px; margin-bottom: 14px; }
  .plat-tile {
    background: var(--surface); border: 1px solid var(--border); border-radius: var(--radius);
    padding: 14px 15px; min-height: 92px; display: flex; flex-direction: column; gap: 2px; }
  .plat-tile.skel { min-height: 92px; }
  .plat-tile-ic { color: var(--muted); margin-bottom: 4px; }
  .plat-tile-ic svg { width: 18px; height: 18px; }
  .plat-tile-val { font-size: 30px; font-weight: 800; line-height: 1.05; letter-spacing: -.02em;
    font-variant-numeric: tabular-nums; color: var(--ink); }
  .plat-tile-lbl { font-size: 11.5px; text-transform: uppercase; letter-spacing: .07em;
    color: var(--muted); font-weight: 700; }

  .plat-table-card { padding-bottom: 6px; }
  .plat-table-scroll { overflow-x: auto; margin: 0 -2px; }
  table.plat-table { width: 100%; border-collapse: collapse; font-size: 13.5px; }
  .plat-table th {
    text-align: left; font-size: 11px; text-transform: uppercase; letter-spacing: .06em;
    color: var(--muted); font-weight: 700; padding: 4px 10px 8px; border-bottom: 1px solid var(--border);
    white-space: nowrap; }
  .plat-table th.plat-num, .plat-table td.plat-num { text-align: right; font-variant-numeric: tabular-nums; }
  .plat-table td { padding: 11px 10px; border-bottom: 1px solid var(--hair); vertical-align: middle; }
  .plat-table tr:last-child td { border-bottom: none; }
  .plat-table td.plat-org .name { font-weight: 700; color: var(--ink); }
  .plat-table td.plat-org .sub { font-size: 11.5px; }
  .plat-created, .plat-plan { white-space: nowrap; font-size: 12.5px; }

  .plat-quota-btn {
    background: var(--surface-2); border: 1px solid var(--border); color: var(--ink);
    border-radius: var(--radius-sm); padding: 5px 10px; font: inherit; font-weight: 700;
    font-variant-numeric: tabular-nums; cursor: pointer; }
  .plat-quota-btn.over { color: var(--bad); border-color: color-mix(in srgb, var(--bad) 45%, var(--border)); }
  .plat-quota-edit { display: inline-flex; align-items: center; gap: 6px; justify-content: flex-end; }
  .plat-quota-edit input { width: 62px; padding: 6px 8px; font-size: 14px; text-align: right; margin: 0; }
  .plat-note { font-size: 11.5px; padding: 8px 2px 4px; }

  /* Clickable org name → drill-in. A full-width text button so the whole cell is
     a hit target, with a subtle chevron affordance (the "back" glyph, rotated). */
  .plat-org-btn {
    display: flex; align-items: center; gap: 8px; width: 100%; background: none;
    border: none; padding: 0; margin: 0; font: inherit; color: inherit; cursor: pointer;
    text-align: left; }
  .plat-org-txt { display: flex; flex-direction: column; min-width: 0; }
  .plat-org-btn .name { font-weight: 700; color: var(--ink); }
  .plat-org-btn .sub { font-size: 11.5px; }
  .plat-org-chev { margin-left: auto; color: var(--muted); flex: none;
    transform: rotate(180deg); opacity: .6; transition: opacity .12s, transform .12s; }
  .plat-org-chev svg { width: 16px; height: 16px; }
  .plat-org-btn:hover .plat-org-chev { opacity: 1; transform: rotate(180deg) translateX(-2px); }
  .plat-org-btn:hover .name { color: var(--accent); }

  /* ── Org drill-in sheet (reuses #modal) ── */
  .plat-detail { display: flex; flex-direction: column; gap: 4px; }
  .plat-detail .modal-head { display: flex; align-items: flex-start; justify-content: space-between;
    gap: 12px; margin-bottom: 6px; }
  .plat-detail .modal-head h2 { margin: 0; font-size: 20px; letter-spacing: -.01em; }
  .plat-detail-title .sub { font-size: 12.5px; margin-top: 2px; }
  .plat-detail .section { margin: 14px 0 8px; }
  .plat-detail-meta { display: grid; grid-template-columns: 1fr 1fr; gap: 8px; margin: 2px 0 4px; }
  .plat-detail-meta > div { display: flex; flex-direction: column; gap: 2px;
    background: var(--surface-2); border: 1px solid var(--hair); border-radius: var(--radius-sm);
    padding: 8px 10px; }
  .plat-detail-meta .k { font-size: 10.5px; text-transform: uppercase; letter-spacing: .06em;
    color: var(--muted); font-weight: 700; }
  .plat-detail-meta .v { font-size: 15px; font-weight: 700; font-variant-numeric: tabular-nums; }
  .plat-owner { display: flex; align-items: center; gap: 10px; padding: 6px 0; }
  .plat-owner .avatar { width: 36px; height: 36px; font-size: 13px; border-radius: 11px; }
  .plat-owner-txt { display: flex; flex-direction: column; min-width: 0; flex: 1; }
  .plat-owner-txt .name { font-weight: 700; }
  .plat-owner-txt .sub { font-size: 12px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
  /* #F11: the org's non-email add-path switch. The state is carried by the chip's
     On/Off LABEL as well as its .on colour, so it never depends on hue alone, and
     the sub-line spells out the consequence rather than restating the setting. */
  .plat-flag { display: flex; align-items: flex-start; gap: 12px; padding: 4px 0 2px; }
  .plat-flag-txt { display: flex; flex-direction: column; gap: 2px; min-width: 0; flex: 1; }
  .plat-flag-txt .name { font-weight: 700; }
  .plat-flag-txt .sub { font-size: 12px; line-height: 1.45; }
  .plat-flag .chip { flex: none; margin-top: 2px; min-width: 52px; text-align: center; }
  /* À la carte entitlements. A child feature is indented under the master switch
     and, while that switch is off, dimmed rather than hidden — an admin has to be
     able to see what turning More back on would restore. The rail carries the
     nesting so the indent survives at 390px, where a wide gutter would not. */
  .plat-feat-child { padding-left: 12px; border-left: 2px solid var(--hair); margin-left: 2px; }
  .plat-feat-locked { opacity: 0.58; }
  .plat-feat-locked .chip[disabled] { cursor: default; }
  .plat-feat-cfg { display: flex; align-items: center; gap: 8px; flex-wrap: wrap; margin-top: 6px; }
  .plat-feat-cfg label { font-size: 12px; color: var(--muted); font-weight: 700; margin: 0; }
  .plat-feat-cfg select { width: auto; min-width: 116px; margin: 0; }
  /* ── More: the shelf for surfaces that do not earn a slot on the bar ────── */
  /* Rows are real <button>s, not divs with handlers, so they are keyboard and
     screen-reader reachable for free. 56px keeps the whole row inside the 44px
     touch-target floor with room to spare — this list is read at arm's length
     mid-shift, not studied. */
  .more-card { padding-bottom: 4px; }
  /* Library rows share this shape rather than restating it — same affordance,
     same touch target, one place to change either. */
  .more-row, .lib-row { display: flex; align-items: center; gap: 12px; width: 100%;
    min-height: 56px; padding: 8px 2px; margin: 0; background: none; border: 0;
    border-top: 1px solid var(--hair); border-radius: 0; color: var(--ink);
    font: inherit; text-align: left; cursor: pointer; }
  .more-row:first-of-type, .lib-row:first-of-type { border-top: 0; }
  .more-row:active, .lib-row:active { background: var(--surface-2); }
  .more-row-ic, .lib-row-ic { flex: none; display: flex; color: var(--accent); }
  .more-row-ic .ic, .lib-row-ic .ic { width: 21px; height: 21px; }
  .more-row-txt, .lib-row-txt { display: flex; flex-direction: column; gap: 1px;
    min-width: 0; flex: 1; }
  .more-row-txt .name, .lib-row-txt .name { font-weight: 700; }
  /* min-width:0 above plus ellipsis here: a long feature or file name must
     shorten rather than shove the chevron off the row at 390px. */
  .more-row-txt .sub, .lib-row-txt .sub { font-size: 12.5px; overflow: hidden;
    text-overflow: ellipsis; white-space: nowrap; }
  .more-row-go, .lib-row-go { flex: none; display: flex; color: var(--muted); }
  .more-row-go .ic, .lib-row-go .ic { width: 18px; height: 18px; }

  /* ── Library ──────────────────────────────────────────────────────────── */
  .lib-head { padding: 2px 0 10px; }
  .lib-title { font-size: 21px; font-weight: 800; margin: 8px 0 0; }
  /* The audience sentence. Always present on a folder, so nobody has to infer
     who can see what from the absence of a chip. */
  .lib-sub { font-size: 12.5px; margin: 3px 0 0; line-height: 1.45; }
  .lib-chip { flex: none; margin-left: 6px; font-size: 10.5px; }
  .lib-note { font-style: italic; }
  .lib-actions { display: flex; gap: 8px; flex-wrap: wrap; margin-top: 12px; }
  .lib-actions .btn { width: auto; flex: 1 1 auto; margin-top: 0; }
  .lib-skel-ic { width: 21px; height: 21px; flex: none; }
  .lib-skel-a { height: 12px; width: 58%; }
  .lib-skel-b { height: 10px; width: 34%; margin-top: 5px; }
  /* The in-app viewer. Images and text only — a PDF has no renderer in this
     build and never reaches here (see library_bp.INLINE_RENDERABLE_TYPES). */
  .lib-view { min-height: 120px; }
  .lib-img { display: block; width: 100%; height: auto; max-height: 70vh;
    object-fit: contain; border-radius: var(--radius-sm); background: var(--surface-2); }
  .lib-text { max-height: 60vh; overflow: auto; white-space: pre-wrap;
    word-break: break-word; font-size: 12.5px; line-height: 1.5;
    background: var(--surface-2); border-radius: var(--radius-sm); padding: 10px; }
  .lib-cant p { font-size: 13.5px; line-height: 1.5; }
  .lib-orig { font-size: 12px; margin: 2px 0 10px; overflow: hidden;
    text-overflow: ellipsis; white-space: nowrap; }
  .plat-tiers { display: flex; flex-wrap: wrap; gap: 8px; }
  .plat-tier { display: flex; flex-direction: column; align-items: center; gap: 1px;
    background: var(--surface-2); border: 1px solid var(--hair); border-radius: var(--radius-sm);
    padding: 8px 14px; min-width: 62px; }
  .plat-tier-n { font-size: 19px; font-weight: 800; font-variant-numeric: tabular-nums; }
  .plat-tier-l { font-size: 10.5px; color: var(--muted); font-weight: 700; }
  .plat-stores { display: flex; flex-direction: column; }
  .plat-store { display: flex; align-items: center; gap: 8px; padding: 7px 0;
    border-bottom: 1px solid var(--hair); font-size: 13.5px; }
  .plat-store:last-child { border-bottom: none; }
  .plat-store-ic { color: var(--muted); flex: none; }
  .plat-store-ic svg { width: 15px; height: 15px; }
  .plat-store .mono { font-weight: 700; font-variant-numeric: tabular-nums; min-width: 52px; }
  .plat-viewas { margin-top: 16px; }

  /* ── "Viewing as <org>" impersonation banner (persistent, above the header) ── */
  /* #F20: the store checklist on the channel sheets. Deliberately the same
     shape team.js's storeChecklist() writes inline (flex row, 8px gap, 7px
     vertical rhythm, auto-width control) so the two multi-store choices in this
     app look like one control rather than two — this is the class version of
     that markup, not a second widget. */
  .ck-row { display: flex; align-items: center; gap: 8px; margin: 7px 0; }
  .ck-row input { width: auto; }
  .ck-list { margin-left: 22px; }
  .ck-list.hidden { display: none; }

  /* #F2 Phase 1: the last-use column. Three states, and colour is never the only
     channel — the words say it too ("Never", "42 days ago"), because this is a
     table somebody scans quickly and a red dot alone means nothing. */
  .lastuse { font-weight: 600; }
  /* Mixed toward --ink rather than used raw. The bare palette values measure
     under the 4.5:1 AA floor on the light surface — in the one theme these two
     exist to stand out in — and this is the same fix #F8's amber tag needed.
     One declaration darkens on cream and lightens on dark. */
  .lastuse.fresh { color: color-mix(in srgb, var(--basil) 58%, var(--ink)); }
  .lastuse.stale { color: color-mix(in srgb, var(--warn) 58%, var(--ink)); }
  .lastuse.never { color: var(--muted); font-style: italic; }

  /* #F4: the recognition subject picker — a token field, not a dropdown.
     Chips sit above the input rather than inline so a long name never squeezes
     the search box to nothing at 390px, and the popup is positioned relative to
     the field so it follows the sheet when it scrolls. */
  .subj-pick { position: relative; margin-bottom: 10px; }
  .subj-chips { display: flex; flex-wrap: wrap; gap: 6px; }
  .subj-chips:not(:empty) { margin-bottom: 7px; }
  .subj-chip {
    display: inline-flex; align-items: center; gap: 5px;
    background: var(--surface-2); border: 1px solid var(--border); color: var(--ink);
    border-radius: 999px; padding: 4px 6px 4px 11px; font-size: 12.5px; font-weight: 600;
    max-width: 100%; }
  .subj-nm { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; min-width: 0; }
  .subj-x svg { width: 13px; height: 13px; }
  .subj-x {
    background: none; border: none; color: var(--muted); cursor: pointer;
    font: inherit; font-size: 12px; line-height: 1; flex: none;
    /* 24px of tappable area around a small glyph — the chip is the label, the
       ✕ is the control, and a 10px hit target on a phone is not one. */
    min-width: 24px; min-height: 24px; display: inline-grid; place-items: center;
    border-radius: 999px; }
  .subj-x:hover { color: var(--ink); background: var(--surface-3); }
  .subj-q { width: 100%; }
  .subj-q:disabled { opacity: .7; }
  .subj-pop {
    position: absolute; left: 0; right: 0; top: 100%; z-index: 40;
    background: var(--surface); border: 1px solid var(--border); border-radius: 12px;
    box-shadow: 0 10px 28px rgba(0,0,0,.18); overflow: hidden auto; max-height: 232px; }
  .subj-pop.hidden { display: none; }
  .subj-pop .mrow { display: flex; align-items: center; gap: 9px; padding: 8px 11px; cursor: pointer; }
  .subj-pop .mrow.sel, .subj-pop .mrow:hover { background: var(--surface-2); }
  .subj-pop .mrow .avatar { width: 26px; height: 26px; font-size: 10.5px; flex: none; }

  /* The New-direct-message picker. Browsable AND searchable: the roster is now the
     whole organization rather than one store, which is far too many people for a
     bare list and still too few to justify hiding everyone behind a query. So the
     list stays, bounded and scrollable, and the box narrows it. The chips reuse
     .subj-chip / .subj-nm / .subj-x above — same control, same tap target. */
  .dm-pick { position: relative; }
  .dm-picked:not(:empty) { margin-bottom: 8px; }
  .dm-count {
    display: block; font-size: 11.5px; font-weight: 700; letter-spacing: .02em;
    text-transform: uppercase; color: var(--muted); margin-bottom: 6px; }
  /* ── Full-screen picker page ──────────────────────────────────────────────
     A bottom sheet is sized by its content, which is wrong for a searchable list on
     a phone: the keyboard takes half the screen and the sheet has to choose between
     showing results and showing its own buttons. It chose badly — the list and the
     Start button both ended up behind the keyboard, and the list moved on every
     keystroke as it shrank.
     A page fixes the frame instead. 100dvh so the keyboard shortens it rather than
     covering it, the actions in a top bar the keyboard can never reach, and the list
     taking every pixel that is left. */
  .modal-wrap.full { align-items: stretch; }
  .modal-wrap.full .modal {
    height: 100dvh; max-height: none; border-radius: 0;
    padding: 0 0 env(safe-area-inset-bottom);
    display: flex; flex-direction: column; overflow: hidden;
  }
  .dmfull { display: flex; flex-direction: column; min-height: 0; flex: 1; }
  /* The bar owns both actions, so neither can be pushed off-screen by the keyboard
     or by a long list. Sticky is not enough — it is a flex sibling that never scrolls. */
  .dmfull-bar {
    flex: none; display: flex; align-items: center; gap: 10px;
    padding: max(env(safe-area-inset-top), 10px) 12px 10px;
    border-bottom: 1px solid var(--border); background: var(--surface);
  }
  .dmfull-bar .dmfull-title {
    flex: 1; min-width: 0; font-family: var(--font-round); font-weight: 800;
    font-size: 17px; letter-spacing: -.01em; text-align: center;
  }
  .dmfull-x, .dmfull-go {
    flex: none; border: 0; background: transparent; cursor: pointer;
    font-family: var(--font-round); font-weight: 800; font-size: 15px;
    padding: 8px 10px; border-radius: 10px; min-width: 62px;
  }
  .dmfull-x { color: var(--muted); text-align: left; }
  .dmfull-go { color: var(--accent); text-align: right; }
  .dmfull-go[disabled] { color: var(--muted); opacity: .5; cursor: not-allowed; }
  .dmfull-body { flex: 1; min-height: 0; display: flex; flex-direction: column; padding: 0 14px; }
  .dmfull-body .subj-q { flex: none; margin: 10px 0 0; }
  .dmfull-lede { flex: none; margin: 10px 0 0; font-size: 12.5px; color: var(--muted); }

  /* Selected people as circles, the way a group chat shows who is in it — a row of
     faces reads at a glance where a row of text chips does not. Horizontally
     scrollable so ten selections do not wrap the list off the screen. */
  .dmfull-picked { flex: none; }
  .dmfull-picked:not(:empty) { padding: 11px 0 3px; border-bottom: 1px solid var(--hair); }
  .dm-faces { display: flex; gap: 13px; overflow-x: auto; padding-bottom: 8px; scrollbar-width: none; }
  .dm-faces::-webkit-scrollbar { display: none; }
  .dm-face {
    flex: none; width: 62px; display: flex; flex-direction: column;
    align-items: center; gap: 5px; background: none; border: 0; padding: 0;
    cursor: pointer; font-family: var(--font-round);
  }
  .dm-face .avatar { width: 48px; height: 48px; font-size: 15px; }
  .dm-face-nm {
    max-width: 100%; overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
    font-size: 11.5px; font-weight: 700; color: var(--muted);
  }
  /* The ✕ rides the circle rather than sitting beside the name: the whole tile is
     the remove control, and a badge says so without spending a second row. */
  .dm-face-av { position: relative; display: grid; place-items: center; }
  .dm-face-x {
    position: absolute; top: -2px; right: -2px; width: 19px; height: 19px;
    border-radius: 50%; background: var(--ink); color: var(--bg);
    display: grid; place-items: center; border: 2px solid var(--surface);
  }
  .dm-face-x svg { width: 10px; height: 10px; }

  /* Inside the page the list is the flexible middle — it takes what the bar, the
     faces and the search box leave, and scrolls. No fixed height to get wrong. */
  .dmfull .dm-list { flex: 1; min-height: 0; height: auto; margin: 10px 0 14px; }

  /* A FIXED height, not a max-height, and that is the whole point wherever this list
     is still used as a SHEET. The sheet is bottom-anchored (.modal-wrap is
     align-items:flex-end), so its height decides where its top edge lands. With a
     max-height the box shrank as the query narrowed the list, which walked the search
     field and the remaining results downwards on every keystroke — straight toward
     the keyboard. A fixed box means filtering changes what is IN the list and never
     where the list IS. */
  .dm-list {
    margin-top: 9px; height: 44dvh; overflow-y: auto; overscroll-behavior: contain;
    -webkit-overflow-scrolling: touch;
    background: var(--surface); border: 1px solid var(--border); border-radius: 12px; }
  .dm-row {
    display: flex; align-items: center; gap: 9px; margin: 0; padding: 8px 11px;
    cursor: pointer; border-bottom: 1px solid var(--border); }
  .dm-row:last-child { border-bottom: 0; }
  .dm-row:hover { background: var(--surface-2); }
  .dm-row input[type="checkbox"] { width: auto; flex: none; margin: 0; }
  .dm-row .avatar { width: 30px; height: 30px; font-size: 11px; flex: none; }
  /* Keeps a shielded owner's name aligned with everyone else's, where the
     checkbox would have been. */
  .dm-box { width: 13px; flex: none; }
  /* Wraps rather than truncating — a picker row exists to identify a person, and
     an ellipsis through a surname is exactly the wrong place to save a line.
     overflow-wrap guards the one case wrapping cannot handle on its own: a very
     long unbroken token, which would otherwise push the row past 390px. */
  .dm-nm { min-width: 0; overflow: hidden; overflow-wrap: anywhere; }
  .dm-row.is-off { opacity: .55; cursor: default; }
  .dm-row.is-off:hover { background: transparent; }
  .dm-list .empty { padding: 22px 12px; }

  /* #F17: the "get the app" browser reminder. Informational, so it uses the
     accent surface rather than --warn — nothing is wrong, the person is just
     missing notifications. Wraps rather than truncating: the whole value of
     this copy is naming the concrete gap, and an ellipsis would eat it. */
  .app-banner {
    display: flex; align-items: flex-start; gap: 10px; padding: 9px 14px;
    font-size: 12.5px; line-height: 1.45;
    background: color-mix(in srgb, var(--accent) 8%, var(--surface));
    color: var(--ink);
    border-bottom: 1px solid color-mix(in srgb, var(--accent) 22%, var(--border)); }
  .app-banner.hidden { display: none; }
  /* A real tap target, matching the convention the rest of the shell follows —
     a dismiss control people miss is a banner that never goes away. */
  .app-banner .iclink { flex: none; margin-top: -2px;
    min-width: 28px; min-height: 28px; display: inline-grid; place-items: center;
    border-radius: 999px; }
  .app-banner .iclink svg { width: 16px; height: 16px; }

  /* First-run photo step: the preview avatar is centred above the buttons. */
  .fr-av { display: block; text-align: center; }
  .fr-av .avatar { margin-left: auto; margin-right: auto; }

  .orgview-banner {
    display: flex; align-items: center; gap: 8px; padding: 8px 14px; font-size: 13px;
    background: color-mix(in srgb, var(--warn) 16%, var(--surface));
    color: var(--ink); border-bottom: 1px solid color-mix(in srgb, var(--warn) 45%, var(--border)); }
  .orgview-banner.hidden { display: none; }
  .orgview-banner .ovb-ic { color: var(--warn); display: inline-flex; flex: none; }
  .orgview-banner .ovb-ic svg { width: 16px; height: 16px; }
  .orgview-banner .ovb-txt { min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
  .orgview-banner .ovb-exit {
    margin-left: auto; display: inline-flex; align-items: center; gap: 4px; flex: none;
    background: var(--surface); border: 1px solid color-mix(in srgb, var(--warn) 45%, var(--border));
    color: var(--ink); border-radius: 999px; padding: 4px 12px; font: inherit; font-size: 12.5px;
    font-weight: 700; cursor: pointer; }
  .orgview-banner .ovb-exit:hover { background: var(--surface-2); }
  .orgview-banner .ovb-exit svg { width: 14px; height: 14px; }

  @media (max-width: 700px) {
    .plat-kpis { grid-template-columns: 1fr 1fr 1fr; gap: 7px; }
    .plat-tile { padding: 11px; min-height: 78px; }
    .plat-tile-val { font-size: 22px; }
    .plat-tile-lbl { font-size: 9.5px; letter-spacing: .04em; }
    /* Table → stacked cards: each row becomes a labeled block so nothing clips at 390px. */
    .plat-table, .plat-table tbody, .plat-table tr, .plat-table td { display: block; width: 100%; }
    .plat-table thead { display: none; }
    .plat-table tr { border: 1px solid var(--border); border-radius: var(--radius-sm);
      padding: 6px 12px; margin-bottom: 10px; }
    .plat-table tr:last-child td { border-bottom: 1px solid var(--hair); }
    .plat-table td { border-bottom: 1px solid var(--hair); padding: 9px 0; text-align: right;
      display: flex; justify-content: space-between; align-items: center; gap: 12px; }
    .plat-table td.plat-org { text-align: left; display: block; }
    .plat-table td.plat-num { text-align: right; }
    .plat-table td[data-th]::before {
      content: attr(data-th); font-size: 11px; text-transform: uppercase; letter-spacing: .05em;
      color: var(--muted); font-weight: 700; }
    .plat-table tr td:last-child { border-bottom: none; }
  }

/* ── Messenger-style long-press reaction bar (global, theme-var only) ────────── */
.msg, .post { position: relative; }
[data-rx] { -webkit-touch-callout: none; }                /* no iOS link/save sheet on hold */
/* Two classes, and the split is the whole point. `.rx-holding` goes on at
   POINTERDOWN — every finger-down on a card, including the first milliseconds of an
   ordinary scroll, because a scroll is only told apart from a hold once the finger has
   travelled 12px. So it must do nothing that is visible, and it doesn't: it suppresses
   text selection, which is what its comment in feed.js always claimed.

   Raising the card is a separate job with a separate trigger. `.rx-lifted` is added by
   openRxBar() alongside the backdrop, and it exists to clear that backdrop (z-index 40)
   — nothing else. Carrying z-index 41 on the pointerdown class meant every swipe that
   began on a post lifted it over BOTH pieces of fixed chrome (the compose + at 16 and
   the tab bar at 15) for the two-to-five frames before the 12px threshold cancelled the
   hold. Cole caught it on video at the bottom of Activity: the + sheared off at the
   card's bottom edge, nothing moving, once per push. Three rounds of overscroll theories
   (v112, v114, v115) chased a compositor that was never involved. */
.rx-holding, .rx-holding * { -webkit-user-select: none; user-select: none; }
.rx-lifted { position: relative; z-index: 41; }
.rxbackdrop { position: fixed; inset: 0; z-index: 40; background: rgba(0, 0, 0, .42); }
.rxbar {
  position: absolute; top: -20px; right: 12px; z-index: 42; max-width: 92vw;
  display: flex; flex-direction: column; gap: 4px; padding: 6px;
  background: var(--surface-2, var(--card, #fff)); border: 1px solid var(--border);
  border-radius: 16px; box-shadow: 0 12px 28px -8px rgba(0, 0, 0, .45);
  animation: rxpop .13s ease-out;
}
.msg.own .rxbar { right: auto; left: 12px; }              /* own bubbles sit right — flip the bar */
.rxbar.below { top: auto; bottom: -20px; }
.rxbar-emojis { display: flex; gap: 2px; padding: 0 3px; }
.rxbar-emojis button {
  min-height: 0; font-size: 22px; line-height: 1; padding: 4px 6px;
  background: none; border: none; cursor: pointer; transition: transform .09s;
}
.rxbar-emojis button:hover, .rxbar-emojis button:active { transform: scale(1.3); }
.rxbar-actions {
  position: fixed; left: 0; right: 0; bottom: calc(68px + env(safe-area-inset-bottom) + var(--rx-keyboard-offset, 0px));
  z-index: 42; display: flex; justify-content: center; gap: 2px; padding: 8px max(8px, env(safe-area-inset-right)) 8px max(8px, env(safe-area-inset-left));
  background: var(--surface-2, var(--card, #fff)); border-top: 1px solid var(--hair); box-shadow: 0 -8px 24px -14px rgba(0, 0, 0, .5);
}
.rxbar-actions .rx-act {
  display: flex; align-items: center; gap: 6px; padding: 6px 10px;
  background: none; border: none; border-radius: 9px; cursor: pointer;
  color: var(--ink); font-size: 13px; font-weight: 600;
}
.rxbar-actions .rx-act svg { width: 16px; height: 16px; }
.rxbar-actions .rx-act:hover, .rxbar-actions .rx-act:active { background: rgba(128, 128, 128, .16); }
.rxbar-actions .rx-del { color: var(--danger, #e5484d); }
.rxbar-actions .rx-del { margin-left: 12px; border-left: 1px solid var(--hair); padding-left: 16px; }
@keyframes rxpop { from { transform: scale(.55); opacity: 0; } }

/* Long-press bar anchors near the bubble side: others past the avatar gutter,
   own hard against the right edge. */
.thread .msg .rxbar { left: 36px; right: auto; }
.thread .msg.own .rxbar { left: auto; right: 0; }

/* ── Full-size chat image viewer (lightbox) ─────────────────────────────────
   Thumbnails signal they open; the viewer itself is intentionally theme-
   independent (a dark backdrop for photo contrast), mirroring the rgba overlay
   precedent in .modal-wrap and platform photo viewers. */
.photo-grid img, img[data-photo-viewer] { cursor: zoom-in; }
.imgviewer {
  position: fixed; inset: 0; z-index: 80;
  background: rgba(0, 0, 0, .92);
  display: flex; align-items: center; justify-content: center;
  opacity: 0; transition: opacity .18s ease;
  overscroll-behavior: contain; touch-action: none;
  padding: env(safe-area-inset-top) env(safe-area-inset-right) env(safe-area-inset-bottom) env(safe-area-inset-left);
}
.imgviewer.show { opacity: 1; }
.imgviewer-img {
  max-width: 100vw; max-height: 100vh;
  object-fit: contain; display: block;
  transform-origin: center center; will-change: transform;
  cursor: zoom-in; touch-action: none;
  user-select: none; -webkit-user-select: none; -webkit-touch-callout: none;
}
.imgviewer-close {
  position: fixed; z-index: 81;
  top: calc(env(safe-area-inset-top) + 10px); right: calc(env(safe-area-inset-right) + 12px);
  width: 44px; height: 44px; border-radius: 50%;
  display: flex; align-items: center; justify-content: center;
  background: rgba(0, 0, 0, .5); border: 1px solid rgba(255, 255, 255, .32);
  color: #fff; cursor: pointer; -webkit-tap-highlight-color: transparent;
}
.imgviewer-close svg { width: 22px; height: 22px; }
@media (prefers-reduced-motion: reduce) { .imgviewer { transition: none; } }

/* ── Profile-photo crop screen (photo-crop.js) ──────────────────────────────
   Sits above BOTH the sheet (z-index 30) and the lightbox (80): it is opened
   from inside the profile sheet and has to survive on top of it, because
   cancelling must return the person to the sheet they started from.

   The panel is themed (it is a form, and it appears over the app), while the
   backdrop stays a fixed dark wash in both themes — same reasoning as the
   lightbox, a photo needs contrast that does not move with the theme. */
.cropper {
  position: fixed; inset: 0; z-index: 90;
  background: rgba(0, 0, 0, .72);
  display: flex; align-items: center; justify-content: center;
  opacity: 0; transition: opacity .18s ease;
  overscroll-behavior: contain; touch-action: none;
  padding: calc(env(safe-area-inset-top) + 12px) 12px calc(env(safe-area-inset-bottom) + 12px);
}
.cropper.show { opacity: 1; }
.cropper-panel {
  background: var(--surface); color: var(--ink);
  border: 1px solid var(--border); border-radius: 20px;
  padding: 16px 16px calc(14px + env(safe-area-inset-bottom));
  box-shadow: var(--shadow);
  max-width: 400px; width: 100%;
  display: flex; flex-direction: column; align-items: center; gap: 10px;
}
.cropper-title { font-weight: 800; font-size: 18px; align-self: flex-start; }
.cropper-hint { font-size: 13px; color: var(--muted); align-self: flex-start; margin-top: -6px; }
/* touch-action:none is what makes pan and pinch ours instead of the WebView's —
   without it the page itself zooms and the crop never receives the gesture. */
.cropper-canvas {
  display: block; border-radius: 14px; background: var(--surface-2, #000);
  touch-action: none; cursor: grab;
  user-select: none; -webkit-user-select: none; -webkit-touch-callout: none;
}
.cropper-canvas:active { cursor: grabbing; }
.cropper-zoom { width: 100%; accent-color: var(--accent); }
.cropper-actions { display: flex; gap: 10px; width: 100%; }
.cropper-actions .btn { width: auto; }
@media (prefers-reduced-motion: reduce) { .cropper { transition: none; } }

/* ── Attendance ─────────────────────────────────────────────────────────────
   Manager surface reached from the More shelf. Reuses the Library drill-in
   header (.lib-head) and the Checklists subnav (.cl-subnav) rather than
   inventing a third of each. Everything below is the row bodies only. */
/* ── The two reference screens, promoted out of the tab row ──────────────────
   Rules and Who's who sit up in the header as small controls instead of taking
   two of five sub-nav slots (Cole, 2026-08-14). Same .iclink the Chats
   "Organize" button uses, so there is one small-secondary-control vocabulary
   rather than a second one invented here. Right-aligned and on their own line so
   they never compete with the title for the top-left, which is where the back
   control lives. */
/* The back control and the reference pills share one row. `margin-left: auto`
   on the pills rather than `justify-content: space-between` on the row, so the
   back button keeps its own left edge whatever else is or is not drawn beside
   it — crew see one pill, a manager sees two, and a build without either sees
   none. */
.att-head-top { display: flex; align-items: center; gap: 10px; min-height: 30px; }
.att-refs { display: flex; gap: 10px; flex-wrap: wrap; justify-content: flex-end;
  margin-left: auto; }
.att-refs .iclink { padding: 7px 14px; border-radius: 999px; background: var(--surface-2);
  border: 1px solid var(--border); font-size: 13px; font-weight: 700; color: var(--ink); }
.att-refs .iclink:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }

/* ── Wins: the positive half ─────────────────────────────────────────────────
   A grid of small stat tiles rather than a list of rows, because these are
   numbers somebody GLANCES at. Auto-fit so one metric fills the row and six wrap
   cleanly, without a breakpoint per count. */
.win-lead { padding: 11px 14px; }
.win-sec { margin: 16px 2px 8px; }
.win-card { padding: 13px 14px; }
.win-who { display: flex; align-items: center; gap: 9px; margin-bottom: 11px; }
.win-av { width: 30px; height: 30px; border-radius: 50%; display: grid; place-items: center;
  color: #fff; font-weight: 800; font-size: 11.5px; font-family: var(--font-round); flex: none; }
.win-nm { font-family: var(--font-round); font-weight: 800; font-size: 15px; }
.win-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(104px, 1fr)); gap: 9px; }
.win-stat { background: var(--surface-2); border-radius: 12px; padding: 10px 11px; min-width: 0; }
/* `good` is the only tone. There is deliberately no bad tone on this screen —
   every other attendance surface already carries those, and a red tile here
   would turn the one positive screen into another place to be told off. */
.win-stat.good { background: color-mix(in srgb, var(--ok) 14%, var(--surface-2)); }
.win-val { font-family: var(--font-round); font-weight: 900; font-size: 22px; line-height: 1.05;
  letter-spacing: -.02em; font-variant-numeric: tabular-nums; color: var(--ink); }
.win-stat.good .win-val { color: var(--ok); }
.win-lbl { font-size: 11.5px; font-weight: 700; color: var(--muted); margin-top: 3px; line-height: 1.3; }
.win-sub { font-size: 10.5px; color: var(--muted); margin-top: 2px; opacity: .85; }
.win-store .section { margin: 0 0 10px; }

.att-counts { display: flex; gap: 14px; flex-wrap: wrap; font-size: 13px;
  color: var(--muted); margin-bottom: 10px; }
.att-counts b { color: var(--text); font-size: 15px; }
.att-row, .att-day { border-top: 1px solid var(--hair); padding: 12px 0; }
.att-row:first-child, .att-day:first-child { border-top: 0; padding-top: 2px; }
/* An Outstanding row on its way out after somebody ruled on it. It collapses
   where it stood and the rows below close over it, instead of the pane blanking
   to "Loading…" and repainting from the top — see attqSlideOut() for why the
   height is measured in JS and why this must be a height transition rather than
   a fade. The padding and the hairline go with it: a row's chrome left sitting
   on nothing is a gap in the list, which reads as a bug rather than as a
   departure. Placed AFTER the :first-child rule above so it wins the tie when
   the row leaving is the top one.
   Keep the duration in step with ATTQ_LEAVE_MS in attendance-queue.js. */
.att-day.att-leaving {
  overflow: hidden;
  opacity: 0;
  transform: translateX(-10px);
  padding-top: 0; padding-bottom: 0;
  border-top-color: transparent;
  transition: height .26s ease, opacity .18s ease, padding .26s ease,
              transform .26s ease, border-top-color .12s linear;
}
/* Belt and braces — the JS already removes the row outright under this setting
   rather than animating it more slowly. */
@media (prefers-reduced-motion: reduce) { .att-day.att-leaving { transition: none; } }
/* The Yesterday tab's counterpart, and the difference between the two is the
   whole point. A ruled shift does NOT leave that list — it stays, wearing a new
   status, and re-sorts inside its card because an unruled no-show sorts ahead of
   a settled one. So it is flashed once where it landed rather than animated
   away: without it, exempting the top row reads as the row vanishing. Colour is
   never the only channel here either — the row has visibly changed its chip and
   grown a ruling note; this only says WHICH row did. See attFlashRow(). */
@keyframes att-ruled-flash {
  from { background: color-mix(in srgb, var(--accent) 18%, transparent); }
  to   { background: transparent; }
}
.att-day.att-ruled-flash {
  animation: att-ruled-flash 1.1s ease-out;
  border-radius: 10px;
}
@media (prefers-reduced-motion: reduce) { .att-day.att-ruled-flash { animation: none; } }
/* A Points row that opens that person's ledger. The affordance is deliberately
   quiet — this is a list somebody scans for a number, not a menu — so it reads
   as tappable on press and on keyboard focus rather than by decorating every
   row. `-webkit-tap-highlight-color` is left alone: the iOS default flash is the
   only press feedback a finger gets before the pane repaints. */
.att-day-open { cursor: pointer; margin: 0 -4px; padding-left: 4px; padding-right: 4px;
  border-radius: 8px; transition: background .12s ease; }
.att-day-open:active { background: var(--hover, rgba(127,127,127,.10)); }
.att-day-open:focus-visible { outline: 2px solid var(--accent); outline-offset: -2px; }
.att-row-head { display: flex; align-items: flex-start; gap: 10px; }
.att-name { font-weight: 650; font-size: 15px; }
.att-sub { font-size: 12.5px; margin-top: 2px; }
.att-note { font-size: 12.5px; margin-top: 6px; color: var(--muted); }
/* Wraps rather than scrolls sideways: at 390px a name list in a rail hides
   candidates behind a swipe, and a candidate nobody sees is a candidate nobody
   picks — which is how the wrong one gets confirmed. */
.att-cands { display: flex; flex-wrap: wrap; gap: 6px; margin-top: 8px; }
.att-cands .btn, .att-results .btn { width: auto; margin-top: 0; padding: 7px 11px;
  font-size: 13px; }
.att-search { margin-top: 8px; }
.att-search input { font-size: 16px; }
.att-results { display: flex; flex-wrap: wrap; gap: 6px; margin-top: 6px; }
/* THE SPECIFICITY TRAP IN THIS FILE. `button.btn` is (0,1,1) and sets
   `width: 100%`, so a BARE class cannot override it however far down it sits —
   `.att-dismiss { width: auto }` looked correct and did nothing at all. Beside a
   name in `.att-row-head` that meant the button took the whole 390px row and
   flex squeezed the name it sits next to down to ZERO pixels. Any new rule here
   that wants a narrow button needs `button.<class>` or a descendant selector,
   which is why `.att-cands .btn` above works and this did not. */
.att-dismiss { padding: 7px 11px; font-size: 13px; }
/* In the flex header only. Standing alone under a review row it is deliberately
   a full-width action, which is the shape it already had. */
.att-row-head .att-dismiss { width: auto; flex: none; margin-top: 0; }
.att-chip { font-size: 11.5px; font-weight: 700; padding: 3px 8px; border-radius: 999px;
  white-space: nowrap; background: var(--surface-2); color: var(--muted); }
.att-ok { color: var(--ok); }
.att-warn { color: var(--warn); }
.att-bad { color: var(--bad); }
.att-warn-card { border-left: 3px solid var(--warn); }
.att-blocks { margin-top: 8px; display: flex; flex-direction: column; gap: 5px; }
.att-block { display: flex; flex-wrap: wrap; align-items: center; gap: 8px;
  font-size: 12.5px; color: var(--muted); }
.att-block-when { font-weight: 650; color: var(--text); }
/* `flex: 0 0 100%` in the wrapping row above, so the minutes ALWAYS begin a new
   line rather than beginning one only when the numbers happen to be long enough
   to push them over. Two rows on one screen wrapped two different ways before
   this, which reads as a rendering fault (Cole, 2026-08-10). */
.att-block-mins { flex: 0 0 100%; display: flex; flex-wrap: wrap; gap: 8px; }
.att-mins { color: var(--warn); }
/* An early ARRIVAL is the one minutes chip that is not a problem, so it opts out
   of the warn colour via .att-ok rather than inheriting it. */
.att-mins.att-ok { color: var(--ok); }
.att-subhead { font-size: 12px; font-weight: 700; letter-spacing: .04em;
  text-transform: uppercase; color: var(--muted); margin: 14px 0 6px; }
.att-points { font-size: 19px; font-weight: 750; font-variant-numeric: tabular-nums; }
.att-standing { display: flex; flex-direction: column; align-items: flex-end; }
.att-band { font-size: 11px; text-align: right; max-width: 140px; }
.att-days { display: flex; flex-wrap: wrap; gap: 5px; margin: 6px 0 4px; }
.att-days .chip { padding: 6px 9px; font-size: 12px; }
.att-tiers { margin-top: 8px; }
.att-tier { display: flex; align-items: center; gap: 6px; margin-bottom: 6px; }
.att-tier input { width: 74px; margin: 0; font-size: 16px; }
.att-tier-lbl { font-size: 12.5px; color: var(--muted); white-space: nowrap; }
/* `button.` prefix, not a bare class — see the note on .att-dismiss above.
   Without it these sit full-width and stack, which is the opposite of inline. */
button.att-inline { width: auto; margin-top: 4px; padding: 7px 11px;
  font-size: 13px; }
.att-store-rule { border-top: 1px solid var(--hair); padding: 12px 0; }
.att-store-rule:first-of-type { border-top: 0; }
/* A store attendance does not score. Dimmed rather than hidden — this is the one
   screen that has to show it, because it is the screen that switches it on. The
   same treatment the à la carte entitlement rows use for a disabled child. */
.att-store-off .att-days, .att-store-off .row { opacity: .45; }
.att-readonly { display: flex; gap: 8px; align-items: flex-start; font-size: 12.5px;
  color: var(--muted); background: var(--surface-2); border-radius: var(--radius-sm);
  padding: 9px 11px; margin-bottom: 12px; }
.att-readonly svg { flex: none; margin-top: 1px; }
.att-more { font-size: 13px; font-weight: 650; color: var(--accent); cursor: pointer;
  padding: 6px 0; list-style: none; }
.att-more::-webkit-details-marker { display: none; }
.att-ruled { background: var(--surface-2); border-radius: var(--radius-sm);
  padding: 8px 10px; margin-top: 8px; }
.att-rule { margin-top: 10px; }
.att-rule .btn { margin-top: 0; }
