/* Fringe Planner — stylesheet
 * Extracted/adapted from plan/design/mock.html. Brand tokens match the home
 * site's css/styles.css (violet #6c4cf1, ink/muted/hairline, 16px radius,
 * soft shadow, Plus Jakarta Sans display + Inter body).
 * The container is wider than the home site's 1120px: the planner's payload is
 * two dense grids (a 31-day availability calendar and a multi-week schedule),
 * which were being squeezed into a narrow strip with big empty margins on wide
 * screens — so this page gets more room, while the prose blocks inside stay
 * capped at a readable width.
 * This stylesheet is self-contained — the planner does not load styles.css.
 */

:root {
  --violet: #6c4cf1;
  --violet-dark: #5a3fd6;
  --ink: #101828;
  --paper: #ffffff;
  --bg: #f7f8fa;
  --muted: #667085;
  --line: #eaecf0;
  --ok: #059669;
  --ok-text: #047857;
  --ok-bg: #e6f7ee;
  --warn: #ef4444;
  /* Performance-date availability palette — taken from edfringe.com's own
     ticket day-picker legend so the calendar reads the same as the official
     site. Violet stays reserved for the date-window instrument + chrome. */
  --fr-avail: #2ac9de;    /* tickets available */
  --fr-2for1: #ffce00;    /* 2-for-1 (solid fallback, e.g. the thin schedule accent) */
  /* 2-for-1 reads as its two halves — available + free — interlaced on the
     diagonal, so the mark says "a bit of both" without a new colour. */
  --fr-2for1-weave: repeating-linear-gradient(45deg, var(--fr-avail) 0 3px, var(--fr-free) 3px 6px);
  --fr-preview: #3438ff;  /* preview show */
  --fr-free: #00bf57;     /* free (ticketed / non-ticketed) */
  --fr-event: #8c4fd9;    /* event-specific */
  --fr-noalloc: #ef3934;  /* no allocation — contact venue */
  --fr-sold-bg: #e6e9ef;  /* sold out — greyed, as the official picker disables it */
  --fr-sold-line: #c6cdda;
  --fr-locked: linear-gradient(120deg, #fdf3c4 0%, #f3d269 45%, #eac247 100%); /* locked-in day mark — a gold bar */
  --fr-ink: #00243d;      /* edfringe.com ink */
  --shadow-soft: 0 12px 30px rgba(16, 24, 40, 0.08);
  --maxw: 1400px;
  --radius: 16px;
  /* The board's body height. The empty drop stage and the availability grid both
     take exactly this, so the search bar under them — and everything below it —
     sits in the same place whether the grid holds nothing or forty shows. */
  --board-h: clamp(280px, 40vh, 460px);
  --sans: "Inter", system-ui, -apple-system, "Segoe UI", Roboto, sans-serif;
  --display: "Plus Jakarta Sans", "Inter", system-ui, sans-serif;
  /* A small violet padlock, used as the cursor on every "click to lock into the
     plan" target (show names, day marks, scheduled blocks) so the affordance is
     shown, not spelled out. Falls back to the pointer where SVG cursors aren't
     honoured. */
  --lock-cursor:
    url("data:image/svg+xml,%3Csvg%20xmlns='http://www.w3.org/2000/svg'%20width='28'%20height='28'%20viewBox='0%200%2024%2024'%3E%3Cpath%20d='M8%2010.5V7.5a4%204%200%200%201%208%200v3'%20fill='none'%20stroke='%23ffffff'%20stroke-width='3.6'%20stroke-linecap='round'/%3E%3Cpath%20d='M8%2010.5V7.5a4%204%200%200%201%208%200v3'%20fill='none'%20stroke='%236c4cf1'%20stroke-width='2'%20stroke-linecap='round'/%3E%3Crect%20x='4.6'%20y='10.5'%20width='14.8'%20height='10.5'%20rx='2.6'%20fill='%236c4cf1'%20stroke='%23ffffff'%20stroke-width='1.4'/%3E%3Ccircle%20cx='12'%20cy='15'%20r='1.5'%20fill='%23ffffff'/%3E%3Crect%20x='11.25'%20y='15'%20width='1.5'%20height='3.2'%20rx='0.75'%20fill='%23ffffff'/%3E%3C/svg%3E")
      13 5, pointer;
  /* The same padlock with its shackle sprung open, for the targets where a
     click *removes* a lock rather than adding one (a locked show's name, and
     the exact performance it's pinned to). Same body, same hotspot — only the
     shackle moves, so the pair reads as one control in two states. */
  --unlock-cursor:
    url("data:image/svg+xml,%3Csvg%20xmlns='http://www.w3.org/2000/svg'%20width='28'%20height='28'%20viewBox='0%200%2024%2024'%3E%3Cpath%20d='M11%2010.5V7.5a4%204%200%200%201%208%200'%20fill='none'%20stroke='%23ffffff'%20stroke-width='3.6'%20stroke-linecap='round'/%3E%3Cpath%20d='M11%2010.5V7.5a4%204%200%200%201%208%200'%20fill='none'%20stroke='%236c4cf1'%20stroke-width='2'%20stroke-linecap='round'/%3E%3Crect%20x='4.6'%20y='10.5'%20width='14.8'%20height='10.5'%20rx='2.6'%20fill='%236c4cf1'%20stroke='%23ffffff'%20stroke-width='1.4'/%3E%3Ccircle%20cx='12'%20cy='15'%20r='1.5'%20fill='%23ffffff'/%3E%3Crect%20x='11.25'%20y='15'%20width='1.5'%20height='3.2'%20rx='0.75'%20fill='%23ffffff'/%3E%3C/svg%3E")
      13 5, pointer;
}
* { box-sizing: border-box; }
html { scroll-behavior: smooth; }
body {
  margin: 0;
  font-family: var(--sans);
  color: var(--ink);
  background: var(--bg);
  line-height: 1.5;
}
[hidden] { display: none !important; }
.container { width: 100%; max-width: var(--maxw); margin: 0 auto; padding-inline: 24px; }
a { color: inherit; }
.sr-only {
  position: absolute; width: 1px; height: 1px; padding: 0; margin: -1px;
  overflow: hidden; clip: rect(0, 0, 0, 0); white-space: nowrap; border: 0;
}

/* ============ Header ============ */
.site-header {
  border-bottom: 1px solid var(--line);
  position: sticky; top: 0; z-index: 50;
  background: rgba(255, 255, 255, 0.95);
  backdrop-filter: blur(6px);
}
/* Three columns, not a flex row with auto margins on the nav: the logo and the
   right-hand cluster are never the same width, so auto margins centre "Now /
   Plan" in the leftover space rather than on the page's centre line. */
.header-inner {
  display: grid;
  grid-template-columns: 1fr auto 1fr;
  align-items: center;
  height: 64px;
  gap: 24px;
}
.header-side { display: flex; align-items: center; gap: 12px; justify-self: end; }
.logo {
  font-family: var(--display); font-weight: 700; font-size: 1.4rem;
  letter-spacing: -0.01em; text-decoration: none; white-space: nowrap;
}
.logo-now { color: var(--violet); }
.main-nav { display: flex; gap: 28px; }
.nav-link {
  text-decoration: none; font-size: 0.98rem; padding-bottom: 2px;
  border-bottom: 2px solid transparent; color: var(--ink);
}
.nav-link:hover { color: var(--violet); }
.nav-link.is-active { color: var(--violet); border-bottom-color: var(--violet); }
.header-hint { font-size: 0.8rem; color: var(--muted); white-space: nowrap; }
/* One debug pill (version + a popover with testing tools + the live average
   reschedule time). A quiet neutral chip — no longer a loud red banner. */
.debug-menu { position: relative; }
.debug-pill {
  display: inline-flex; align-items: center; gap: 6px; white-space: nowrap;
  font: inherit; font-size: 0.66rem; font-weight: 700; letter-spacing: 0.02em;
  color: var(--muted); background: var(--bg); border: 1px solid var(--line);
  border-radius: 999px; padding: 4px 11px; cursor: pointer;
  font-variant-numeric: tabular-nums;
}
.debug-pill::before { content: ""; width: 7px; height: 7px; border-radius: 50%; background: #c2c8d2; flex: 0 0 auto; }
.debug-pill:hover { border-color: #d4d8e0; color: var(--ink); }
.debug-pill[aria-expanded="true"] { border-color: var(--violet); color: var(--ink); }
.debug-pill:focus-visible { outline: 3px solid var(--violet); outline-offset: 2px; }
/* The popover, anchored under the pill. */
.debug-pop {
  position: absolute; top: calc(100% + 8px); right: 0; z-index: 50;
  min-width: 220px; display: flex; flex-direction: column; gap: 10px;
  background: var(--paper); border: 1px solid var(--line); border-radius: 12px;
  padding: 12px; box-shadow: var(--shadow-soft);
}
.debug-pop[hidden] { display: none; }
.debug-pop__item {
  font: inherit; font-size: 0.82rem; font-weight: 600; text-align: left; cursor: pointer;
  color: var(--ink); background: var(--bg); border: 1px solid var(--line);
  border-radius: 8px; padding: 8px 10px;
}
.debug-pop__item:hover { border-color: var(--violet); background: rgba(108, 76, 241, 0.06); }
.debug-pop__item:focus-visible { outline: 3px solid var(--violet); outline-offset: 2px; }
.debug-pop__stat { margin: 0; font-size: 0.74rem; color: var(--muted); font-variant-numeric: tabular-nums; }
/* The header is a single nowrap flex row, so a narrow page dropped chrome
   progressively. The debug pill now folds the telemetry + testing tools into one
   small chip, so only the decorative date tagline needs to go on narrow pages. */
@media (max-width: 940px) { .header-hint { display: none; } }

/* ============ Page head ============ */
/* Title left, running count right, on one line. `baseline` puts the two
   different type sizes on the same line of writing rather than centring the
   smaller one against the larger; on a narrow page the count wraps under the
   title and stays right-aligned. */
.page-head {
  padding: 44px 0 8px;
  display: flex; align-items: baseline; justify-content: space-between;
  gap: 8px 24px; flex-wrap: wrap;
}
.page-title {
  font-family: var(--display); font-weight: 700; letter-spacing: -0.01em;
  font-size: 2.3rem; line-height: 1.12; margin: 0;
}

.panel {
  background: var(--paper);
  border: 1px solid var(--line);
  border-radius: var(--radius);
  box-shadow: var(--shadow-soft);
}

/* ============ Loading / error states ============ */
.state-panel {
  max-width: 660px; margin: 40px auto; padding: 40px 32px;
  text-align: center; border-radius: var(--radius);
  background: var(--paper); border: 1px solid var(--line); box-shadow: var(--shadow-soft);
}
.state-spinner {
  width: 34px; height: 34px; margin: 0 auto 16px; border-radius: 50%;
  border: 3px solid var(--line); border-top-color: var(--violet);
  animation: spin 0.8s linear infinite;
}
@keyframes spin { to { transform: rotate(360deg); } }
.state-title { font-family: var(--display); font-weight: 700; font-size: 1.15rem; margin: 0 0 6px; }
.state-sub { color: var(--muted); font-size: 0.92rem; margin: 0; }
.state-error .state-icon {
  width: 44px; height: 44px; margin: 0 auto 14px; border-radius: 50%;
  display: grid; place-items: center; color: var(--warn); background: rgba(239, 68, 68, 0.1);
}
.state-error .btn { margin-top: 16px; }

/* ============ The board (empty state and grid share one card) ============ */
/* `relative` anchors the drop stage's own overlays. The colour key used to be
   parked in the bottom padding and revealed on hover; it now sits in normal
   flow at the foot of the card (see .cal-legend), so the padding no longer
   reserves room for it. */
.board { position: relative; padding: 22px 28px 22px; }

/* One sentence, sized like a heading because it *is* the heading now. */
.board-count {
  margin: 0 0 0 auto; min-width: 0; text-align: right;
  font-family: var(--display); font-weight: 700; font-size: 1.32rem;
  letter-spacing: -0.01em; color: var(--muted);
}
/* The two numbers carry the news, so they're the only coloured ink in the line:
   what's planned in violet, what you've picked in ink. */
.board-count .bc-planned { color: var(--violet-dark); }
.board-count .bc-selected { color: var(--ink); }
.board-count.is-live { color: var(--ink); }
.board-count.bump { animation: countbump 0.4s ease; }
/* Clear sits in the grid's top-left corner (see .cal-head-left), so it has the
   show-name column's width and no more: `max-width` keeps the armed question
   inside that column instead of running under the day numbers. */
.board-clear {
  flex: 0 0 auto; max-width: 100%; font: inherit; font-size: 0.78rem; font-weight: 600;
  color: var(--muted); background: #fff;
  border: 1px solid var(--line); border-radius: 999px;
  padding: 5px 13px; cursor: pointer;
  line-height: 1.25; text-align: center;
}
.board-clear[hidden] { display: none; }
.board-clear:hover { color: var(--warn); border-color: #f3bcbc; }
.board-clear:focus-visible { outline: 3px solid var(--violet); outline-offset: 2px; }
/* Armed: the first click turned it into a question naming what is about to be
   thrown away, and the second click answers it. Red, because by then it is a
   destructive button rather than a quiet utility one. */
.board-clear.is-armed {
  color: #fff; background: var(--warn); border-color: var(--warn); font-weight: 700;
  /* The question is longer than the column is wide, so it wraps to two lines —
     which the corner cell already has room for, so arming shifts nothing. */
  font-size: 0.72rem; padding: 5px 11px; border-radius: 12px;
}
.board-clear.is-armed:hover { color: #fff; border-color: var(--warn); }

/* The body — the drop stage and the grid are the same box, one height. */
.board-body { height: var(--board-h); }

/* The empty-grid stage: faint horizontal lanes (echoing the calendar's lanes)
   with the drop box floating centred over them, opaque so it reads as a card
   sitting on the waiting grid. */
.intake-stage {
  position: relative;
  display: flex; flex-direction: column; align-items: center; justify-content: center;
  gap: 12px; text-align: center;
  padding: 24px;
  border: 1px solid var(--line); border-radius: 10px;
  background: repeating-linear-gradient(to bottom, #fbfbfd 0 28px, #eef0f5 28px 29px);
}
.intake-stage .dropzone {
  width: 100%; max-width: 460px;
  background: var(--paper);
  box-shadow: var(--shadow-soft);
}

.dropzone {
  display: flex; flex-direction: column; align-items: center; gap: 6px;
  text-align: center; padding: 34px 24px 30px;
  border: 2px dashed #d5d9e2; border-radius: var(--radius);
  background: #fbfbfd;
  transition: border-color 0.15s ease, background 0.15s ease;
  cursor: pointer;
}
.dropzone:hover, .dropzone:focus-within, .dropzone.is-dragover {
  border-color: var(--violet); background: rgba(108, 76, 241, 0.04);
}
.dz-icon {
  width: 46px; height: 46px; display: grid; place-items: center;
  color: var(--violet); background: rgba(108, 76, 241, 0.1);
  border-radius: 14px; margin-bottom: 6px;
}
.dz-line1 { font-weight: 700; font-size: 1.02rem; margin-bottom: 12px; }
.btn {
  display: inline-flex; align-items: center; gap: 8px;
  font: inherit; font-weight: 700; font-size: 0.95rem;
  border: none; border-radius: 12px; padding: 12px 22px; cursor: pointer;
  color: #fff; background: var(--violet);
  box-shadow: 0 8px 20px rgba(108, 76, 241, 0.28);
  transition: transform 0.12s ease, box-shadow 0.12s ease, background 0.12s ease;
  text-decoration: none;
}
.btn:hover { background: var(--violet-dark); transform: translateY(-1px); }
.btn:focus-visible { outline: 3px solid var(--violet); outline-offset: 3px; }
.btn:disabled { opacity: 0.55; cursor: not-allowed; transform: none; }
.link-quiet {
  font-weight: 700; color: var(--violet-dark); text-decoration: none;
  border-bottom: 1px solid rgba(108, 76, 241, 0.35);
  background: none; border-top: none; border-left: none; border-right: none;
  font: inherit; cursor: pointer; padding: 0;
}
.link-quiet:hover { color: var(--violet); border-bottom-color: var(--violet); }

/* Upload feedback. A usable file switches the board to the grid, so this panel
   only ever carries a problem — and reads like one. */
.upload-summary {
  display: flex; align-items: center; gap: 14px;
  max-width: 560px;
  border: 1px solid #f5c9c9; background: #fef3f2;
  border-radius: 12px; padding: 13px 16px;
}
.us-icon {
  flex: 0 0 auto; width: 30px; height: 30px; border-radius: 50%;
  display: grid; place-items: center; color: #fff; background: var(--warn);
}
.us-body { flex: 1; min-width: 0; text-align: left; }
.us-main { font-weight: 700; font-size: 0.95rem; color: #b42318; }
.us-sub { font-size: 0.8rem; color: var(--muted); }
.us-file {
  flex: 0 0 auto; max-width: 40%; font-size: 0.74rem; font-weight: 600; color: var(--muted);
  background: #fff; border: 1px solid var(--line); border-radius: 999px; padding: 4px 10px;
  white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
}
/* Dropping a file we won't even try to read gets its own refusal state on the
   box itself, so the rejection lands where the pointer already is. */
.dropzone.is-reject { border-color: var(--warn); background: #fef3f2; }

/* ============ Show search (build / top up the grid one show at a time) ==== */
/* One component, one home — directly under the board body, whichever state the
   board is in, so the bar is the same width in the same place before and after
   the grid has data. */
.ss-slot { width: 100%; margin-top: 16px; }
.show-search { position: relative; text-align: left; }
/* Deliberately heavier than the page's other inputs: this is the main way in
   for anyone without a favourites export, so it reads as the primary control. */
.ss-bar {
  display: flex; align-items: center; gap: 10px;
  background: #fff; border: 2px solid #d8dbe4; border-radius: 14px;
  padding: 9px 9px 9px 16px; box-shadow: 0 6px 18px rgba(16, 24, 40, 0.07);
  transition: border-color 0.12s ease, box-shadow 0.12s ease;
}
.ss-bar:hover { border-color: #c3c8d6; }
.ss-bar:focus-within, .show-search.is-open .ss-bar {
  border-color: var(--violet);
  box-shadow: 0 0 0 3px rgba(108, 76, 241, 0.15);
}
.ss-icon { flex: 0 0 auto; display: grid; place-items: center; color: var(--violet); }
.ss-input {
  flex: 1 1 auto; min-width: 0; border: none; outline: none; background: none;
  font: inherit; font-size: 1.04rem; font-weight: 600; color: var(--ink); padding: 4px 0;
}
.ss-input::placeholder { color: #9aa3b4; font-weight: 500; }
.ss-tools-btn {
  flex: 0 0 auto; display: inline-flex; align-items: center; gap: 6px;
  font: inherit; font-size: 0.78rem; font-weight: 600; color: var(--muted);
  background: var(--bg); border: 1px solid var(--line); border-radius: 999px;
  padding: 6px 12px; cursor: pointer; white-space: nowrap;
}
.ss-tools-btn:hover { color: var(--ink); border-color: #d5d9e2; }
.ss-tools-btn[aria-expanded="true"] {
  color: var(--violet-dark); border-color: var(--violet);
  background: rgba(108, 76, 241, 0.07);
}
.ss-tools-btn:focus-visible { outline: 3px solid var(--violet); outline-offset: 2px; }
/* Active-filter count on the tools button. */
.ss-badge {
  min-width: 17px; height: 17px; border-radius: 999px; padding: 0 5px;
  display: grid; place-items: center;
  font-size: 0.64rem; font-weight: 800; color: #fff; background: var(--violet);
}

/* The expanded line of search tools, below the box — like the results, and like
   every other dropdown on the page, it opens downward. Each facet is a chip that
   drops a panel of checkable options — the same instrument the Now page's
   filters use, in this page's palette. */
.ss-tools { display: flex; flex-wrap: wrap; align-items: center; gap: 8px; margin-top: 8px; }
.filter-chip { position: relative; }
.chip-trigger {
  display: inline-flex; align-items: center; gap: 5px; max-width: 210px;
  font: inherit; font-size: 0.8rem; font-weight: 700; color: var(--ink);
  background: #fff; border: 1px solid var(--line); border-radius: 999px;
  padding: 6px 12px; cursor: pointer;
}
/* One glyph per facet, as on the Now page's filter chips. */
.chip-ico { flex: 0 0 auto; font-size: 0.9rem; line-height: 1; }
.chip-trigger:hover { border-color: #d5d9e2; }
.chip-trigger:focus-visible { outline: 3px solid var(--violet); outline-offset: 2px; }
.filter-chip.is-set .chip-trigger {
  border-color: var(--violet); color: var(--violet-dark);
  background: rgba(108, 76, 241, 0.05);
}
.filter-chip.is-disabled .chip-trigger { opacity: 0.55; cursor: not-allowed; }
.chip-value { min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.chip-caret { flex: 0 0 auto; color: var(--muted); font-size: 0.66rem; }
.chip-money { flex: 0 0 auto; font-weight: 800; color: var(--muted); }
.filter-chip.is-set .chip-money { color: var(--violet); }

/* The dropped panel. Anchored under its chip; JS nudges it back inside the
   viewport when a wrapped chip row would push it off an edge. */
.chip-panel {
  position: absolute; z-index: 60; top: calc(100% + 6px); left: 0;
  min-width: 250px;
  background: var(--paper); border: 1px solid var(--line); border-radius: 12px;
  box-shadow: var(--shadow-soft); padding: 14px 14px 15px;
}
.chip-panel[hidden] { display: none; }
.chip-panel--mini { min-width: 215px; }
.panel-title {
  display: block; margin: 0 0 10px;
  font-size: 0.7rem; font-weight: 800; text-transform: uppercase; letter-spacing: 0.05em;
  color: var(--muted);
}
.panel-title--row { display: flex; align-items: baseline; justify-content: space-between; gap: 10px; }
/* "everything!" — a quiet link beside the question, not a button competing
   with the options for attention. */
.panel-everything {
  font: inherit; font-weight: 700; font-size: 0.72rem; text-transform: none; letter-spacing: 0;
  white-space: nowrap; color: var(--violet); background: none; border: none; padding: 0;
  cursor: pointer; text-decoration: underline; text-underline-offset: 2px;
}
.panel-everything:hover { color: var(--violet-dark); }
.panel-everything:focus-visible { outline: 3px solid var(--violet); outline-offset: 2px; border-radius: 4px; }
.panel-options { display: grid; gap: 2px; }
.panel-options--scroll { max-height: 44vh; overflow-y: auto; overscroll-behavior: contain; }
.panel-option {
  display: flex; align-items: center; gap: 9px;
  padding: 6px 7px; border-radius: 8px; cursor: pointer;
  font-size: 0.84rem; font-weight: 600;
}
.panel-option:hover { background: var(--bg); }
.panel-option input { flex: 0 0 auto; accent-color: var(--violet); width: 15px; height: 15px; }
.panel-option.is-empty { opacity: 0.55; }
/* The grey per-option show count, pushed to the row's right edge. */
.opt-count {
  margin-left: auto; flex: 0 0 auto;
  font-size: 0.74rem; font-weight: 700; color: #98a2b3; font-variant-numeric: tabular-nums;
}
.panel-note { margin: 2px 0 0; font-size: 0.78rem; color: var(--muted); font-style: italic; }
/* Type-to-narrow box inside a long option list (the venue panel). */
.panel-filter {
  width: 100%; margin: 0 0 8px; font: inherit; font-size: 0.82rem; font-weight: 600;
  color: var(--ink); background: #fff;
  border: 1px solid var(--line); border-radius: 9px; padding: 6px 10px;
}
.panel-filter::placeholder { color: #98a2b3; font-weight: 500; }
.panel-filter:focus-visible { outline: 3px solid var(--violet); outline-offset: 1px; }
.ss-reset {
  font: inherit; font-size: 0.76rem; font-weight: 600; color: var(--muted);
  background: none; border: none; padding: 2px 4px; cursor: pointer;
  text-decoration: underline; text-underline-offset: 2px; text-decoration-color: #c6cdda;
}
.ss-reset:hover { color: var(--ink); text-decoration-color: currentColor; }
.ss-reset:focus-visible { outline: 3px solid var(--violet); outline-offset: 2px; border-radius: 4px; }

/* The results popup — an overlay under the bar, never a layout push. */
.ss-pop {
  position: absolute; left: 0; right: 0; top: calc(100% + 6px); z-index: 45;
  background: var(--paper); border: 1px solid var(--line); border-radius: 12px;
  box-shadow: 0 18px 40px rgba(16, 24, 40, 0.18);
  max-height: 342px; overflow-y: auto; overscroll-behavior: contain;
}
.ss-results { list-style: none; margin: 0; padding: 6px; }
/* One line per show: star, title, then quiet meta pushed to the right. */
.ss-row {
  display: flex; align-items: center; gap: 8px;
  padding: 3px 10px 3px 4px; border-radius: 8px; cursor: pointer;
}
.ss-row:hover, .ss-row.is-active { background: rgba(108, 76, 241, 0.06); }
.ss-star {
  flex: 0 0 auto; width: 28px; height: 28px; border-radius: 8px;
  display: grid; place-items: center;
  font-size: 1rem; line-height: 1; color: #b0b7c3;
  border: none; background: none; cursor: pointer;
}
.ss-star:hover { background: rgba(108, 76, 241, 0.12); color: var(--violet-dark); }
.ss-star:focus-visible { outline: 3px solid var(--violet); outline-offset: 1px; }
.ss-row.is-on .ss-star { color: #e0b23a; }
.ss-row-title {
  min-width: 0; font-family: var(--display); font-weight: 600; font-size: 0.84rem;
  white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
}
.ss-row.is-on .ss-row-title { color: var(--violet-dark); }
.ss-row-meta {
  margin-left: auto; flex: 0 0 auto; max-width: 46%;
  font-size: 0.72rem; color: var(--muted); font-variant-numeric: tabular-nums;
  white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
}
.ss-empty, .ss-foot { margin: 0; padding: 10px 14px; font-size: 0.8rem; color: var(--muted); }
.ss-foot { border-top: 1px solid var(--line); padding: 7px 14px; font-size: 0.72rem; }

/* A category row — the query named a genre, subgenre or venue rather than a
   show. It sits above the show hits and is deliberately a different animal: a
   filter glyph instead of a star, the kind spelled out in violet small caps, and
   a tinted band so a click that changes the *filters* never looks like a click
   that adds one show. */
.ss-row--facet {
  background: rgba(108, 76, 241, 0.045);
  padding: 5px 10px 5px 6px; margin-bottom: 2px;
}
.ss-row--facet:hover, .ss-row--facet.is-active { background: rgba(108, 76, 241, 0.13); }
.ss-facet-ico {
  flex: 0 0 auto; width: 26px; height: 26px; border-radius: 8px;
  display: grid; place-items: center; color: var(--violet-dark);
  background: rgba(108, 76, 241, 0.12);
}
.ss-row--facet .ss-row-title { font-weight: 700; }
.ss-facet-kind {
  flex: 0 0 auto;
  font-size: 0.62rem; font-weight: 800; letter-spacing: 0.06em; text-transform: uppercase;
  color: var(--violet-dark); background: rgba(108, 76, 241, 0.12);
  border-radius: 999px; padding: 2px 7px;
}
.ss-facet-hint {
  margin-left: auto; flex: 0 0 auto;
  font-size: 0.7rem; font-weight: 600; color: var(--violet-dark);
  font-variant-numeric: tabular-nums; white-space: nowrap;
}
/* The line between the category rows and the show rows. */
.ss-results .ss-row--facet:last-of-type { margin-bottom: 6px; }

/* ============ Availability calendar ============ */
/* The count line's numbers pulse when the plan changes. */
@keyframes countbump {
  0% { transform: scale(1); }
  45% { transform: scale(1.06); color: var(--violet); }
  100% { transform: scale(1); }
}

/* The availability-status legend colours mirror edfringe.com's own day-picker
   legend (see plan.js segClass / the --fr-* tokens below). */
.opt-select {
  font: inherit; font-size: 0.84rem; font-weight: 600; color: var(--ink);
  border: 1px solid var(--line); border-radius: 10px; background: #fff;
  padding: 0 10px; height: 34px; cursor: pointer;
}
.opt-select:focus-visible { outline: 3px solid var(--violet); outline-offset: 2px; }
.btn-sm { padding: 8px 14px; font-size: 0.84rem; border-radius: 10px; box-shadow: 0 4px 12px rgba(108, 76, 241, 0.22); }
.legend { display: flex; gap: 10px 16px; flex-wrap: wrap; }
.lg { display: inline-flex; align-items: center; gap: 7px; font-size: 0.76rem; color: var(--muted); }
.lg i { width: 14px; height: 11px; border-radius: 3px; display: block; }
.lg-avail i   { background: var(--fr-avail); }
.lg-2for1 i   { background: var(--fr-2for1-weave); }
.lg-preview i { background: var(--fr-preview); }
.lg-free i    { background: var(--fr-free); }
.lg-event i   { background: var(--fr-event); }
.lg-noalloc i { background: var(--fr-noalloc); }
/* The gold bar the grid draws for the performance the plan picked. */
.lg-locked i { background: var(--fr-locked); box-shadow: inset 0 0 0 1px #e0b23a; }

/* --- The calendar canvas --- */
/* .cal-wrap is the scroll container: the sticky block (window-control rail +
   day header) stays at its top while the lane list scrolls beneath it. Its
   height is the board's, so a grid of two shows and a grid of forty leave the
   count line above and the search bar below in exactly the same place. */
.cal-wrap {
  position: relative;
  overflow-y: auto; overflow-x: hidden;
  border-radius: 10px;
}
/* Fills the scroller even when a couple of lanes wouldn't, so the date-window
   overlay (absolute to this box) spans the whole board rather than stopping
   under the last row. */
.cal-inner { position: relative; min-height: 100%; }

.cal-sticky { position: sticky; top: 0; z-index: 4; background: var(--paper); }

/* minmax(0, …) on the day track lets the 31 columns shrink to the available
   width instead of holding their content's min-width and forcing the whole grid
   (day header + lanes) to overflow — which clipped the "In plan" column and the
   window flags on a narrower page. */
.cal-row { display: grid; grid-template-columns: 184px minmax(0, 1fr) 104px; gap: 0 12px; align-items: center; }

.cal-head { border-bottom: 1px solid var(--line); padding-bottom: 6px; }
/* The corner above the show names, holding Clear. The min-height is the armed
   two-line pill's height, so the header — and the whole grid under it — stays
   put whether the button is idle, armed, or hidden on an empty board. */
.cal-head-left {
  display: flex; align-items: center; min-width: 0; min-height: 40px;
}
/* The mirror of the left corner: the Legend toggle stacked over the Status
   heading. Both corners sit inside the scroller, so neither costs the card any
   height — the board is the same box whether it holds the grid or the dropzone. */
.cal-head-right {
  display: flex; flex-direction: column; align-items: center; gap: 4px; min-height: 40px;
  justify-content: center;
}
.cal-head-right-label { font-size: 0.62rem; font-weight: 600; color: #98a2b3; text-align: center; line-height: 1.15; }
.days { display: grid; grid-template-columns: repeat(31, minmax(0, 1fr)); gap: 3px; }
.day-col { text-align: center; line-height: 1.15; }
.day-dow { display: block; font-size: 0.56rem; font-weight: 600; color: #b0b7c3; }
.day-num { display: block; font-size: 0.7rem; font-weight: 600; color: var(--muted); font-variant-numeric: tabular-nums; }
/* Weekend days (Sat/Sun) carry a faint violet wash and a darker, bolder date —
   enough to pick the weekends out of the row at a glance, quiet enough that it
   never competes with the availability colours in the cells below. */
.day-col.wknd { background: rgba(108, 76, 241, 0.055); border-radius: 4px; }
.day-col.wknd .day-num { color: var(--ink); font-weight: 700; }
.day-col.wknd .day-dow { color: #9aa0ad; }
.day-col.pre .day-num, .day-col.pre .day-dow { color: #cfd4de; }

.lanes { position: relative; z-index: 1; }
/* One tight single-line row per show — this screen is an overview of the shows
   you want, so each row is deliberately compact (title only, no meta line). */
.lane { min-height: 22px; padding: 1px 0; border-bottom: 1px solid #f1f2f6; }
.lane:last-child { border-bottom: none; }
.lane-label { min-width: 0; display: flex; align-items: center; gap: 4px; }
.lane-title {
  font-family: var(--display); font-weight: 600; font-size: 0.8rem; line-height: 1.2;
  white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
}
/* The must-see pin — hidden unless the row is forced into the plan. */
.lane-pin { flex: 0 0 auto; font-size: 0.72rem; display: none; }
.lane--forced .lane-pin { display: inline; }
/* Drop this show from the list. Pushed to the right of the label, faint until
   the row is hovered/focused so it stays out of the way on a dense grid. */
.lane-remove {
  flex: 0 0 auto; margin-left: auto;
  border: 0; background: none; padding: 0 2px; cursor: pointer;
  font-size: 0.95rem; line-height: 1; color: #c2c8d2;
  opacity: 0.35; transition: opacity 0.12s ease, color 0.12s ease;
}
.lane:hover .lane-remove, .lane-remove:focus-visible { opacity: 1; }
.lane-remove:hover { color: var(--warn); }
/* A scheduled row gets a faint green wash so the grid mirrors the plan (the
   Status column carries the ✓ In plan text; no coloured left bar). */
.lane--scheduled { background: rgba(5, 150, 105, 0.05); }
/* The name and the performance marks are the two click targets (lock the show /
   lock that performance). The padlock cursor carries that affordance. */
.lane-label { cursor: var(--lock-cursor); }
.lane-label:hover .lane-title { text-decoration: underline; }
/* Where the click would *un*lock, the cursor says so: a locked show's name
   (clicking lifts the lock) and the one cell it is pinned to (clicking unpins
   it). Every other cell in a locked lane still adds a lock — it re-pins the
   show to that performance — so those keep the closed padlock. */
.lane--forced .lane-label, .cell--pin { cursor: var(--unlock-cursor); }
.lane-track { display: grid; grid-template-columns: repeat(31, minmax(0, 1fr)); gap: 3px; align-items: center; height: 100%; }

/* A day cell holds one coloured segment per performance; two performances on a
   day render as two side-by-side segments — the split IS the "two shows that
   day" cue. Single-performance days read as one solid bar. */
.cell { height: 10px; border-radius: 3px; display: flex; gap: 0; overflow: hidden; cursor: var(--lock-cursor); }
.cell:hover { box-shadow: 0 0 0 2px rgba(16, 24, 40, 0.3); }
.cell-multi { gap: 1.5px; }
.seg { flex: 1 1 0; min-width: 0; border-radius: 3px; }
/* The performance the plan actually *set* for this show — the grid mirrors the
   itinerary, turning the chosen day/time into a gold bar (the higher-specificity
   .seg.seg--selected wins over the availability colour), so a locked-in mark
   reads as a little gold bar rather than a green-ringed availability chip. */
.seg.seg--selected, .seg.seg--pinned { background: var(--fr-locked); position: relative; }
.seg--selected, .seg--pinned { box-shadow: inset 0 0 0 1px #e0b23a, 0 1px 2px rgba(190, 145, 20, 0.4); }

/* Flat gold at 10px reads as "a yellow mark" rather than as metal, so the marks
   catch the light: a specular band travels the length of every gold bar. The
   start is phase-staggered by day column and lane (--sheen-col / --sheen-lane,
   set at render time by renderLanes / buildDayCells), so the highlight crosses
   the grid as one diagonal wave rather than as forty marks flashing
   independently — the picks read as a set.
   The highlight is a ::before because the mark's own background is already
   spoken for (the gold gradient), and it animates background-position rather
   than a transform because a pinned mark's lock badge escapes the mark upward:
   the seg can never be overflow:hidden, so a travelling child couldn't be
   clipped to the bar. */
.seg--selected::before, .seg--pinned::before {
  content: ""; position: absolute; inset: 0; border-radius: inherit;
  pointer-events: none; background-repeat: no-repeat;
  background-image: linear-gradient(102deg,
    rgba(255, 255, 255, 0) 38%,
    rgba(255, 255, 255, 0.9) 50%,
    rgba(255, 255, 255, 0) 62%);
  background-size: 260% 100%;
  animation: segSheen 3.2s linear infinite;
  /* The vars carry positive seconds and are negated here — a negative delay
     starts the mark part-way through its sweep, which is what staggers them. */
  animation-delay: calc(-1 * (var(--sheen-col, 0s) + var(--sheen-lane, 0s)));
}
@keyframes segSheen {
  from { background-position: 150% 0; }
  to   { background-position: -110% 0; }
}
/* Gold arriving is the grid's one piece of good news — a performance just
   entered the plan — and a silent recolour on a 10px bar is easy to miss when
   a replan moves several at once. So the mark announces itself: a single
   swell with a gold glow, applied by applyVerdicts only to marks that were
   *not* gold a moment ago (see seg--justlocked), never on a re-render. */
.seg--justlocked { animation: segLockIn 0.5s ease-out; }
@keyframes segLockIn {
  0%   { transform: scaleY(1); box-shadow: inset 0 0 0 1px #e0b23a, 0 0 0 0 rgba(234, 194, 71, 0.85); }
  45%  { transform: scaleY(2.1); box-shadow: inset 0 0 0 1px #e0b23a, 0 0 0 5px rgba(234, 194, 71, 0); }
  100% { transform: scaleY(1); box-shadow: inset 0 0 0 1px #e0b23a, 0 1px 2px rgba(190, 145, 20, 0.4); }
}
@media (prefers-reduced-motion: reduce) {
  .seg--justlocked { animation: none; }
  /* No sheen either — the mark falls back to flat gold, which still carries the
     meaning on its own. */
  .seg--selected::before, .seg--pinned::before { animation: none; opacity: 0; }
}
/* A whole-show pin: every performance gets a dashed gold border (you've forced
   the show in; the scheduler still picks which one, and that one goes solid gold
   via .seg--selected). Outline — not box-shadow — so it can be dashed; the
   negative offset pulls it inside the mark so overflow:hidden doesn't clip it. */
.seg--forced-all { outline: 1.5px dashed #e0b23a; outline-offset: -1.5px; }
/* A pinned exact performance: the same gold bar, plus a lock badge that sits
   above the mark. The badge deliberately overlaps the row above, so the pinned
   cell drops its overflow clip to let it escape upward. */
.seg--pinned { position: relative; }
.cell--pin { overflow: visible; }
.seg-pin {
  position: absolute; left: 50%; bottom: 100%;
  transform: translate(-50%, 30%);
  font-size: 0.82rem; line-height: 1; pointer-events: none; z-index: 6;
  filter: drop-shadow(0 1px 1px rgba(16, 24, 40, 0.35));
}
.seg-avail   { background: var(--fr-avail); }
.seg-2for1   { background: var(--fr-2for1-weave); }
.seg-preview { background: var(--fr-preview); }
.seg-free    { background: var(--fr-free); }
.seg-event   { background: var(--fr-event); }
.seg-noalloc { background: var(--fr-noalloc); }
/* Sold out shares the red "no allocation" mark — both mean "no online ticket". */
.seg-sold    { background: var(--fr-noalloc); }

/* Fixed-height row so the pushpin (a tall glyph) can't stretch a lane taller
   than its neighbours — there's no vertical room for that. Content is clamped
   and centred; the pin sits just left of the status pill. */
.lane-status {
  font-size: 0.72rem; line-height: 1.1; text-align: center;
  display: flex; align-items: center; justify-content: center; gap: 2px;
  height: 20px; overflow: hidden;
}
.lane-status .st-pin { font-size: 0.62rem; line-height: 1; flex: 0 0 auto; }
.lane-status .st-plan {
  font-weight: 700; color: var(--ok-text); white-space: nowrap;
  background: var(--ok-bg); border-radius: 999px; padding: 2px 7px;
}
.lane-status .st-in { color: var(--violet-dark); font-weight: 600; white-space: nowrap; }
.lane-status .st-no { color: #c2c8d2; font-weight: 700; }
/* "Can't fit" — catchable but crowded out; muted, no alarm. */
.lane-status .st-cant { color: var(--muted); font-weight: 600; white-space: nowrap; }
/* "Sold out" — greyed, matching the sold-out day marks. */
.lane-status .st-sold { color: #98a2b3; font-weight: 700; white-space: nowrap; }
/* "Bad dates" — a dates problem the window scrubber fixes; quiet violet. */
.lane-status .st-dates { color: var(--violet-dark); font-weight: 600; white-space: nowrap; }
/* Every show that isn't in the plan reads the same, whatever the reason (out of
   window, can't fit, too early/late, sold out): one dimmed title. A scheduled or
   forced row is never dimmed — it made the plan. Out-of-window shows also recede
   their day marks, to the same transparency, since none fall in your dates;
   in-window shows keep their marks bright (those marks still mean something). */
.lane:not(.lane--scheduled) .lane-title { opacity: 0.42; }
.lane--out:not(.lane--scheduled) .lane-track { opacity: 0.42; }

/* --- Window controls: the rail (sticky, above the day header) --- */
/* All dragging lives here: the two handle tabs resize the window; the space
   between them slides the whole window. */
.win-rail { position: relative; height: 38px; }
/* The between-handles band both slides the window and carries the window
   length + the optimizer link (the old separate toolbar folded in here). */
.rail-band {
  position: absolute; top: 6px; bottom: 0;
  display: flex; align-items: center; justify-content: center; gap: 8px;
  overflow: hidden; padding: 0 8px;
  background: rgba(108, 76, 241, 0.07);
  border-radius: 8px 8px 0 0;
  cursor: grab; touch-action: none;
}
.rail-band:hover { background: rgba(108, 76, 241, 0.12); }
.rail-band:active { cursor: grabbing; }
/* The window length — the "grip" text; inert so a drag starts through it. */
.rail-len {
  font-size: 0.7rem; font-weight: 700; color: var(--violet-dark);
  white-space: nowrap; font-variant-numeric: tabular-nums; pointer-events: none;
}
/* A mild, low-key "Optimize?" link. Interactive (it opens the popover), so it
   re-enables pointer events the band's label opts out of. */
.rail-opt {
  font: inherit; font-size: 0.68rem; font-weight: 600; color: var(--muted);
  background: none; border: none; padding: 0; cursor: pointer; pointer-events: auto;
  text-decoration: underline; text-underline-offset: 2px; text-decoration-color: #b7bcc9;
  white-space: nowrap;
}
.rail-opt:hover { color: var(--violet-dark); text-decoration-color: currentColor; }
.rail-opt:focus-visible { outline: 3px solid var(--violet); outline-offset: 2px; border-radius: 4px; }

/* The optimizer popover (positioned in JS, fixed, so it escapes the scroller). */
.rail-pop {
  position: fixed; z-index: 60; min-width: 224px;
  display: flex; flex-direction: column; gap: 12px;
  background: var(--paper); border: 1px solid var(--line); border-radius: 12px;
  padding: 14px; box-shadow: var(--shadow-soft);
}
.rail-pop[hidden] { display: none; }
.rail-pop-row {
  display: flex; align-items: center; justify-content: space-between; gap: 12px;
  font-size: 0.84rem; font-weight: 600; color: var(--ink);
}
.rail-pop-row input {
  width: 66px; font: inherit; font-size: 0.86rem; font-weight: 600; color: var(--ink);
  border: 1px solid var(--line); border-radius: 10px; background: #fff; padding: 6px 8px;
}
.rail-pop-check { display: flex; align-items: center; gap: 8px; font-size: 0.84rem; color: var(--ink); cursor: pointer; }
.rail-pop-check input { accent-color: var(--violet); width: 15px; height: 15px; }
.rail-pop .btn-sm { justify-content: center; }
.rail-pop-row input:focus-visible, .rail-pop-check input:focus-visible { outline: 3px solid var(--violet); outline-offset: 2px; }

.win-handle {
  position: absolute; top: 0; bottom: 0; width: 26px;
  transform: translateX(-50%);
  cursor: grab; touch-action: none; z-index: 1;
}
.win-handle:active { cursor: grabbing; }
/* The tab's stem: a short violet line down to the day header, continuing
   visually into the band edge below the header. */
.win-handle::before {
  content: ""; position: absolute; left: 50%; top: 6px; bottom: 0;
  width: 2px; margin-left: -1px; background: var(--violet);
}
/* The draggable knob. A raised pill button with a grip on its outer edge, so it
   reads as grab-and-slide rather than a static date label. Tabs anchor OUTWARD
   from their stem (start points left, end points right) so they never collide
   even when the window is dragged narrow. The "0 2px 0" layer is a solid bottom
   edge that gives the button height; the press state below sinks it. */
/* An outlined chip planted on the stem line — reads as a little flag/marker on
   the date, not a text label. A quiet "From"/"To" cap disambiguates the two
   ends; the inner-bottom corner squares off where the chip meets its stem so it
   looks planted.
   Deliberately border-only rather than a solid violet block: two filled chips
   sitting above the grid pulled the eye harder than the shows underneath them,
   which are the thing you came to read. Outlined, they stay findable without
   competing — and the hover/press states below still fill in, so grabbing one
   confirms itself. */
.win-flag {
  position: absolute; top: 2px;
  display: inline-flex; align-items: baseline; gap: 5px;
  font-size: 0.74rem; font-weight: 800; color: var(--violet-dark);
  background: #fff;
  border: 1.5px solid var(--violet);
  border-radius: 9px;
  padding: 4px 9px; white-space: nowrap;
  box-shadow: 0 2px 5px rgba(16, 24, 40, 0.08);
  font-variant-numeric: tabular-nums;
  cursor: grab;
  transition: transform 0.1s ease, box-shadow 0.1s ease, background 0.12s ease, color 0.12s ease;
}
.win-flag .wf-cap {
  font-size: 0.56rem; font-weight: 700; letter-spacing: 0.06em; text-transform: uppercase; opacity: 0.7;
}
.win-handle--start .win-flag { right: 50%; margin-right: -1px; border-bottom-right-radius: 2px; }
.win-handle--end .win-flag { left: 50%; margin-left: -1px; border-bottom-left-radius: 2px; }
.win-handle:hover .win-flag, .win-handle:focus-visible .win-flag {
  background: var(--violet); color: #fff;
  transform: translateY(-1px);
  box-shadow: 0 6px 14px rgba(90, 63, 214, 0.35);
}
/* Pressed: settle the chip for a tactile grab. */
.win-handle:active .win-flag {
  transform: translateY(0); cursor: grabbing;
  box-shadow: 0 2px 6px rgba(90, 63, 214, 0.4);
}
.win-handle:focus-visible { outline: none; }
.win-handle:focus-visible .win-flag { box-shadow: 0 0 0 3px rgba(108, 76, 241, 0.4), 0 6px 14px rgba(16, 24, 40, 0.2); }

/* --- Window overlay over the lanes: purely visual, never intercepts the
   pointer — lane cells keep their hover tooltips. --- */
.win { position: absolute; top: 0; bottom: 0; z-index: 2; pointer-events: none; }
/* The weekend stripes: below the lanes and the window overlay, so they tint the
   board without touching any of its interactions (see layoutWeekendStripes). */
.wknd-cols { position: absolute; inset: 0; pointer-events: none; z-index: 0; }
.wknd-col { position: absolute; top: 0; bottom: 0; background: rgba(108, 76, 241, 0.04); }

.festend { position: absolute; top: 0; bottom: 0; background: rgba(234, 236, 240, 0.35); border-right: 1px dashed #d5d9e2; pointer-events: none; z-index: 1; }
.win-dim { position: absolute; top: 0; bottom: 0; background: rgba(247, 248, 250, 0.72); }
.win-band {
  position: absolute; top: 0; bottom: 0;
  background: rgba(108, 76, 241, 0.05);
  border-left: 2px solid var(--violet); border-right: 2px solid var(--violet);
}
/* Draggable grips on the band's two vertical lines — a thin ew-resize strip
   centred on each edge. pointer-events re-enabled here (the overlay itself is
   inert), so the rest of the grid keeps its cell hovers. */
.win-edge {
  position: absolute; top: 0; bottom: 0; width: 12px; margin-left: -6px;
  z-index: 3; pointer-events: auto; cursor: ew-resize; touch-action: none;
}
.win-edge::before {
  content: ""; position: absolute; top: 0; bottom: 0; left: 50%; width: 2px;
  margin-left: -1px; background: transparent; transition: background 0.12s ease;
}
.win-edge:hover::before, .win-edge.dragging::before { background: rgba(108, 76, 241, 0.55); }
/* A grip planted on each edge line. The lines themselves are 2px of window
   chrome and read as a boundary, not a control — so the date window got resized
   from the flags up top and these were never touched. The glyph is the
   invitation: a graspable knob, on the line, where the hand already is.

   Same pill as every other draggable boundary (see the shared grip rule below),
   turned a quarter so its bars run along the vertical line it grabs — one grab
   affordance across the whole planner rather than two dialects.

   `--grip-y` is set by positionWindowGrips() to the middle of the *visible*
   grid. The overlay spans the full scroll height, so a plain `top: 50%` centres
   on the middle of the lane list — which on a long list is wherever the list
   happens to be halfway, often nowhere near the screen. */
.win-edge::after {
  content: ""; position: absolute; left: 50%; top: var(--grip-y, 50%);
  width: 26px; height: 12px; border-radius: 7px;
  transform: translate(-50%, -50%) rotate(90deg);
  background-color: var(--violet);
  background-image: linear-gradient(#fff, #fff), linear-gradient(#fff, #fff);
  background-repeat: no-repeat; background-size: 12px 1.5px;
  background-position: center 4px, center 7.5px;
  box-shadow: 0 1px 4px rgba(16, 24, 40, 0.3);
  transition: transform 0.12s ease;
}
.win-edge:hover::after, .win-edge.dragging::after {
  transform: translate(-50%, -50%) rotate(90deg) scale(1.12);
}

/* Performance-time tooltip (follows the cursor over day cells). */
/* The day-mark popup. A 10px coloured bar can't say what it is, and the old
   dark one-liner only gave the times back — so hovering opens a real card:
   what the show is, every performance that day with its ticket status, what
   the plan did with it, and (once the descriptions sidecar has landed) a line
   of blurb. Anchored to the cell rather than trailing the cursor, because a
   card this size chasing the pointer is unreadable. */
.cal-tip {
  position: fixed; z-index: 60; pointer-events: none;
  width: max-content; max-width: 320px;
  background: var(--paper); color: var(--ink);
  border: 1px solid var(--line); border-radius: 12px;
  padding: 11px 13px;
  box-shadow: 0 10px 30px rgba(16, 24, 40, 0.18);
  font-size: 0.78rem; line-height: 1.45;
}
.cal-tip[hidden] { display: none; }
.tip-title {
  margin: 0; font-family: var(--display); font-weight: 700; font-size: 0.92rem;
  line-height: 1.25; color: var(--ink);
}
.tip-meta { margin: 3px 0 0; font-size: 0.72rem; color: var(--muted); }
.tip-date {
  margin: 9px 0 4px; font-size: 0.7rem; font-weight: 700;
  letter-spacing: 0.04em; text-transform: uppercase; color: var(--muted);
}
.tip-perfs { list-style: none; margin: 0; padding: 0; display: grid; gap: 3px; }
.tip-perf {
  display: flex; align-items: center; gap: 7px;
  font-size: 0.78rem; color: var(--ink); font-variant-numeric: tabular-nums;
}
.tip-perf i { width: 12px; height: 10px; border-radius: 3px; flex: 0 0 auto; }
.tip-perf b { font-weight: 700; }
.tip-status { color: var(--muted); font-size: 0.72rem; }
/* The performance the plan picked, called out in the same gold as its mark. */
.tip-perf--planned .tip-status { color: #8a6d10; font-weight: 700; }
.tip-desc {
  margin: 9px 0 0; padding-top: 8px; border-top: 1px solid var(--line);
  font-size: 0.75rem; line-height: 1.5; color: var(--muted);
}
.tip-hint {
  margin: 8px 0 0; font-size: 0.7rem; font-weight: 600; color: var(--violet-dark);
}

/* The grid owns the card's whole width now that the key floats over it. */
.board-main { display: flex; align-items: stretch; gap: 0; }
.board-main > .cal-wrap { flex: 1 1 auto; min-width: 0; }

/* The key, as a popup over the grid's top-right corner. `fixed` (placed in JS
   from the Legend button, like .rail-pop) rather than absolute: the button
   lives inside the calendar's scroll box, which would clip a panel positioned
   against it. Nothing under the popup moves when it opens or closes. */
.cal-legend {
  position: fixed; z-index: 60; width: 176px;
  background: var(--paper); border: 1px solid var(--line); border-radius: 12px;
  padding: 13px 14px; box-shadow: var(--shadow-soft);
}
.cal-legend[hidden] { display: none; }
/* Stacked rather than the wrapping row it was under the grid — one mark per
   line reads as a key to scan down. */
.cal-legend .legend { flex-direction: column; gap: 9px; flex-wrap: nowrap; }

/* The toggle in the grid's right corner, over the Status heading — the same
   pill as Clear opposite it, a size down to sit in that narrow column. Its
   `is-on` state is the panel's only other affordance, so it has to read as
   pressed. */
.board-legend-btn {
  flex: 0 0 auto; max-width: 100%; font: inherit; font-size: 0.7rem; font-weight: 600;
  color: var(--muted); background: #fff;
  border: 1px solid var(--line); border-radius: 999px;
  padding: 3px 11px; cursor: pointer; line-height: 1.3;
}
.board-legend-btn:hover { color: var(--ink); border-color: #c9cedb; }
.board-legend-btn:focus-visible { outline: 3px solid var(--violet); outline-offset: 2px; }
.board-legend-btn.is-on {
  color: var(--violet); border-color: var(--violet);
  background: rgba(108, 76, 241, 0.07);
}
.board-legend-btn[hidden] { display: none; }


/* ============ Allocate / itinerary (the plan panel) ============ */
.plan-card { padding: 28px 28px 26px; margin-top: 22px; }
.plan-head {
  display: flex; align-items: flex-start; justify-content: space-between;
  gap: 20px 28px; flex-wrap: wrap;
}
.plan-head-text { min-width: 0; max-width: 640px; }
.plan-title {
  font-family: var(--display); font-weight: 700; font-size: 1.4rem;
  letter-spacing: -0.01em; margin: 0 0 6px;
}
.plan-sub { color: var(--muted); font-size: 0.95rem; margin: 0; }
.plan-sub #planWindowLabel { color: var(--ink); font-weight: 600; }
/* The primary call-to-action — a touch larger than the standard button. */
.btn-plan { font-size: 1rem; padding: 13px 24px; flex: 0 0 auto; }
.btn-ghost {
  color: var(--violet-dark); background: #fff;
  border: 1.5px solid var(--violet); box-shadow: none;
}
.btn-ghost:hover { background: rgba(108, 76, 241, 0.06); color: var(--violet-dark); }
.btn-ext {
  font-size: 0.66rem; font-weight: 700; letter-spacing: 0.05em;
  padding: 2px 6px; border-radius: 6px; margin-left: 2px;
  background: rgba(108, 76, 241, 0.12); color: var(--violet-dark);
}

.plan-result { margin-top: 24px; border-top: 1px solid var(--line); padding-top: 22px; }

/* Live controls — sit directly above the schedule they shape; every change
   re-plans instantly. Day hours + meal breaks are also draggable on the graphic. */
/* One slim strip in place of the old three-fieldset panel: segments divided by
   thin rules, each phrasing its inputs as a value in a sentence fragment. The
   strip wraps whole segments when the row runs out (the dividers on a wrapped
   row read as list separators, which is what they are). */
.plan-controls {
  display: flex; flex-wrap: wrap; align-items: center; gap: 8px 0;
  margin-bottom: 20px; padding: 9px 4px;
  background: #fafaff; border: 1px solid var(--line); border-radius: 12px;
}
.pc-seg {
  display: flex; flex-wrap: wrap; align-items: center; gap: 6px 7px;
  padding: 0 9px; min-height: 30px; min-width: 0;
  border-left: 1px solid var(--line);
}
.pc-seg:first-child { border-left: none; }
/* The connective tissue: "Day", "shows/day", "between shows", and the range
   dashes. Muted so the values are what the eye lands on. */
.pc-cap {
  text-transform: uppercase; letter-spacing: 0.05em;
  font-size: 0.66rem; font-weight: 700; color: var(--muted); white-space: nowrap;
}
.pc-dash { color: var(--muted); font-weight: 600; font-size: 0.84rem; }
/* .ctl is the flash target the conflict pills scroll to (see wireConflictJumps):
   one value plus its "⚠ Prevents N" chip, inline in the strip. */
.ctl { display: inline-flex; align-items: center; gap: 6px; }
/* Time + number inputs, matched to the .opt-select chrome at the strip's own
   30px height (the shared .opt-select rule says 34px, for its other homes). */
.ctl-time, .ctl-num {
  font: inherit; font-size: 0.84rem; font-weight: 600; color: var(--ink);
  border: 1px solid var(--line); border-radius: 8px; background: #fff;
  padding: 0 7px; height: 30px;
}
.plan-controls .opt-select { height: 30px; border-radius: 8px; padding: 0 8px; font-size: 0.84rem; }
.ctl-num { width: 52px; }
/* Plain HH:MM clock text boxes (day start / day end). */
.ctl-clock { width: 66px; text-align: center; font-variant-numeric: tabular-nums; letter-spacing: 0.02em; }
.ctl-clock:invalid { border-color: var(--warn); }
.ctl-time:focus-visible, .ctl-num:focus-visible { outline: 3px solid var(--violet); outline-offset: 2px; }
/* Travel-mode segmented control. (Class name avoids the calendar's .seg.) */
.tmode { display: inline-flex; border: 1px solid var(--line); border-radius: 8px; overflow: hidden; background: #fff; height: 30px; }
.tmode-btn {
  font: inherit; font-size: 0.8rem; font-weight: 600; color: var(--muted);
  background: #fff; border: none; padding: 0 10px; cursor: pointer;
  border-right: 1px solid var(--line); white-space: nowrap;
}
.tmode-btn:last-child { border-right: none; }
.tmode-btn:hover { background: rgba(108, 76, 241, 0.05); }
.tmode-btn.is-on { background: var(--violet); color: #fff; }
.tmode-word { margin-left: 5px; }
/* Phone: the strip becomes a stacked list; shave the segment padding and let
   the travel buttons stand on their emoji (each keeps an aria-label + title). */
@media (max-width: 520px) {
  .pc-seg { padding: 0 8px; }
  .tmode-word { display: none; }
  .tmode-btn { padding: 0 12px; }
}
/* Meal-break toggles: the checkbox hides behind a pill that wears its state
   (＋ off / ✓ on), and the time boxes only exist while the meal is on — an
   off meal is one word, not a row of disabled-looking clocks. */
.meal { position: relative; display: inline-flex; flex-wrap: wrap; align-items: center; gap: 6px; cursor: pointer; min-width: 0; }
.meal input[type="checkbox"] { position: absolute; opacity: 0; pointer-events: none; }
.meal-pill {
  display: inline-flex; align-items: center; gap: 5px;
  font-size: 0.78rem; font-weight: 700; color: var(--muted); line-height: 1;
  border: 1px dashed #c9cede; border-radius: 999px; padding: 7px 11px; background: #fff;
  white-space: nowrap;
}
.meal-pill::before { content: "＋"; font-size: 0.72rem; }
.meal input:checked ~ .meal-pill {
  color: var(--violet-dark); background: rgba(108, 76, 241, 0.08);
  border: 1px solid rgba(108, 76, 241, 0.35);
}
.meal input:checked ~ .meal-pill::before { content: "✓"; }
.meal input:focus-visible ~ .meal-pill { outline: 3px solid var(--violet); outline-offset: 2px; }
.meal:hover .meal-pill { border-color: var(--violet); }
.meal input:not(:checked) ~ .meal-times { display: none; }
.meal-times { display: inline-flex; align-items: center; gap: 4px; }
/* Slightly shorter than the strip's other boxes so the pill + times pair
   doesn't tower over its neighbours. Fixed width with the browser's clock
   icon hidden — the icon doubled each box's width for a picker the step
   attribute makes near-useless, and four of them cost the strip a line. */
.meal .meal-time { padding: 0 5px; height: 28px; font-size: 0.78rem; width: 84px; text-align: center; }
.meal .meal-time::-webkit-calendar-picker-indicator { display: none; }
.meal-dash { color: var(--muted); }

.plan-summary { font-size: 0.92rem; color: var(--ink); margin-bottom: 14px; }
.plan-summary b { color: var(--violet-dark); font-weight: 800; }

/* --- The schedule graphic --- */
.schedule-wrap {
  overflow-x: auto; border: 1px solid var(--line); border-radius: 12px;
  background:
    linear-gradient(#fff, #fff) padding-box,
    repeating-linear-gradient(90deg, #fbfbfd 0 1px, transparent 1px 100%);
  padding: 0;
}
/* position:relative anchors the draggable day-hours / meal overlay. */
.schedule { display: flex; align-items: stretch; min-width: min-content; width: 100%; position: relative; }
/* Left time gutter: a head spacer (matching the day headers) then a body whose
   hour labels line up with the day columns' gridlines. */
.sch-gutter { flex: 0 0 52px; display: flex; flex-direction: column; border-right: 1px solid var(--line); }
.sch-gutter-head { height: var(--sch-head-h, 46px); border-bottom: 1px solid var(--line); }
.sch-gutter-body { position: relative; flex: 1 0 auto; }
.sch-hour {
  position: absolute; right: 8px; font-size: 0.68rem; font-weight: 600;
  color: #b0b7c3; font-variant-numeric: tabular-nums; transform: translateY(-50%);
}
/* Day columns flex to share the available width so every scheduled day fits on
   screen; they only fall back to horizontal scroll below the min-width floor.
   The floor is deliberately tight (44px) so even a long multi-week window fits
   the page width before scrolling — a packed plan reads better as narrow columns
   than as an off-screen header. */
.sch-day {
  flex: 1 1 0; min-width: 44px; border-right: 1px solid var(--line);
  display: flex; flex-direction: column;
}
.sch-day:last-child { border-right: none; }
/* A day in the window with nothing planned. It is drawn — otherwise three
   columns read as three consecutive days when a blank Wednesday sits between
   them — but collapsed to a sliver, greyed, and stripped of the weekday and
   count its width can't hold. The min-width override is what lets it go
   narrower than a real column. (The px width is kept in step with
   SCH_EMPTY_COL_PX in plan.js, which does the arithmetic for the others.) */
.sch-day--empty {
  flex: 0 0 30px; min-width: 30px;
  background: repeating-linear-gradient(45deg, #fbfbfd 0 5px, #f4f5f8 5px 10px);
}
.sch-day--empty .sch-day-head { background: #f7f7fa; padding: 6px 2px; }
.sch-day--empty .sch-body { background-image: none; opacity: 0.5; }
.sch-dow--empty {
  font-size: 0.66rem; font-weight: 700; color: #a9afbd; text-align: center;
}
.sch-day-head {
  height: var(--sch-head-h, 46px); box-sizing: border-box;
  display: flex; flex-direction: column; justify-content: center;
  padding: 6px 8px; border-bottom: 1px solid var(--line);
  position: sticky; top: 0; background: var(--paper); z-index: 1;
  overflow: hidden;
}
/* Same weekend cue as the availability calendar's header, so a Sat/Sun reads
   the same on both grids. The head is sticky over the day's canvas, so the
   tint is composited over paper rather than set as a translucent wash. */
.sch-day.wknd .sch-day-head { background: #f4f2fe; }
.sch-dow { font-size: 0.7rem; font-weight: 700; color: var(--ink); line-height: 1.15; }
/* At a narrow column the date drops under the weekday rather than overflowing. */
.sch-dow .sch-date { display: inline-block; color: var(--muted); font-weight: 600; }
.sch-day-count { font-size: 0.66rem; color: var(--muted); }
/* The day's time canvas: hour gridlines via a repeating gradient, shows
   positioned absolutely against the shared axis. */
.sch-body {
  position: relative; flex: 1 0 auto;
  background-image: repeating-linear-gradient(
    to bottom, transparent 0 calc(var(--sch-hour-h) - 1px), var(--line) calc(var(--sch-hour-h) - 1px) var(--sch-hour-h)
  );
  background-position: 0 0;
}
/* `overflow: visible` so the genre badge can sit astride the left rule (see
   .sch-emoji). The text inside does its own clipping, so nothing else escapes. */
.sch-show {
  position: absolute; left: 3px; right: 3px; overflow: visible;
  border-radius: 7px; padding: 1px 4px 1px 9px;
  background: #fff; border: 1px solid var(--line);
  border-left: 4px solid var(--seg, var(--violet));
  box-shadow: 0 2px 6px rgba(16, 24, 40, 0.07);
  font-size: 0.72rem; line-height: 1.15;
  display: flex; align-items: flex-start; cursor: var(--lock-cursor);
  text-decoration: none; color: var(--ink); /* it's an <a> — strip link chrome */
}
.sch-show:hover { box-shadow: 0 4px 12px rgba(16, 24, 40, 0.16); border-color: #d5d9e2; z-index: 3; }
.sch-show:focus-visible { outline: 3px solid var(--violet); outline-offset: 1px; z-index: 3; }
/* A locked-in (forced) show wears a green ring + a lock badge, so the schedule
   shows *why* the plan looks the way it does — a pinned show is the only kind
   that can land inside a meal break or outside your day hours. The badge is a
   filled chip in the top-right corner so it reads on a small block too. */
.sch-show--pinned { border-color: var(--ok); box-shadow: inset 0 0 0 1.5px var(--ok), 0 2px 6px rgba(16, 24, 40, 0.1); }
.sch-show--pinned:hover { box-shadow: inset 0 0 0 1.5px var(--ok), 0 4px 12px rgba(16, 24, 40, 0.16); }
.sch-pin {
  position: absolute; top: 1px; right: 1px; z-index: 3; pointer-events: none;
  width: 16px; height: 16px; display: grid; place-items: center;
  font-size: 0.64rem; line-height: 1;
  background: #fff; border: 1.5px solid var(--ok); border-radius: 50%;
  box-shadow: 0 1px 3px rgba(16, 24, 40, 0.3);
}
/* The genre badge rides the left rule rather than sitting inside the block —
   a disc centred on the 4px accent line, breaking the border. That reclaims the
   width it used to take out of the title, and turns the accent colour into
   something that reads as a label rather than a stripe. Centred on the line:
   the line is 4px outside the padding box, so -2px is its middle, less half the
   disc. Vertically it sits at the block's midpoint — centred on the accent line
   in both axes, so it reads as a marker for the whole block rather than a
   bullet hanging off the first line. */
.sch-emoji {
  position: absolute; left: -11px; top: 50%; transform: translateY(-50%); z-index: 2;
  width: 18px; height: 18px; border-radius: 50%;
  display: grid; place-items: center;
  font-size: 0.76rem; line-height: 1;
  background: #fff; border: 1.5px solid var(--seg, var(--violet));
  box-shadow: 0 1px 3px rgba(16, 24, 40, 0.18);
}
.sch-body-text { min-width: 0; flex: 1; }
/* The title carries the block. Regular weight (not bold) reads calmer and fits
   more of the name before the 2-line clamp bites. */
.sch-name {
  font-family: var(--display); font-weight: 500; color: var(--ink); font-size: 0.72rem;
  display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical; overflow: hidden;
}
/* Time and venue share one line under the title — a line each spent most of a
   small block on chrome. Both are faint: the name is what's being looked for. */
.sch-meta {
  display: flex; align-items: baseline; gap: 4px; min-width: 0;
  color: var(--muted); font-size: 0.58rem; line-height: 1.25;
}
.sch-time { flex: 0 0 auto; font-weight: 600; opacity: 0.75; font-variant-numeric: tabular-nums; }
.sch-venue { min-width: 0; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
/* Short blocks drop the clock and give the title one line — see SCH_TIGHT_PX,
   which sets this in buildScheduleBlock. */
.sch-show--tight .sch-time { display: none; }
.sch-show--tight .sch-name { -webkit-line-clamp: 1; }
/* Tight columns shed chrome so the title keeps the room: narrow drops the time,
   tiny pulls the genre badge back inside and trims padding to the bone. */
.schedule.cols-narrow .sch-show { padding-right: 3px; }
.schedule.cols-narrow .sch-time { display: none; }
.schedule.cols-tiny .sch-show { left: 2px; right: 2px; padding-left: 5px; border-left-width: 3px; }
.schedule.cols-tiny .sch-emoji { display: none; }
.schedule.cols-tiny .sch-venue { display: none; }
/* Left-accent colours reuse the edfringe.com availability palette. The 2-for-1
   accent is a single 4px rule, too thin for the diagonal weave, so it takes the
   teal "available" half of that pairing. */
.seg-avail   { --seg: var(--fr-avail); }
.seg-2for1   { --seg: var(--fr-avail); }
.seg-preview { --seg: var(--fr-preview); }
.seg-free    { --seg: var(--fr-free); }
.seg-event   { --seg: var(--fr-event); }
.seg-noalloc { --seg: var(--fr-noalloc); }

.schedule-empty { margin: 0; padding: 28px 20px; text-align: center; color: var(--muted); font-size: 0.9rem; }

/* --- Travel legs: a narrow block in the gap between shows < 1h apart --- */
/* A pill only as wide as its text, centred in the gap — it used to stretch the
   full column and box a handful of words in a dashed frame, spending a band of
   the schedule on very little. Now it costs its own width and carries the gap
   arithmetic (journey, distance, what's left over) instead of just the journey. */
.sch-leg {
  position: absolute; left: 50%; transform: translateX(-50%);
  max-width: calc(100% - 8px); overflow: hidden;
  display: flex; align-items: center; justify-content: center; gap: 3px;
  border-radius: 999px; padding: 0 6px;
  background: #f4f2fb;
  border: 1px solid #e4dff7; color: var(--violet-dark);
  font-size: 0.56rem; font-weight: 600; line-height: 1; min-height: 11px;
  font-variant-numeric: tabular-nums;
  pointer-events: auto;
}
.sch-leg .leg-emoji { font-size: 0.66rem; }
.sch-leg .leg-text { white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }

/* --- Draggable day-hours / meal-break overlay --- */
/* left/top/height are set inline (gutter width, head height, axis height);
   right:0 stretches it across every day column so the lines/bands span them. */
.sch-overlay { position: absolute; right: 0; pointer-events: none; z-index: 4; }
/* Shaded "before day starts" / "after day ends" zones. */
.sch-zone {
  position: absolute; left: 0; right: 0;
  background: repeating-linear-gradient(-45deg, rgba(16,24,40,0.05) 0 6px, rgba(16,24,40,0.02) 6px 12px);
  border-block: 1px solid rgba(16, 24, 40, 0.08);
}
/* The two full-width draggable day boundary lines. */
.sch-dayline {
  position: absolute; left: 0; right: 0; height: 0; z-index: 6;
  border-top: 2px solid var(--violet); pointer-events: none;
}
.sch-dayline .dl-grip {
  position: absolute; left: 0; top: -5px; width: 100%; height: 11px;
  cursor: ns-resize; pointer-events: auto; touch-action: none;
}
/* Every constraint the user owns on the board — the day-start / day-end lines,
   the meal bands, the arrival / departure trip blocks — names itself with the
   same label pill: violet text in a violet-bordered pill over a transparent
   fill, so "this is a boundary you set" reads the same everywhere. When the
   constraint is shutting shows out, the label gains the same red conflict
   triangle the lane status pills use (.excl-mark — hover it for the list). */
.sch-dayline .dl-flag,
.sch-meal .meal-label,
.sch-trip .sch-trip-label {
  position: absolute; left: 50%; transform: translateX(-50%);
  display: inline-flex; align-items: center; gap: 4px;
  max-width: calc(100% - 8px);
  font-size: 0.6rem; font-weight: 700; line-height: 1.4;
  color: var(--violet-dark);
  background: transparent; border: 1px solid var(--violet); border-radius: 999px;
  padding: 0 7px;
  white-space: nowrap; font-variant-numeric: tabular-nums;
  pointer-events: none; z-index: 1;
}
/* The live-updating text inside the pill; a narrow day column trims it with an
   ellipsis rather than pushing the conflict triangle out of the pill. */
.sch-dayline .ov-text, .sch-meal .ov-text, .sch-trip .ov-text {
  min-width: 0; overflow: hidden; text-overflow: ellipsis;
}
.sch-dayline .dl-flag { bottom: 8px; }
.sch-dayline--end .dl-flag { bottom: auto; top: 8px; }
/* While its boundary is grabbed (or hovered), the pill fills in — the same
   emphasis for all three label kinds. */
.sch-dayline.dragging .dl-flag, .sch-dayline .dl-grip:hover ~ .dl-flag,
.sch-meal.dragging .meal-label,
.sch-trip.dragging .sch-trip-label { background: var(--violet); color: #fff; }
/* Meal-break band: draggable to move, edge handles to resize. Its background
   now matches the "before day starts / after day ends" zones (a neutral
   diagonal hatch) so every "you can't be here" region on the board reads the
   same; only the 🍽 label carries the meal identity. */
.sch-meal {
  position: absolute; left: 0; right: 0; z-index: 5;
  background: repeating-linear-gradient(-45deg, rgba(16,24,40,0.05) 0 6px, rgba(16,24,40,0.02) 6px 12px);
  cursor: grab; pointer-events: auto; touch-action: none; overflow: visible;
}
.sch-meal.dragging { cursor: grabbing; }
/* Pinned to the top of the band (like the day-end flag) rather than floating in
   the middle, so it stays put as the band is dragged / resized. */
.sch-meal .meal-label { top: 4px; }
/* Playful faint emoji scatter behind a blocker region (meal band, day-start /
   day-end zone, or trip block) — deterministic emoji on a fixed pixel grid
   (populateDecors). Always visible — only the swim animation below is gated on
   the pointer being on the board (the .is-grid-hover class). */
.sch-decor {
  position: absolute; inset: 0; overflow: hidden; pointer-events: none; z-index: 0;
  opacity: 0.22;
}
.sch-decor span { position: absolute; display: block; line-height: 1; }
/* While the board is live (hovering, or mid-drag) the scatter gently swims:
   each emoji bobs and sways in place. `translate`/`rotate` are the independent
   transform properties, so they compose with the per-span `transform`
   (translate(-50%,-50%) rotate(...)) set inline — the centring + base tilt are
   preserved and only a small drift is layered on. Staggered by :nth-child so
   the shoal never moves in lockstep. Gated to .is-grid-hover so it costs
   nothing while idle. */
.schedule.is-grid-hover .sch-decor span { animation: decor-swim 4.4s ease-in-out infinite; }
.schedule.is-grid-hover .sch-decor span:nth-child(2n) { animation-duration: 5.6s; animation-delay: -1.2s; }
.schedule.is-grid-hover .sch-decor span:nth-child(3n) { animation-duration: 6.4s; animation-delay: -2.6s; }
.schedule.is-grid-hover .sch-decor span:nth-child(5n) { animation-duration: 5s; animation-delay: -0.6s; }
@keyframes decor-swim {
  0%, 100% { translate: 0 0; rotate: -3deg; }
  50%      { translate: 0 -3px; rotate: 3deg; }
}

/* "Getting there" / "getting out" blocks — one per trip, inside their own day
   column. Same hatch as the zones/meals; drag the inner edge to resize. */
.sch-trip {
  position: absolute; left: 0; right: 0; z-index: 5;
  background: repeating-linear-gradient(-45deg, rgba(16,24,40,0.05) 0 6px, rgba(16,24,40,0.02) 6px 12px);
  pointer-events: auto; touch-action: none; overflow: visible;
}
/* The label hugs the block's draggable inner edge: arrival just above its
   bottom glyph, departure just below its top glyph (both inside the block). */
.sch-trip--arrival .sch-trip-label { bottom: 4px; }
.sch-trip--departure .sch-trip-label { top: 8px; }
.sch-trip.dragging {
  background: repeating-linear-gradient(-45deg, rgba(108,76,241,0.12) 0 6px, rgba(108,76,241,0.04) 6px 12px);
}

/* --- Partner nag: one small suggestion pill inside a board region (the night
   below "day ends", the getting-there / getting-out blocks), with an × that
   closes it for good. A quiet paper chip on the region's hatch — it must read as
   an offer, never compete with the shows. --- */
.sch-nag {
  position: absolute; z-index: 2;
  display: flex; align-items: stretch; gap: 0;
  /* The overlay it sits in is pointer-events: none so drags pass through. */
  pointer-events: auto;
  background: var(--paper); border: 1px solid var(--line); border-radius: 999px;
  box-shadow: 0 1px 4px rgba(16, 24, 40, 0.12);
  overflow: hidden; max-width: calc(100% - 8px);
}
.sch-nag[hidden] { display: none; }
.sch-nag .nag-cta {
  display: flex; align-items: center; gap: 4px; min-width: 0;
  padding: 3px 4px 3px 7px; text-decoration: none;
  font-size: 0.6rem; font-weight: 700; color: var(--violet-dark); white-space: nowrap;
}
.sch-nag .nag-cta:hover { background: rgba(108, 76, 241, 0.08); color: var(--violet); }
.sch-nag .nag-cta:focus-visible { outline: 3px solid var(--violet); outline-offset: -2px; }
.sch-nag .nag-emoji { font-size: 0.74rem; line-height: 1; }
.sch-nag .nag-text { overflow: hidden; text-overflow: ellipsis; }
.sch-nag .nag-x {
  flex: none; border: 0; border-left: 1px solid var(--line); background: transparent;
  padding: 0 5px; cursor: pointer; line-height: 1;
  font-size: 0.72rem; color: var(--muted);
}
.sch-nag .nag-x:hover { background: rgba(16, 24, 40, 0.06); color: var(--ink); }
.sch-nag .nag-x:focus-visible { outline: 3px solid var(--violet); outline-offset: -2px; }
/* Night zone: centred, hugging the bottom of the board — the small hours, where
   "where am I sleeping?" actually bites. The zone spans every column, so there
   is room for the full question. */
.sch-zone .sch-nag { bottom: 5px; left: 50%; transform: translateX(-50%); }
/* Trip blocks: stretched across their one narrow day column, against the
   block's outer edge so the draggable inner edge stays clear. */
.sch-trip .sch-nag { left: 4px; right: 4px; justify-content: center; }
.sch-trip--arrival .sch-nag { top: 5px; }
.sch-trip--departure .sch-nag { bottom: 5px; }
/* A many-day plan squeezes each column: on a narrow one the trip nag keeps only
   its emoji (the full question stays in the link's tooltip). The night zone is
   board-wide, so its text is never dropped. */
.schedule.cols-narrow .sch-trip .sch-nag .nag-text { display: none; }
.schedule.cols-narrow .sch-trip .sch-nag .nag-cta { padding-right: 5px; }

/* --- Shared drag handle: every draggable boundary (day-start/end lines, meal
   edges, trip inner edge) shows the same violet line + centred grip pill, so
   they all read as one grab affordance. The meal/trip resize strips straddle
   their edge; the day line already draws its own line via border-top. --- */
.sch-meal .meal-resize,
.sch-trip .meal-resize {
  position: absolute; left: 0; right: 0; height: 14px; cursor: ns-resize; touch-action: none; z-index: 2;
}
.sch-meal .meal-resize--top,
.sch-trip .meal-resize--top { top: -7px; }
.sch-meal .meal-resize--bottom,
.sch-trip .meal-resize--bottom { bottom: -7px; }
.sch-meal .meal-resize::before,
.sch-trip .meal-resize::before {
  content: ""; position: absolute; left: 0; right: 0; top: 50%;
  height: 2px; transform: translateY(-50%); background: var(--violet); pointer-events: none;
}
.dl-grip::after,
.sch-meal .meal-resize::after,
.sch-trip .meal-resize::after {
  content: ""; position: absolute; left: 50%; top: 50%; transform: translate(-50%, -50%);
  width: 26px; height: 12px; border-radius: 7px;
  background-color: var(--violet);
  background-image: linear-gradient(#fff, #fff), linear-gradient(#fff, #fff);
  background-repeat: no-repeat; background-size: 12px 1.5px;
  background-position: center 4px, center 7.5px;
  box-shadow: 0 1px 4px rgba(16, 24, 40, 0.3);
  transition: transform 0.12s ease; pointer-events: none;
}
.dl-grip:hover::after,
.sch-meal .meal-resize:hover::after,
.sch-trip .meal-resize:hover::after,
.sch-meal.dragging .meal-resize::after,
.sch-trip.dragging .meal-resize::after { transform: translate(-50%, -50%) scale(1.12); }

/* --- Hover card over a scheduled block --- */
.sch-card {
  position: fixed; z-index: 70; width: 268px; pointer-events: none;
  background: var(--paper); border: 1px solid var(--line); border-radius: 12px;
  box-shadow: 0 14px 34px rgba(16, 24, 40, 0.22); overflow: hidden;
}
/* Show the whole poster at its natural aspect ratio — no crop, no letterbox. */
.sch-card .card-img { background: #eef0f5; line-height: 0; }
.sch-card .card-img img { width: 100%; height: auto; display: block; }
.sch-card .card-body { padding: 10px 12px 12px; }
.sch-card .card-genre { font-size: 0.68rem; font-weight: 700; color: var(--violet-dark); }
.sch-card .card-title { font-family: var(--display); font-weight: 700; font-size: 0.98rem; line-height: 1.2; margin: 2px 0 4px; }
.sch-card .card-meta { font-size: 0.74rem; color: var(--muted); font-variant-numeric: tabular-nums; }
.sch-card .card-blurb { margin-top: 6px; font-size: 0.76rem; color: var(--ink); line-height: 1.4; }
.sch-card .card-hint { margin-top: 8px; font-size: 0.66rem; color: #98a2b3; }

/* Export actions */
.plan-actions {
  display: flex; align-items: center; gap: 12px; flex-wrap: wrap;
  margin-top: 22px; padding-top: 18px; border-top: 1px solid var(--line);
}
.plan-actions-note { font-size: 0.76rem; color: #98a2b3; margin-left: auto; }

/* The "now get it into Google Calendar" panel, revealed by the ICS download. */
.ics-howto {
  position: relative; margin-top: 14px; padding: 14px 40px 14px 16px;
  background: #f6f4ff; border: 1px solid #ddd7fb; border-radius: 12px;
  animation: icsHowtoIn 0.25s ease-out;
}
.ics-howto[hidden] { display: none; }
@keyframes icsHowtoIn { from { opacity: 0; transform: translateY(-4px); } to { opacity: 1; transform: none; } }
@media (prefers-reduced-motion: reduce) { .ics-howto { animation: none; } }
.ics-howto-title {
  margin: 0 0 8px; font-family: var(--display); font-weight: 700;
  font-size: 0.92rem; color: var(--ink);
}
.ics-howto-steps {
  margin: 0; padding-left: 20px;
  display: grid; gap: 5px; font-size: 0.82rem; line-height: 1.45; color: var(--ink);
}
.ics-howto-steps b { font-weight: 700; }
.ics-howto-steps a { color: var(--violet-dark); font-weight: 600; }
.ics-howto-note { margin: 10px 0 0; font-size: 0.76rem; line-height: 1.5; color: var(--muted); }
.ics-howto-x {
  position: absolute; top: 9px; right: 9px;
  width: 26px; height: 26px; display: grid; place-items: center;
  font: inherit; font-size: 1.05rem; line-height: 1; cursor: pointer;
  color: var(--muted); background: #fff; border: 1px solid #ddd7fb; border-radius: 8px;
}
.ics-howto-x:hover { color: var(--ink); }
.ics-howto-x:focus-visible { outline: 3px solid var(--violet); outline-offset: 2px; }

/* ============ Footer ============ */
.site-footer { background: var(--ink); color: #d9d9d9; padding: 28px 0; margin-top: 56px; }
.footer-inner { display: flex; align-items: center; justify-content: space-between; gap: 20px; }
.footer-copy { color: #8c8c8c; font-size: 0.85rem; }
/* The site-version popup on the copyright line (shared/version-popup.js builds
   the element; this is its skin). No help cursor — the popup is the
   affordance, and it must be reachable by keyboard and touch, not by hover
   alone. */
.footer-version { position: relative; }
.footer-version:focus-visible { outline: 3px solid var(--violet); outline-offset: 2px; }
.version-pop {
  position: absolute;
  bottom: calc(100% + 8px);
  left: 0;
  z-index: 20;
  white-space: nowrap;
  padding: 6px 10px;
  border-radius: 8px;
  background: #1f2430;
  color: #fff;
  font-size: 0.78rem;
  font-weight: 600;
  box-shadow: 0 6px 18px rgba(0, 0, 0, 0.28);
}
.version-pop::after {
  content: "";
  position: absolute;
  top: 100%;
  left: 14px;
  border: 6px solid transparent;
  border-top-color: #1f2430;
}

/* ============ Live re-plan animation ============ */
/* A newly placed show eases in; the same show rescheduled flies to its new slot
   (transform set inline via FLIP); a dropped show leaves as a fading ghost. */
.sch-show--enter { animation: schEnter 0.34s cubic-bezier(0.2, 0.7, 0.3, 1) both; }
@keyframes schEnter {
  from { opacity: 0; transform: translateY(9px) scale(0.96); }
  to { opacity: 1; transform: none; }
}
.sch-show--moving { z-index: 5; transition: transform 0.42s cubic-bezier(0.22, 0.61, 0.36, 1); will-change: transform; }
.sch-ghost {
  position: absolute; z-index: 4; margin: 0; pointer-events: none;
  animation: schLeave 0.3s ease forwards;
}
@keyframes schLeave {
  from { opacity: 0.85; }
  to { opacity: 0; transform: scale(0.94); }
}
.sch-day--enter { animation: schColEnter 0.4s cubic-bezier(0.2, 0.7, 0.3, 1) both; }
@keyframes schColEnter {
  from { opacity: 0; transform: translateY(7px); }
  to { opacity: 1; transform: none; }
}
/* The date-window lines glide on a discrete jump ("Pick my best dates" / arrow
   keys); the drag path never gets this class, so it still tracks the pointer. */
#win.is-sliding .win-dim, #win.is-sliding .win-band,
#winRail.is-sliding .rail-band, #winRail.is-sliding .win-handle {
  transition: left 0.42s cubic-bezier(0.22, 0.61, 0.36, 1), width 0.42s cubic-bezier(0.22, 0.61, 0.36, 1);
}

/* ============ "Not placeable" diagnostics ============ */
/* Past-midnight hour labels on the schedule axis read a touch quieter. */
.sch-hour--late { color: #c9cede; font-style: italic; }
/* Amber verdict for a show ruled out only by the day-hours / meal-break window. */
/* "Not placeable" is too wide for the status column on one line — let it wrap
   rather than clip (it's plain text, not a pill). */
.lane-status .st-blocked { color: #9a6700; font-weight: 700; }
/* The blocked verdicts you can actually do something about (a meal break, the
   day's hours) are buttons: hover explains, click goes to the setting. Styled
   as text so a row of them doesn't read as a row of controls — the red
   triangle, not a button chrome, is what earns the glance. */
.lane-status .st-conflict {
  font: inherit; font-size: 0.72rem; font-weight: 700; line-height: 1.1;
  display: inline-flex; align-items: center; gap: 3px;
  color: #9a6700; background: none; border: none; padding: 0;
  cursor: pointer; white-space: nowrap;
}
.lane-status .st-conflict:hover { text-decoration: underline; text-underline-offset: 2px; }
.lane-status .st-conflict:focus-visible { outline: 3px solid var(--violet); outline-offset: 2px; border-radius: 4px; }
.lane-status .st-warn { color: var(--warn); font-size: 0.62rem; }
/* The setting a conflict pill sends you to, flashed on arrival so the scroll
   lands on something rather than just somewhere. */
.ctl--flash { animation: ctlFlash 1.4s ease; border-radius: 10px; }
@keyframes ctlFlash {
  0%, 100% { box-shadow: 0 0 0 0 rgba(239, 68, 68, 0); }
  20%, 55% { box-shadow: 0 0 0 4px rgba(239, 68, 68, 0.3); }
}
@media (prefers-reduced-motion: reduce) {
  .ctl--flash { animation: none; }
}
.lane--blocked:not(.lane--scheduled) { background: rgba(226, 168, 21, 0.06); }
.lane--flash { animation: laneFlash 1.1s ease; }
@keyframes laneFlash {
  0%, 100% { background: transparent; }
  25% { background: rgba(226, 168, 21, 0.22); }
}
/* "⚠ Prevents N" chip next to a blocking control. */
.ctl-blocked {
  align-self: center; font: inherit; font-size: 0.66rem; font-weight: 700;
  color: #9a6700; background: #fff4d6; border: 1px solid #f0c65a; border-radius: 999px;
  padding: 2px 8px; cursor: pointer; white-space: nowrap; line-height: 1.3;
}
.ctl-blocked:hover { background: #ffeab3; }
.ctl-blocked:focus-visible { outline: 3px solid var(--violet); outline-offset: 2px; }
.meal-blocked { align-self: center; margin-left: 2px; }

/* The conflict triangle on a board label pill (day line, meal band, trip
   block) — the same mark the lane status pills carry, so the board and the
   grid say "conflict" the same way. Interactive: hover it for the list of
   shows the constraint excludes. */
.excl-mark {
  flex: none; pointer-events: auto; cursor: help;
  color: var(--warn); font-size: 0.66rem; line-height: 1;
}
/* The excludes-list hover popup — a narrower cousin of the show card. */
.excl-pop {
  position: fixed; z-index: 70; pointer-events: none; max-width: 190px;
  background: var(--paper); border: 1px solid var(--line); border-radius: 10px;
  box-shadow: 0 10px 26px rgba(16, 24, 40, 0.2); padding: 8px 10px;
}
.excl-pop-head {
  font-size: 0.62rem; font-weight: 800; color: #c77700;
  text-transform: uppercase; letter-spacing: 0.04em; margin-bottom: 5px;
}
.excl-pop-list { margin: 0; padding: 0; list-style: none; }
.excl-pop-list li {
  font-size: 0.72rem; color: var(--ink); line-height: 1.35;
  white-space: nowrap; overflow: hidden; text-overflow: ellipsis; max-width: 170px;
}
.excl-pop-more { margin-top: 4px; font-size: 0.66rem; font-weight: 700; color: var(--muted); }

@media (prefers-reduced-motion: reduce) {
  .board-count.bump { animation: none; }
  .sch-show--enter, .sch-show--moving, .sch-ghost, .sch-day--enter, .lane--flash { animation: none; transition: none; }
  .schedule.is-grid-hover .sch-decor span { animation: none; }
  #win.is-sliding .win-dim, #win.is-sliding .win-band,
  #winRail.is-sliding .rail-band, #winRail.is-sliding .win-handle { transition: none; }
  html { scroll-behavior: auto; }
}
