/*
 * board.css — THE WALL AND THE RECORD: the board surface (2026-10-25).
 * Direction: .claude/plans/frontend-redesign/direction.md (layout 4.1).
 *
 * The board is an instrument wall: a 48px rail, a 40px sub-rail, six channel
 * bays. Lines carry structure, lamps carry state, mono carries the machine.
 * Laws honored here: every color is a token; no serif on this surface; no
 * animation longer than 640ms inside anything SSE-swapped (the flatline
 * caret rides exactly at the cap); no loops inside polled containers
 * beyond it.
 *
 * This sheet also still dresses the shared rail chrome (.app-header and
 * friends) that admin/writeups render until their slices land, plus the
 * .activity-list base modals.css layers onto — marked CROSS-SURFACE KEEP.
 */

/* ==========================================================================
 * THE INSTRUMENT RAIL (48px)
 * ========================================================================== */

.app-header {
  display: flex;
  align-items: center;
  gap: var(--sp-4);
  height: var(--rail-h);
  padding: 0 var(--sp-4);
  background: var(--s-panel);
  border-bottom: 1px solid var(--line);
  box-shadow: var(--shadow-bevel);
}

.header-brand {
  display: flex;
  align-items: center;
  gap: var(--sp-2);
  flex: none;
}

/* The house mark: the hex-lamp, accent-lit. The ring logo, icon pile and
   watermark are retired into it (direction: iconography). */
.rail-mark { color: var(--sig); }

.wordmark {
  font-family: var(--font-mono);
  font-size: var(--t-ui);
  font-weight: 600;
  letter-spacing: 0.18em;
  color: var(--text-label);
  white-space: nowrap;
}

.wordmark .brand-accent { color: var(--sig); }

/* Event mast: deterministic emblem + name + status lamp, mono. */
.event-mast {
  display: flex;
  align-items: center;
  gap: var(--sp-2);
  padding-left: var(--sp-4);
  border-left: 1px solid var(--line);
  min-width: 0;
}

.event-mast .event-name {
  font-family: var(--font-mono);
  font-size: var(--t-ui);
  color: var(--text-max);
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}

.event-mast .event-status {
  font-family: var(--font-mono);
  font-size: var(--t-meta);
  color: var(--text-label);
  white-space: nowrap;
}

/* Live readouts: mono numerals under micro-captions. The numerals live on
   stable ids (board.js updateHeaderStats rewrites their text only). */
.header-stats {
  margin-left: auto;
  display: flex;
  align-items: center;
  gap: var(--sp-4);
  flex: none;
}

.stat-item {
  display: flex;
  flex-direction: column;
  align-items: flex-end;
  line-height: 1.15;
  min-width: 0;
}

.stat-val {
  font-size: var(--t-ui);
  font-weight: 600;
  color: var(--text-max);
  white-space: nowrap;
}

/* Odometer reels (f3): digits roll to their new value (320ms settle). The
   reels are machine display only — the wrapper's aria-label carries the
   value for readers. '.ready' gates the transition so a (re)build lands
   parked at its target instead of rolling in from zero. */
.stat-val .odo {
  display: inline-flex;
}

.stat-val .odo-d {
  display: inline-block;
  height: 1.15em;
  overflow: hidden;
}

.stat-val .odo-reel {
  display: flex;
  flex-direction: column;
}

.stat-val .odo-reel.ready {
  transition: transform var(--t-settle) var(--ease-needle);
}

.stat-val .odo-reel span,
.stat-val .odo-p {
  height: 1.15em;
  line-height: 1.15em;
}

.stat-val .odo-p {
  display: inline-block;
}

.stat-lbl {
  font-family: var(--font-mono);
  font-size: var(--t-meta);
  letter-spacing: 0.08em;
  color: var(--text-label);
  white-space: nowrap;
}

.stat-item-live {
  padding-left: var(--sp-4);
  border-left: 1px solid var(--line);
}

.live-sync-indicator {
  display: inline-flex;
  align-items: center;
  gap: var(--sp-1);
  font-family: var(--font-mono);
  font-size: var(--t-ui);
  color: var(--st-good);
  white-space: nowrap;
}

