/* ═══════════════════════════════════════════════════════════════════════
   🎴 DECK MODE — slice 1.5
   ───────────────────────────────────────────────────────────────────────
   Everything here is gated behind `body.deck-mode`, which deck.js toggles
   from the 🎴 top-toolbar button. Nothing in this file may affect regular
   Dispatch — if a rule isn't under a .deck-mode ancestor, it's a bug.

   ⚠️ The left panel's five multi-select filters are LIVE widgets that
   other code physically relocates, and an EMPTY #filter-ward blanks the
   entire roster. This file only ever HIDES them — never removes, never
   restructures. Same for the panel header tray: deck.js RE-PARENTS those
   buttons into the map rail so their popovers and wiring survive.

   Visual DNA is lifted from Stampede Tactics' "Riot Press" template
   (tactics/card.css): black poster stock, a #5AE200 rail down the left
   edge with registration ticks, a scissored white paper panel, hard edges
   everywhere, solid never-blurred offset shadows. Deliberately NOT
   lifted: the transform-scale rig. Tactics scales a fixed 320×447 card
   with transform:scale() because its cards are static art; ours hold a
   live <textarea> and a scrolling message thread, and scaled text inputs
   render blurry and hit-test badly. Deck cards use real CSS sizing.
   ═══════════════════════════════════════════════════════════════════════ */

body.deck-mode {
  --dk-green: #5AE200;
  --dk-green-dk: #43a800;
  --dk-ink: #000;
  --dk-paper: #fff;
  --dk-well: #ededed;
  --dk-amber: #FF8E00;
  --dk-red: #EC2029;
  /* the two PAF guide hues this block was missing. Tactics' palette
     statement is "Green #5AE200 · Orange #FF8E00 · Blue #00B9FF · Yellow
     #FFEB00, neutrals White / Black / Gray #ededed — that is the whole
     guide". They were hard-coded as literals in five places; name them. */
  --dk-blue: #00B9FF;
  --dk-yellow: #FFEB00;
  /* ⚠️ ONE size, used by BOTH faces. A card does not change shape when
     you turn it over (Justin 2026-08-08: "when the card flips, it should
     be the same width"). The back scrolls internally instead of growing.
     63:88 poker ratio, scaled up from Tactics' 320×447 for legibility
     since ours holds a real interface. */
  --dk-card-w: 360px;
  --dk-card-h: min(503px, calc(100vh - 96px));
}

/* ── 1 · LAYOUT: the panels go away ───────────────────────────────────── */
body.deck-mode { grid-template-columns: var(--sidebar-width) 1fr 0; }
body.deck-mode.left-panel-collapsed { grid-template-columns: 0 1fr 0; }
body.deck-mode .shift-panel { display: none !important; }
body.deck-mode #workflow-panel { display: none !important; }
/* ⚠️ The workflow panel is a COLUMN, not a bottom row. The live layout is
   locked to "side", and dispatch.js writes `grid-template-columns: 390px
   1fr` as an INLINE style on .map-container with #map at grid-column 2.
   An inline declaration beats any normal stylesheet rule, so reclaiming
   the panel's space needs !important on both the track list and the map's
   placement — a grid-template-ROWS override does nothing here. */
body.deck-mode .map-container {
  grid-template-columns: 1fr !important;
  grid-template-rows: 1fr !important;
}
body.deck-mode #map { grid-column: 1 !important; grid-row: 1 !important; }
body.deck-mode .workflow-collapse-bar,
body.deck-mode .workflow-resize-handle,
body.deck-mode #btn-collapse-right { display: none !important; }

/* 📅 The date switcher is absolutely positioned with a left offset,
   translateX and a max-width all computed for the 390px workflow-panel
   column. With the panel gone it lands off-centre, and the max-width
   squeezes it until the controls wrap. Centre it on the map instead. */
body.deck-mode .map-container > .date-nav {
  left: 50% !important;
  right: auto !important;
  transform: translateX(-50%) !important;
  max-width: calc(100% - 96px) !important;
  flex-wrap: nowrap !important;
  white-space: nowrap;
}

/* Re-parented popovers: .ideas-panel is fixed to --header-height, which
   doesn't survive the move, and .stats-menu is absolute inside a wrapper
   that now lives in the narrow map rail. Pin both under the header and
   cap their height so they can't run off the bottom of the screen. */
body.deck-mode .ideas-panel {
  top: 56px !important;
  max-height: calc(100vh - 76px) !important;
}
body.deck-mode #map-rail .stats-menu {
  position: fixed !important;
  top: 56px !important;
  right: 46px !important;
  max-height: calc(100vh - 76px) !important;
}

/* ── 2 · THE LEFT COLUMN ──────────────────────────────────────────────
   ⚠️ #volunteer-panel is RETIRED in regular Dispatch — style.css §v1215
   sets `#volunteer-panel, #shift-panel { display: none !important }` and
   _applySidePanelPrefs force-adds .collapsed at boot. But
   renderVolunteerList() still fills #volunteer-list, which is exactly why
   _wfActFullCardClone can clone a live chip out of it. Deck mode brings
   the real panel back rather than rebuilding a list beside it: every
   badge, activity color, drag handler and filter binding is already there
   and already maintained. Overriding an !important rule needs !important
   back. */
body.deck-mode #volunteer-panel {
  display: flex !important;
  width: auto !important;
  min-width: 0 !important;
  border-right: 1px solid #ccc;
}
body.deck-mode #volunteer-panel .panel-scroll { flex: 1 1 auto; min-height: 0; overflow-y: auto; }

/* 🚨 style.css:14764 does `body.left-panel-collapsed #btn-collapse-left {
   display: none !important }` — correct when the panel was PERMANENTLY
   retired (there was nothing to come back to), but deck mode resurrects
   it, so collapsing the list stranded the dispatcher with no way to
   reopen it. Force the handle back and pin it to the viewport's left
   edge as a tab, so it's reachable even though its column is 0-wide. */
body.deck-mode.left-panel-collapsed #btn-collapse-left,
body.deck-mode #volunteer-panel.collapsed .panel-collapse-btn {
  display: flex !important;
  position: fixed !important;
  left: 0 !important;
  right: auto !important;
  top: 50% !important;
  height: 96px !important;
  width: 24px !important;
  transform: translateY(-50%);
  z-index: 1300;
  background: var(--dk-ink);
  color: var(--dk-paper);
  border: 2px solid var(--dk-ink);
  border-left: none;
  box-shadow: 3px 3px 0 rgba(0, 0, 0, .4);
  font-size: 12px;
  font-weight: 800;
  cursor: pointer;
}
body.deck-mode.left-panel-collapsed #btn-collapse-left:hover {
  background: var(--dk-green);
  color: var(--dk-ink);
}
[data-theme="dark"] body.deck-mode #volunteer-panel { border-right-color: var(--border); }

body.deck-mode .volunteer-panel .panel-header { display: none !important; }
body.deck-mode .volunteer-panel .panel-tools-left { padding: 8px 8px 8px; }
body.deck-mode #volunteer-search {
  width: 100%;
  border: 2px solid var(--dk-ink);
  border-radius: 0;
  font-weight: 600;
}
[data-theme="dark"] body.deck-mode #volunteer-search { border-color: var(--border); }

/* 🎚️ FILTERS — permanently visible, two per row (Justin 2026-08-08: "the
   filters should permanently be there … they all take up way too much
   space. I feel like there can easily be two per row").
   Stock is a wrapping flex whose .filter-row is `flex: 1` with the label
   INLINE, so each row claims a full line and four filters cost four rows.
   A 2-column grid with the label stacked above the widget fits them in
   two rows at half the height.
   ⚠️ #filter-tag-row (Team, CO-only) and #filter-inbox-senders-row carry
   INLINE display:none and are shown by JS. Grid items that are display:none
   occupy no cell, so they slot in correctly when turned on — do NOT force
   them visible here. */
body.deck-mode .volunteer-panel .filter-section {
  display: grid;
  grid-template-columns: 1fr 1fr;
  gap: 5px 6px;
  margin-top: 5px;
}
/* Label and dropdown share ONE row inside each cell (Justin 2026-08-08).
   At ~129px per cell that's tight, so the label never shrinks and the
   dropdown takes whatever is left and ellipsises its selection — .ms-btn
   already carries overflow/text-overflow, it just needs `min-width: 0` on
   the flex chain or it refuses to shrink below its content width. */
body.deck-mode .volunteer-panel .filter-row {
  display: flex;
  flex-direction: row;
  align-items: center;
  gap: 3px;
  flex: none;
  min-width: 0;
}
body.deck-mode .volunteer-panel .filter-label {
  flex: 0 0 auto;
  font-size: 8.5px;
  font-weight: 700;
  letter-spacing: .02em;
  text-transform: uppercase;
  opacity: .7;
  white-space: nowrap;
}
body.deck-mode .volunteer-panel .filter-row .ms-host { flex: 1 1 auto; min-width: 0; }
body.deck-mode .volunteer-panel .ms-btn {
  font-size: 10.5px;
  padding: 3px 14px 3px 4px;
  min-width: 0;
}

/* 📨 / 📌 the bulk actions — Dispatch's own row, un-hidden and restyled.
   It already sits below the filters and directly above the list's region
   header, which is where these belong. */
body.deck-mode .volunteer-panel .bulk-actions-row {
  display: flex !important;
  gap: 4px;
  margin-top: 6px;
}
body.deck-mode .volunteer-panel .bulk-action-btn {
  flex: 1 1 0;
  min-width: 0;
  padding: 6px 3px;
  font-size: 9.5px; font-weight: 700; letter-spacing: 0;
  color: var(--dk-paper);
  background: var(--dk-ink);
  border: 2px solid var(--dk-ink);
  border-radius: 0;
  cursor: pointer;
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}
body.deck-mode .volunteer-panel .bulk-action-btn:hover {
  background: var(--dk-green); color: var(--dk-ink);
}
/* the senders toggle is a full-width chip, not a half-width dropdown */
body.deck-mode #filter-inbox-senders-row { grid-column: 1 / -1; }

