/* Hand-authored overrides that can't live in the Tailwind-compiled
   app.css (make css overwrites it). Loaded AFTER app.css so these win
   the cascade. Cached by the browser, unlike the inline base.html copy
   this replaced. The x-cloak/timing-critical block stays inline in base.html. */

/* Overview table (Production songs, Booking gigs). Class-based to avoid
   repeating identical inline styles on every <tr>/<td>. */
.ov-row { cursor: pointer; border-bottom: 1px solid #e5e7eb; }
.ov-row:hover:not(.bg-indigo-50) { background: #f0f6ff; }
.ov-th-sticky { position: sticky; left: 0; z-index: 20; background: #f9fafb;
  border-bottom: 2px solid #d1d5db; border-right: 2px solid #d1d5db;
  white-space: nowrap; min-width: 200px; }
.ov-th-progress { background: #f9fafb; border-bottom: 2px solid #d1d5db;
  border-right: 1px solid #d1d5db; white-space: nowrap; min-width: 130px; }
.ov-th-section { background: #f9fafb; border-bottom: 2px solid #d1d5db;
  border-right: 1px solid #d1d5db; min-width: 80px; max-width: 140px; }
.ov-th-anchor { background: #f9fafb; border-bottom: 2px solid #d1d5db; min-width: 32px; }
.ov-th-label { display: flex; align-items: center; gap: 5px; padding: 7px 12px;
  font-size: 11px; font-weight: 600; text-transform: uppercase;
  letter-spacing: 0.05em; color: #6b7280; }
.ov-th-section-inner { display: flex; flex-direction: column; align-items: center;
  justify-content: center; padding: 6px 4px; position: relative; }
.ov-th-section-name { font-size: 9px; font-weight: 600; text-transform: uppercase;
  letter-spacing: 0.04em; color: #9ca3af; max-width: 100%; overflow: hidden;
  text-overflow: ellipsis; white-space: nowrap; }
.ov-th-section-row { display: flex; align-items: center; justify-content: center;
  gap: 4px; min-width: 0; }

/* Horizontal scroll affordance for the three overview grids. The grids are
   wider than the viewport and their opaque sticky cells paint over any
   background-attachment shadow trick, so the edge fade is a sibling overlay on
   a relative wrapper and grid_scroll.js toggles ov-can-left / ov-can-right on
   that wrapper. The scrollbar is forced visible because an overlay scrollbar
   fades out and leaves no hint that the grid continues. */
.ov-scroll-wrap { position: relative; }
.ov-scroll { scrollbar-width: thin; scrollbar-color: #cbd5e1 transparent; }
.ov-scroll::-webkit-scrollbar { height: 10px; }
.ov-scroll::-webkit-scrollbar-track { background: transparent; }
.ov-scroll::-webkit-scrollbar-thumb { background: #cbd5e1; border-radius: 9999px;
  border: 3px solid transparent; background-clip: content-box; }
.ov-scroll-fade { position: absolute; top: 0; right: 0; bottom: 0; width: 32px;
  pointer-events: none; opacity: 0; transition: opacity 150ms ease;
  background: linear-gradient(to left, rgba(17, 24, 39, 0.16), rgba(17, 24, 39, 0)); }
.ov-can-right > .ov-scroll-fade { opacity: 1; }

.ov-td-title-sticky { position: sticky; left: 0; z-index: 10; background: #fff;
  border-right: 2px solid #d1d5db; border-bottom: 1px solid #e5e7eb; min-width: 200px; }
.ov-td-progress { background: #fff; border-right: 1px solid #e5e7eb;
  border-bottom: 1px solid #e5e7eb; min-width: 130px; vertical-align: top; position: relative; }
.ov-td-cell { background: #fff; border-right: 1px solid #e5e7eb;
  border-bottom: 1px solid #e5e7eb; min-width: 80px; max-width: 140px;
  text-align: center; vertical-align: top; }
.ov-td-anchor { background: #fff; border-bottom: 1px solid #e5e7eb; min-width: 32px; }
.ov-td-add-sticky { position: sticky; left: 0; z-index: 5; background: #f9fafb; }
.ov-td-add-fill { background: #f9fafb; }
.ov-title-badge { display: flex; width: 28px; height: 28px; align-items: center;
  justify-content: center; border-radius: 4px; background: #e0e7ff; color: #4338ca;
  font-size: 12px; font-weight: 700; flex-shrink: 0; }

/* ── Layout grid columns via plain CSS ────────────────────────────────
   The Tailwind Play CDN fails to emit rules for comma-containing
   arbitrary grid-template values (e.g. grid-cols-[minmax(0,1fr)_320px])
   in some Safari/WebKit builds — the rule is never generated, so the
   two-column layout collapses to a single full-width stack. Comma-free
   arbitrary utilities (text-[11px] etc.) and the plain `grid`/`hidden`
   display classes are unaffected, which is why only the column templates
   broke. WebKit applies the equivalent *static* CSS correctly, so the
   app's structural grid-template-columns live here instead of as
   arbitrary utilities. These set columns only — display stays on the
   element via Tailwind `grid`/`lg:grid`/`hidden sm:grid`, and gap/padding
   stay as Tailwind classes too. Breakpoints mirror Tailwind
   (sm 640, lg 1024, xl 1280). */
.as-cols-auto-1fr { grid-template-columns: auto 1fr; }
.as-cols-bounce-row { grid-template-columns: 24px minmax(0, 1fr) auto; }
.as-cols-bounce-head { grid-template-columns: 24px minmax(0, 3fr) minmax(140px, 2fr) 72px 96px 76px 44px; }
@media (min-width: 640px) {
  .as-cols-bounce-row { grid-template-columns: 24px minmax(0, 3fr) minmax(140px, 2fr) 72px 96px 76px 44px; }
}
@media (min-width: 1024px) {
  .as-cols-main-320 { grid-template-columns: minmax(0, 1fr) 320px; }
  .as-cols-main-340 { grid-template-columns: minmax(0, 1fr) 340px; }
  /* Bounce review: the waveform stays the dominant column, but the comment
     pane now takes a proportional share so it widens with the card instead of
     staying pinned to a narrow fixed rail (min keeps it readable when tight). */
  .as-cols-bounce-review { grid-template-columns: minmax(0, 1fr) minmax(340px, 0.52fr); }
  .as-cols-2-1 { grid-template-columns: 2fr 1fr; }
  .as-cols-settings { grid-template-columns: 14rem minmax(0, 1fr); }
}
@media (min-width: 1280px) {
  .as-cols-bounce-review { grid-template-columns: minmax(0, 1fr) minmax(380px, 0.62fr); }
  .as-cols-lander-index { grid-template-columns: minmax(0, 0.95fr) minmax(0, 1.35fr); }
  .as-cols-lander-analytics { grid-template-columns: minmax(0, 1.15fr) minmax(320px, 0.85fr); }
  .as-cols-lander-builder { grid-template-columns: minmax(0, 1.15fr) minmax(360px, 0.85fr); }
}

/* Release-credits matrix: track-name column headers rotated to read bottom-up,
   so full song titles fit above the narrow toggle columns (desktop grid only —
   mobile uses the stacked chip list). */
.as-vhead {
  writing-mode: vertical-rl;
  transform: rotate(180deg);
  white-space: nowrap;
  max-height: 9rem;
  overflow: hidden;
  text-overflow: ellipsis;
}

/* ── Select control ───────────────────────────────────────────────────
   A native <select> ignores the input signature: whatever padding and
   radius it is given, the UA still paints its own chrome, so a select and
   a text input in the same row came out different heights with different
   corners (Workspace Settings showed two of each side by side). One class
   fixes it everywhere: `appearance: none` plus the exact geometry of the
   `.input` component in static/src/app.css (min-height 2.125rem, rounded-md,
   1px gray-300, py-1.5/px-3, text-sm), and our own chevron so removing the
   UA arrow does not remove the affordance.

   It sets no width or margin — those stay Tailwind utilities on the element
   (`w-full`, `flex-1`, `mt-1`), because a width here would beat `w-40` in
   the cascade: overrides.css loads after app.css.

   `.as-select-xs` is the dense variant, for the rows whose neighbouring
   controls are text-xs (inline status badges, the timeline, transcode). */
.as-select {
  -webkit-appearance: none;
  -moz-appearance: none;
  appearance: none;
  min-height: 2.125rem;
  border-radius: 0.375rem;
  border: 1px solid #d1d5db;
  background-color: #fff;
  background-image: url("data:image/svg+xml;charset=utf-8,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16' fill='none' stroke='%236b7280' stroke-width='1.5' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='m4 6 4 4 4-4'/%3E%3C/svg%3E");
  background-repeat: no-repeat;
  background-position: right 0.5rem center;
  background-size: 1rem 1rem;
  padding: 0.375rem 2rem 0.375rem 0.75rem;
  font-size: 0.875rem;
  line-height: 1.25rem;
  color: #111827;
}
.as-select-xs {
  min-height: 1.75rem;
  background-position: right 0.375rem center;
  background-size: 0.875rem 0.875rem;
  padding: 0.25rem 1.625rem 0.25rem 0.5rem;
  font-size: 0.75rem;
  line-height: 1rem;
}
.as-select:focus {
  outline: none;
  border-color: #6366f1;
  box-shadow: 0 0 0 2px rgba(99, 102, 241, 0.55);
}
.as-select:disabled {
  background-color: #f9fafb;
  color: #6b7280;
  cursor: not-allowed;
}
.dark .as-select {
  background-color: #1f2937;
  border-color: #4b5563;
  color: #f3f4f6;
  background-image: url("data:image/svg+xml;charset=utf-8,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16' fill='none' stroke='%239ca3af' stroke-width='1.5' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='m4 6 4 4 4-4'/%3E%3C/svg%3E");
}
.dark .as-select:disabled { background-color: #111827; color: #6b7280; }
/* Validation error — the red-400 border the input signature uses. It has to be
   its own class: `.as-select` sets the border shorthand and would beat a
   `border-red-400` utility, which app.css emits before this file. */
.as-select-error,
.dark .as-select-error { border-color: #f87171; }

/* ── Dark theme ───────────────────────────────────────────────────────────
   Light is the default and what we optimise for. Dark is purely additive:
   a `.dark` class on <html> (toggled in the sidebar, persisted in
   localStorage) activates Tailwind `dark:` variants plus this baseline.

   This baseline remaps the handful of surface/text/border utilities that
   dominate the templates so the whole app is legible in dark mode without
   editing all 259 templates. Per-view refinement layers `dark:` variants
   on top; because those compile to equal-or-higher specificity, they win
   over these rules where present. */
html.dark { background-color: #0b1120; color-scheme: dark; }
.dark body { color: #e5e7eb; }

/* Native form controls that don't go through the .input class still need a
   readable dark surface (selects especially — their option list also follows
   color-scheme above). */
.dark select,
.dark textarea,
.dark input[type="text"],
.dark input[type="number"],
.dark input[type="email"],
.dark input[type="url"],
.dark input[type="search"],
.dark input[type="tel"],
.dark input[type="date"],
.dark input[type="datetime-local"],
.dark input[type="time"],
.dark input[type="password"] {
  background-color: #1f2937;
  color: #f3f4f6;
  border-color: #4b5563;
}
.dark input::placeholder,
.dark textarea::placeholder { color: #9ca3af; }

.dark .bg-white { background-color: #1f2937; }
.dark .bg-gray-50 { background-color: #111827; }
/* Opacity-modified gray-50 (e.g. bg-gray-50/60, /40) isn't caught by the
   rule above; left untouched it composites to a light overlay that clashes
   with the remapped light text. Remap to a faint elevation instead. */
.dark .bg-gray-50\/80,
.dark .bg-gray-50\/70,
.dark .bg-gray-50\/60,
.dark .bg-gray-50\/50,
.dark .bg-gray-50\/40 { background-color: rgba(255, 255, 255, 0.04); }
.dark .bg-gray-100 { background-color: #1f2937; }
/* gray-200/300 are mostly progress-bar rails and toggle tracks — left light
   they glare white on dark. */
.dark .bg-gray-200 { background-color: #374151; }
.dark .bg-gray-300 { background-color: #4b5563; }
/* Low-progress (0–24%) badges and fills use slate; left light they read as
   bright grey pills that draw the eye. Darken so they recede like the muted
   grey they are on light theme, while the coloured higher bands stay vivid. */
.dark .bg-slate-300 { background-color: #2c333f; }
.dark .bg-slate-400 { background-color: #374151; }

/* Overview tables (song + gig) draw their sticky header/first column from
   hardcoded light .ov-* surfaces; remap them so dark theme reaches them. */
.dark .ov-row { border-color: #374151; }
.dark .ov-row:hover:not(.bg-indigo-50) { background: #374151; }
.dark .ov-th-sticky,
.dark .ov-th-progress,
.dark .ov-th-section,
.dark .ov-th-anchor { background: #111827; border-color: #4b5563; }
.dark .ov-th-label { color: #9ca3af; }
.dark .ov-td-title-sticky,
.dark .ov-td-progress,
.dark .ov-td-cell,
.dark .ov-td-anchor { background: #1f2937; border-color: #374151; }
.dark .ov-td-add-sticky,
.dark .ov-td-add-fill { background: #111827; }
.dark .ov-title-badge { background: rgba(99, 102, 241, 0.22); color: #a5b4fc; }

.dark .text-gray-900 { color: #f3f4f6; }
.dark .text-gray-800 { color: #e5e7eb; }
.dark .text-gray-700 { color: #d1d5db; }
.dark .text-gray-600 { color: #9ca3af; }
.dark .text-gray-500 { color: #9ca3af; }

.dark .border-gray-100 { border-color: #374151; }
.dark .border-gray-200 { border-color: #374151; }
.dark .border-gray-300 { border-color: #4b5563; }
.dark .divide-gray-200 > :not([hidden]) ~ :not([hidden]) { border-color: #374151; }
.dark .divide-gray-100 > :not([hidden]) ~ :not([hidden]) { border-color: #374151; }
.dark .ring-gray-100 { --tw-ring-color: #374151; }
.dark .ring-gray-200 { --tw-ring-color: #374151; }
.dark .ring-gray-300 { --tw-ring-color: #4b5563; }

/* Colored accents — soft tinted chips/banners read as bright blocks on a
   dark surface, so remap the hues actually used in templates to low-alpha
   tints, lighten the accent text, and dim the colored rings. Only the
   families present in the codebase are listed. */
.dark .bg-indigo-50 { background-color: rgba(99, 102, 241, 0.15); }
.dark .bg-indigo-100 { background-color: rgba(99, 102, 241, 0.22); }
/* Opacity-modifier variants (e.g. the selected-bounce-row highlight) bake the
   alpha into the colour, so they miss the remaps above and glare as bright
   lavender. Remap each used variant, scaling the subtle tint by the modifier. */
.dark .bg-indigo-50\/40 { background-color: rgba(99, 102, 241, 0.07); }
.dark .bg-indigo-50\/60 { background-color: rgba(99, 102, 241, 0.10); }
.dark .bg-indigo-50\/80 { background-color: rgba(99, 102, 241, 0.12); }
.dark .bg-indigo-50\/85 { background-color: rgba(99, 102, 241, 0.13); }
.dark .bg-indigo-100\/90 { background-color: rgba(99, 102, 241, 0.20); }
.dark .bg-red-50 { background-color: rgba(239, 68, 68, 0.15); }
.dark .bg-red-100 { background-color: rgba(239, 68, 68, 0.22); }
.dark .bg-amber-50 { background-color: rgba(245, 158, 11, 0.15); }
.dark .bg-amber-100 { background-color: rgba(245, 158, 11, 0.22); }
.dark .bg-green-50 { background-color: rgba(16, 185, 129, 0.15); }
.dark .bg-green-100 { background-color: rgba(16, 185, 129, 0.22); }
.dark .bg-emerald-50 { background-color: rgba(16, 185, 129, 0.15); }
.dark .bg-emerald-100 { background-color: rgba(16, 185, 129, 0.22); }
.dark .bg-yellow-50 { background-color: rgba(234, 179, 8, 0.15); }
.dark .bg-yellow-100 { background-color: rgba(234, 179, 8, 0.22); }
.dark .bg-blue-50 { background-color: rgba(59, 130, 246, 0.15); }
.dark .bg-blue-100 { background-color: rgba(59, 130, 246, 0.22); }
.dark .bg-rose-100 { background-color: rgba(244, 63, 94, 0.20); }
/* Violet is the AI-provenance badge and sky the audio-quality ramp — both are
   sanctioned per-type tokens (FRONTEND.md §4.4.2), so both need the tint
   treatment or they glare as bright pills on a dark card. */
.dark .bg-violet-100 { background-color: rgba(139, 92, 246, 0.22); }
.dark .bg-sky-100 { background-color: rgba(14, 165, 233, 0.22); }
.dark .bg-orange-100 { background-color: rgba(249, 115, 22, 0.20); }

.dark .text-indigo-600 { color: #818cf8; }
.dark .text-indigo-700 { color: #a5b4fc; }
.dark .text-indigo-800 { color: #a5b4fc; }
.dark .text-red-600 { color: #f87171; }
.dark .text-red-700 { color: #fca5a5; }
.dark .text-red-800 { color: #fca5a5; }
.dark .text-amber-600 { color: #fbbf24; }
.dark .text-amber-700 { color: #fcd34d; }
.dark .text-amber-800 { color: #fcd34d; }
.dark .text-amber-900 { color: #fde68a; }
.dark .text-green-600 { color: #4ade80; }
.dark .text-green-700 { color: #4ade80; }
.dark .text-green-800 { color: #86efac; }
.dark .text-emerald-600 { color: #34d399; }
.dark .text-emerald-700 { color: #34d399; }
.dark .text-emerald-800 { color: #6ee7b7; }
.dark .text-yellow-600 { color: #facc15; }
.dark .text-yellow-700 { color: #fde047; }
.dark .text-yellow-800 { color: #fde047; }
.dark .text-blue-600 { color: #60a5fa; }
.dark .text-blue-700 { color: #93c5fd; }
.dark .text-blue-800 { color: #93c5fd; }
.dark .text-rose-600 { color: #fb7185; }
.dark .text-rose-700 { color: #fb7185; }
.dark .text-orange-800 { color: #fdba74; }
.dark .text-violet-700 { color: #c4b5fd; }
.dark .text-sky-700 { color: #7dd3fc; }

.dark .ring-indigo-200 { --tw-ring-color: rgba(99, 102, 241, 0.40); }
.dark .ring-red-200 { --tw-ring-color: rgba(239, 68, 68, 0.40); }
.dark .ring-amber-200 { --tw-ring-color: rgba(245, 158, 11, 0.40); }
.dark .ring-amber-300 { --tw-ring-color: rgba(245, 158, 11, 0.55); }
.dark .border-amber-300 { border-color: rgba(245, 158, 11, 0.55); }
.dark .ring-green-200 { --tw-ring-color: rgba(16, 185, 129, 0.40); }
.dark .ring-yellow-200 { --tw-ring-color: rgba(234, 179, 8, 0.40); }
.dark .ring-blue-200 { --tw-ring-color: rgba(59, 130, 246, 0.40); }

/* Hover surfaces — light-grey/tint hovers flash bright on dark. */
.dark .hover\:bg-gray-50:hover { background-color: #374151; }
.dark .hover\:bg-gray-100:hover { background-color: #4b5563; }
.dark .hover\:bg-indigo-50:hover { background-color: rgba(99, 102, 241, 0.20); }
.dark .hover\:bg-indigo-100:hover { background-color: rgba(99, 102, 241, 0.28); }
.dark .hover\:bg-red-50:hover { background-color: rgba(239, 68, 68, 0.18); }
.dark .hover\:bg-red-100:hover { background-color: rgba(239, 68, 68, 0.26); }

/* Custom `primary` palette (scaffolded webapptemplate pages use primary-*
   instead of indigo-*; primary-50/100 are near-white and glare on dark —
   e.g. the active settings-tab pill). text-primary-50/100 stay light: they
   are light text on a primary-600 fill. */
.dark .bg-primary-50 { background-color: rgba(59, 130, 246, 0.15); }
.dark .bg-primary-100 { background-color: rgba(59, 130, 246, 0.22); }
.dark .text-primary-600 { color: #60a5fa; }
.dark .text-primary-700 { color: #93c5fd; }
.dark .text-primary-800 { color: #93c5fd; }
.dark .ring-primary-50 { --tw-ring-color: rgba(59, 130, 246, 0.25); }
.dark .ring-primary-100 { --tw-ring-color: rgba(59, 130, 246, 0.35); }
.dark .border-primary-50 { border-color: rgba(59, 130, 246, 0.25); }
.dark .border-primary-100 { border-color: rgba(59, 130, 246, 0.35); }
.dark .hover\:bg-primary-50:hover { background-color: rgba(59, 130, 246, 0.18); }

/* Overview table (hardcoded hex above) — dark equivalents. */
.dark .ov-row { border-bottom-color: #374151; }
.dark .ov-row:hover:not(.bg-indigo-50) { background: #1e293b; }
.dark .ov-th-sticky,
.dark .ov-th-progress,
.dark .ov-th-section,
.dark .ov-th-anchor { background: #111827; border-bottom-color: #374151; border-right-color: #374151; }
.dark .ov-td-title-sticky,
.dark .ov-td-progress,
.dark .ov-td-cell,
.dark .ov-td-anchor { background: #1f2937; border-right-color: #374151; border-bottom-color: #374151; }
.dark .ov-td-add-sticky { background: #111827; }
.dark .ov-th-label { color: #9ca3af; }
.dark .ov-th-section-name { color: #6b7280; }
.dark .ov-scroll { scrollbar-color: #4b5563 transparent; }
.dark .ov-scroll::-webkit-scrollbar-thumb { background: #4b5563; background-clip: content-box; }
.dark .ov-scroll-fade { background: linear-gradient(to left, rgba(0, 0, 0, 0.45), rgba(0, 0, 0, 0)); }
.dark .ov-title-badge { background: #312e81; color: #c7d2fe; }

/* Setlist per-song waveform preview: the inline SVG (display.waveform_svg) fills
   its width-scaled box; preserveAspectRatio="none" stretches it to the box. */
svg.setlist-wave { display: block; width: 100%; height: 100%; }
.dark .setlist-wave-box { background: #1e1b4b; }

/* Between-rows insert strip. On hover-capable pointers the buttons appear on
   group-hover and the tap zone stays out of the way. On touch there is no hover:
   the pill would sit invisible-but-clickable over the top edge of every row and
   eat taps meant for the row, so it goes inert and a tap zone reveals it first. */
.ov-inserter-tap { display: none; }
@media (hover: none) {
  .ov-inserter-tap { display: block; }
  .ov-inserter-pill { pointer-events: none; }
}

/* Style-pack slot previews: the SVG is inlined rather than loaded through an
   <img>, because a slot's logo is a same-origin <image href> that the browser
   refuses to fetch inside an image document. Inlined, it scales to its box. */
.slot-preview svg { display: block; width: 100%; height: auto; }

/* A file's folder path is disambiguated by its tail, not its head — three
   "WindowImage.jpg" rows all read "Songs/recording-test/Projects/baby com…"
   and become indistinguishable. Overflowing at the start keeps the part that
   differs. `unicode-bidi: plaintext` keeps the text itself left-to-right; only
   which end clips is reversed. The bidi algorithm still lays the Latin text out
   left-to-right; `unicode-bidi: plaintext` must NOT be added here, because
   inferring the paragraph direction from the first letter puts the ellipsis
   back on the right. */
.ov-path-tail {
  direction: rtl;
  text-align: left;
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}

/* Nothing is behind a card but the footage it will sit over, and a flat black
   fill reads as a finished black frame rather than as an overlay. The checker
   is the only thing in the app that means "this area is empty", so it is one
   class used everywhere a card is shown: the pack grid, the timeline's overlay
   blocks, the material shelf's thumbnails and the authoring stage. */
.as-checker {
  background-color: #ffffff;
  background-image:
    linear-gradient(45deg, #d4d4d8 25%, transparent 25%, transparent 75%, #d4d4d8 75%),
    linear-gradient(45deg, #d4d4d8 25%, transparent 25%, transparent 75%, #d4d4d8 75%);
  background-size: 16px 16px;
  background-position: 0 0, 8px 8px;
}

/* A 16px square is bigger than some of the boxes a card is drawn in — the shelf
   thumbnail is 40px tall — and reads as two grey wedges rather than as a
   pattern. */
.as-checker-sm {
  background-size: 8px 8px;
  background-position: 0 0, 4px 4px;
}

.dark .as-checker {
  background-color: #27272a;
  background-image:
    linear-gradient(45deg, #3f3f46 25%, transparent 25%, transparent 75%, #3f3f46 75%),
    linear-gradient(45deg, #3f3f46 25%, transparent 25%, transparent 75%, #3f3f46 75%);
}

/* ── Boosted-navigation progress bar (boost.js) ───────────────────────
   hx-boost replaced the document load with an XHR, and with it went the
   browser's own tab spinner. This is what replaces it: a 3px bar pinned to
   the top of the viewport, shown 100ms into a boosted navigation and hidden
   when the response lands. Plain CSS rather than a text/tailwindcss block —
   Safari runs Alpine ahead of the JIT, so anything timing-critical that lives
   in the JIT block flashes. z-index sits above the sidebar (40) and the mini
   player (90) and below the error toasts (100); the bar is at the top of the
   viewport, the player at the bottom, so the two never meet. */
.nav-progress {
  position: fixed;
  top: 0;
  left: 0;
  right: 0;
  height: 3px;
  z-index: 95;
  overflow: hidden;
  pointer-events: none;
  background: rgba(79, 70, 229, 0.18);
}
.nav-progress[hidden] { display: none; }
.nav-progress-bar {
  height: 100%;
  width: 100%;
  transform-origin: 0 50%;
  background: #4f46e5;
  animation: nav-progress-sweep 1.2s cubic-bezier(0.4, 0, 0.2, 1) infinite;
}
@keyframes nav-progress-sweep {
  0% { transform: translateX(-100%) scaleX(0.5); }
  55% { transform: translateX(0) scaleX(0.85); }
  100% { transform: translateX(100%) scaleX(0.5); }
}
.dark .nav-progress { background: rgba(129, 140, 248, 0.22); }
.dark .nav-progress-bar { background: #818cf8; }

/* The clicked link, acknowledged in the frame of the click rather than when
   the response arrives. It is a link that is about to be replaced, so dimming
   it is enough — no layout change, nothing to un-do on the way out. */
.nav-pending { opacity: 0.55; transition: opacity 80ms linear; }

@media (prefers-reduced-motion: reduce) {
  /* Still a bar, still the same 100ms delay — just no travelling animation. */
  .nav-progress-bar { animation: none; transform: none; }
  .nav-pending { transition: none; }
}