/* The LIVE lamp breathes — legal: the rail is a stable page wrapper. */
.pulse-dot {
  width: 6px;
  height: 6px;
  border-radius: var(--r-lamp);
  background: var(--st-good);
  box-shadow: var(--lamp-glow) var(--st-good);
  animation: dot-breathe var(--c-breathe) var(--ease-drift) infinite;
}

/* Wall heartbeat (f2): one 640ms glow pass over the hex-lamp when a real
   swarm event lands (claim, capture, note, state move). Page-level chrome —
   the poll law never applies; board.js's pulse() restarts it per event. */
.rail-mark.beat {
  animation: lamp-beat var(--dur-4) var(--ease-snap);
}

@keyframes lamp-beat {
  0%, 100% { filter: none; }
  35% { filter: drop-shadow(var(--lamp-glow) var(--sig)); }
}

.header-actions {
  display: flex;
  align-items: center;
  gap: var(--sp-2);
  flex: none;
}

.user-pill {
  display: inline-flex;
  align-items: center;
  gap: var(--sp-2);
  padding: 2px var(--sp-2);
  background: var(--s-pit);
  border: 1px solid var(--line);
  border-radius: var(--r-chamfer);
  font-family: var(--font-mono);
  font-size: var(--t-meta);
  color: var(--text-body);
  white-space: nowrap;
}

.role-tag {
  padding: 0 var(--sp-1);
  border: 1px solid var(--line-tick);
  border-radius: var(--r-chamfer);
  color: var(--text-label);
  text-transform: uppercase;
  letter-spacing: 0.06em;
  font-size: var(--t-meta);
}

.role-tag.captain {
  border-color: var(--sig);
  color: var(--sig);
}

/* ==========================================================================
 * THE SUB-RAIL (40px): channel lamps, offline latch, terminal search,
 * the happening toggle.
 * ========================================================================== */

.sub-bar {
  display: flex;
  align-items: center;
  gap: var(--sp-3);
  height: var(--subrail-h);
  padding: 0 var(--sp-4);
  background: var(--s-pit);
  border-bottom: 1px solid var(--line-soft);
}

.filters-group {
  display: flex;
  align-items: center;
  gap: var(--sp-1);
  min-width: 0;
}

.sub-bar .filters-label {
  font-family: var(--font-mono);
  font-size: var(--t-meta);
  color: var(--text-label);
  margin-right: var(--sp-1);
  white-space: nowrap;
}

/* Category filter lamps: an unlit hue dot; latching lights the border and
   fills the socket with the hue's dim. "All" and the offline latch carry no
   category hue (accent / good respectively). */
.filters-group .filter-btn:not([data-category="all"])::before {
  content: "";
  width: 6px;
  height: 6px;
  border-radius: var(--r-lamp);
  background: var(--text-faint);
  opacity: 0.65;
  flex: none;
}

.filter-btn[data-category="pwn" i]::before { background: var(--cat-pwn); }
.filter-btn[data-category="web" i]::before { background: var(--cat-web); }
.filter-btn[data-category="crypto" i]::before { background: var(--cat-crypto); }
.filter-btn[data-category="rev" i]::before { background: var(--cat-rev); }
.filter-btn[data-category="forensics" i]::before { background: var(--cat-forensics); }
.filter-btn[data-category="misc" i]::before { background: var(--cat-misc); }
.filter-btn[data-category="stego" i]::before { background: var(--cat-stego); }
.filter-btn[data-category="unknown" i]::before { background: var(--cat-misc); }

.filter-btn[data-category="pwn" i].active { border-color: var(--cat-pwn); background: var(--cat-pwn-dim); }
.filter-btn[data-category="web" i].active { border-color: var(--cat-web); background: var(--cat-web-dim); }
.filter-btn[data-category="crypto" i].active { border-color: var(--cat-crypto); background: var(--cat-crypto-dim); }
/* rev shares THE yellow (tokens ship no --cat-rev-dim; the warn dim is it) */
.filter-btn[data-category="rev" i].active { border-color: var(--cat-rev); background: var(--st-warn-dim); }
.filter-btn[data-category="forensics" i].active { border-color: var(--cat-forensics); background: var(--cat-forensics-dim); }
.filter-btn[data-category="misc" i].active { border-color: var(--cat-misc); background: var(--cat-misc-dim); }
.filter-btn[data-category="stego" i].active { border-color: var(--cat-stego); background: var(--cat-stego-dim); }
.filter-btn[data-category="unknown" i].active { border-color: var(--cat-misc); background: var(--cat-misc-dim); }

