/* v3 component styles — geometry and chrome from the design atlas
 * (claude-plans/v3-inventory.md maps every value to its atlas turn).
 * Tokens live in v3-tokens.css; per-card aura backgrounds are computed by
 * js/v3/aura.js. Look values up in the atlas before changing anything.
 * v3.1 layers on: fluid type via --fs-* tokens, the desktop body (day rail,
 * dialogs, auto-fill grid), and the unannounced quality floor (focus-visible
 * everywhere, 44px touch targets, reduced-motion). */

/* The focus + motion floor (AX-2) MOVED to v3-tokens.css on 2026-07-12.
 * It lived here, and 404.html does not load this file — so the 404 page, which
 * runs two infinite animations, ignored prefers-reduced-motion entirely. An
 * accessibility floor that only applies to pages someone remembered to wire up
 * is not a floor. It now sits in the tokens file, which every styled page
 * loads, so the next standalone page inherits it without anyone thinking. */

/* ---- artist card (the atom) ------------------------------------------------ */
.card {
  position: relative; min-height: 64px;
  border: 1px solid var(--hairline); border-radius: var(--r-card);
  display: flex; align-items: center; justify-content: center;
  padding: 9px 11px 20px; text-align: center; overflow: hidden;
  cursor: pointer; -webkit-touch-callout: none; user-select: none;
  font-family: inherit; color: inherit; /* role=button div, keyboard-first (AX-1) */
}
.card.animated { background-size: 180% 180%; animation: gradShift 12s ease-in-out infinite; }
.card .name { position: relative; font-weight: 700; font-size: var(--fs-card); line-height: 1.2; }
.card .time { position: relative; font-size: 9px; font-weight: 600; }
.card.timed { flex-direction: column; gap: 2px; } /* search results carry stage · time */

/* Set-times grid cell variant: same card, tighter corners, time line.
 * Content is TOP-aligned: a short set's card clips at the bottom padding,
 * never mid-name — centered content shaved the top off the artist name
 * (Kevin's screenshot, 2026-07-12). Top edge = start time, like a printed
 * festival grid. */
/* border-box is load-bearing for the lane split (CELLS ONLY — the base card
 * keeps content-box so wall-grid geometry stays atlas-exact; Codex arc gate
 * P3): wall.js sets width% + margin-left% that must sum inside the column,
 * and content-box added padding+border on top, pushing split cards into the
 * neighbor stage (Kevin's screenshot, 2026-07-12).
 * Content centers vertically (Kevin's call) — `safe` falls back to start on
 * short cards so overflow can never shave the name again. */
.card.cell { flex-direction: column; gap: 1px; padding: 5px 8px 17px;
  justify-content: center; justify-content: safe center; box-sizing: border-box; }
.card.cell .name { font-size: 12px; line-height: 1.15; max-width: 100%;
  /* Two lines beat a mid-name ellipsis on narrow lane-split cards. */
  white-space: normal; overflow-wrap: anywhere;
  display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical; overflow: hidden;
  /* flex:none is the REAL clipped-name fix (measured on staging 2026-07-12):
   * a short card's content box is ~22px, and default flex-shrink squeezed the
   * name below one line-height — its own overflow:hidden then shaved the
   * glyphs. The name never shrinks; a too-short card crops time descenders
   * at the bottom instead, never the name. */
  flex: none; }
.card.cell .time { flex: none; }
/* A tall cell (a set of three hours or more) reads like a printed grid: the
 * name at the top edge where the set starts, the end time at the bottom edge
 * — centring put the name mid-column, off-screen, and the column read as an
 * empty slab (Despacio, Kevin's screenshot, 2026-08-31). */
.card.cell.tall { justify-content: flex-start; padding-top: 9px; }
.card.cell.tall .until { position: absolute; left: 0; right: 0; bottom: 6px; text-align: center;
                         font-size: 9px; font-weight: 600; opacity: .85; }
.card.cell .corner-about { bottom: 3px; left: 5px; }
.card.cell .corner-who { bottom: 3px; right: 4px; }
.card.cell .chip-notes { height: 12px; padding: 0 5px; font-size: 8px; }
.card.cell .chip-spotify { height: 11px; padding: 0 5px; font-size: 8px; }

.corner-about { position: absolute; bottom: 4px; left: 6px; display: flex; align-items: center; gap: 3px; }
.corner-who { position: absolute; bottom: 4px; right: 5px; display: flex; align-items: center; }
.chip-notes {
  display: inline-flex; align-items: center; height: 13px; padding: 0 6px;
  border-radius: var(--r-bubble); background: var(--notes-fill);
  border: 1px solid var(--notes-stroke); color: #fff; font-size: 8.5px; font-weight: 800; line-height: 1;
  font-family: inherit; /* the count chip is a real <button> now (audit 4.4) */
}
.chip-spotify {
  display: inline-flex; align-items: center; gap: 3px; height: 12px; padding: 0 6px;
  border-radius: var(--r-pill); background: var(--spotify-fill);
  border: 1px solid var(--spotify-stroke); color: #fff; font-size: 8.5px; font-weight: 800; line-height: 1;
}
/* High-affinity corner glow (followed + 5+ songs): a soft Spotify-green
 * mini-aura rising from the badge corner — the aura language, not a border.
 * Absolutely positioned inside the already-relative card: zero layout shift,
 * never intercepts taps, sits under the chips. */
.spot-glow {
  position: absolute; left: 0; bottom: 0; width: 58%; height: 58%;
  pointer-events: none; border-radius: inherit;
  background: radial-gradient(120% 120% at 8% 96%, rgba(103, 185, 138, .34) 0%, rgba(103, 185, 138, .12) 45%, transparent 72%);
}
.corner-about, .corner-who { position: absolute; z-index: 1; }
.card.cell .spot-glow { width: 70%; height: 45%; }