/* ── 2b · LIST HEADER: switcher + filters ─────────────────────────────
   Deck mode hides the whole workflow tab strip, so without this there is
   no route to anything but the activist list. */
/* 🫧 PRESET BUBBLES — the top row of the column, above the search and
   filters. Circles, matching the map controls. Each drives the existing
   engagement multi-select. */
body.deck-mode .dk-presets {
  padding: 8px 8px 4px;
  background: var(--dk-ink);
}
/* five presets + ➕ on the visible row; everything else in a scrollable
   tray below it, so adding presets never reshapes the top row */
body.deck-mode .dk-preset-row {
  display: flex; gap: 5px; justify-content: space-between;
}
body.deck-mode .dk-preset-tray {
  display: flex; flex-wrap: nowrap; gap: 5px;
  margin-top: 5px;
  padding-bottom: 4px;
  max-width: 100%;
  overflow-x: auto;
  overflow-y: hidden;
  scrollbar-width: thin;
}
body.deck-mode .dk-preset-tray[hidden] { display: none; }
body.deck-mode .dk-preset-tray .dk-preset { flex: 0 0 auto; }
body.deck-mode .dk-preset-empty { color: #999; font-size: 10px; padding: 4px 2px; white-space: nowrap; }
body.deck-mode .dk-preset-more { border-style: dashed; }
body.deck-mode .dk-preset {
  flex: 1 1 0;
  aspect-ratio: 1 / 1;
  max-width: 46px;
  display: flex; align-items: center; justify-content: center;
  font-size: 19px; line-height: 1;
  background: var(--dk-paper);
  color: var(--dk-ink);
  border: 2px solid var(--dk-ink);
  border-radius: 50% !important;   /* beats style.css:16's hard-edge sweep */
  cursor: pointer;
  transition: transform 70ms linear, background 70ms linear;
}
body.deck-mode .dk-preset:hover { transform: translateY(-2px); }
body.deck-mode .dk-preset.on {
  background: var(--dk-green);
  box-shadow: 0 0 0 2px var(--dk-paper);
}
/* presets are an activist-list tool */
body.deck-mode.deck-list-events .dk-presets { display: none !important; }

/* Events mode swaps the activist search/filters/bulk actions for the
   event filters — and vice versa. */
body.deck-mode.deck-list-events .volunteer-panel .panel-tools-left { display: none !important; }
body.deck-mode:not(.deck-list-events) #dk-evfilters { display: none !important; }
body.deck-mode #dk-evfilters { padding: 8px; }
/* the event filters use plain <select>s rather than the multi-select
   widget, so give them the .ms-btn look without its background arrow */
body.deck-mode #dk-evfilters select.ms-btn {
  width: 100%;
  background-image: none;
  padding: 3px 4px;
  height: 21px;
}

/* ── 2c · SIMPLIFIED CHIPS ────────────────────────────────────────────
   Justin 2026-08-08: the left list "doesn't need to show the ladder
   status, what groups they're in, and stuff like that … once they're
   clicked on, all that stuff can show on the card."
   ⚠️ Scoped to #volunteer-list ONLY. The card mounts a CLONE of the same
   .volunteer-card markup (#dk-chipmount), and that clone must keep every
   badge — it's the whole reason the detail is worth hiding here. */
body.deck-mode #volunteer-list .vol-status,
body.deck-mode #volunteer-list .vol-week-host,
body.deck-mode #volunteer-list .vol-week-attend,
body.deck-mode #volunteer-list .vol-notebook,
body.deck-mode #volunteer-list .vol-boe,
body.deck-mode #volunteer-list .vol-minibadges-row,
body.deck-mode #volunteer-list .vol-name-alerts,
body.deck-mode #volunteer-list .badge-tip,
body.deck-mode #volunteer-list .badge { display: none !important; }
body.deck-mode #volunteer-list .volunteer-card {
  cursor: pointer;
  border-left: 4px solid transparent;
  padding-top: 5px; padding-bottom: 5px;
  transition: border-left-color 90ms linear, transform 90ms linear;
}
body.deck-mode #volunteer-list .volunteer-card:hover {
  border-left-color: var(--dk-green);
  transform: translateX(2px);
}
body.deck-mode #volunteer-list .volunteer-card.dk-open {
  border-left-color: var(--dk-green);
  box-shadow: inset 0 0 0 2px var(--dk-ink);
}
body.deck-mode #volunteer-list .vol-name-row { font-weight: 600; }
/* ⋯ the per-chip overflow menu is redundant in deck mode — the card IS
   the "more about them" surface (Justin 2026-08-08) */
body.deck-mode #volunteer-list .vol-open-menu-btn { display: none !important; }

/* 📩 UNREAD — the one badge deck mode keeps, because "someone wrote to me"
   is the only roster signal you can't get from the card. */
body.deck-mode #volunteer-list .dk-unread {
  display: inline-flex; align-items: center; gap: 1px;
  margin-left: auto;
  padding: 1px 6px;
  font-size: 10px; font-weight: 800; line-height: 1.4;
  background: #00B9FF;
  color: var(--dk-ink);
  border: 2px solid var(--dk-ink);
}
body.deck-mode #volunteer-list .volunteer-card.dk-has-unread {
  border-left-color: #00B9FF;
  background: #f2fbff;
}
[data-theme="dark"] body.deck-mode #volunteer-list .volunteer-card.dk-has-unread { background: #10222b; }
/* (the 📩 narrowing is applied inline by paintUnread, in the same pass as
   the badges — a CSS rule here would blank the roster during the debounced
   re-render; see the note in deck.js) */