.filter-btn[data-category].active { color: var(--text-max); }

/* Offline-ok latch: the human's filter, lit good when latched. */
#filter-offline-btn.active {
  border-color: var(--st-good);
  background: var(--st-good-dim);
  color: var(--text-max);
}

#filter-offline-btn.active::before { background: var(--st-good); opacity: 1; }

.sub-bar-search {
  margin-left: auto;
  display: flex;
  align-items: center;
  gap: var(--sp-2);
  flex: none;
}

/* Terminal search: a pit socket, mono, no rounded comfort. */
.search-box {
  display: flex;
  align-items: center;
  gap: var(--sp-1);
  height: 28px;
  padding: 0 var(--sp-2);
  background: var(--s-card);
  border: 1px solid var(--line);
  border-radius: var(--r-chamfer);
  color: var(--text-label);
  transition: border-color var(--t-fast) var(--ease-snap);
}

.search-box:focus-within { border-color: var(--sig); }

/* No outline suppression: the global :focus-visible double-line ring (base.css)
   is the focus law; the socket's border shift is the secondary cue. */
.search-input {
  width: 220px;
  background: transparent;
  border: none;
  color: var(--text-max);
  font-family: var(--font-mono);
  font-size: var(--t-meta);
}

.search-input::placeholder { color: var(--text-faint); }

/* The happening toggle: fresh-dot docks to its corner and lights only when
   hidden content actually changed (board.js owns the .fresh class). */
.happening-toggle { position: relative; white-space: nowrap; }

.happening-toggle .fresh-dot {
  position: absolute;
  top: -3px;
  right: -3px;
  opacity: 0;
  transform: scale(0.4);
  transition: opacity var(--t-fast) var(--ease-snap),
    transform var(--t-fast) var(--ease-snap);
  pointer-events: none;
}

.happening-toggle.fresh .fresh-dot { opacity: 1; transform: none; }

/* Invisible htmx SSE swap listener (board-reset) */
.sse-listener { display: none; }

/* ==========================================================================
 * THE WALL: six channel bays
 * ========================================================================== */

.board-container {
  position: relative; /* the happening dock's positioning context */
  z-index: 1;
  padding: var(--sp-3) var(--sp-4);
}

.kanban-grid {
  display: grid;
  /* Six channels fit a wide monitor; below 1599px the wall switches to
     channel-at-a-time horizontal flow instead of squashing cards. The
     264px floor means a narrow band just above the breakpoint can still
     overflow, so the grid (not the page) owns the scroll at any width. */
  grid-template-columns: repeat(6, minmax(264px, 1fr));
  gap: var(--sp-3);
  align-items: start;
  overflow-x: auto;
  padding-bottom: var(--sp-2);
}

@media (max-width: 1599px) {
  .kanban-grid {
    grid-auto-flow: column;
    grid-auto-columns: minmax(264px, 1fr);
  }
}

/* One bay: a well on the wall. The 2px channel bar rides the top edge; the
   left edge carries a tick rule like a meter track. */
.kanban-column {
  position: relative;
  display: flex;
  flex-direction: column;
  min-width: 0;
  background: var(--s-pit);
  border: 1px solid var(--line-soft);
  border-top: 2px solid var(--ch-backlog);
  border-radius: var(--r-panel);
  /* rail + sub-rail + container padding + a breath of margin */
  max-height: calc(100vh - var(--rail-h) - var(--subrail-h) - 40px);
}

.kanban-column::before {
  content: "";
  position: absolute;
  left: 0;
  top: 0;
  bottom: 0;
  width: 3px;
  background: repeating-linear-gradient(
    to bottom,
    var(--line-tick) 0 1px,
    transparent 1px 8px
  );
  opacity: 0.45;
  border-radius: 0 0 0 var(--r-chamfer);
  pointer-events: none;
}