/* ---- Spotify scan ticker: covers flick by over a real counter + bar ------- */
.scan-tile {
  width: 56px; height: 56px; flex: none; border-radius: 10px; overflow: hidden;
  background: var(--card-unpicked, #1C1731); border: 1px solid var(--border-card, rgba(255,255,255,.08));
  transition: box-shadow .25s ease;
}
.scan-tile img { width: 100%; height: 100%; object-fit: cover; display: block; animation: scan-pop .3s ease; }
.scan-tile.find { box-shadow: 0 0 0 1px var(--spotify-stroke), 0 0 14px rgba(103, 185, 138, .45); }
@keyframes scan-pop { from { opacity: 0; transform: scale(1.06); } to { opacity: 1; transform: scale(1); } }
.scan-bar { width: 100%; height: 3px; border-radius: 2px; background: rgba(255,255,255,.08); overflow: hidden; }
.scan-bar-fill { height: 100%; width: 0%; border-radius: 2px; background: var(--spotify-stroke); transition: width .4s ease; }
@media (prefers-reduced-motion: reduce) {
  .scan-tile img { animation: none; }
  .scan-bar-fill { transition: none; }
}
/* Wall-side scan pill: the library read keeps going when you leave the drill —
 * this says so. Above the dock, never interactive. */
.spot-pill {
  position: fixed; right: 12px; bottom: calc(70px + env(safe-area-inset-bottom, 0px));
  z-index: 35; padding: 7px 12px; border-radius: var(--r-pill);
  background: rgba(10, 8, 18, .92); border: 1px solid var(--spotify-stroke);
  color: var(--spotify-stroke); font-size: 11px; font-weight: 700; pointer-events: none;
}
@media (min-width: 720px) { .spot-pill { bottom: 16px; } }
.mark { height: 12px; border-radius: var(--r-pill); color: #fff; font-weight: 800;
        display: inline-flex; align-items: center; justify-content: center; margin-left: 3px; flex: none; }
/* Weekend tag (ST-3): quiet top-left marker for W1/W2-only artists in Both view */
.chip-weekend { position: absolute; top: 4px; left: 5px; font-size: 8px; font-weight: 800;
                letter-spacing: .06em; color: var(--text-tertiary); border: 1px solid var(--border-card);
                border-radius: var(--r-pill); padding: 1px 5px; background: rgba(10, 8, 18, .6); }
/* The '+n' ghost only appears over saturated multi-person auras (3+ pickers)
 * — it gets the same white-on-scrim treatment as every other mark (audit 8.1). */
.mark.ghost { border: 1px dashed rgba(255, 255, 255, .55); background: rgba(12, 10, 20, .55);
              color: #fff; padding: 0 4px; font-size: 7.5px; }

/* ---- people ----------------------------------------------------------------- */
.person-chip { color: #fff; font-size: 11.5px; font-weight: 600; padding: 5px 12px;
               -webkit-touch-callout: none; user-select: none; /* a long press must never raise the OS text callout */
               border-radius: var(--r-pill); border: none; cursor: pointer; }
.person-chip.you { font-weight: 700; }
/* A chip's job is to say "this person, in their colour". On the wall and in the
 * crew list it is also a button. In the Settings fest card it is a roster — and
 * it was rendering as an inert <span> that still inherited cursor:pointer, so it
 * looked exactly like the two chips that DO something and silently did nothing
 * when tapped. Same component, honest about which job it is doing here. */
.person-chip.static { cursor: default; }
.person-chip.add { background: transparent; border: 1.5px dashed var(--border-emphasis);
                   color: var(--text-secondary); padding: 4px 10px; }
.avatar { width: 17px; height: 17px; border-radius: var(--r-pill); color: #fff; font-size: 8px;
          font-weight: 800; display: inline-flex; align-items: center; justify-content: center; flex: none; }
.avatar-cluster { display: inline-flex; }
.avatar-cluster .avatar + .avatar { margin-left: -5px; }
.avatar.lg { width: 30px; height: 30px; font-size: 12px; }

/* ---- toolbar (wall header row) ---------------------------------------------- */
.search-pill {
  display: inline-flex; align-items: center; gap: 7px; background: var(--card);
  border: 1px solid var(--border-card); border-radius: var(--r-pill);
  padding: 7px 13px; color: var(--text-secondary); font-size: 12.5px;
}
.search-pill input { background: transparent; border: none; outline: none; color: var(--text-primary);
                     font-size: 12.5px; font-family: var(--font-ui); width: 100%; }
.sort-chip {
  display: inline-flex; align-items: center; gap: 4px; background: var(--card);
  border: 1px solid var(--border-card); border-radius: var(--r-pill);
  padding: 7px 12px; color: #E5E7EB; font-size: 11.5px; font-weight: 700; cursor: pointer;
  font-family: var(--font-ui);
}
.sort-chip .caret { color: var(--text-tertiary); transition: transform .12s ease; }
.sort-chip[aria-expanded="true"] .caret { transform: rotate(180deg); }

/* Sort popover listbox (DT-7): part of the app's control family — quiet
 * surface, live caret, full keyboard (arrows / Enter / Esc / typeahead). */
.sort-wrap { position: relative; display: inline-flex; }
.sort-pop {
  position: absolute; top: calc(100% + 6px); right: 0; z-index: 35;
  min-width: 168px; background: #141021; border: 1px solid var(--border-card);
  border-radius: 12px; padding: 5px; margin: 0; list-style: none;
  box-shadow: 0 12px 34px rgba(0, 0, 0, .5);
}
.sort-pop [role="option"] {
  display: flex; align-items: center; gap: 8px;
  padding: 8px 11px; border-radius: 8px;
  color: var(--text-body); font-size: 13px; font-weight: 600; cursor: pointer;
}
.sort-pop [role="option"]:hover, .sort-pop [role="option"].kb-active {
  background: rgba(255, 255, 255, .08);
}
/* Brand, not fest accent: a selected sort option is app chrome. The accent is
 * reserved for fest name / active day tab / stage headers / current-fest border. */
.sort-pop [role="option"][aria-selected="true"] { color: rgb(var(--brand)); }
.sort-pop .check { width: 12px; flex: none; font-size: 11px; }
.notes-chip {
  display: inline-flex; align-items: center; gap: 5px; background: transparent;
  border: 1.5px solid var(--notes-chip-stroke); color: var(--notes-chip-text);
  font-weight: 700; font-size: 11.5px; padding: 6px 12px; border-radius: var(--r-bubble); cursor: pointer;
}
.notes-chip .count { background: rgba(139, 123, 255, .25); border-radius: var(--r-pill);
                     padding: 0 6px; font-size: 10px; }
.toolbar-divider { width: 1px; height: 22px; background: var(--hairline); margin: 0 6px; }

/* ---- day header rule --------------------------------------------------------- */
/* Jumping to a day (tab tap) must land the header BELOW the sticky chrome,
 * not under it — app.js measures the rail + stage strip into --jump-offset. */
.day-rule { display: flex; align-items: baseline; gap: 10px; scroll-margin-top: var(--jump-offset, 8px); }
.day-rule .day { font-family: var(--font-display); letter-spacing: var(--track-display);
                 font-size: var(--fs-day); color: var(--text-header); }
.day-rule .date { color: var(--text-tertiary); font-size: 10px; font-weight: 700; }
.day-rule .line { flex: 1; height: 1px; background: var(--hairline); }

/* ---- desktop day rail (DT-1) --------------------------------------------------- */
/* ≥720 the dock dies; the rail is its desktop body — sticky under the top of
 * the window, YOU chip jumps to top, tabs scrollspy with the wall. */
.day-rail { display: none; }
@media (min-width: 720px) {
  .day-rail {
    display: flex; align-items: center; gap: 18px;
    position: sticky; top: 0; z-index: 25;
    /* Full-VIEWPORT, like the timetable it sits over: the grids run to the
     * window's edges (the .times-wrap rule), so a rail only as wide as the
     * shell box left cards showing beside it on any wider window (Kevin's
     * screenshots, 2026-08-31). Symmetric padding keeps the rail's own
     * content aligned with the shell's edges. */
    width: 100vw; box-sizing: border-box;
    margin: 4px calc(50% - 50vw) 0;
    padding: 9px max(var(--sp-gutter), calc(50vw - var(--shell-max) / 2));
    /* Near-opaque: at .78 the header's subtitle text ghosted through
     * mid-scroll and read as broken overlap (Kevin, 2026-07-12). */
    background: rgba(12, 10, 20, .94);
    backdrop-filter: blur(10px); -webkit-backdrop-filter: blur(10px);
    border-bottom: 1px solid var(--hairline);
  }
  /* Same anatomy as the dock (unified 2026-07-12, Kevin note 1.1): your
   * avatar circle, the day tabs, the fest name + sync dot. One component
   * vocabulary, two positions. */
  .day-rail .you { width: 26px; height: 26px; font-size: 10px; position: relative; }
  .day-rail .fest-link { margin-left: auto; }
  .day-rail .rail-days { display: flex; gap: 20px; overflow-x: auto; }
  .day-rail .day-tab {
    font-family: var(--font-display); letter-spacing: var(--track-label);
    font-size: var(--fs-micro); text-transform: uppercase;
    color: var(--text-tertiary); background: none; border: none;
    border-bottom: 2px solid transparent; padding: 2px 0 3px;
    cursor: pointer; flex: none;
  }
  .day-rail .day-tab.active { color: rgb(var(--fest)); border-bottom-color: rgb(var(--fest)); }
}

/* ---- notes ------------------------------------------------------------------- */
/* (The boxed note-row/bubble anatomy retired 2026-08-29 — the aura notes in
 * the threads section further down are the one note language now.) */
/* One text field everywhere words are written — the sheet's bottom composer,
 * an inline reply, an edit in place. It grows with what you type (notes.js
 * measures) instead of hiding 500 characters in one scrolling line. */
.composer { display: flex; gap: 8px; align-items: flex-end; }
.n-field {
  /* border-box because this file has no global one: a content-box field with
     12px of side padding overflows its column by 26px, and `.n-inline` clips
     the overflow so the right border simply vanishes (caught in the walk). */
  box-sizing: border-box; width: 100%; min-width: 0; background: var(--card);
  border: 1px solid var(--border-input); border-radius: var(--r-card);
  padding: 10px 12px; color: var(--text-primary); font-size: 12.5px; line-height: 1.4;
  font-family: var(--font-ui); outline: none; resize: none; overflow-y: auto;
}
.n-field::placeholder { color: var(--text-tertiary); }
/* Only the bottom composer is a ROW, where the field takes the free space. In
   the column layouts (an inline reply, an edit in place) `flex: 1` would
   stretch the field's HEIGHT and fight the auto-grow. */
.composer .n-field { flex: 1; }
.n-field:focus-visible { border-color: var(--border-emphasis); }

/* ---- bottom sheet / desktop dialog (DT-2) --------------------------------------- */
.sheet-backdrop { position: fixed; inset: 0; background: rgba(0, 0, 0, .45); z-index: 40; }
.sheet {
  position: fixed; left: 0; right: 0; bottom: 0; z-index: 41;
  background: var(--dock); border-radius: var(--r-sheet);
  box-shadow: 0 -8px 30px rgba(0, 0, 0, .5);
  padding: 16px; display: flex; flex-direction: column; gap: 12px;
  max-height: 72vh; overflow-y: auto;
  animation: sheetIn .15s ease;
}
@keyframes sheetIn { from { opacity: 0; transform: scale(.98); } }
.sheet .grabber { width: 36px; height: 4px; border-radius: var(--r-pill);
                  background: var(--border-card); margin: 0 auto; flex: none;
                  touch-action: none; cursor: grab; padding: 6px 30px; background-clip: content-box; }
.sheet .sheet-title { font-family: var(--font-display); letter-spacing: .04em;
                      font-size: 19px; color: var(--text-header); }
.sheet .sheet-close { width: 30px; height: 30px; flex: none; border-radius: var(--r-pill);
                      background: var(--card); border: 1px solid var(--border-card);
                      color: var(--text-secondary); font-size: 12px; cursor: pointer;
                      display: inline-flex; align-items: center; justify-content: center; }
.note-action { background: none; border: none; padding: 0; cursor: pointer;
               color: var(--text-tertiary); font-size: 10px; font-weight: 700;
               text-decoration: underline; text-underline-offset: 2px; }
/* A full-width strip pinned to the bottom of a 1440px window is a phone
 * pattern stretched — ≥720 the same surface is a centered dialog. */
@media (min-width: 720px) {
  .sheet {
    inset: auto; left: 50%; top: 50%; right: auto; bottom: auto;
    translate: -50% -50%;
    width: min(560px, 92vw); max-height: 80vh;
    border-radius: 20px;
    box-shadow: 0 18px 50px rgba(0, 0, 0, .55);
  }
  .sheet .grabber { display: none; }
}

/* ---- dock (mobile only) ------------------------------------------------------ */
.dock {
  position: fixed; left: 0; right: 0; bottom: 0; z-index: 30;
  background: var(--dock); border-top: 1px solid var(--hairline);
  padding: 9px 16px calc(9px + env(safe-area-inset-bottom));
  display: flex; align-items: center; gap: 14px;
}
.dock .you { width: 26px; height: 26px; font-size: 10px; position: relative; }
/* Centered when they fit, scrollable from the START when they don't — plain
 * justify-content:center clips the first tabs unreachably at 390px (AX-6). */
.dock .days { flex: 1; display: flex; gap: 24px; overflow-x: auto; }
.dock .day-tab:first-child { margin-left: auto; }
.dock .day-tab:last-child { margin-right: auto; }
.dock .day-tab { font-weight: 700; font-size: 12px; padding-bottom: 3px;
                 border-bottom: 2px solid transparent; color: var(--text-tertiary);
                 background: none; border-top: none; border-left: none; border-right: none;
                 cursor: pointer; font-family: var(--font-ui); flex: none; position: relative; }
/* 44px touch floor on coarse pointers without moving a pixel of layout:
 * expand the hit area, not the element (AX-6).
 *
 * This used to NAME six selectors, and the naming was the bug. Every control
 * added since — the person chips, the Notes chip, the day-rule "+ note" chip,
 * every ghost/tonal button in Settings and in all five drills, the toggles, the
 * colour swatches, the whole desktop day rail — simply never got added to the
 * list, and measured 17–28px on a phone. Three independent walkers found it in
 * three different places, which is the tell that it was never six bugs: it was
 * one list nobody remembers to edit (finish pass, 2026-07-12).
 *
 * So it applies to BUTTONS, and every future control inherits it by being what
 * it already is. The floor stops being something you have to remember.
 *
 * -14px vertical clears the floor from the shortest real control (17px -> 45px).
 * Horizontal stays at -5px on purpose: chips sit ~6px apart in a row, and a
 * generous horizontal grab would let one chip swallow taps meant for its
 * neighbour — in a crowd, that is a wrong pick under someone's name. */
/* Two ways to reach the floor, and picking the wrong one is a real hazard.
 *
 * REAL HEIGHT is the default. Every button gets min-height:44px unless it opts
 * out. It cannot overlap anything, so it is always safe, and a control nobody
 * remembers to think about lands safe automatically.
 *
 * BORROWED SPACE (an expanded ::after) is the exception, for the handful of
 * controls where 44px of real height would wreck the design — the person chips,
 * the day tabs, the little glyph buttons. Those opt out of min-height and take a
 * hit area instead, and each one has been checked for room around it.
 *
 * The first cut of this rule expanded EVERY button unconditionally by 30px, and
 * Codex caught what that does: the Settings list stacks its rows with no gaps,
 * so each row's hit area reached 15px into its neighbours, and the later-painted
 * sibling wins an overlap. Tapping "Stay offline" could fire "Bulk paste picks"
 * — and a tap near the bottom of "Switch crew" could arm "Forget this crew".
 * A touch-target fix that hands you a destructive mis-tap is not a fix.
 * Unconditional growth is not a floor. (finish pass, 2026-07-12) */
@media (pointer: coarse) {
  /* --- the safe default ------------------------------------------------- */
  button { min-height: 44px; }
  /* An input cannot carry a pseudo-element either way. */
  #search-input { min-height: 44px; }

  /* Settings rows are DIVs, so `button` never reaches them — and the row is the
   * real target now that tapping the label flips the toggle. Give the row the
   * height, and let the 40x24 switch inside it keep its designed size. */
  .list-row { min-height: 44px; }

  /* --- the opt-outs: small controls that take borrowed space instead ------ */
  .person-chip, .notes-chip, .chip-notes, .note-action, .f-chip,
  .dock .day-tab, .day-rail .day-tab,
  .back-btn, .gear-btn, .fest-link, .sheet-close,
  button.avatar,
  .toggle,      /* a 40x24 switch; stretching its track to 44 breaks it, and the
                 * whole row is the tap target anyway */
  .card button {
    min-height: 0;
  }
  .person-chip, .notes-chip, .chip-notes, .note-action, .f-chip,
  .dock .day-tab, .day-rail .day-tab,
  .back-btn, .gear-btn, .fest-link, .sheet-close,
  button.avatar {
    position: relative;
  }
  /* Chips and day tabs sit in ROWS, ~5px apart. Vertical is where they were
   * unusable (17-28px); horizontal stays at -2px so one chip cannot swallow the
   * tap meant for its neighbour — in a crowd, that is a pick recorded under the
   * wrong person's name. */
  .person-chip::after, .notes-chip::after, .note-action::after, .f-chip::after,
  .dock .day-tab::after, .day-rail .day-tab::after {
    content: ''; position: absolute; inset: -14px -2px;
  }
  /* Narrow glyph controls are 17-28px WIDE, so a tall-but-thin target is still a
   * miss — and they are isolated, so they can take the horizontal room.
   * `button.avatar` on purpose: the avatars inside cards are spans, and giving
   * THEM a hit area would steal taps from the pick underneath. */
  .back-btn::after, .gear-btn::after, .fest-link::after,
  .sheet-close::after, .chip-notes::after, button.avatar::after {
    content: ''; position: absolute; inset: -14px -12px;
  }

  /* The ZOOMED card is a deliberate preview with room — its chips get real
   * height and their own borrowed space (outranks the card-wide ::after
   * cancel below by specificity). The sheet header's chips grow the same way. */
  .zoom-card .f-chip { height: 30px; padding: 0 13px; font-size: 11px; position: relative; min-height: 0; }
  .zoom-card .f-chip::after { content: ''; position: absolute; inset: -7px; }
  .sheet-card .f-chip { height: 28px; padding: 0 13px; font-size: 11px; }

  /* Controls INSIDE a card get neither: the card IS the tap target, and an
   * expanded edit-note button would steal taps meant for the pick underneath it,
   * on the densest surface in the app. */
  .card button::after { content: none; }
}
.dock .day-tab .num { font-size: 9px; font-weight: 600; margin-left: 2px; }
.dock .day-tab.active { color: rgb(var(--fest)); font-weight: 800; border-bottom-color: rgb(var(--fest)); }
/* Shared between dock and day rail (one component, two positions). */
.fest-link { display: inline-flex; align-items: center; gap: 6px; background: none;
             border: none; cursor: pointer; flex: none; }
.fest-link .fest-name { font-family: var(--font-display); letter-spacing: .04em;
                        font-size: 13px; color: rgb(var(--fest)); }
.you-avatar { border: 1.5px solid #fff; background: none; cursor: pointer; padding: 0; }
.dock.hidden { display: none; }
@media (min-width: 720px) { .dock { display: none; } } /* desktop has no dock */

/* class names match js/sync.js setSyncStatus: sync-online|syncing|offline|error|blocked */
.sync-dot { width: 7px; height: 7px; border-radius: var(--r-pill); background: var(--sync-ok); display: inline-block; }
.sync-dot.sync-syncing { background: var(--sync-syncing); }
.sync-dot.sync-offline { background: var(--sync-offline); }
/* Something is wrong and it is not just the radio — say so in the same color
 * the Settings label says it in. */
.sync-dot.sync-error, .sync-dot.sync-blocked { background: var(--sync-error); }

/* ---- settings ---------------------------------------------------------------- */
.micro-label { color: var(--text-tertiary); font-size: var(--fs-micro); font-weight: 800;
               letter-spacing: var(--track-label); text-transform: uppercase; }
.settings-card { background: var(--card); border: 1px solid var(--border-card);
                 border-radius: var(--r-settings); padding: 14px; }
.settings-card.current { border: 1.5px solid rgba(var(--fest), .55); }
.settings-list { background: var(--card); border: 1px solid var(--border-card);
                 border-radius: var(--r-settings); overflow: hidden; }
.settings-list .list-row { padding: 13px 14px; display: flex; align-items: center; gap: 10px;
                           border-bottom: 1px solid var(--hairline); }
.settings-list .list-row:last-child { border-bottom: none; }
.list-row .row-title { color: var(--text-primary); font-weight: 700; font-size: 13.5px; }
.list-row .row-sub { color: var(--text-tertiary); font-size: 11px; font-weight: 600; }
.list-row .chev { color: var(--text-tertiary); margin-left: auto; }

/* fest row (landing + settings "other festivals") — padding rides the gutter */
.fest-row { display: flex; align-items: center; gap: 11px; background: var(--card);
            border: 1px solid var(--border-card); border-radius: var(--r-settings);
            padding: 12px clamp(14px, 1.6vw, 20px); cursor: pointer; }
.fest-row .fest-name { font-family: var(--font-display); letter-spacing: .04em;
                       font-size: 16px; white-space: nowrap; }
.fest-row .fest-name .yr { font-size: .65em; opacity: .75; }
.fest-row .fest-dates { color: var(--text-tertiary); font-size: 10.5px; font-weight: 600; margin-top: 2px;
                        display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical; overflow: hidden; }
.fest-row .chev { color: var(--text-tertiary); flex: none; }

/* multi-pick create: a selected fest wears the brand ring (never --fest — the
   fest isn't "ours" yet, it's a candidate) */
.fest-row.sel-fest { outline: 2px solid rgba(var(--brand), .9); outline-offset: -2px; }

/* landing YOU card (21a + me link) — the fest-row silhouette, brand-tinted */
.landing-you-card { display: flex; align-items: center; gap: 11px; background: var(--card);
                    border: 1px solid var(--border-card); border-radius: var(--r-settings);
                    padding: 12px clamp(14px, 1.6vw, 20px); }
.landing-you-card .mini-copy { color: var(--text-tertiary); font-size: 10.5px; font-weight: 600;
                               line-height: 1.45; margin-top: 2px; overflow-wrap: anywhere; }
.dashed-row { display: flex; align-items: center; justify-content: center; gap: 8px;
              border: 1.5px dashed var(--border-input); border-radius: var(--r-settings);
              padding: 11px; color: var(--text-secondary); font-weight: 700; font-size: 12px;
              background: none; cursor: pointer; width: 100%; }

/* ---- set-times grid ----------------------------------------------------------- */
/* The hour rail lives OUTSIDE the horizontal scroller (CORE-2): stages swipe,
 * the time axis never leaves the screen. Rail + grid share the exact same
 * grid-template-rows string (set inline) so hours align with cards.
 * All day scrollers mirror ONE scrollLeft (wall.js wireTimesScrollSync), and
 * the sticky stage strip above carries the column names for every day —
 * scroll-snap was removed with the per-day header rows: snapping any one
 * scroller would fight the mirrored position. */
.times-wrap { display: flex; align-items: flex-start; }
.times-rail { display: grid; width: 40px; flex: none; row-gap: 4px; }
.times-scroll { overflow-x: auto; -webkit-overflow-scrolling: touch;
                flex: 1; min-width: 0; }
/* The timetable wall goes full-viewport ≥720 (DT-5, the run's one aesthetic
 * risk): a real festival timetable pinned flat. The rail column stays aligned
 * with the shell's left edge; body overflow-x:clip guarantees no page wiggle.
 * The stage strip mirrors the exact same geometry or its columns would sit
 * offset from the day grids below it. */
@media (min-width: 720px) {
  .times-wrap {
    width: 100vw;
    margin-inline: calc(50% - 50vw);
    /* Asymmetric on purpose (audit 3.2): the LEFT edge aligns with the shell
     * (reading rhythm holds), the RIGHT runs to the window minus a gutter —
     * "as many columns as fit the window" (F5), not re-capped at shell-max. */
    padding-inline: max(var(--sp-gutter), calc(50vw - var(--shell-max) / 2)) var(--sp-gutter);
    box-sizing: border-box;
  }
}
.times-grid { display: grid; gap: 4px; min-width: 100%; } /* columns set inline: repeat(n, minmax(150px,1fr)) */
.stage-head { font-family: var(--font-display); letter-spacing: var(--track-display);
              color: rgb(var(--fest)); font-size: 12.5px;
              min-width: 0; padding: 0 8px; text-align: center;
              line-height: 32px; height: 32px; box-sizing: border-box;
              white-space: nowrap;
              background: var(--card); border-radius: var(--r-card); }
/* The ellipsis lives on an inner label, not the head: `overflow: hidden` on
 * the button itself would clip the ::after that gives it a 44px tap target. */
.stage-head .label { display: block; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }

/* The sticky stage strip (one for the whole page): stage names pin below the
 * sticky chrome while every day scrolls under them. Lives outside the
 * horizontal scrollers — position:sticky can't escape an overflow container
 * (same physics that put the hour rail outside the scroller). --rail-h is
 * measured by app.js: the day rail's height on desktop, 0 on mobile. */
.stage-strip {
  position: sticky; top: var(--rail-h, 0px); z-index: 24;
  background: rgba(12, 10, 20, .94);
  backdrop-filter: blur(10px); -webkit-backdrop-filter: blur(10px);
  padding-bottom: 4px;
}
/* The strip's GEOMETRY is the day grids' geometry, full stop: it carries the
 * `times-wrap` class so the ≥720 full-viewport rule above sizes it exactly
 * like every day grid — same width, same asymmetric padding — and its
 * columns land over theirs. The 2026-08-30 "full-bleed" override here
 * (shell-box margins) broke that on any window wider than the shell: the
 * strip went narrower than the grids, its columns shrank out of alignment
 * (a stage head over the wrong column), and cards scrolled out beside it
 * (Kevin's screenshots, 2026-08-31). Below 720 the wrap has no special
 * geometry, so the strip bleeds to the shell box on its own. */
@media (max-width: 719.98px) {
  .stage-strip { margin-inline: calc(-1 * var(--sp-gutter)); padding-inline: var(--sp-gutter); }
}
.stage-strip .strip-rail { width: 40px; flex: none; } /* aligns with .times-rail */
.stage-strip .times-scroll { scrollbar-width: none; }
.stage-strip .times-scroll::-webkit-scrollbar { display: none; }
.hour-label { text-align: right; padding-right: 6px; font-size: 9.5px;
              color: var(--text-tertiary); font-weight: 600; position: relative; top: -7px; }
/* Everything-else column (ST-2): activities + stage-less sets, chronological.
 * Quiet by design — it shares the grid but never competes with the stages. */
.ee-col { display: flex; flex-direction: column; gap: 4px; align-items: stretch; min-height: 0; }
.ee-item { display: flex; flex-direction: column; gap: 1px; background: var(--card);
           border: 1px solid var(--hairline); border-radius: var(--r-card); padding: 6px 9px; }
.ee-item .ee-time { color: var(--text-tertiary); font-size: 9px; font-weight: 700; }
.ee-item .ee-name { color: var(--text-body); font-size: 11.5px; font-weight: 600; line-height: 1.25; }
.ee-item .ee-venue { color: var(--text-tertiary); font-size: 9.5px; font-weight: 600; }

/* ---- wall grid + undo toast ---------------------------------------------------- */
/* Density scales with the window (DT-3): as many ~176px columns as fit, so a
 * 1080px shell breathes at 5-6 columns instead of stretching 4. */
.wall-grid { display: grid; grid-template-columns: repeat(2, 1fr); gap: 6px; }
@media (min-width: 720px) { .wall-grid { grid-template-columns: repeat(auto-fill, minmax(176px, 1fr)); gap: 7px; } }
@media (min-width: 1100px) { .wall-grid { gap: 9px; } }
.undo-toast {
  position: fixed; left: 50%; transform: translateX(-50%);
  bottom: calc(64px + env(safe-area-inset-bottom)); z-index: 50;
  background: var(--dock); border: 1px solid var(--border-emphasis);
  border-radius: var(--r-row); padding: 10px 14px;
  display: flex; align-items: center; gap: 12px;
  color: var(--text-body); font-size: 12.5px; font-weight: 600;
  box-shadow: 0 6px 24px rgba(0, 0, 0, .5);
}
.undo-btn { background: none; border: none; color: var(--tonal-text);
            font-weight: 800; font-size: 12.5px; cursor: pointer; }

/* ---- misc -------------------------------------------------------------------- */
/* Tiny equalizer — the app's "working" pulse (crew create, lineup research).
 * The global reduced-motion rule freezes it into three quiet bars. */
.eq-loader { display: inline-flex; align-items: flex-end; gap: 2.5px; height: 13px; vertical-align: middle; }
/* Brand, not fest accent — this loader runs while a crew is being created,
 * i.e. before a festival has been picked at all. */
.eq-loader span { width: 3px; border-radius: 2px; background: rgb(var(--brand)); height: 30%;
                  animation: eqBounce .9s ease-in-out infinite; }
.eq-loader span:nth-child(2) { animation-delay: .15s; }
.eq-loader span:nth-child(3) { animation-delay: .3s; }
@keyframes eqBounce { 0%, 100% { height: 30%; } 50% { height: 100%; } }

.back-btn { color: var(--text-secondary); font-size: 16px; background: none; border: none; cursor: pointer; }
.screen-title { font-family: var(--font-display); letter-spacing: var(--track-display);
                font-size: 18px; color: var(--text-header); }
.seg { border: 1px solid var(--border-input); color: var(--text-secondary); font-weight: 600;
       font-size: 12px; padding: 7px 13px; border-radius: var(--r-pill); background: none; cursor: pointer; }
.seg.active { background: rgba(var(--brand), .14); color: #fff; border-color: transparent; font-weight: 700; }


/* ---- threads + the grown card (2026-08-29 round) --------------------------------- */
/* Aura notes: no boxes — a note is text on a wash of its author's hue (set as
 * --wash per note by notes.js), name above the words, one left gutter, replies
 * one gutter in. Pinned = a stronger wash, never a stroke. */
.n-list { display: flex; flex-direction: column; }
.n-thread { display: flex; flex-direction: column; gap: 6px; }
.n-thread + .n-thread { margin-top: 14px; }
.n-list.grouped .n-thread + .n-thread { margin-top: 10px; }
.n-replies { display: flex; flex-direction: column; gap: 6px; margin-left: 22px; }
.n-note { display: grid; grid-template-columns: 20px 1fr; column-gap: 10px; align-items: start;
          padding: 9px 12px 9px 10px; border-radius: 12px;
          background: radial-gradient(150% 200% at 0% 40%, var(--wash) 0%, transparent 68%); }
.n-note .avatar { margin-top: 1px; }
.n-reply { grid-template-columns: 16px 1fr; padding: 7px 12px 7px 10px; }
.n-body { min-width: 0; display: flex; flex-direction: column; gap: 2px; }
.n-head { display: flex; align-items: baseline; gap: 7px; flex-wrap: wrap;
          color: var(--text-secondary); font-size: 10.5px; font-weight: 600; }
.n-head .n-who { color: var(--text-primary); font-weight: 700; font-size: 12.5px; }
.n-text { color: var(--text-primary); font-size: 13.5px; line-height: 1.42; overflow-wrap: anywhere; }
.n-note.stub { background: var(--card); }
.n-note.stub .stub-avatar { width: 20px; height: 20px; border-radius: var(--r-pill);
                            border: 1px dashed var(--border-emphasis); }
.n-note.stub .n-text { color: var(--text-tertiary); font-style: italic; font-size: 12px; }
.n-empty { color: var(--text-tertiary); font-size: 12px; font-weight: 600; text-align: center; padding: 10px 0; }

/* Actions live at the HEAD line's trailing edge (Kevin, 2026-08-30) and are
 * revealed by hover, a press-and-hold, or keyboard focus — two quiet words at
 * most: Pin on any root, plus Edit on your own. The head row already spans the
 * note, so revealing reserves nothing and moves nothing; an earlier build put
 * this line UNDER the words and had to hold 15px open on every note forever to
 * stop the list shoving itself around under the cursor. */
.n-head .note-action { text-decoration: none; font-size: 10.5px; }
.n-head .note-action.on { color: var(--tonal-text); }
/* The reply count is a FACT about the note, sitting with the name and the time
 * — brighter than an action, never hidden, and still the way into the fold. */
.n-head .n-count { color: var(--text-secondary); font-weight: 700; }
.n-acts { margin-left: auto; display: inline-flex; align-items: baseline; gap: 6px;
          opacity: 0; transform: translateY(-2px); pointer-events: none;
          transition: opacity .14s cubic-bezier(.4, 0, .2, 1),
                      transform .14s cubic-bezier(.2, 1.15, .35, 1); }
/* Three doors to the same two words, so nobody is locked out: the mouse hovers,
 * a finger holds (notes.js sets .revealed), a keyboard simply arrives — the
 * focus fallback is required, not optional. `.open` is for editing, where the
 * line is a MODE and not a hover state. */
.n-note.revealed > .n-body > .n-head > .n-acts,
.n-note:focus-within > .n-body > .n-head > .n-acts,
.n-acts.open { opacity: 1; transform: none; pointer-events: auto; }
@media (hover: hover) and (pointer: fine) {
  .n-note:hover > .n-body > .n-head > .n-acts { opacity: 1; transform: none; pointer-events: auto; }
}

/* The head-line actions have graduated into the floor's isolated-glyph case.
 * `.note-action` takes the TIGHT borrowed box (-14px -2px, up at the 44px floor
 * block) because it used to sit in a RUN of four tokens — Edit · Delete · Reply
 * · Pin — where a generous horizontal grab would let one swallow the tap meant
 * for its neighbour. That reason is gone: there are at most two now, alone at
 * the note's trailing edge with nothing to their right, and measured at
 * 16.3 x 12px the tight box left them 40 x 20 — under the floor in BOTH
 * directions. This is the "isolated, so they can take the horizontal room"
 * upgrade the floor's own comment describes, and per its rule the list only
 * ever upgrades. It lives here rather than in the floor block so the notes'
 * selectors stay together; the floor block is the place to look for the others.
 *
 * Nothing to collide with: a note carries at most two actions, only ONE note is
 * ever revealed at a time (notes.js), and an unrevealed note's actions are
 * `pointer-events: none` — so the grown box can only ever overlap dead space. */
@media (pointer: coarse) {
  .n-head .n-acts .note-action::after { content: ''; position: absolute; inset: -16px -14px; }
}

/* THE OPEN DOOR: every thread ends with this row, always visible, in the
 * current viewer's own colour at a whisper. It is the whole reply affordance —
 * no note carries a Reply, so "reply to a reply" cannot be asked for and one
 * level deep holds structurally. 44px on both platforms: this is a primary
 * target, not a text link, so it takes real height rather than borrowed space. */
.n-door { display: flex; align-items: center; gap: 10px; width: 100%; min-height: 44px;
          padding: 0 12px 0 10px; border: none; border-radius: 12px; cursor: pointer;
          text-align: left; font-family: var(--font-ui);
          background: radial-gradient(150% 200% at 0% 50%, var(--wash) 0%, transparent 68%);
          transition: background-color .14s cubic-bezier(.4, 0, .2, 1); }
.n-door .avatar { opacity: .75; }
.n-door-label { color: var(--text-tertiary); font-size: 12.5px; font-weight: 600; }
@media (hover: hover) and (pointer: fine) {
  .n-door:hover { background-color: rgba(255, 255, 255, .035); }
  .n-door:hover .avatar { opacity: 1; }
  .n-door:hover .n-door-label { color: var(--text-secondary); }
}
/* The composer the door became, and the bar under any field: the safe actions
 * lead, and in an editor Delete sits at the far end away from Save — never its
 * neighbour. The counter closes the line, once it is news. */
.n-inline { display: grid; grid-template-columns: 16px 1fr; column-gap: 10px;
            align-items: start; padding: 4px 12px 2px 10px; overflow: hidden; }
.n-inline-body { min-width: 0; display: flex; flex-direction: column; gap: 4px; }
.n-fieldbar { display: flex; align-items: baseline; gap: 6px; min-height: 15px;
              color: var(--text-tertiary); font-size: 10.5px; font-weight: 600; }
.n-fieldbar .note-action { text-decoration: none; font-size: 10.5px; }
.n-fieldbar .note-action.on { color: var(--tonal-text); }
.n-fieldbar .n-del { color: var(--danger); }
.n-left { margin-left: auto; color: var(--text-tertiary); font-size: 10px; font-weight: 700;
          font-variant-numeric: tabular-nums; }
.n-left.at-cap { color: var(--danger); }

/* The sheet's header is the card, grown once more — centred, breathing only
 * when the card would (.animated; reduced-motion and low-power still win). */
.sheet-card { position: relative; overflow: hidden; display: flex; flex-direction: column; gap: 7px;
              align-items: center; text-align: center; padding: 16px 16px 17px;
              border: 1px solid var(--hairline); border-radius: 12px; }
.sheet-card.animated { background-size: 180% 180%; animation: gradShift 12s ease-in-out infinite; }
.sheet-card > * { position: relative; }
.sheet-card .sheet-close { position: absolute; top: 12px; right: 12px;
                           background: rgba(10, 8, 18, .55); border-color: rgba(255, 255, 255, .22); color: #fff; }
.sheet-card .f-name { font-size: 19px; font-weight: 700; color: var(--text-primary); line-height: 1.2; }
/* The grown lines (sub · who · chips) travel as one block so the zoom can
 * fade them together; the sheet header lays them out the same way. */
.f-grown { display: flex; flex-direction: column; align-items: center; gap: 7px; min-width: 0; }
.f-sub { color: rgba(255, 255, 255, .78); font-size: 11.5px; font-weight: 600;
         display: inline-flex; align-items: center; gap: 6px; }
/* WHERE gets its own row under WHEN; a venue with a maps entry is a door. */
.f-where { color: rgba(255, 255, 255, .85); font-size: 11.5px; font-weight: 600;
           display: inline-flex; align-items: center; gap: 5px; }
a.f-where { color: #fff; text-decoration: none; border-bottom: 1px solid rgba(255,255,255,.35); padding-bottom: 1px; }
a.f-where:hover { border-bottom-color: rgba(255,255,255,.75); }
.f-where .pin { flex: none; opacity: .85; }
/* The same door in the wall header and the Settings fest card: inherits the
 * line's colour and size, underlined only when it actually opens a map. */
.fest-place { display: inline-flex; align-items: center; gap: 3px; color: inherit; }
a.fest-place { text-decoration: none; border-bottom: 1px solid currentColor; padding-bottom: 1px; opacity: .92; }
a.fest-place:hover { opacity: 1; }
.fest-place .pin { flex: none; opacity: .8; }
.day-sub { color: var(--text-tertiary); margin-top: -6px; }
.f-who { display: flex; flex-wrap: wrap; justify-content: center; gap: 5px 6px; }
/* Quieter than the words around them — a crew can be eight people
 * (Kevin, 2026-08-30: "not worth making them way bigger than the note text"). */
.f-pill { display: inline-flex; align-items: center; gap: 5px; color: #fff; font-size: 11px;
          font-weight: 600; padding: 0 9px; height: 20px; border-radius: var(--r-pill); line-height: 1; }
.f-pill.you { font-weight: 800; }
.f-pill b { font-size: 7.5px; font-weight: 800; letter-spacing: .1em; opacity: .9; line-height: 1;
            transform: translateY(1px); /* optical: small caps sit on the name's baseline */ }
.f-chips { display: flex; justify-content: center; align-items: center; gap: 6px; flex-wrap: wrap; }
.f-chip { display: inline-flex; align-items: center; gap: 4px; height: 17px; padding: 0 9px;
          font-size: 10px; font-weight: 800; color: #fff; border-radius: var(--r-pill); border: none;
          line-height: 1; font-family: inherit; }
.f-chip.notes { border-radius: var(--r-bubble); background: rgba(108, 91, 212, .62); }
button.f-chip.notes { cursor: pointer; }
.f-chip.spot { background: rgba(18, 138, 62, .6); }

/* The sheet's bottom composer writes NEW notes, and only those — its reply
 * state retired 2026-08-30 when replying moved inline, to the thread you
 * pressed. It can no longer re-aim a typed draft at a root you scrolled past. */
.composer-wrap { display: flex; flex-direction: column; gap: 4px; }

/* The day whisper: nothing until someone writes, then the newest note as one
 * soft wash at the day's door; the rule's ✎ chip stays the add door. */
.day-whisper { display: flex; align-items: center; gap: 9px; width: 100%; min-width: 0; border: none;
               cursor: pointer; text-align: left; color: var(--text-body); font-family: var(--font-ui);
               font-size: 12px; line-height: 1.35; margin: -4px 0 2px; padding: 7px 12px 7px 10px;
               border-radius: 12px; background: radial-gradient(150% 200% at 0% 50%, var(--wash) 0%, transparent 68%); }
.day-whisper .who { color: var(--text-primary); font-weight: 700; flex: none; }
.day-whisper .text { flex: 1; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.day-whisper .more { color: var(--text-tertiary); font-size: 10.5px; font-weight: 700; flex: none; white-space: nowrap; }
.add-first-note { align-self: flex-start; }

/* The zoom (2026-08-30): an OVERLAY, never a reflow. card-facts.js draws the
 * grown card in this fixed layer, centred on the resting card and clamped to
 * the viewport; the resting card's CONTENT steps back underneath while its
 * wash stays (opacity on children, not visibility — the card keeps focus and
 * its accessible name). Nothing on the wall changes size, so siblings never
 * move. The bloom (scale + fade) and the grown lines' cascade run through
 * the Web Animations API; the shadow eases in
 * here. z-index 36: above the day rail (25) and the dock (30), below sheets
 * (40) and toasts (50). */
#zoom-layer { position: fixed; inset: 0; z-index: 36; pointer-events: none; }
.zoom-slot { position: absolute; width: max-content; pointer-events: none; }
.zoom-slot::before { content: ''; position: absolute; inset: 0; border-radius: var(--r-card);
                     box-shadow: 0 10px 28px rgba(0, 0, 0, .38); opacity: 0; transition: opacity .38s ease; } /* softened 2026-08-30 — 18/50/.55 read as a second card stacked behind a bright aura */
.zoom-slot.shown::before { opacity: 1; }
/* The hit target is the whole grown box from the first frame; the SURFACE
 * under it carries the wash and the border — so a tap lands while the card
 * is still blooming. */
.zoom-card { position: relative; box-sizing: border-box; min-height: inherit; pointer-events: auto;
             display: flex; flex-direction: column; align-items: center; justify-content: center; gap: 7px;
             padding: 12px 14px 12px; text-align: center; cursor: pointer;
             -webkit-touch-callout: none; user-select: none; color: inherit; font-family: inherit; }
.zoom-card .z-surface { position: absolute; inset: 0; overflow: hidden; border: 1px solid var(--hairline); border-radius: var(--r-card); }
.zoom-card .z-surface.animated { background-size: 180% 180%; animation: gradShift 12s ease-in-out infinite; }
.zoom-card > :not(.z-surface) { position: relative; z-index: 2; } /* content over both surfaces */
.zoom-card .f-name { font-size: 18px; font-weight: 700; line-height: 1.2; color: var(--text-primary); } /* the zoom's hero: the name grows (13.5–15px at rest) */
/* After a pick the old wash lingers over the new one and thins away. */
.zoom-card .z-surface-old { pointer-events: none; z-index: 1; }
/* Every grown row is centred, same as the sheet card. (The 2026-08-30
 * "corner-true" experiment pushed people right and chips left; with one
 * pill and one chip it read as two strays flung to the edges — Kevin,
 * 2026-08-31. The corner CONNECTION lives in the slide-in directions,
 * not in resting alignment.) */
/* While zoomed, the resting card's CONTENT steps back but its wash stays —
 * the wall keeps its colour under the bloom and there is never a hole.
 * (Hiding the whole card was why a frame-0 twin had to repaint it inside
 * the overlay — one rendering of everything now.) The transition lives on
 * the BASE state, not on .zoom-source: a state-only transition dies with
 * the class, so the restore snapped to full opacity under the still-fading
 * overlay — a double print on the way out (Codex gate, 2026-08-30). The
 * .09s matches card-facts.js MATERIALIZE_MS. Low Power's global transition
 * kill already covers this; reduced-motion gets its own. */
.card > * { transition: opacity .09s ease; }
.card.zoom-source > * { opacity: 0; }
@media (prefers-reduced-motion: reduce) { .card > * { transition: none; } }

/* Afters/event cards: the day and time on one line, the venue on the next. */
.card.timed .time { white-space: pre-line; }

/* ---- wall filters + the now line (2026-08-27) ----------------------------------- */
/* People filter: selected chips wear a ring, the rest step back. On the
 * timetable a non-matching card DIMS (the clock keeps its shape); on lineup
 * lists it hides. Dimmed cards stay tappable — a filter is a view, never a
 * lock on picking. */
.person-chip.selected { box-shadow: 0 0 0 1.5px rgba(255, 255, 255, .85); }
.person-chip.faded { opacity: .42; }
.person-chip.everyone { background: transparent; border: 1px solid var(--border-emphasis); color: var(--text-body); }
.person-chip.everyone .x { color: var(--text-tertiary); margin-left: 4px; }
.card.dim { opacity: .28; }
.section-empty { color: var(--text-tertiary); font-size: 12px; font-weight: 600; padding: 2px 0 4px; }

/* Stage solo: the stage heads are buttons now. The soloed head carries an
 * inset ring in the fest accent (it IS a stage header — one of the accent's
 * four homes) and a quiet "all stages" affix; folded stages become 34px
 * rails with the name set vertically. */
.stage-head { cursor: pointer; border: none; font-weight: inherit; }
.stage-head[aria-pressed="true"] { box-shadow: inset 0 0 0 1px rgba(var(--fest), .6); }
.stage-head .solo-off { font-family: var(--font-ui); font-size: 9px; letter-spacing: 0; font-weight: 700;
                        color: var(--text-tertiary); margin-left: 6px; }
.stage-head.rail { padding: 0; font-size: 9px; letter-spacing: .06em; color: var(--text-tertiary); }
.stage-head.rail .label { writing-mode: vertical-rl; transform: rotate(180deg); line-height: 34px; height: 32px; overflow: visible; }
/* The now line: brand violet, not the fest accent — it is app chrome that
 * happens to sit on a timetable. Drawn inside the grid (position: relative
 * on the grid and the rail) so it scrolls with the day; the ticker in app.js
 * moves it every minute without a repaint. */
.times-grid, .times-rail { position: relative; }
.now-line { position: absolute; left: 0; right: 0; height: 2px; background: rgb(var(--brand));
            box-shadow: 0 0 6px rgba(var(--brand), .8); pointer-events: none; z-index: 3; }
.now-label { position: absolute; right: 4px; transform: translateY(-50%); background: rgb(var(--brand));
             color: var(--page); font-size: 8px; font-weight: 800; letter-spacing: .08em;
             padding: 1px 5px; border-radius: var(--r-pill); pointer-events: none; z-index: 3; white-space: nowrap; }
/* On touch devices the stage strip row IS 44px — a borrowed ::after can't
 * escape the strip's own scroll container, so the target has to be real.
 * Desktop keeps the designed 32px. The inline row template on the strip
 * grid is 32px; the !important here is the one deliberate override. */
@media (pointer: coarse) {
  .stage-strip .times-grid { grid-template-rows: 44px !important; }
  .stage-head { height: 44px; line-height: 44px; }
  .stage-head.rail .label { height: 44px; line-height: 34px; }
}