body.deck-mode .dk-preset { position: relative; }
body.deck-mode .dk-preset-n,
body.deck-mode .dk-preset-f {
  position: absolute;
  min-width: 16px; height: 16px;
  display: flex; align-items: center; justify-content: center;
  padding: 0 3px;
  font-size: 9px; font-weight: 800; line-height: 1;
  color: var(--dk-ink);
  border: 2px solid var(--dk-ink);
  border-radius: 50% !important;
}
/* unread top-right, flagged bottom-right — two independent tallies */
body.deck-mode .dk-preset-n { top: -3px; right: -3px; background: #00B9FF; }
body.deck-mode .dk-preset-f { bottom: -3px; right: -3px; background: var(--dk-red); color: var(--dk-paper); }
/* the chip's inline expansion is replaced by the card */
body.deck-mode #volunteer-list .vol-contact { display: none !important; }
body.deck-mode.deck-list-events #volunteer-list,
body.deck-mode.deck-list-inbox #volunteer-list { display: none !important; }
body.deck-mode:not(.deck-list-events) #dk-events { display: none !important; }
body.deck-mode:not(.deck-list-inbox) #dk-inbox { display: none !important; }
/* the Inbox has its own list; the activist search/filters don't apply */
body.deck-mode.deck-list-inbox .volunteer-panel .panel-tools-left,
body.deck-mode.deck-list-inbox .dk-presets { display: none !important; }

/* 📥 INBOX ROWS — one per sender, unread first. */
body.deck-mode .dk-inrow {
  display: flex; flex-direction: column; gap: 2px;
  width: 100%; padding: 8px 10px;
  text-align: left;
  background: var(--dk-paper);
  border: none; border-bottom: 1px solid #ddd; border-left: 4px solid transparent;
  cursor: pointer;
}
body.deck-mode .dk-inrow:hover { background: #f6f6f6; border-left-color: #00B9FF; }
body.deck-mode .dk-inrow-unread { background: #f2fbff; border-left-color: #00B9FF; }
body.deck-mode .dk-inrow-nomatch { opacity: .7; cursor: default; }
body.deck-mode .dk-inrow-top { display: flex; align-items: center; gap: 6px; }
body.deck-mode .dk-inrow-i { font-size: 15px; line-height: 1; }
body.deck-mode .dk-inrow-name { font-size: 12.5px; font-weight: 700; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
/* 🚩 flag-for-later — always present on a row so it can be SET, not just
   read. Ghosted at 22% when off; full strength on a flagged thread. */
body.deck-mode .dk-inrow-flag {
  flex: 0 0 auto;
  margin-left: auto;
  font-size: 11px; line-height: 1;
  padding: 2px 3px;
  opacity: .22;
  filter: grayscale(1);
  cursor: pointer;
}
body.deck-mode .dk-inrow-flag:hover { opacity: .7; filter: none; }
body.deck-mode .dk-inrow-flag.on { opacity: 1; filter: none; }
/* amber, matching the rail badge and the region-tab counts — one colour
   means "unread" everywhere in the inbox (Justin 2026-08-19) */
body.deck-mode .dk-inrow-n {
  margin-left: auto;
  padding: 1px 6px;
  font-size: 10px; font-weight: 800;
  background: var(--dk-amber); color: var(--dk-ink);
  border: 2px solid var(--dk-ink);
}
body.deck-mode .dk-inrow-bot { display: flex; align-items: baseline; gap: 6px; }
body.deck-mode .dk-inrow-body {
  flex: 1 1 auto; min-width: 0;
  font-size: 10.5px; color: #555;
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
body.deck-mode .dk-inrow-when { flex: 0 0 auto; font-size: 9.5px; color: #888; }
[data-theme="dark"] body.deck-mode .dk-inrow { background: var(--card-bg); border-bottom-color: var(--border); }

/* 📥 REGION TABS — All + one per campaign, above the sender rows (Justin
   2026-08-19). Sticky inside .panel-scroll so switching regions never
   means scrolling back up a long thread list. Horizontal scroll (not
   wrap) keeps the list rows starting at a fixed height however many
   state regions exist. */
body.deck-mode .dk-intabs {
  position: sticky; top: 0; z-index: 3;
  display: flex; gap: 3px;
  padding: 5px 6px;
  background: var(--dk-ink);
  border-bottom: 2px solid var(--dk-ink);
  overflow-x: auto; overflow-y: hidden;
  scrollbar-width: none;
}
body.deck-mode .dk-intabs::-webkit-scrollbar { display: none; }
body.deck-mode .dk-intab {
  flex: 0 0 auto;
  display: flex; align-items: center; gap: 4px;
  padding: 4px 8px;
  font-size: 10px; font-weight: 800; letter-spacing: .04em;
  text-transform: uppercase;
  white-space: nowrap;
  color: var(--dk-paper);
  background: transparent;
  border: 2px solid var(--dk-paper);
  border-radius: 0;
  cursor: pointer;
}
body.deck-mode .dk-intab:hover { background: #333; }
body.deck-mode .dk-intab.on {
  background: var(--dk-green);
  border-color: var(--dk-green);
  color: var(--dk-ink);
}
body.deck-mode .dk-intab-n {
  min-width: 14px; height: 14px;
  display: flex; align-items: center; justify-content: center;
  padding: 0 3px;
  font-size: 9px; font-weight: 800; line-height: 1;
  background: var(--dk-amber); color: var(--dk-ink);
  border: 1px solid var(--dk-ink);
  border-radius: 50% !important;
}

/* unread tally on the 📥 vertical tab.
   ⚠️ It still LAYS OUT horizontal-tb — inheriting the tab's vertical-rl
   would stack a two-digit count into a column of single digits — and is
   then turned as one block by transform: rotate(90deg), so the number
   lies down with the sideways rail (Justin 2026-08-19: "it needs to be
   rotated because the rest of the bar is sideways"). Amber rather than
   the inbox blue so the count reads as a nag instead of chrome. */
body.deck-mode .dk-vtab-n {
  position: absolute; top: 4px; right: 3px;
  min-width: 16px; height: 16px;
  display: flex; align-items: center; justify-content: center;
  padding: 0 3px;
  writing-mode: horizontal-tb;
  transform: rotate(90deg);
  transform-origin: center;
  font-size: 9px; font-weight: 800; line-height: 1;
  background: var(--dk-amber); color: var(--dk-ink);
  border: 2px solid var(--dk-ink);
  border-radius: 50% !important;
}
body.deck-mode .dk-vtab-n-flag { background: var(--dk-red); color: var(--dk-paper); }
body.deck-mode .dk-vtab { position: relative; }

/* ── 2d · EVENT ROWS ──────────────────────────────────────────────── */
body.deck-mode .dk-evrow {
  display: flex; flex-direction: column; gap: 3px;
  width: 100%;
  padding: 8px 10px;
  text-align: left;
  background: var(--dk-paper);
  border: none;
  border-bottom: 1px solid #ddd;
  border-left: 4px solid transparent;
  cursor: pointer;
}
body.deck-mode .dk-evrow:hover { background: #f6f6f6; border-left-color: var(--dk-green); }
body.deck-mode .dk-evrow.dk-open { border-left-color: var(--dk-green); box-shadow: inset 0 0 0 2px var(--dk-ink); }
body.deck-mode .dk-evrow-top { display: flex; align-items: center; gap: 6px; }
body.deck-mode .dk-evrow-name { font-size: 13px; font-weight: 700; line-height: 1.25; }
body.deck-mode .dk-evrow-bot { display: flex; align-items: center; gap: 8px; }
body.deck-mode .dk-evrow-time { flex: 1 1 auto; font-size: 11px; color: #555; }
/* day divider — the list runs Monday→Sunday, so each date gets a header */
body.deck-mode .dk-evday {
  padding: 5px 10px;
  font-size: 10px; font-weight: 800; letter-spacing: .06em; text-transform: uppercase;
  background: var(--dk-ink);
  color: var(--dk-paper);
  position: sticky; top: 0; z-index: 2;
}
body.deck-mode .dk-evday-today { background: var(--dk-green); color: var(--dk-ink); }
[data-theme="dark"] body.deck-mode .dk-evrow { background: var(--card-bg); border-bottom-color: var(--border); }

/* 🐛 THE STAFFING COUNT — confirmed over needed, tentative alongside.
   Pikmin-style: the number that matters is how many are actually
   carrying, and the ones merely hovering nearby are shown apart and
   dimmer so they can never be mistaken for staffing. */
body.deck-mode .dk-count {
  display: inline-flex; align-items: baseline;
  padding: 1px 7px;
  font-weight: 800; font-size: 12px;
  border: 2px solid var(--dk-ink);
  background: var(--dk-well);
}
body.deck-mode .dk-count.filled { background: var(--dk-green); }
body.deck-mode .dk-count.partial { background: #FFEB00; }
body.deck-mode .dk-count.empty { background: var(--dk-paper); color: var(--dk-red); border-color: var(--dk-red); }
body.deck-mode .dk-count-sep { opacity: .5; margin: 0 1px; }
body.deck-mode .dk-count-need { opacity: .75; }
body.deck-mode .dk-tent {
  margin-left: 5px;
  font-size: 11px; font-style: italic; font-weight: 700;
  color: #888;
}
body.deck-mode .dk-count-big { font-size: 26px; padding: 2px 12px; }
body.deck-mode .dk-count-big + .dk-tent { font-size: 14px; }

/* ── 3 · MAP CONTROLS ────────────────────────────────────────────────
   Justin 2026-08-08: the map icons should be twice as big and read as
   INDIVIDUAL circles rather than one connected strip.
   Stock .map-rail is a single white rounded box with `overflow: hidden`
   and buttons that share 1px borders — that shared chrome is exactly what
   makes them look joined, so all of it has to be unset (background,
   border, shadow, overflow) and each button given its own outline. */
/* ⚠️ style.css:14929 pins the rail with `top: auto !important; bottom:
   60px !important`, so vertical centering needs !important on all three
   of top/bottom/transform or the rail snaps back to bottom-aligned. */
/* 🏷️ STAFFING TAG above each shift flag — going / needed / tentative, so
   the board reads at a glance without opening a card.
   ⚠️ pointer-events:none, and anchored ABOVE the flag: the flag is a drop
   target and a click target, and nothing of ours may sit in its way. */
body.deck-mode .dk-tag-icon { background: none; border: none; pointer-events: none !important; }
body.deck-mode .dk-tag {
  display: inline-flex; align-items: baseline; gap: 1px;
  padding: 1px 5px;
  font-size: 11px; font-weight: 800; line-height: 1.35;
  white-space: nowrap;
  background: var(--dk-paper);
  border: 2px solid var(--dk-ink);
  box-shadow: 2px 2px 0 rgba(0, 0, 0, .45);
  pointer-events: none;
}
body.deck-mode .dk-tag i { font-style: normal; opacity: .6; }
body.deck-mode .dk-tag u { text-decoration: none; font-style: italic; opacity: .75; margin-left: 3px; font-size: 10px; }
body.deck-mode .dk-tag-full { background: var(--dk-green); }
body.deck-mode .dk-tag-part { background: #FFEB00; }
body.deck-mode .dk-tag-none { background: var(--dk-paper); color: var(--dk-red); border-color: var(--dk-red); }

/* 🐎 The herd may never block the flag it's charging at. The satellites
   are decorative; the flag is the click and drop target (Justin
   2026-08-08: "it's really hard to click on the shift flag now with the
   animations of all of the activists bashing into it"). Leaflet marks the
   markers interactive:false, but their PANE still hit-tests, so the glyphs
   swallow clicks aimed at the flag underneath. */
body.deck-mode .leaflet-rsvpSat-pane,
body.deck-mode .rsvp-sat-icon,
body.deck-mode .rsvp-runner,
body.deck-mode .rsvp-runner * { pointer-events: none !important; }
/* and give the flags themselves the top of the stack */
body.deck-mode .shift-flag-marker,
body.deck-mode .leaflet-marker-icon.shift-flag-marker { cursor: pointer; }

body.deck-mode #map-rail {
  top: 50% !important;
  bottom: auto !important;
  transform: translateY(-50%) !important;
  right: 12px;
  gap: 6px;
  padding: 0;
  background: none;
  border: none;
  box-shadow: none;
  overflow: visible;
  max-height: calc(100% - 24px);
}
body.deck-mode #map-rail .map-rail-btn,
body.deck-mode #map-rail .nav-toggle {
  /* 42px — dialled back from 56 (Justin 2026-08-08: the right-hand
     controls were crowding the map). Still clearly bigger than the stock
     30px, and still individual circles. */
  width: 42px; height: 42px;
  padding: 0; margin: 0;
  display: flex; align-items: center; justify-content: center;
  font-size: 19px; line-height: 1;
  background: var(--dk-paper);
  color: var(--dk-ink);
  border: 2px solid var(--dk-ink);
  border-top: 2px solid var(--dk-ink);   /* stock uses border-top as the shared divider */
  /* ⚠️ !important is required: style.css:16 sweeps `* { border-radius: 0
     !important }` — the reskin's "hard edges are the law of the house"
     rule. Round map controls are a deliberate, local exception to it. */
  border-radius: 50% !important;
  box-shadow: 3px 3px 0 rgba(0, 0, 0, .45);
  flex: 0 0 auto;
}
body.deck-mode #map-rail .map-rail-btn:hover,
body.deck-mode #map-rail .nav-toggle:hover { background: var(--dk-green); }
body.deck-mode #map-rail .map-rail-btn.active,
body.deck-mode #map-rail .dk-rail-btn.on {
  background: var(--dk-green); color: var(--dk-ink); border-color: var(--dk-ink);
}
/* the separator has no place in a row of free-floating circles */
body.deck-mode #map-rail .dk-rail-sep { display: none; }
/* badges pinned to a round button need to ride its edge */
body.deck-mode #map-rail .inbox-badge { top: -2px; right: -2px; }

/* 🔍 Search back at the map's BOTTOM-RIGHT (Justin 2026-08-08). Pinned
   top-right it collided with the centred date picker at common widths —
   both want the same band, and the date nav can't move without losing its
   centring. The bottom-right corner is free. */
body.deck-mode .map-container > #map-search-box,
body.deck-mode .map-container > .map-search-box {
  top: auto !important;
  bottom: 10px !important;
  right: 12px !important;
  left: auto !important;
  width: 280px;
  z-index: 1101;
}

/* ➕/➖ Zoom moved to the right-hand side. Leaflet puts it in
   .leaflet-top.leaflet-left, so the corner CONTAINER has to move —
   restyling .leaflet-control-zoom alone leaves it anchored left.
   Centred vertically in its own column just left of the circle rail, so
   the whole right-hand cluster reads as centred rather than
   bottom-anchored. */
body.deck-mode .leaflet-top.leaflet-left {
  left: auto !important;
  right: 78px !important;
  top: 50% !important;
  bottom: auto !important;
  transform: translateY(-50%) !important;
}
body.deck-mode .leaflet-top.leaflet-left .leaflet-control-zoom {
  display: flex; flex-direction: column; gap: 6px;
  border: none; box-shadow: none; background: none;
  margin: 0;
}
body.deck-mode .leaflet-control-zoom a {
  width: 56px; height: 56px; line-height: 52px;
  font-size: 26px;
  border: 2px solid var(--dk-ink);
  border-radius: 50% !important;   /* see the hard-edge note above */
  background: var(--dk-paper);
  color: var(--dk-ink);
  box-shadow: 3px 3px 0 rgba(0, 0, 0, .45);
}
body.deck-mode .leaflet-control-zoom a:hover { background: var(--dk-green); }

/* ── 3b · SIDEWAYS LIST-SWITCHER TABS ────────────────────────────────
   🐎 Activists / 🗓️ Events down the map's LEFT edge, labels turned on
   their side (Justin 2026-08-08 — swapped here from the column header).
   ⚠️ In `writing-mode: vertical-rl` the DEFAULT `text-orientation: mixed`
   rotates Latin letters 90° but keeps emoji UPRIGHT (they carry
   Vertical_Orientation=U). `text-orientation: sideways` is what forces
   everything — emoji included — to lie down with the text, so the icon
   and the word read at the same angle (Justin 2026-08-08). */
body.deck-mode .dk-vtabs {
  position: absolute;
  left: 0; top: 84px;
  z-index: 620;
  display: flex; flex-direction: column; gap: 6px;
}
body.deck-mode .dk-vtab {
  writing-mode: vertical-rl;
  text-orientation: sideways;
  display: flex; align-items: center; gap: 6px;
  padding: 12px 5px;
  font-size: 11px; font-weight: 700; letter-spacing: .05em;
  text-transform: uppercase;
  color: var(--dk-paper);
  background: var(--dk-ink);
  border: 2px solid var(--dk-ink);
  border-left: none;
  border-radius: 0;
  cursor: pointer;
  box-shadow: 3px 3px 0 rgba(0,0,0,.35);
  white-space: nowrap;
}
body.deck-mode .dk-vtab:hover { background: #333; }
body.deck-mode .dk-vtab.on {
  background: var(--dk-green);
  border-color: var(--dk-ink);
  color: var(--dk-ink);
}
/* the icon lies down with the label — inherits the tab's vertical
   writing-mode + sideways orientation, so no override here */
body.deck-mode .dk-vtab-i { font-size: 15px; line-height: 1; }

/* ── 3b2 · 🌐 THE ONLINE STATION ─────────────────────────────────────
   Online events have no map pin, so this globe IS their pin: parked at
   the map's top-left, it accepts the same drag-to-staff gesture. The
   orbiting ring + slow spin mark it as a live "somewhere else" rather
   than just another toolbar button. */
body.deck-mode .dk-online {
  position: absolute;
  left: 12px; top: 12px;
  z-index: 640;
  width: 60px; height: 60px;
  display: flex; align-items: center; justify-content: center;
  background: var(--dk-ink);
  border: 2px solid var(--dk-ink);
  border-radius: 50% !important;   /* beats the global hard-edge sweep */
  box-shadow: 3px 3px 0 rgba(0, 0, 0, .45);
  cursor: pointer;
  overflow: visible;
}
body.deck-mode .dk-online-globe {
  font-size: 30px; line-height: 1;
  animation: dkGlobeSpin 9s linear infinite;
  filter: drop-shadow(0 0 4px rgba(0, 185, 255, .9));
}
@keyframes dkGlobeSpin {
  0%, 100% { transform: rotate(-9deg) scale(1); }
  50%      { transform: rotate(9deg) scale(1.08); }
}
/* the orbit — a ring that sweeps around the globe */
body.deck-mode .dk-online-ring {
  position: absolute; inset: -6px;
  border: 2px dashed var(--dk-green);
  border-radius: 50% !important;
  opacity: .85;
  animation: dkGlobeOrbit 5s linear infinite;
  pointer-events: none;
}
@keyframes dkGlobeOrbit {
  from { transform: rotate(0deg) scaleY(.42); }
  to   { transform: rotate(360deg) scaleY(.42); }
}
body.deck-mode .dk-online:hover,
body.deck-mode .dk-online.dk-online-hot { background: #00B9FF; }
body.deck-mode .dk-online.dk-online-hot { box-shadow: 0 0 0 5px var(--dk-green), 3px 3px 0 rgba(0,0,0,.45); }

/* the picker card it opens */
.dk-onlinepick {
  pointer-events: auto;
  position: relative;
  width: var(--dk-card-w);
  max-height: var(--dk-card-h);
  background: var(--dk-ink);
  box-shadow: 7px 7px 0 var(--dk-ink);
  display: flex;
}
.dk-onlinepick * { border-radius: 0; box-sizing: border-box; }
.dk-onlinepick .dk-paper { position: relative; inset: auto; margin: 6px 6px 6px 15px; flex: 1 1 auto; min-width: 0; }
.dk-onlist { flex: 1 1 auto; overflow-y: auto; min-height: 0; }
.dk-onrow {
  display: flex; flex-direction: column; gap: 3px;
  width: 100%; padding: 8px 10px;
  text-align: left;
  background: var(--dk-paper);
  border: none; border-bottom: 1px solid #ddd; border-left: 4px solid transparent;
  cursor: pointer;
}
.dk-onrow:hover { background: #f6f6f6; border-left-color: #00B9FF; }
.dk-onrow-top { font-size: 12.5px; line-height: 1.25; }
.dk-onrow-bot { display: flex; align-items: center; gap: 8px; }
.dk-onrow-when { flex: 1 1 auto; font-size: 10.5px; color: #555; }

/* ── 3c · TETHER ─────────────────────────────────────────────────────
   The line drawn from the subject's pin to the open card. Sits under the
   card layer (z 1200) but over the map, and never eats a click. */
#dk-tether {
  position: fixed; inset: 0;
  width: 100vw; height: 100vh;
  z-index: 1150;
  pointer-events: none;
}
#dk-tether line {
  stroke: var(--dk-green, #5AE200);
  stroke-width: 3;
  stroke-dasharray: 7 6;
  filter: drop-shadow(0 0 1px rgba(0,0,0,.85));
}
#dk-tether circle { fill: none; stroke: var(--dk-green, #5AE200); stroke-width: 3; }

/* ── 4 · THE CARD LAYER ──────────────────────────────────────────────
   Fixed over the map's right side. The layer is click-through so the map
   keeps working around the card. */
/* 🃏 THE CARD SITS ON THE LEFT, hard against the list it works with
   (Justin 2026-08-19: "move the pop-up card to appear on the left side of
   the screen instead … the user is going to be constantly switching between
   the left panel and then the card on the right. If the card was just to the
   right of the left panel but still past the tabs on the side").
   Geometry: the grid gives column 1 to the roster at --sidebar-width, then
   .map-container starts — and .dk-vtabs is pinned at ITS left:0, ~28px wide.
   So the card's left edge is sidebar + a clearance that keeps the sideways
   rail fully exposed. The layer is viewport-FIXED, so it has to add the
   sidebar itself rather than inherit the grid track. */
#deck-layer {
  --dk-vtab-clear: 38px;
  position: fixed;
  top: 0; bottom: 0;
  left: calc(var(--sidebar-width, 280px) + var(--dk-vtab-clear));
  right: auto;
  width: min(460px, 46vw);
  z-index: 1200;
  pointer-events: none;
  display: flex;
  align-items: center;
  justify-content: center;
  /* the top pad clears the app header — the layer is viewport-fixed, so
     without it a tall card rides over the region picker and the ⚙️ tray */
  padding: 56px 16px 16px;
}
/* the roster can be collapsed away; the rail clearance still stands */
body.deck-mode.left-panel-collapsed #deck-layer { left: var(--dk-vtab-clear); }
body:not(.deck-mode) #deck-layer { display: none; }

.dk-card {
  pointer-events: auto;
  position: relative;
  /* ⚠️ ONE size. Flipping must not resize the card. */
  width: var(--dk-card-w);
  height: var(--dk-card-h);
  perspective: 1400px;
}
/* 🚀 The card expands out of wherever its subject stands on the map, and
   is drawn back into that pin when dismissed. deck.js sets --dk-dx/--dk-dy
   to the pin's offset from the card's centre; when the subject is off
   screen it sets neither and no animation class is added, so the card just
   appears rather than flying in from an absurd coordinate. */
@keyframes dkFly {
  from { transform: translate(var(--dk-dx, 0), var(--dk-dy, 0)) scale(.05); opacity: 0; }
  55%  { opacity: 1; }
  to   { transform: translate(0, 0) scale(1); opacity: 1; }
}
@keyframes dkFlyBack {
  from { transform: translate(0, 0) scale(1); opacity: 1; }
  to   { transform: translate(var(--dk-dx, 0), var(--dk-dy, 0)) scale(.05); opacity: 0; }
}
/* ⚠️ NO fill-mode on the fly-in. With `both` the card holds the `from`
   keyframe until the animation runs — and a BACKGROUNDED TAB freezes CSS
   animations, so the card sat invisible at scale(.05) on top of its pin
   and never arrived. With no fill the card's resting state is its normal
   one: if the animation is frozen, blocked, or unsupported, the card
   simply appears. The effect is decoration; it must never be load-bearing.
   flyback DOES need `forwards` (it has to stay gone), but closeCard tears
   the node out on a timeout backstop, so a frozen tab can't strand it. */
.dk-card.dk-fly { animation: dkFly 420ms cubic-bezier(.2, .85, .3, 1); }
.dk-card.dk-flyback { animation: dkFlyBack 300ms cubic-bezier(.5, 0, .8, .4) forwards; }

.dk-flip {
  position: absolute; inset: 0;
  transform-style: preserve-3d;
  transition: transform 380ms cubic-bezier(.2, .8, .3, 1);
}
.dk-card.dk-flipped .dk-flip { transform: rotateY(180deg); }

/* ═══ 🟩 THE PAF FACE ═══════════════════════════════════════════════════
   Re-inked 2026-08-19 from Stampede Tactics' CURRENT card, `.stc.pafc` /
   `.stc.pafg` (tactics/card.css v282 / v353) — NOT the older "Riot Press"
   face this file was built against, which Tactics retired.
   Justin: "Stampede Tactics did use to have that art style … but it has
   since been changed to a more simplified PAF branded version."

   THE WHOLE LANGUAGE, in six lines:
     white paper · 3px ink rules · 7px hard offset shadow · Roboto 900
     horizontal BANDS stacked top to bottom, one rule between each
     ONE colour band carries identity; grey #ededed wells carry the rest
     the art is a flat grey well with the glyph on a colour DISC
     circles are reserved for identity marks (the disc, count coins)
     a 9px green foot closes the card
   Gone with the old face: the pinstripe grain, the starburst, the halftone
   corner, the speed lines, the scissor clip-path, the diagonal art cut,
   Archivo Black, the emoji knockout, every rotation and skew.
   ⚠️ NEVER double a rule. A band closes with ONE 3px line — if a band has
   border-bottom its successor must not also have border-top, or the seam
   prints 6px, which is not a weight this face owns. */
.dk-face {
  position: absolute; inset: 0;
  backface-visibility: hidden;
  -webkit-backface-visibility: hidden;
  background: var(--dk-paper);
  color: var(--dk-ink);
  border: 3px solid var(--dk-ink);
  box-shadow: 7px 7px 0 var(--dk-ink);
  font-family: 'Roboto', 'Segoe UI', system-ui, sans-serif;
  /* stays visible: .dk-paper does the clipping, and the flip rig below
     depends on nothing here. */
  overflow: visible;
}
.dk-back { transform: rotateY(180deg); }

/* 🚨 THE FACE-DOWN SIDE MUST NOT EAT CLICKS.
   `backface-visibility: hidden` only stops a face from being PAINTED — it
   stays in the hit-test tree. Without this, the front face sits over the
   flipped card and swallows every click meant for the back: ↩, ✕, Send,
   Call and the whole message box were dead to a real mouse.
   ⚠️ This is invisible to scripted tests, because element.click()
   dispatches straight at the node and skips hit-testing entirely. Verify
   card controls with a REAL coordinate click, never .click(). */
.dk-card:not(.dk-flipped) .dk-back { pointer-events: none; }
.dk-card.dk-flipped .dk-front { pointer-events: none; }

/* every hard edge in the house */
.dk-face *, .dk-face *::before, .dk-face *::after {
  border-radius: 0;
  box-sizing: border-box;
}

/* the stock grain is OFF — the PAF face prints on clean paper */
.dk-face::before { content: none; }

/* 🟩 THE GREEN FOOT. This is the SAME ELEMENT that used to be the Riot
   face's green side-rail with its registration ticks — re-purposed rather
   than deleted, so nothing that walks the shell changes shape. PAF closes a
   card with 9px of green along the bottom edge (.pafc__foot) and carries no
   side rail at all.
   ⚠️ It overlays the paper's last 12px, so .dk-paper reserves that height
   in its own padding — putting the reserve on .dk-paper's BOX instead would
   shrink the flex column and squeeze the scrolling detail block. */
.dk-rail {
  position: absolute;
  left: 0; right: 0; bottom: 0; top: auto;
  width: auto; height: 9px;
  background: var(--dk-green);
  border-top: 3px solid var(--dk-ink);
  z-index: 4;
  pointer-events: none;
}
.dk-rail::after { content: none; }   /* the print-shop ticks go */

/* 🏷️ THE CORNER STAMP — PAF's `.pafc__stamp--head`: a black word in the
   card's own top-left corner. It does NOT break the frame; this face has
   no element outside its border. Both FRONTS pass one now (they passed
   none before, so the mark only ever appeared on the backs). */
.dk-tab {
  position: absolute;
  top: 0; left: 0; right: auto;
  z-index: 5;
  padding: 3px 8px 4px;
  font-family: 'Roboto', 'Segoe UI', system-ui, sans-serif;
  font-size: 9.5px; font-weight: 900; letter-spacing: .12em;
  text-transform: uppercase;
  color: var(--dk-paper);
  background: var(--dk-ink);
  border: 0;
  box-shadow: none;
  pointer-events: none;
}

/* the paper panel — full bleed inside the border, no scissor cut */
.dk-paper {
  position: absolute; inset: 0;
  background: var(--dk-paper);
  display: flex; flex-direction: column;
  overflow: hidden; z-index: 2;
  color: var(--dk-ink);
  clip-path: none;
  padding-bottom: 12px;   /* the foot's own height — see .dk-rail */
}
[data-theme="dark"] .dk-paper { background: var(--dk-paper); }

/* ── 5 · CARD FRONT ─────────────────────────────────────────────────── */
/* 🏷️ TITLE BAND — PAF's `.pafc__head`. The type colour is spent HERE and
   in the art disc, nowhere else on the card. Event cards take the orange
   (the app's own "this is a Stampede action" hue); activist cards take the
   grey well, exactly as the Tactics game face does.
   The name splits: headline on top, the parenthetical below it in a quieter
   voice inside the SAME band, so "Canvass (Park Hill)" reads as one object
   instead of a title nobody finishes. */
.dk-nameplate {
  flex: 0 0 auto;
  padding: 9px 12px 10px;
  padding-left: 12px;
  background: var(--dk-well);
  color: var(--dk-ink);
  border-bottom: 3px solid var(--dk-ink);
  overflow: hidden;
}
/* the corner stamp lives at the top-LEFT, so the first line clears it */
.dk-paper > .dk-nameplate { padding-top: 20px; }
.dk-name-main {
  font-family: 'Roboto', 'Segoe UI', system-ui, sans-serif;
  font-weight: 900;
  font-size: 20px; line-height: 1.02; letter-spacing: .004em;
  text-transform: uppercase;
  display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical;
  overflow: hidden;
}
.dk-name-sub {
  margin-top: 3px;
  font-weight: 700; font-size: 12.5px; line-height: 1.2; letter-spacing: .01em;
  opacity: .78;
  display: -webkit-box; -webkit-line-clamp: 1; -webkit-box-orient: vertical;
  overflow: hidden;
}
.dk-paper-event .dk-nameplate { background: var(--dk-amber); }

/* 🖼️ ART WELL — PAF's `.pafc__art`: a flat grey well, no clip, no filter,
   no starburst, no halftone, no speed lines. GREY on purpose, not the type
   colour: the colour already speaks twice (the band and the disc) and a
   third full panel of it turned every card into a slab.
   ⚠️ Height is held at 132px deliberately. The PAF face lets its art flex,
   but this card carries far more below it (buttons, stats, chips, people),
   .dk-paper is overflow:hidden, and a taller well squeezes .dk-detail —
   which on the event front is the ONLY route to the unassign chips. */
.dk-art {
  flex: 0 0 auto;
  position: relative;
  height: 132px;
  display: flex; align-items: center; justify-content: center;
  background: var(--dk-well);
  border-bottom: 3px solid var(--dk-ink);
  overflow: hidden;
  clip-path: none;
  filter: none;
}
.dk-art::before, .dk-art::after { content: none; }

/* ⭕ THE DISC — the glyph rides a type-coloured circle with a hard black
   ring and a hard offset shadow, PAF's `.pafc__art-em`. A circle in a
   hard-edged app on purpose: it is an identity mark, the same carve-out the
   house makes for avatars and count coins.
   ⚠️ `border-radius: 50% !important` is required twice over — style.css:16
   is `* { border-radius: 0 !important }`, and `.dk-face *` above re-asserts
   it locally. */
.dk-art-icon {
  position: relative; z-index: 2;
  width: 112px; height: 112px;
  display: flex; align-items: center; justify-content: center;
  background: var(--dk-green);
  border: 3px solid var(--dk-ink);
  border-radius: 50% !important;
  box-shadow: 6px 7px 0 rgba(0, 0, 0, .3);
  transform: none;
  filter: none;
}
.dk-paper-event .dk-art-icon { background: var(--dk-amber); }

/* 🎨 THE GLYPH KEEPS ITS COLOUR — the one place this face deliberately
   parts company with Tactics.
   PAF inks every glyph to a silhouette ("all of the emojis should be
   silhouetted in the PAF version of the event cards"), and it works there
   because TACTICS_EMOJI.parse swaps every emoji for an <img class="fx"> from
   its own icon set BEFORE the filter lands — a set drawn to read as a
   solid shape. Dispatch has no such step: these are native platform emoji,
   and brightness(0) turns 🚪 into an unrecognisable black rectangle and 🎉
   into a blob. Verified on screen before deciding.
   The disc, the colour band, the ruled bands and the green foot carry the
   PAF language without it. On the activist card there is a second reason:
   that glyph is the person's identity emoji, shared with their map pin and
   sidebar chip, so inking it would break recognition across the whole app.
   ⚠️ Keep the glyph in its OWN element regardless. `filter` applies to an
   element's entire rendered output, so anything filtered here would eat the
   disc's background, border and shadow too — which is exactly the bug this
   split was made to avoid. deck.js's artIcon() exists for that. */
.dk-art-glyph {
  display: block;
  font-size: 60px; line-height: 1;
  font-style: normal;
}
.dk-art-ev .dk-art-icon { background: var(--dk-amber); }

/* 🎛️ BUTTON ROW — icons only, one compact row. Justin 2026-08-08: "for
   the buttons, we don't even need that much space. Just use the icons."
   Each still carries a title so the meaning is one hover away. */
.dk-btnrow {
  flex: 0 0 auto;
  display: flex; gap: 4px;
  padding: 8px 10px;
  border-bottom: 3px solid var(--dk-ink);
}
.dk-ibtn {
  flex: 1 1 0;
  display: flex; align-items: center; justify-content: center;
  height: 38px;
  font-size: 19px; line-height: 1;
  text-decoration: none;
  color: var(--dk-ink);
  background: var(--dk-paper);
  border: 2px solid var(--dk-ink);
  cursor: pointer;
  transition: transform 70ms linear, box-shadow 70ms linear, background 70ms linear;
}
.dk-ibtn:hover {
  background: var(--dk-green);
  transform: translate(-2px, -2px);
  box-shadow: 3px 3px 0 var(--dk-ink);
}
.dk-ibtn:active { transform: none; box-shadow: none; }
.dk-ibtn { position: relative; }
.dk-ibtn-n {
  position: absolute; top: -6px; right: -6px;
  min-width: 15px; height: 15px;
  display: flex; align-items: center; justify-content: center;
  padding: 0 3px;
  font-size: 9px; font-weight: 800; line-height: 1;
  background: var(--dk-ink); color: var(--dk-paper);
  border: 1px solid var(--dk-paper);
}

/* 👥 WHO is on the shift — confirmed and tentative listed separately,
   each person as their own icon + name (Justin 2026-08-08). */
.dk-people { margin-top: 4px; }
.dk-people-lbl {
  font-size: 9.5px; font-weight: 800; letter-spacing: .05em; text-transform: uppercase;
  margin: 6px 0 3px;
}
.dk-people-lbl b { font-size: 11px; }
.dk-people-row { display: flex; flex-wrap: wrap; gap: 3px; }
.dk-people-none { font-size: 10px; font-style: italic; opacity: .6; }
.dk-person {
  display: inline-flex; align-items: center; gap: 3px;
  max-width: 100%;
  padding: 2px 5px;
  font-size: 10px; font-weight: 700; line-height: 1.3;
  border: 2px solid var(--dk-ink);
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
.dk-person-i { font-size: 11px; }
.dk-person-go { background: var(--dk-green); }
.dk-person-tent { background: var(--dk-well); border-style: dashed; }
.dk-person-more { background: var(--dk-ink); color: var(--dk-paper); }

/* the event's art slot leans on the type icon, so give it a bit more air */
.dk-art-ev .dk-art-icon { font-size: 58px; }

/* 🏷️ DETAIL CHIPS — two per row instead of one full-width line each, and
   printed as chips rather than raw text. */
.dk-detail {
  flex: 1 1 auto;
  min-height: 0;
  overflow-y: auto;
  padding: 8px 10px 10px;
  background: var(--dk-paper);
}
.dk-chips {
  display: grid;
  grid-template-columns: 1fr 1fr;
  gap: 4px;
  margin-bottom: 5px;
}
.dk-chips-wide { grid-template-columns: 1fr; }
.dk-chip {
  display: flex; align-items: center; gap: 4px;
  min-width: 0;
  padding: 3px 6px;
  font-size: 10.5px; font-weight: 700; line-height: 1.3;
  background: var(--dk-well);
  border: 2px solid var(--dk-ink);
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
.dk-chip-i { flex: 0 0 auto; font-size: 11px; }
.dk-chip-ok { background: var(--dk-green); }
.dk-chip-warn { background: #FFEB00; }
.dk-chip-wide { white-space: normal; }

.dk-actions {
  flex: 1 1 auto;
  display: flex; flex-direction: column; gap: 6px;
  padding: 10px;
  overflow-y: auto;
}
.dk-actions-tight { gap: 5px; padding: 8px 10px 10px; }
.dk-act {
  flex: 0 0 auto;
  display: flex; align-items: center; gap: 8px;
  padding: 9px 10px;
  font-family: 'Roboto', 'Segoe UI', system-ui, sans-serif;
  font-size: 13px; font-weight: 700; letter-spacing: .02em;
  text-align: left; text-decoration: none;
  color: var(--dk-ink);
  background: var(--dk-paper);
  border: 2px solid var(--dk-ink);
  cursor: pointer;
  transition: transform 70ms linear, box-shadow 70ms linear, background 70ms linear;
}
.dk-act:hover {
  background: var(--dk-green);
  transform: translate(-2px, -2px);
  box-shadow: 3px 3px 0 var(--dk-ink);
}
.dk-act:active { transform: translate(0, 0); box-shadow: none; }
.dk-act-n {
  margin-left: auto;
  font-size: 11px;
  background: var(--dk-ink); color: var(--dk-paper);
  padding: 1px 6px;
}

/* event front */
.dk-evhead {
  flex: 0 0 auto;
  display: flex; align-items: flex-start; gap: 7px;
  padding: 12px 10px 8px;
  background: var(--dk-ink);
  color: var(--dk-paper);
}
.dk-evhead-name {
  font-family: 'Roboto', 'Segoe UI', system-ui, sans-serif;
  font-weight: 900;
  font-size: 15px; line-height: 1.2;
}
.dk-evhead-icon { font-size: 22px; line-height: 1; flex: 0 0 auto; }
.dk-evmeta { flex: 0 0 auto; padding: 8px 10px; background: var(--dk-well); border-bottom: 2px solid var(--dk-ink); }
.dk-evmeta-row { font-size: 12px; line-height: 1.5; }
.dk-nohost { color: var(--dk-amber); font-weight: 700; }

/* team tags */
.dk-teams { display: flex; flex-wrap: wrap; gap: 3px; margin-top: 5px; }
.dk-team {
  padding: 1px 6px;
  font-size: 10px; font-weight: 700;
  background: var(--dk-ink); color: var(--dk-paper);
}

/* 📊 at-a-glance dispatcher numbers. A zero is deliberately faded rather
   than hidden — "nobody has asked them yet" is itself the signal. */
.dk-stats {
  flex: 0 0 auto;
  display: flex; flex-wrap: wrap; gap: 4px;
  padding: 8px 10px;
  border-bottom: 2px solid var(--dk-ink);
}
.dk-stat {
  display: inline-flex; align-items: center; gap: 3px;
  padding: 2px 7px;
  font-size: 12px; font-weight: 800;
  border: 2px solid var(--dk-ink);
  background: var(--dk-paper);
}
.dk-stat-i { font-size: 11px; }
.dk-stat-zero { opacity: .38; font-weight: 600; }
.dk-stat-host { background: #FFEB00; }
.dk-stat-ok { background: var(--dk-green); }
.dk-stat-tent { background: var(--dk-well); }
.dk-stat-ask { background: #FFEB00; }
.dk-stat-msg { background: #00B9FF; }
.dk-band {
  flex: 0 0 auto;
  display: flex; align-items: center; gap: 8px;
  padding: 10px;
  border-bottom: 3px solid var(--dk-ink);
}
.dk-band-label {
  font-family: 'Roboto', 'Segoe UI', system-ui, sans-serif;
  font-weight: 900;
  font-size: 10px; letter-spacing: .1em; text-transform: uppercase;
}

/* ── 6 · CARD BACK ──────────────────────────────────────────────────── */
.dk-backhead {
  flex: 0 0 auto;
  display: flex; align-items: center; gap: 8px;
  padding: 8px 10px;
  background: var(--dk-ink);
  color: var(--dk-paper);
  font-family: 'Roboto', 'Segoe UI', system-ui, sans-serif;
  font-weight: 900;
  font-size: 13px; letter-spacing: .06em; text-transform: uppercase;
}
.dk-backhead-title { flex: 1 1 auto; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.dk-backhead-phone { font-family: 'Roboto', sans-serif; font-size: 11px; font-weight: 400; opacity: .8; }
.dk-backhead-phone.dk-nophone { color: var(--dk-amber); opacity: 1; }
.dk-iconbtn {
  flex: 0 0 auto;
  width: 24px; height: 24px;
  display: flex; align-items: center; justify-content: center;
  background: transparent;
  border: 2px solid var(--dk-paper);
  color: var(--dk-paper);
  font-size: 12px;
  cursor: pointer;
}
.dk-iconbtn:hover { background: var(--dk-green); border-color: var(--dk-green); color: var(--dk-ink); }

.dk-pane { flex: 1 1 auto; display: flex; flex-direction: column; min-height: 0; }
.dk-pane-scroll { overflow-y: auto; padding: 8px; }
/* The reparented openActivistInfoModal — strip every trace of popup
   chrome so it reads as flat card content. Mirrors style.css's
   `.wf-act-info-col .self-info-modal-backdrop.wf-act-info-inline`, which
   is the ONE part of that rule set scoped to the Activists tab; the
   .wf-act-solo section-picking rules are global and reused as-is. */
.dk-pane .self-info-modal-backdrop.wf-act-info-inline {
  position: static !important; inset: auto !important; display: block !important;
  width: 100% !important; height: auto !important; max-height: none !important;
  padding: 0 !important; margin: 0 !important; background: transparent !important;
  backdrop-filter: none !important; -webkit-backdrop-filter: none !important;
  z-index: auto !important; box-shadow: none !important;
}
.dk-pane .self-info-modal-body { padding: 0 !important; }
.dk-pane .self-info-section { font-size: 12px; }
.dk-pane .self-info-emoji-panel { max-height: none !important; }
.dk-desc { font-size: 12px; line-height: 1.5; white-space: pre-wrap; }

/* 📨 SMS carts re-homed onto <body> by deck.js's _mountSMSModalBelowMap
   wrapper. Only a skin — the layout is the stock centred-modal CSS. */
body.deck-mode .sms-modal-overlay.dk-sms .sms-modal {
  border: 3px solid var(--dk-ink);
  border-top: 6px solid var(--dk-green);
  box-shadow: 7px 7px 0 var(--dk-ink);
  max-width: calc(100vw - 32px);
}

/* 📅 THE WEEK PANE, restacked for a 360px card. The desktop row lays
   day · name · time · score out on ONE line, and at card width the
   flexible middle — the shift's NAME, the only part that says what is
   being recommended — collapsed to an ellipsis (Justin 2026-08-19: "the
   actual shift that it's recommending, I don't think it even shows what
   it is"). Two lines here: name on top at full width, then the day/time/
   score meta beneath it. */
.dk-pane .wf-act-rec-row {
  display: grid;
  grid-template-columns: auto 1fr auto;
  grid-template-areas:
    'name name name'
    'when time score';
  gap: 2px 6px;
  align-items: center;
  padding: 6px 8px;
}
.dk-pane .wf-act-rec-name {
  grid-area: name;
  white-space: normal;
  overflow: visible;
  text-overflow: clip;
  font-size: 12.5px;
  line-height: 1.25;
}
.dk-pane .wf-act-rec-when { grid-area: when; flex: 0 0 auto; opacity: .8; }
.dk-pane .wf-act-rec-time { grid-area: time; justify-self: start; opacity: .8; }
.dk-pane .wf-act-rec-row .wfs-add-res-score { grid-area: score; justify-self: end; }
/* ➕ Tentative sits beside the row on desktop; at card width it gets its
   own full-width line under it so neither has to shrink. */
.dk-pane .wf-act-week-rec { flex-direction: column; gap: 2px; }
.dk-pane .wf-act-week-rec .wf-act-week-add { width: 100%; padding: 4px; }
/* 🦜 Ask about all — a real, tappable line of its own rather than a chip
   crowded into the section heading. */
.dk-pane .wf-act-rec-head { display: flex; flex-wrap: wrap; align-items: center; gap: 5px; }
.dk-pane .wf-act-cart-btn { flex: 1 1 100%; padding: 5px; text-align: center; }

/* 💬 Contact — mirrors the Activists tab's pane markup so the SAME
   machinery drives it: renderSMSHistoryInModal paints .sms-modal-history,
   and sendInboxReply reads #inbox-reply-body / #inbox-reply-status BY ID.
   (Those ids are why deck.js clears #workflow-panel-body on entry.) */
.dk-history {
  flex: 1 1 auto; min-height: 0; overflow-y: auto;
  padding: 8px;
  background: var(--dk-well);
  border-bottom: 2px solid var(--dk-ink);
}
.dk-compose { flex: 0 0 auto; padding: 8px; display: flex; flex-direction: column; gap: 6px; }
.dk-compose textarea {
  width: 100%; min-height: 56px; resize: vertical;
  padding: 7px;
  font-family: 'Roboto', 'Segoe UI', system-ui, sans-serif;
  font-size: 13px;
  color: var(--dk-ink); background: var(--dk-paper);
  border: 2px solid var(--dk-ink);
}
.dk-compose textarea:focus { outline: none; border-color: var(--dk-green-dk); }
.dk-compose-btns { display: flex; gap: 6px; }
.dk-compose-btns button {
  flex: 1 1 auto;
  padding: 8px;
  font-size: 13px; font-weight: 700;
  border: 2px solid var(--dk-ink);
  background: var(--dk-green); color: var(--dk-ink);
  cursor: pointer;
}
.dk-compose-btns button:hover { box-shadow: 3px 3px 0 var(--dk-ink); transform: translate(-2px, -2px); }
.dk-compose-btns .dk-call-btn { background: var(--dk-paper); }
.dk-reply-status { font-size: 11px; min-height: 14px; color: var(--dk-ink); }
.dk-nophone-note { padding: 10px; font-size: 12px; background: var(--dk-well); border: 2px solid var(--dk-amber); }

/* ── 6b · 🐎 THE STAMPEDE ────────────────────────────────────────────
   Two travel animations (Justin 2026-08-08): things that fly, fly — an
   arcing glide with a wing-beat. Everything else stampedes: a low, fast
   ground run with a gallop bob, echoing the map's own rsvpRun/rsvpGallop
   pair so the deck and the map read as one world.
   deck.js sets --tx/--ty (delta to the card), --arc (peak height) and
   --face (-1 when travelling right; the glyphs natively face LEFT). */
#dk-travel {
  position: fixed; inset: 0;
  z-index: 1400;
  pointer-events: none;
  overflow: hidden;
}
.dk-trav {
  position: absolute;
  font-size: 30px; line-height: 1;
  will-change: transform;
  filter: drop-shadow(0 0 2px #fff) drop-shadow(0 2px 3px rgba(0,0,0,.5));
}
/* 🕊️ FLY — a high arc, banking into the turn */
.dk-trav-fly { animation: dkFly 900ms cubic-bezier(.35, .05, .3, 1) forwards; }
@keyframes dkFly {
  0%   { transform: translate(0, 0) scaleX(var(--face, 1)) rotate(0deg); }
  50%  { transform: translate(calc(var(--tx) * .5), calc(var(--ty) * .5 + var(--arc))) scaleX(var(--face, 1)) rotate(-8deg); }
  100% { transform: translate(var(--tx), var(--ty)) scaleX(var(--face, 1)) rotate(0deg) scale(.5); opacity: .15; }
}
/* 🐎 RUN — hugs the ground; the gallop rides on an inner element-less
   second animation so the bob is independent of the travel path */
.dk-trav-run {
  animation: dkStampede 820ms cubic-bezier(.3, .8, .35, 1) forwards,
             dkGallop 130ms ease-in-out infinite alternate;
}
@keyframes dkStampede {
  0%   { transform: translate(0, 0) scaleX(var(--face, 1)); }
  100% { transform: translate(var(--tx), var(--ty)) scaleX(var(--face, 1)) scale(.55); opacity: .15; }
}
@keyframes dkGallop {
  from { margin-top: 0; }
  to   { margin-top: -7px; }
}

/* 🎯 the card as a drop target */
.dk-card.dk-drop-on .dk-face {
  box-shadow: 0 0 0 4px var(--dk-green), 7px 7px 0 var(--dk-ink);
}
.dk-card.dk-drop-on { transform: scale(1.02); transition: transform 90ms linear; }

/* 🎉 FULLY STAFFED — the shift lifts off the ground when confirmed
   reaches needed. Fires only on the transition into full (deck.js), so a
   card that opens already-full doesn't jump. */
.dk-card.dk-full .dk-band { background: var(--dk-green); }
.dk-card.dk-lift { animation: dkLift 1100ms cubic-bezier(.3, .9, .4, 1); }
@keyframes dkLift {
  0%   { transform: translateY(0) rotate(0deg); }
  25%  { transform: translateY(-16px) rotate(-1.5deg); }
  55%  { transform: translateY(-8px) rotate(1deg); }
  80%  { transform: translateY(-12px) rotate(-.5deg); }
  100% { transform: translateY(0) rotate(0deg); }
}

/* a tentative can be whistled back off the shift — say so */
.dk-person-tent { cursor: pointer; }
.dk-person-tent:hover {
  background: #FF8E00;
  text-decoration: line-through;
}

/* ── 7 · EMPTY / MOTION ─────────────────────────────────────────────── */
.dk-empty { padding: 18px; font-size: 12px; text-align: center; color: #666; }

@media (prefers-reduced-motion: reduce) {
  .dk-flip, .dk-act, .dk-compose-btns button,
  body.deck-mode #volunteer-list .volunteer-card { transition: none !important; }
  .dk-act:hover, .dk-compose-btns button:hover { transform: none; }
  /* the fly still has to RUN (closeCard waits on animationend) — just make
     it instant rather than removing the animation entirely */
  .dk-card.dk-fly, .dk-card.dk-flyback { animation-duration: 1ms !important; }
  /* the stampede is pure garnish; the assignment already happened */
  .dk-trav { display: none !important; }
  .dk-card.dk-lift { animation: none !important; }
}

@media (max-width: 900px) {
  #deck-layer { width: 100%; left: 0; padding: 56px 8px 8px; }
  body.deck-mode { --dk-card-w: min(360px, calc(100vw - 32px)); }
}

/* ═══ 📱 PHONES (v14) ══════════════════════════════════════════════════
   Deck mode became the ONLY mode on 2026-08-20, and mobile.css had not
   been touched since July — so every phone inherited a layout designed
   around a mouse and a 1400px window. What that actually cost, measured
   in a 390×844 frame:

     · the 📁 Tabs bottom tab landed on #workflow-panel, which deck mode
       sets to display:none — a BLANK GREY SCREEN, one of three tabs;
     · 🚩 Shifts rendered 0px wide (the right panel is collapsed), so the
       bar had a dead gap in it;
     · that left the 🗓️ Events and 📥 Inbox lists reachable ONLY through
       #dk-vtabs — a 27px sideways strip lying across the map's left edge,
       which is both an undiscoverable control and a hole in the map;
     · the card's fixed 503px height cleared a 844px phone and collided
       with the tab bar on anything shorter.

   The fix is one idea: ON A PHONE THE BOTTOM TAB BAR IS THE ONLY
   NAVIGATION. mobile.js builds a deck-flavoured bar (Map · Activists ·
   Events · Inbox) whose list tabs drive _deckSetList, and everything
   below takes the desktop-only furniture back off the screen.
   ═══════════════════════════════════════════════════════════════════ */
@media (max-width: 768px) {
  /* 🚫 the sideways rail. Its three lists are the bottom bar's three list
     tabs now, and a duplicate navigation that also covers the map is worse
     than no navigation at all. ⚠️ Do NOT delete buildVTabs over this — the
     strip is still the desktop control, and deck.js's click-away allowlist
     names #dk-vtabs. Hidden here, alive there. */
  body.deck-mode #dk-vtabs { display: none !important; }

  /* 🃏 the card has to clear the tab bar. #deck-layer centres the card in
     its own padding box, so the bar's height belongs in the padding —
     shrinking the card alone would still let a tall card sit under it.
     ⚠️ --mobile-tabbar-height is declared in mobile.css; the fallback is
     load-bearing for the case where that file fails to load. */
  body.deck-mode #deck-layer {
    padding-bottom: calc(var(--mobile-tabbar-height, 60px) + var(--mobile-safe-bottom, 0px) + 8px);
  }
  body.deck-mode {
    --dk-card-h: min(503px, calc(100vh - 56px - var(--mobile-tabbar-height, 60px) - var(--mobile-safe-bottom, 0px) - 24px));
  }

  /* 🎴 the preset strip SCROLLS instead of clipping. Six discs plus ➕ is
     ~430px of content in a 390px window; unscrolled, the last two were
     simply unreachable. */
  body.deck-mode .dk-presets {
    overflow-x: auto;
    overflow-y: hidden;
    flex-wrap: nowrap;
    -webkit-overflow-scrolling: touch;
    scrollbar-width: none;
  }
  body.deck-mode .dk-presets::-webkit-scrollbar { display: none; }
  body.deck-mode .dk-presets > * { flex: 0 0 auto; }

  /* 🚨 mobile.css:947 hides .volunteer-panel and [data-view="activists"] under
     body.left-panel-collapsed. That rule is about the RETIRED left panel —
     but deck mode UN-RETIRES it and mounts all three of its lists inside, and
     all three list tabs ride that same data-view. So one stale class would
     blank the deck's only surface AND three quarters of its tab bar at once.
     Deck's own un-retirement already overrides the panel on desktop; this is
     the phone half of the same override. */
  body.deck-mode.left-panel-collapsed .volunteer-panel { display: flex !important; }
  body.deck-mode #mobile-tabbar[data-flavour="deck"] button[data-view="activists"] { display: flex !important; }

  /* 🌐 the online station moves DOWN, out from under the search bar.
     mobile.js's liftMapSearch pulls #map-search-box out of the hidden
     bottom toolbar and pins it `position:fixed` across the top of the map
     (≈46-87px into .map-container). The station's desktop `top:12px` put
     the globe — and the flags flying off its right shoulder — directly
     beneath it, so the two overlapped in the map's busiest corner.
     ⚠️ MOVE IT, NEVER HIDE IT: the globe is the drop target that staffs an
     ONLINE event, which has no map pin of its own to drop onto. Hiding it
     would silently remove the only way to assign someone to one. */
  body.deck-mode .dk-online { top: 96px; }
}

/* ═══ 🟩 PAF FACE — THE TAIL ════════════════════════════════════════════
   The last few surfaces the main reskin above doesn't reach, plus the
   defects three adversarial reviews found in the first draft. Kept at the
   end of the file so it wins on source order where it needs to.
   ══════════════════════════════════════════════════════════════════════ */

/* ⚠️ SCOPED TO THE CARD ON PURPOSE. `.dk-count` / `.dk-tent` are SHARED
   furniture — countHTML also builds the events-list rows (.dk-evrow) and
   the online picker (.dk-onrow), both of which sit on WHITE. Restyling the
   bare class to a paper fill made the staffing count a white pill on a
   white row, i.e. invisible, in the most-scanned element of the left rail. */
body.deck-mode .dk-face .dk-count {
  background: var(--dk-paper);
  border: 3px solid var(--dk-ink);
  font-weight: 900;
}
body.deck-mode .dk-face .dk-count.empty {
  color: var(--dk-ink);
  border-color: var(--dk-ink);
  background: var(--dk-amber);   /* not --dk-red: red is a fifth hue, off-guide */
}

/* an empty chip row must not print a band. deck.js emits the availability
   row unconditionally, so an activist with no prefs would otherwise get a
   bare ruled strip sitting just above the green foot. */
body.deck-mode .dk-face .dk-chips:empty { display: none; }

/* the fourth neutral, swept. #666 is not in the guide. */
body.deck-mode .dk-face .dk-empty,
body.deck-mode .dk-onlinepick .dk-empty { color: var(--dk-ink); opacity: .62; }

/* the card's scrollbars follow the card, not the app's green.
   ⚠️ The :hover state needs its own rule — `.dk-face ::-webkit-scrollbar-thumb`
   is (0,1,1) and does not cover style.css's (0,0,2) `…-thumb:hover`, so the
   thumb flipped back to green the moment you touched it. */
body.deck-mode .dk-face ::-webkit-scrollbar-thumb,
body.deck-mode .dk-face ::-webkit-scrollbar-thumb:hover,
body.deck-mode .dk-onlinepick ::-webkit-scrollbar-thumb,
body.deck-mode .dk-onlinepick ::-webkit-scrollbar-thumb:hover { background: var(--dk-ink); }
body.deck-mode .dk-face ::-webkit-scrollbar-track,
body.deck-mode .dk-face ::-webkit-scrollbar-corner { background: var(--dk-well); }

/* 🐎 the back's header is a band like every other band on this face */
body.deck-mode .dk-backhead { border-bottom: 3px solid var(--dk-ink); padding-top: 20px; }

/* 📄 the description pane gets PAF's centred voice */
body.deck-mode .dk-desc { font-size: 14.5px; line-height: 1.32; }

/* the staffing tag is its own Leaflet layer, so it never inherited
   .shift-flag-faded's opacity from the flag it belongs to — an other-day
   count read as bright as today's (Justin 2026-08-19). */
body.deck-mode .dk-tag-faded { opacity: .5; }

/* 🚩 the halyard planted in the 🌐 online station. Online events have no
   coordinate, so the globe IS their pin — and it flies the same cloths as
   every place on the map, so it reads as somewhere with events rather than
   as a toolbar button (Justin 2026-08-19).
   ⚠️ pointer-events:none is load-bearing: the BUTTON owns the click and the
   drag-drop, and a flag that ate either would break assigning to online. */
body.deck-mode .dk-online-flag {
  position: absolute;
  /* planted on the globe's RIGHT SHOULDER, flying clear of the black disc —
     tucked at left:42/bottom:2 it sat mostly ON the disc, where the black
     no-host cloth and the black pole vanished entirely (seen on screen). */
  left: 48px; bottom: 22px;
  z-index: 2;
  pointer-events: none;
  transform: scale(.9);
  transform-origin: bottom left;
  /* the white halo is what keeps black cloths legible over the disc AND
     over any dark map tile behind the station */
  filter: drop-shadow(0 0 1.5px #fff) drop-shadow(2px 2px 0 rgba(0, 0, 0, .45));
}
body.deck-mode .dk-online-flag svg { display: block; overflow: visible; }
/* the globe needs room on its right for the cloths to fly into */
body.deck-mode .dk-online { overflow: visible; }

/* 🫧 working-group bubbles riding the art's LEFT edge — the Tactics rail
   idiom (.pafc__rail), on the activist card (Justin 2026-08-19). Circles on
   purpose: identity marks, the house's one carve-out from hard edges. */
body.deck-mode .dk-art-rail {
  position: absolute;
  left: 8px; top: 50%;
  transform: translateY(-50%);
  z-index: 3;
  display: flex; flex-direction: column; align-items: flex-start; gap: 4px;
  max-height: calc(100% - 12px);
  flex-wrap: wrap;
  pointer-events: none;   /* a column of marks, not a click target on the art */
}
body.deck-mode .dk-art-bubble {
  width: 34px; height: 34px;
  display: flex; align-items: center; justify-content: center;
  background: var(--dk-paper);
  border: 3px solid var(--dk-ink);
  border-radius: 50% !important;   /* beats style.css:16 + the .dk-face sweep */
  font-size: 16px; line-height: 1; font-style: normal;
  pointer-events: auto;            /* the title tooltip still works */
}

/* 🖼️ event photographs from Tactics — cover the window edge to edge; the
   name-matched SERIES art letterboxes (it is a composed graphic, cropping
   eats its edges) against its own ground colour, exactly as Tactics does. */
body.deck-mode .dk-art-photo {
  position: absolute; inset: 0;
  width: 100%; height: 100%;
  object-fit: cover;
}
body.deck-mode .dk-art-photo.dk-art-series {
  object-fit: contain;
  background: var(--dk-series-bg, #000);
}

/* 🔍 the event search — the green-left-bar input language every Dispatch
   search wears (see the Stampede reskin layer §13l) */
body.deck-mode .dk-evsearch { padding: 8px 8px 0; }
body.deck-mode .dk-evsearch input {
  width: 100%;
  padding: 7px 9px;
  font: 500 13px 'Roboto', 'Segoe UI', system-ui, sans-serif;
  color: var(--dk-ink);
  background: var(--dk-paper);
  border: 1px solid #ccc;
  border-left: 6px solid var(--dk-green);
}
body.deck-mode .dk-evsearch input:focus { outline: none; border-color: var(--dk-ink); border-left-color: var(--dk-green); }
/* the search belongs to the Events list alone */
body.deck-mode:not(.deck-list-events) .dk-evsearch { display: none !important; }

/* two emoji on the disc — smaller and hard-nowrapped so the pair sits side
   by side inside the circle instead of stacking out of it */
body.deck-mode .dk-art-glyph-pair {
  font-size: 36px;
  white-space: nowrap;
  letter-spacing: -2px;
}