.col-claimed { border-top-color: var(--ch-claimed); }
.col-stuck { border-top-color: var(--ch-stuck); }
.col-captured { border-top-color: var(--ch-captured); }
.col-solved { border-top-color: var(--ch-solved); }
.col-writeup_posted { border-top-color: var(--ch-writeup); }

/* Bay mast: state lamp + channel name (mono, hue-lit) + count. */
.column-header {
  display: flex;
  justify-content: space-between;
  align-items: center;
  gap: var(--sp-2);
  padding: var(--sp-2) var(--sp-3);
  flex: none;
  min-width: 0;
}

.column-title {
  display: flex;
  align-items: center;
  gap: var(--sp-2);
  min-width: 0;
}

.column-name {
  font-family: var(--font-mono);
  font-size: var(--t-meta);
  font-weight: 600;
  text-transform: uppercase;
  letter-spacing: 0.08em;
  color: var(--ch-backlog);
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}

.col-claimed .column-name { color: var(--ch-claimed); }
.col-stuck .column-name { color: var(--ch-stuck); }
.col-captured .column-name { color: var(--ch-captured); }
.col-solved .column-name { color: var(--ch-solved); }
.col-writeup_posted .column-name { color: var(--ch-writeup); }

.column-count {
  flex: none;
  min-width: 22px;
  text-align: center;
  padding: 0 var(--sp-1);
  background: var(--s-void);
  border: 1px solid var(--line-soft);
  border-radius: var(--r-chamfer);
  color: var(--text-label);
  font-size: var(--t-meta);
}

/* The card rack: scrolls inside the bay's height, always a drag target. */
.column-cards {
  display: flex;
  flex-direction: column;
  gap: var(--sp-2);
  padding: var(--sp-2);
  overflow-y: auto;
  min-height: 120px;
  flex: 1;
}

/* board.js folds fully-empty bays away — but only while a filter runs. */
.kanban-column.is-empty { display: none; }

/* Flatline plate: an open channel with no signal. Dashed baseline, terse
   copy, a block caret blinking at the 640ms cap (the plate lives inside
   the SSE-swapped grid; nothing longer is legal there). Hidden the moment
   a card enters the bay — drag or swap, either way. */
.column-empty {
  display: flex;
  align-items: baseline;
  gap: 5px;
  margin: var(--sp-1) var(--sp-1) var(--sp-2);
  padding-bottom: var(--sp-2);
  border-bottom: 1px dashed var(--line-tick);
  font-family: var(--font-mono);
  font-size: var(--t-meta);
  color: var(--text-label);
}

.column-cards:has(> .kanban-card) .column-empty { display: none; }

.flatline-caret {
  flex: none;
  width: 7px;
  height: 12px;
  background: currentColor;
  animation: caret-blink 640ms linear infinite;
}

@keyframes caret-blink {
  0%, 49% { opacity: 1; }
  50%, 100% { opacity: 0; }
}

/* ==========================================================================
 * CARDS — channel instruments
 * ========================================================================== */

.kanban-card {
  position: relative;
  background: var(--s-card);
  border: 1px solid var(--line);
  border-top: 2px solid var(--ch-backlog);
  border-radius: var(--r-chamfer);
  padding: var(--sp-2) var(--sp-3);
  cursor: grab;
  transition:
    background var(--t-fast) var(--ease-snap),
    transform var(--t-fast) var(--ease-snap),
    border-color var(--t-fast) var(--ease-snap);
}

/* The card's own channel: its state rides the top bar (data-state is the
   contract; the hue selectors hang off it). */
.kanban-card[data-state="claimed"] { border-top-color: var(--ch-claimed); }
.kanban-card[data-state="stuck"] { border-top-color: var(--ch-stuck); }
.kanban-card[data-state="captured"] { border-top-color: var(--ch-captured); }
.kanban-card[data-state="solved"] { border-top-color: var(--ch-solved); }
.kanban-card[data-state="writeup_posted"] { border-top-color: var(--ch-writeup); }

.kanban-card:hover {
  background: var(--s-card-hi);
  border-left-color: var(--line-tick);
  border-right-color: var(--line-tick);
  border-bottom-color: var(--line-tick);
  transform: translateY(-1px);
}

.kanban-card:active { cursor: grabbing; }

/* Title row: state lamp + category mark + the human line. */
.card-head {
  display: flex;
  align-items: flex-start;
  gap: var(--sp-1);
  margin-bottom: var(--sp-1);
}

.card-head .lamp { margin-top: 5px; }

.card-cat {
  display: inline-flex;
  margin-top: 1px;
  color: var(--cat-misc);
}

.kanban-card[data-category="pwn" i] .card-cat { color: var(--cat-pwn); }
.kanban-card[data-category="web" i] .card-cat { color: var(--cat-web); }
.kanban-card[data-category="crypto" i] .card-cat { color: var(--cat-crypto); }
.kanban-card[data-category="rev" i] .card-cat { color: var(--cat-rev); }
.kanban-card[data-category="forensics" i] .card-cat { color: var(--cat-forensics); }
.kanban-card[data-category="stego" i] .card-cat { color: var(--cat-stego); }
.kanban-card[data-category="unknown" i] .card-cat { color: var(--cat-misc); }

.card-title {
  flex: 1;
  min-width: 0;
  font-size: var(--t-body);
  font-weight: 500;
  color: var(--text-max);
  line-height: 1.4;
  overflow-wrap: anywhere;
}

/* Machine readout under the title: points + category, mono meta. */
.card-readout {
  display: flex;
  gap: var(--sp-2);
  margin-bottom: var(--sp-1);
  font-family: var(--font-mono);
  font-size: var(--t-meta);
  color: var(--text-label);
}

/* Flags (pills): the board keeps its historical .card-pill class (modal and
   gallery still render it), so the box recipe lives here; hue variants come
   from components.css (.pill-offline/-manual/-breakthrough/-verifying/
   -captured/-writeup) plus the two channel variants below. */
.card-meta {
  display: flex;
  flex-wrap: wrap;
  gap: var(--sp-1);
  align-items: center;
}

/* Box only — never a color: board.css loads after components.css, so any
   border/color here would stomp every variant hue below it. */
.card-pill {
  display: inline-flex;
  align-items: center;
  gap: var(--sp-1);
  padding: 1px var(--sp-2);
  border: 1px solid;
  border-radius: var(--r-chamfer);
  font-family: var(--font-mono);
  font-size: var(--t-meta);
  line-height: 1.5;
  white-space: nowrap;
}

.kanban-card .card-pill .icon { width: 12px; height: 12px; }

.pill-swarm {
  border-color: var(--ch-writeup);
  color: var(--ch-writeup);
  background: var(--ch-writeup-dim);
}

.pill-missing {
  border-color: var(--st-serious);
  color: var(--st-serious);
  background: var(--st-serious-dim);
}

/* Holders: author-voice chips (components) with avatars + the lease timer. */
.card-holders {
  display: flex;
  flex-direction: column;
  gap: var(--sp-1);
  margin-top: var(--sp-2);
  padding-top: var(--sp-2);
  border-top: 1px dashed var(--line-soft);
}

.holder-row {
  display: flex;
  justify-content: space-between;
  align-items: center;
  gap: var(--sp-2);
  min-width: 0;
}

.holder-row .claim-timer { flex: none; }

/* Lease depletion (f4): the trace between chip and timer drains as the
   lease runs — width set by board.js's 1s tick from data-expires-at minus
   the last-activity anchor, so the total is the lease TTL itself. No
   transition on purpose: polled swaps can't fight it, and the kill switch
   needs no case. Urgent (<5 min) turns serious and rides the 640ms cap —
   the flatline caret's precedent for loops inside swapped containers. */
.holder-row .lease-bar {
  flex: 1 1 auto;
  align-self: center;
  height: 2px;
  margin: 0 var(--sp-1);
  background: var(--line-soft);
  border-radius: var(--r-lamp);
  overflow: hidden;
}

.holder-row .lease-bar i {
  display: block;
  height: 100%;
  background: var(--ch-claimed);
}

.holder-row .lease-bar i.urgent {
  background: var(--st-serious);
  animation: lease-blink 640ms steps(1, end) infinite;
}

@keyframes lease-blink {
  0%, 49% { opacity: 1; }
  50%, 100% { opacity: 0.4; }
}

.kanban-card .chip-op .icon { width: 12px; height: 12px; }

/* The solved strip. */
.card-solved { margin-top: var(--sp-2); font-size: var(--t-meta); }

.solved-by {
  display: inline-flex;
  align-items: center;
  gap: 4px;
  color: var(--st-good);
}

.solved-by .icon { width: 12px; height: 12px; }

.solved-by strong { font-weight: 600; color: inherit; }

/* Unread badges (client-added to the card root) dock the top-right corner. */
.kanban-card > .unread-badge {
  position: absolute;
  top: calc(-1 * var(--sp-1));
  right: calc(-1 * var(--sp-1));
  z-index: 3;
  box-shadow: 0 0 0 2px var(--s-pit);
}

/* Drag: the ghost is a dashed socket on the pit; the chosen card lifts to
   the ridge (SortableJS applies .sortable-chosen to the dragged element). */
.kanban-card.sortable-ghost {
  background: var(--sig-dim);
  border: 1px dashed var(--sig);
  opacity: 0.55;
}

.kanban-card.sortable-chosen {
  background: var(--s-ridge);
}

/* T16 live-activity pulse: one 640ms ring, no iterations — legal inside the
   SSE-swapped grid where the old 4.5s triple-pulse was not. */
.kanban-card.card-pulse {
  animation: card-pulse var(--dur-4) var(--ease-snap) both;
}

@keyframes card-pulse {
  0% { box-shadow: 0 0 0 0 var(--sig-dim); }
  100% { box-shadow: 0 0 0 8px transparent; }
}

/* ==========================================================================
 * HAPPENING DOCK — chrome only. The feed content (#happening-list inner
 * HTML, .happening-row and friends) is the happening panel's surface; the
 * shell here guarantees: closed = zero flow pixels, open never reflows the
 * grid, and the visible pane keys off the wrapper's data-tab (the one
 * element the 5s innerHTML swap never replaces).
 * ========================================================================== */

.happening-dock {
  position: absolute;
  top: var(--sp-3);
  right: var(--sp-4);
  width: min(440px, calc(100vw - 2 * var(--sp-5)));
  max-height: calc(100% - 2 * var(--sp-3));
  display: flex;
  flex-direction: column;
  background: var(--s-panel);
  backdrop-filter: blur(14px);
  -webkit-backdrop-filter: blur(14px);
  border: 1px solid var(--line);
  border-radius: var(--r-panel);
  box-shadow: var(--shadow-float);
  z-index: 20; /* above bay masts, below modals */
  visibility: hidden;
  opacity: 0;
  transform: translateY(-6px);
  pointer-events: none;
  transition:
    opacity var(--t-fast) var(--ease-snap),
    transform var(--t-fast) var(--ease-snap),
    visibility var(--t-fast);
}

.happening-dock.open {
  visibility: visible;
  opacity: 1;
  transform: none;
  pointer-events: auto;
}

.happening-tabs {
  display: flex;
  align-items: center;
  gap: var(--sp-1);
  padding: var(--sp-2) var(--sp-3);
  border-bottom: 1px solid var(--line-soft);
  flex: none;
}

.happening-tab {
  background: transparent;
  border: none;
  padding: var(--sp-1) var(--sp-2);
  border-radius: var(--r-chamfer);
  color: var(--text-label);
  font-family: var(--font-mono);
  font-size: var(--t-meta);
  text-transform: uppercase;
  letter-spacing: 0.06em;
  cursor: pointer;
}

.happening-tab:hover { color: var(--text-max); }

.happening-tab.active {
  background: var(--sig-dim);
  color: var(--sig);
}

.happening-close {
  margin-left: auto;
  display: flex;
  background: none;
  border: none;
  padding: var(--sp-1);
  border-radius: var(--r-chamfer);
  color: var(--text-label);
  cursor: pointer;
}

.happening-close:hover { color: var(--text-max); }

.happening-body {
  padding: var(--sp-2) var(--sp-3) var(--sp-3);
  overflow-y: auto;
  min-height: 0;
  display: flex;
  flex-direction: column;
  gap: var(--sp-2);
}

/* The visible pane keys off the wrapper's data-tab — board.js toggles the
   attribute, never pane classes, so a poll swap mid-read cannot flip it. */
#happening-list[data-tab="onnow"] .pane-recent,
#happening-list[data-tab="recent"] .pane-onnow { display: none; }

.happening-pane {
  display: flex;
  flex-direction: column;
  gap: var(--sp-1);
  min-width: 0;
}

.happening-pane h4 {
  margin: 0 0 var(--sp-1);
  font-family: var(--font-mono);
  font-size: var(--t-meta);
  font-weight: 600;
  text-transform: uppercase;
  letter-spacing: 0.06em;
  color: var(--text-label);
}

/* Provisional feed rows (content-owned classes, chrome-level styling so the
   dock reads whole until the happening surface lands its own sheet). */
.happening-row {
  display: flex;
  align-items: center;
  gap: var(--sp-2);
  padding: var(--sp-1) var(--sp-2);
  background: var(--s-pit);
  border-left: 2px solid var(--line-tick);
  border-radius: var(--r-chamfer);
  font-size: var(--t-ui);
  min-width: 0;
}

.happening-row .chip { flex: none; }

/* Fresh tick (f6): a row younger than 30s carries the signal edge. Static
   by design — the 5s poll redraws it (or clears it) with no animation to
   suppress; color is signal, not decoration. */
.happening-row.is-fresh {
  border-left-color: var(--sig);
}

.happening-title {
  color: var(--text-max);
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
  min-width: 0;
}

.happening-text {
  flex: 1;
  color: var(--text-label);
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
  min-width: 0;
}

.activity-kind,
.activity-time {
  flex: none;
  font-family: var(--font-mono);
  font-size: var(--t-meta);
  color: var(--text-label);
}

.happening-empty {
  padding: var(--sp-1) var(--sp-2);
  color: var(--text-label);
  font-size: var(--t-ui);
}

/* ==========================================================================
 * PAGE-LOAD ENTRANCE — rail and bays stagger-rise. CSS only, one-shot,
   applied to stable page structure; total run of the last bay (300ms delay
   + 320ms travel) stays under the 640ms cap. Fragments swapped later
   re-seating via .seat-in (base.css) are unaffected.
 * ========================================================================== */

@keyframes rise-in {
  0% { opacity: 0; transform: translateY(8px); }
  100% { opacity: 1; transform: none; }
}

.app-header {
  animation: rise-in var(--t-settle) var(--ease-needle) both;
}

.sub-bar {
  animation: rise-in var(--t-settle) var(--ease-needle) 40ms both;
}

.kanban-column { animation: rise-in var(--t-settle) var(--ease-needle) both; }
.kanban-column:nth-child(2) { animation-delay: 60ms; }
.kanban-column:nth-child(3) { animation-delay: 120ms; }
.kanban-column:nth-child(4) { animation-delay: 180ms; }
.kanban-column:nth-child(5) { animation-delay: 240ms; }
.kanban-column:nth-child(6) { animation-delay: 300ms; }

/* ==========================================================================
 * PHONE (max-width 640px) — same guarantees, rewritten: the header wraps,
 * filters scroll horizontally, the wall stacks and the page scrolls
 * (columns never clip), touch targets are finger-sized.
 * ========================================================================== */

@media (max-width: 640px) {
  .app-header {
    flex-wrap: wrap;
    height: auto;
    min-height: var(--rail-h);
    padding: var(--sp-2) var(--sp-3);
    row-gap: var(--sp-2);
  }

  .header-stats { gap: var(--sp-3); }

  .stat-item { align-items: flex-start; }

  .header-actions {
    width: 100%;
    flex-wrap: wrap;
    row-gap: var(--sp-1);
  }

  /* finger-sized controls (components.css raises .filter-btn/.btn too) */
  .filter-btn,
  .btn { min-height: 36px; }

  .sub-bar {
    height: auto;
    min-height: var(--subrail-h);
    padding: var(--sp-2) var(--sp-3);
    overflow-x: auto;
    flex-wrap: nowrap;
  }

  .filters-group { flex-wrap: nowrap; }

  .search-input { width: 130px; }

  .sub-bar-search { margin-left: 0; flex-wrap: nowrap; }

  /* the wall stacks; the page scrolls, no clipped columns */
  .kanban-grid {
    grid-template-columns: minmax(0, 1fr);
    grid-auto-flow: row;
    overflow-x: visible;
  }

  .kanban-column { max-height: none; }

  .happening-dock {
    width: calc(100vw - 2 * var(--sp-3));
    right: var(--sp-3);
  }
}

/* ==========================================================================
 * CROSS-SURFACE KEEPS — classes other not-yet-rewritten surfaces render
 * (card modal, writeups gallery, admin, dash team notes) that this sheet
 * historically dressed. Their slices delete these when they land.
 * ========================================================================== */

/* Legacy brand chrome (admin/writeups headers still render the ring mark). */
.brand-title {
  display: inline-flex;
  align-items: center;
  gap: var(--sp-2);
  font-family: var(--font-mono);
  font-weight: 600;
  letter-spacing: 0.18em;
  color: var(--text-label);
  text-decoration: none;
}

.viper-brand-wrapper {
  position: relative;
  width: 26px;
  height: 26px;
  display: flex;
  align-items: center;
  justify-content: center;
  flex: none;
}

.viper-hud-ring {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  color: var(--sig);
}

.brand-logo-icon { width: 20px; height: 20px; }

.event-badge {
  display: flex;
  align-items: center;
  gap: var(--sp-2);
  font-family: var(--font-mono);
  font-size: var(--t-ui);
  color: var(--text-body);
  min-width: 0;
}

.event-badge .event-status { font-size: var(--t-meta); color: var(--text-label); }

/* Category badge + points (card modal head, writeups gallery cards). */
.category-badge {
  display: inline-flex;
  align-items: center;
  gap: 4px;
  padding: 1px var(--sp-1);
  border: 1px solid transparent;
  border-radius: var(--r-chamfer);
  font-family: var(--font-mono);
  font-size: var(--t-meta);
  text-transform: uppercase;
  letter-spacing: 0.05em;
}

.category-badge .icon { width: 12px; height: 12px; }

.cat-pwn { color: var(--cat-pwn); border-color: var(--cat-pwn); background: var(--cat-pwn-dim); }
.cat-web { color: var(--cat-web); border-color: var(--cat-web); background: var(--cat-web-dim); }
.cat-crypto { color: var(--cat-crypto); border-color: var(--cat-crypto); background: var(--cat-crypto-dim); }
.cat-rev { color: var(--cat-rev); border-color: var(--cat-rev); background: var(--st-warn-dim); }
.cat-forensics { color: var(--cat-forensics); border-color: var(--cat-forensics); background: var(--cat-forensics-dim); }
.cat-misc { color: var(--cat-misc); border-color: var(--cat-misc); background: var(--cat-misc-dim); }
.cat-stego { color: var(--cat-stego); border-color: var(--cat-stego); background: var(--cat-stego-dim); }
.cat-unknown {
  color: var(--cat-misc);
  border-color: var(--cat-misc);
  background: transparent;
  border-style: dashed;
}

.points-badge {
  font-family: var(--font-mono);
  font-size: var(--t-ui);
  font-weight: 600;
  color: var(--text-label);
  font-variant-numeric: tabular-nums;
  flex: none;
}

/* Activity timeline base — modals.css layers the modal specifics on this. */
.activity-list {
  display: flex;
  flex-direction: column;
  gap: var(--sp-1);
  max-height: 200px;
  overflow-y: auto;
}

.activity-item {
  font-size: var(--t-ui);
  padding: var(--sp-2) var(--sp-3);
  background: var(--s-card);
  border-radius: var(--r-chamfer);
  border-left: 3px solid var(--line);
}

.activity-time {
  font-size: var(--t-meta);
  color: var(--text-label);
  font-family: var(--font-mono);
  float: right;
  margin-left: var(--sp-2);
}

/* Dash team notes list (the board's composer left in T22; dash renders it). */
#team-notes-list {
  display: flex;
  flex-direction: column;
  gap: var(--sp-1);
  max-height: 220px;
  overflow-y: auto;
}

#team-notes-list .chip-op { flex: none; white-space: nowrap; }
