/* Pipeline Viewer Custom Styles */

/* === Pipeline Flow === */

.pipeline-flow {
  min-height: 100px;
}

.pipeline-flow a {
  transition: transform 0.2s ease, box-shadow 0.2s ease;
}

.pipeline-flow a:hover {
  transform: translateY(-2px);
}

.pipeline-flow a:focus-visible {
  outline: 2px solid oklch(var(--p));
  outline-offset: 2px;
  border-radius: 0.5rem;
}

/* Connector line animation for in-progress steps */
@keyframes pulse-connector {
  0%, 100% {
    opacity: 1;
  }
  50% {
    opacity: 0.5;
  }
}

.pipeline-flow .bg-info {
  animation: pulse-connector 2s ease-in-out infinite;
}

/* === Modern Surfaces: Elevation tokens ===
 * Near-invisible layered shadows + 1px hairline border. Target look is
 * Linear / Stripe / Vercel — subtle enough to disappear on a busy page,
 * crisp enough to define boundaries. Replaces daisyUI's default "Bootstrap-y"
 * card shadow.
 */
:root {
  --shadow-1: 0 1px 2px rgba(17, 17, 17, 0.04),
              0 1px 3px rgba(17, 17, 17, 0.06);
  --shadow-2: 0 2px 4px rgba(17, 17, 17, 0.04),
              0 4px 8px rgba(17, 17, 17, 0.06);
  --shadow-3: 0 8px 16px rgba(17, 17, 17, 0.06),
              0 16px 32px rgba(17, 17, 17, 0.08);
}

/* === Cards === */

.card {
  box-shadow: var(--shadow-1);
  border: 1px solid oklch(var(--b3));
  border-radius: 0.75rem;
  transition:
    transform 200ms cubic-bezier(0.2, 0, 0, 1),
    box-shadow 200ms cubic-bezier(0.2, 0, 0, 1),
    border-color 200ms cubic-bezier(0.2, 0, 0, 1);
}

/* daisyUI's .shadow-md / .shadow-lg on cards collapses to layered shadow */
.card.shadow-md,
.card.shadow-lg {
  box-shadow: var(--shadow-2);
}

/* Only apply hover effect to clickable cards, not container cards */
a.card:hover,
.workflow-card:hover,
.feature-card:hover {
  transform: translateY(-1px);
  box-shadow: var(--shadow-2);
  border-color: oklch(var(--p) / 0.3);
}

/* === Steps Table === */

.table th {
  font-weight: 600;
  text-transform: uppercase;
  font-size: 0.7rem;
  letter-spacing: 0.05em;
}

.table td {
  vertical-align: middle;
}

/* Subtle row hover */
.table tbody tr:hover {
  background-color: oklch(var(--b2) / 0.5);
}

/* In-progress row highlight */
.table tbody tr.bg-info\/5 {
  background-color: oklch(var(--in) / 0.05);
}

/* Failed row highlight */
.table tbody tr.bg-error\/5 {
  background-color: oklch(var(--er) / 0.05);
}

/* Mobile: Stack table on small screens */
@media (max-width: 768px) {
  .table-responsive-stack thead {
    display: none;
  }

  .table-responsive-stack tbody tr {
    display: block;
    margin-bottom: 1rem;
    border: 1px solid oklch(var(--b3));
    border-radius: 0.5rem;
    padding: 0.5rem;
  }

  .table-responsive-stack tbody td {
    display: flex;
    justify-content: space-between;
    padding: 0.5rem;
    border: none;
  }

  .table-responsive-stack tbody td::before {
    content: attr(data-label);
    font-weight: 600;
    text-transform: uppercase;
    font-size: 0.7rem;
    color: oklch(var(--bc) / 0.6);
  }
}

/* === Status Badges === */

.badge svg {
  flex-shrink: 0;
}

.progress {
  height: 8px;
}

.loading-spinner {
  color: currentColor;
}

/* === Step Card Borders === */

.border-l-success {
  border-left-color: oklch(var(--su));
}

.border-l-info {
  border-left-color: oklch(var(--in));
}

.border-l-error {
  border-left-color: oklch(var(--er));
}

.border-l-base-300 {
  border-left-color: oklch(var(--b3));
}

/* === Navigation === */

.breadcrumbs {
  padding-top: 0;
  padding-bottom: 0;
}

/* === Utilities === */

/* Screen reader only - hide visually but keep accessible (C-I1) */
.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;
}

.truncate {
  max-width: 100%;
}

.stats {
  background: inherit;
}

/* === Animations === */

main {
  animation: fadeIn 0.3s ease-out;
}

@keyframes fadeIn {
  from {
    opacity: 0;
    transform: translateY(10px);
  }
  to {
    opacity: 1;
    transform: translateY(0);
  }
}

/* === Responsive === */

@media (max-width: 1024px) {
  .pipeline-flow .flex-col .min-h-8 {
    min-height: 2rem;
  }
}

/* === Mermaid Diagram Accessibility (I5) === */

/* U4: Padding to prevent focus outline clipping from overflow-x-auto */
.mermaid-container {
  padding: 8px;
}

.mermaid-container svg g.node:focus {
  outline: 3px solid oklch(var(--p));
  outline-offset: 4px;
}

.mermaid-container svg g.node:focus-visible {
  outline: 3px solid oklch(var(--p));
  outline-offset: 4px;
}

.mermaid-container svg g.node {
  cursor: pointer;
}

/* === Feedback Form Progressive Disclosure === */

/* U6: Dismissed state - keep suggestion visible but dimmed */
[data-state="dismissed"] .bg-info\/10 {
  opacity: 0.5;
}

[data-state="dismissed"] p {
  opacity: 0.7;
}

/* Optional: Progress indicator for forms with many issues (U4) */
.feedback-progress {
  font-size: 0.875rem;
  color: oklch(var(--bc) / 0.6);
}

/* === Brand: Headings ===
 * Apply Satoshi to all heading elements to match obviouslynot.ai's
 * type ramp. The marketing site uses Satoshi 700 with tight letter-
 * spacing for h1/h2/h3 (.hero h1, .section-headline, .builder-card h3,
 * .step-content h3, .proof-value) — that compressed, editorial-weight
 * look is part of the brand voice and the largest single drift from
 * marketing if not applied. Body text stays Inter (the app default).
 *
 * font-family is the only property we set here: weights and sizes are
 * left to Tailwind utility classes / daisyUI components so individual
 * pages retain their existing visual hierarchy.
 */
h1, h2, h3, h4 {
  font-family: 'Satoshi', 'Inter', sans-serif;
  letter-spacing: -0.01em;
}

/* === Brand: Obviously≠ logo ===
 * Matches obviouslynot.ai/static/css/main.css .logo treatment so the app
 * and the marketing site share the same brand mark. Satoshi-900 wordmark
 * with a purple→cyan gradient on the ≠ glyph (set via background-clip).
 * Brand color reference: #4A3F78 (purple) and #2DC5E8 (cyan), drawn from
 * /static/favicon.svg gradient stops.
 */
:root {
  --brand-purple: #4A3F78;
  --brand-cyan: #2DC5E8;
  --brand-purple-light: #BFA8FF;
  --brand-rose: #D4637A;
  --brand-gray-bg: #F7F8FA;
  --brand-gray-light: #e5e5e5;
}

/* === daisyUI Theme: obviouslynot ===
 * Maps daisyUI's semantic color slots to the brand palette so the writer
 * app and the marketing site at obviouslynot.ai share the same visual
 * identity. Brand role assignments follow platform/docs/conventions/ui-ux.md
 * § "Color usage rules":
 *
 *   primary    (--p)  = #2DC5E8 brand cyan    — primary CTAs. App surface
 *                                               leads with the brand color
 *                                               (marketing reserves cyan
 *                                               for hover; the app reads
 *                                               more brand-forward).
 *   secondary  (--s)  = #4A3F78 brand purple  — secondary actions, accents
 *   accent     (--a)  = #BFA8FF purple-light  — soft accent fills, hover
 *                                               borders (platform doc role)
 *   neutral    (--n)  = #111111 near-black    — chrome / dark surfaces
 *   base-100   (--b1) = #ffffff white         — page surface
 *   base-200   (--b2) = #F7F8FA gray-bg       — subtle surface (body bg)
 *   base-300   (--b3) = #e5e5e5 gray-light    — dividers / borders
 *   base-content (--bc) = #111111             — primary text
 *   info       (--in) = #2DC5E8 brand cyan    — info alerts (same hue as
 *                                               primary; shape/role
 *                                               distinguishes the two)
 *   success/warning/error inherit daisyUI defaults so status colors keep
 *   their universally-recognized semantics (green/amber/red).
 *
 * Content (foreground) tokens are chosen for AAA contrast against each
 * background: cyan is bright (L=76%) so primary text uses BLACK (9.23:1);
 * purple is dark (L=41%) so secondary text uses WHITE (9.24:1).
 *
 * OKLCH values computed from the source hex via sRGB→Oklab→Oklch
 * (precision verified in-browser against the canonical brand hex values).
 * Hue is omitted (kept at 0) for pure-grayscale tokens because chroma=0
 * makes hue meaningless.
 *
 * Layout knobs (radii, animations) lifted from the previous "corporate"
 * theme so spacing and motion feel unchanged; only the palette shifts.
 */
[data-theme="obviouslynot"] {
  color-scheme: light;
  /* Primary — #2DC5E8 brand cyan (text: black for AAA contrast) */
  --p:   76.30%   0.1283 218.02;
  --pc:  17.76%   0       0;
  /* Secondary — #4A3F78 brand purple (text: white) */
  --s:   40.71%   0.0935 290.50;
  --sc: 100%      0       0;
  /* Accent — #BFA8FF purple-light (text: black) */
  --a:   78.38%   0.1235 295.56;
  --ac:  17.76%   0       0;
  /* Neutral — #111111 near-black (chrome / dark surfaces) */
  --n:   17.76%   0       0;
  --nc: 100%      0       0;
  /* Base surfaces */
  --b1: 100%      0       0;        /* #ffffff */
  --b2:  97.89%   0.0029 264.54;    /* #F7F8FA */
  --b3:  92.19%   0       0;        /* #e5e5e5 */
  --bc:  17.76%   0       0;        /* #111111 */
  /* Status: info uses brand cyan; success/warning/error keep daisyUI
   * defaults so red still reads as "danger" rather than decorative rose. */
  --in:  76.30%   0.1283 218.02;    /* brand cyan */
  --inc: 17.76%   0       0;
  --su:  64.80%   0.150  160;       /* daisyUI default */
  --suc:  0%      0       0;
  --wa:  84.71%   0.199   83.87;    /* daisyUI default */
  --wac:  0%      0       0;
  --er:  71.76%   0.221   22.18;    /* daisyUI default */
  --erc:  0%      0       0;
  /* Layout knobs (carried from corporate so motion/spacing feel unchanged) */
  --rounded-box: 0.5rem;
  --rounded-btn: 0.5rem;
  --rounded-badge: 0.5rem;
  --tab-radius: 0.5rem;
  --animation-btn: 0.25s;
  --animation-input: 0.2s;
  --btn-focus-scale: 0.95;
  --border-btn: 1px;
  --tab-border: 1px;
}

.brand-logo {
  /* !important needed because daisyUI's .btn ships with font-weight:600
   * and we can't reorder the daisyUI CDN stylesheet from this file.
   * Mirrors obviouslynot.ai's .logo { Satoshi 900 / -0.5px / 1.25rem }. */
  font-family: 'Satoshi', 'Inter', sans-serif !important;
  font-weight: 900 !important;
  font-size: 1.25rem !important;
  letter-spacing: -0.5px !important;
  /* gap:0 keeps the wordmark and the ≠ visually contiguous despite the
   * .btn flex layout daisyUI applies. */
  gap: 0;
  text-transform: none;
}

.brand-not-equal {
  /* Inline SVG mirroring Font Awesome's fa-not-equal Duotone shape
   * (two stacked paths: bars + slash) with both paths filled via
   * linear gradients (purple→cyan and cyan→purple) so the glyph
   * matches obviouslynot.ai's .logo .fad.fa-not-equal treatment
   * pixel-for-pixel without taking a Font Awesome Pro dependency.
   * Sized in em so the icon scales with the surrounding wordmark. */
  width: 1.1em;
  height: 1.1em;
  margin-left: 2px;
  display: inline-block;
  vertical-align: -0.18em;
  flex-shrink: 0;
}

/* === Modern Status Badges (pill + dot pattern) ===
 * Replaces daisyUI's solid-fill badges with the Linear/Vercel/Stripe pattern:
 * soft tint background, saturated text, leading colored dot. The dot is a
 * radial-gradient background so no markup changes are needed.
 *
 * Tints stay at /0.08–0.10 so badges remain readable on white card surfaces.
 * Borders at /0.25 give just enough definition without competing with the dot.
 */
.badge {
  font-weight: 500;
  letter-spacing: 0.01em;
  font-variant-numeric: tabular-nums;
}

.badge-info,
.badge-success,
.badge-warning,
.badge-error,
.badge-primary,
.badge-secondary,
.badge-accent {
  background-color: transparent;
  padding-left: 0.85rem;
  background-repeat: no-repeat;
  background-position: 0.35rem center;
  background-size: 0.4rem 0.4rem;
  /* leading dot: radial gradient anchored at left-center */
  background-image: radial-gradient(circle at 0.2rem center, currentColor 0.18rem, transparent 0.19rem);
}

.badge-info {
  color: oklch(var(--in));
  background-color: oklch(var(--in) / 0.08);
  background-image: radial-gradient(circle at 0.2rem center, oklch(var(--in)) 0.18rem, transparent 0.19rem);
  border-color: oklch(var(--in) / 0.25);
}
.badge-success {
  color: oklch(var(--su));
  background-color: oklch(var(--su) / 0.08);
  background-image: radial-gradient(circle at 0.2rem center, oklch(var(--su)) 0.18rem, transparent 0.19rem);
  border-color: oklch(var(--su) / 0.25);
}
.badge-warning {
  color: oklch(var(--wa));
  background-color: oklch(var(--wa) / 0.10);
  background-image: radial-gradient(circle at 0.2rem center, oklch(var(--wa)) 0.18rem, transparent 0.19rem);
  border-color: oklch(var(--wa) / 0.30);
}
.badge-error {
  color: oklch(var(--er));
  background-color: oklch(var(--er) / 0.08);
  background-image: radial-gradient(circle at 0.2rem center, oklch(var(--er)) 0.18rem, transparent 0.19rem);
  border-color: oklch(var(--er) / 0.25);
}
.badge-primary {
  color: oklch(var(--p) / 0.85);
  background-color: oklch(var(--p) / 0.12);
  background-image: radial-gradient(circle at 0.2rem center, oklch(var(--p)) 0.18rem, transparent 0.19rem);
  border-color: oklch(var(--p) / 0.30);
}
.badge-secondary {
  color: oklch(var(--s));
  background-color: oklch(var(--s) / 0.10);
  background-image: radial-gradient(circle at 0.2rem center, oklch(var(--s)) 0.18rem, transparent 0.19rem);
  border-color: oklch(var(--s) / 0.30);
}

/* Outline variant: keep daisy's outline style but force consistent letter-spacing */
.badge-outline {
  background-color: transparent;
}

/* === Modern Focus Rings ===
 * Visible focus indicator on every keyboard-focused interactive element.
 * Critical for a11y; also signals "this app cares" to keyboard power users.
 */
:focus-visible {
  outline: 2px solid oklch(var(--p));
  outline-offset: 2px;
  border-radius: 0.25rem;
}

.btn:focus-visible {
  outline: 2px solid oklch(var(--p));
  outline-offset: 3px;
}

input:focus-visible,
textarea:focus-visible,
select:focus-visible {
  outline: 2px solid oklch(var(--p) / 0.5);
  outline-offset: 0px;
  border-color: oklch(var(--p));
}

a:focus-visible {
  outline-offset: 4px;
}

/* === Tabular numerals on status displays ===
 * Stops timestamps, counters, and durations from jumping as digits change.
 * Applied to elements that display numbers via class, attribute, or tag.
 */
.tabular-nums,
.stat-value,
.stat-figure,
time,
.font-mono,
[data-tabular],
.progress-text,
.duration {
  font-variant-numeric: tabular-nums;
}

/* === Command Palette (Stage F) ===
 * Cmd-K modal. Centered panel with a search input, scrollable results
 * list, and a footer of keyboard hints. Backdrop is the default
 * <dialog>::backdrop with a 200ms fade.
 */
.cmd-palette {
  border: 0;
  padding: 0;
  background: transparent;
  max-width: min(640px, 92vw);
  width: 100%;
  margin: auto;
  top: 15vh;
  z-index: 50;
}
.cmd-palette[open] {
  display: block;
}
.cmd-palette::backdrop {
  background: rgba(17, 17, 17, 0.4);
  backdrop-filter: blur(2px);
  -webkit-backdrop-filter: blur(2px);
  animation: cmd-fade-in 200ms ease;
}
@keyframes cmd-fade-in {
  from { opacity: 0; }
  to   { opacity: 1; }
}
.cmd-palette__panel {
  background: oklch(var(--b1));
  border: 1px solid oklch(var(--b3));
  border-radius: 0.875rem;
  box-shadow: var(--shadow-3);
  overflow: hidden;
  display: flex;
  flex-direction: column;
  animation: cmd-pop 220ms cubic-bezier(0.2, 0, 0, 1);
}
@keyframes cmd-pop {
  from { opacity: 0; transform: translateY(-6px) scale(0.98); }
  to   { opacity: 1; transform: translateY(0) scale(1); }
}
.cmd-palette__search {
  display: flex;
  align-items: center;
  gap: 0.625rem;
  padding: 0.875rem 1rem;
  border-bottom: 1px solid oklch(var(--b3));
}
.cmd-palette__search-icon {
  width: 1.125rem;
  height: 1.125rem;
  flex-shrink: 0;
  color: oklch(var(--bc) / 0.5);
}
.cmd-palette__search input {
  flex: 1 1 auto;
  border: 0;
  outline: none;
  background: transparent;
  font-size: 0.9375rem;
  color: oklch(var(--bc));
  padding: 0;
}
.cmd-palette__search input::placeholder {
  color: oklch(var(--bc) / 0.5);
}
.cmd-palette__kbd,
.cmd-palette__footer kbd {
  display: inline-flex;
  align-items: center;
  padding: 0.0625rem 0.375rem;
  border: 1px solid oklch(var(--b3));
  border-radius: 0.25rem;
  background: oklch(var(--b2));
  font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
  font-size: 0.6875rem;
  font-weight: 500;
  color: oklch(var(--bc) / 0.7);
}
.cmd-palette__results {
  list-style: none;
  margin: 0;
  padding: 0.375rem 0;
  max-height: 50vh;
  overflow-y: auto;
}
.cmd-palette__item {
  display: flex;
  align-items: center;
  gap: 0.625rem;
  padding: 0.5rem 1rem;
  cursor: pointer;
  font-size: 0.875rem;
  color: oklch(var(--bc));
  transition: background 120ms ease;
}
.cmd-palette__item[aria-selected="true"] {
  background: oklch(var(--p) / 0.08);
}
.cmd-palette__item:hover {
  background: oklch(var(--bc) / 0.04);
}
.cmd-palette__dot {
  width: 0.5rem;
  height: 0.5rem;
  border-radius: 50%;
  flex-shrink: 0;
}
.cmd-palette__dot--route   { background: oklch(var(--bc) / 0.4); }
.cmd-palette__dot--patent  { background: oklch(var(--s)); }
.cmd-palette__dot--scan    { background: oklch(var(--p)); }
.cmd-palette__dot--tool    { background: oklch(var(--a)); }
.cmd-palette__label {
  flex: 1 1 auto;
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}
.cmd-palette__hint {
  flex-shrink: 0;
  font-size: 0.75rem;
  color: oklch(var(--bc) / 0.5);
}
.cmd-palette__empty {
  padding: 1.25rem 1rem;
  text-align: center;
  font-size: 0.875rem;
  color: oklch(var(--bc) / 0.5);
}
.cmd-palette__footer {
  display: flex;
  gap: 1rem;
  flex-wrap: wrap;
  padding: 0.625rem 1rem;
  background: oklch(var(--b2));
  border-top: 1px solid oklch(var(--b3));
  font-size: 0.75rem;
  color: oklch(var(--bc) / 0.5);
}

/* === Patent List Toolbar (Stage E) ===
 * Search input + sort dropdowns above the patents grid. HTMX-driven:
 * input debounces at 300ms, select fires on change. URL updates via
 * hx-push-url so sort + filter state is shareable and survives reload.
 */
.patent-toolbar {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 0.75rem;
  padding: 0.5rem;
  background: oklch(var(--b1));
  border: 1px solid oklch(var(--b3));
  border-radius: 0.625rem;
  margin-bottom: 1rem;
}
.patent-toolbar__search {
  display: inline-flex;
  align-items: center;
  gap: 0.5rem;
  flex: 1 1 18rem;
  min-width: 12rem;
  padding: 0.375rem 0.625rem;
  background: oklch(var(--b2));
  border: 1px solid transparent;
  border-radius: 0.5rem;
  color: oklch(var(--bc) / 0.6);
  transition: border-color 150ms ease, background 150ms ease;
}
.patent-toolbar__search:focus-within {
  background: oklch(var(--b1));
  border-color: oklch(var(--p) / 0.4);
  color: oklch(var(--bc));
}
.patent-toolbar__input {
  flex: 1 1 auto;
  border: 0;
  background: transparent;
  outline: none;
  font-size: 0.875rem;
  color: oklch(var(--bc));
  min-width: 0;
}
.patent-toolbar__input::placeholder {
  color: oklch(var(--bc) / 0.5);
}
.patent-toolbar__sort {
  display: inline-flex;
  align-items: center;
  gap: 0.5rem;
  font-size: 0.875rem;
  color: oklch(var(--bc) / 0.7);
}
.patent-toolbar__sort-label {
  white-space: nowrap;
}

/* === Modern Empty States ===
 * Gradient icon disc + headline + body + primary/secondary actions.
 * Replaces the "card with a gray icon and a sentence" daisyUI default.
 * Used by EmptyState (no patents) and ScansEmptyState (no scans).
 */
.empty-state {
  display: flex;
  flex-direction: column;
  align-items: center;
  text-align: center;
  padding: 4rem 1.5rem;
  background: oklch(var(--b1));
  border: 1px solid oklch(var(--b3));
  border-radius: 0.875rem;
  box-shadow: var(--shadow-1);
}
.empty-state__icon {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 4rem;
  height: 4rem;
  border-radius: 50%;
  background: linear-gradient(135deg,
    oklch(var(--p) / 0.12),
    oklch(var(--s) / 0.12));
  border: 1px solid oklch(var(--p) / 0.20);
  color: oklch(var(--p));
  margin-bottom: 1.25rem;
}
.empty-state__icon svg {
  width: 2rem;
  height: 2rem;
  stroke-width: 1.75;
}
.empty-state__title {
  font-family: 'Satoshi', 'Inter', sans-serif;
  font-weight: 700;
  font-size: 1.375rem;
  letter-spacing: -0.01em;
  margin: 0 0 0.5rem;
}
.empty-state__body {
  max-width: 38rem;
  color: oklch(var(--bc) / 0.65);
  line-height: 1.55;
  margin: 0 0 1.5rem;
}
.empty-state__actions {
  display: flex;
  flex-wrap: wrap;
  gap: 0.5rem;
  justify-content: center;
}

/* === Sticky Page Header (Stage D) ===
 * Holds the page title + actions; sticks to the top during scroll.
 * Hairline bottom border fades in only when scrolled (.is-scrolled),
 * toggled by inline JS in layout.templ. Backdrop-blur on the base-200
 * surface; solid fallback for browsers without backdrop-filter.
 */
.page-header {
  position: sticky;
  top: 0;
  z-index: 30;
  background: oklch(var(--b2) / 0.85);
  backdrop-filter: blur(8px);
  -webkit-backdrop-filter: blur(8px);
  padding: 1rem 0;
  margin-bottom: 1rem;
  border-bottom: 1px solid transparent;
  transition: border-color 200ms ease;
}
.page-header.is-scrolled {
  border-bottom-color: oklch(var(--b3));
}
@supports not (backdrop-filter: blur(8px)) {
  .page-header { background: oklch(var(--b2)); }
}
.page-header__breadcrumbs {
  margin-bottom: 0.25rem;
}
.page-header__row {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 1rem;
  flex-wrap: wrap;
}
.page-header__title {
  margin: 0;
  font-family: 'Satoshi', 'Inter', sans-serif;
  font-weight: 700;
  font-size: 1.75rem;
  letter-spacing: -0.01em;
}
.page-header__actions {
  display: flex;
  align-items: center;
  gap: 0.5rem;
  flex-shrink: 0;
  flex-wrap: wrap;
}

/* === Top Progress Bar ===
 * Vercel/GitHub-style 2px progress bar across the top of the viewport
 * during HTMX requests. Toggled by /static/js/transitions.js listening
 * to htmx:beforeRequest and htmx:afterRequest. Indeterminate animation;
 * brand-cyan gradient.
 */
#top-progress {
  position: fixed;
  top: 0;
  left: 0;
  right: 0;
  height: 2px;
  z-index: 200;
  background: transparent;
  pointer-events: none;
  opacity: 0;
  transition: opacity 180ms ease;
}
#top-progress[data-loading="true"] {
  opacity: 1;
}
#top-progress::before {
  content: "";
  position: absolute;
  inset: 0;
  background: linear-gradient(
    90deg,
    transparent 0%,
    oklch(var(--p)) 20%,
    oklch(var(--s)) 50%,
    oklch(var(--p)) 80%,
    transparent 100%
  );
  background-size: 200% 100%;
  animation: top-progress-slide 1.2s cubic-bezier(0.4, 0, 0.2, 1) infinite;
}
@keyframes top-progress-slide {
  0%   { background-position: 200% 0; }
  100% { background-position: -200% 0; }
}
@media (prefers-reduced-motion: reduce) {
  #top-progress::before { animation: none; }
}

/* === Skeleton Loaders (Stage C) ===
 * Shown during HTMX requests via hx-indicator. Shimmer animation reads
 * as "loading" without the spinning-wheel anxiety pattern. Use by adding
 * hx-indicator="#some-skeleton" to a trigger and placing a hidden
 * <div class="skeleton htmx-indicator"> next to the target.
 */
.skeleton {
  background: linear-gradient(
    90deg,
    oklch(var(--b2)) 25%,
    oklch(var(--b3)) 37%,
    oklch(var(--b2)) 63%
  );
  background-size: 400% 100%;
  animation: skeleton-shimmer 1.4s ease infinite;
  border-radius: 0.5rem;
  min-height: 1rem;
}
@keyframes skeleton-shimmer {
  0%   { background-position: 100% 50%; }
  100% { background-position: 0%   50%; }
}
.htmx-indicator {
  display: none;
}
.htmx-request .htmx-indicator,
.htmx-request.htmx-indicator {
  display: block;
}

/* === View Transitions (Stage C) ===
 * 200ms crossfade on full-page navigations. Only active when the browser
 * supports document.startViewTransition (Chrome 111+, Safari 18+).
 */
@view-transition {
  navigation: auto;
}
::view-transition-old(root),
::view-transition-new(root) {
  animation-duration: 200ms;
  animation-timing-function: cubic-bezier(0.2, 0, 0, 1);
}

/* === Modern Toast Stack (Stage C, Sonner-style) ===
 * Bottom-right stack, capped at 4 visible. Slide-in from right + fade.
 * One entry per toast; auto-dismiss is JS-driven (see toast.js).
 *
 * Toast types use the same tint pattern as badges (color/0.08 bg,
 * saturated text, colored leading icon) so the visual language stays
 * coherent with the rest of the status surface.
 */
.toast-stack {
  position: fixed;
  bottom: 1rem;
  right: 1rem;
  z-index: 100;
  display: flex;
  flex-direction: column;
  gap: 0.5rem;
  list-style: none;
  margin: 0;
  padding: 0;
  pointer-events: none;
  max-width: min(calc(100vw - 2rem), 28rem);
}
.toast-item {
  pointer-events: auto;
  display: flex;
  align-items: flex-start;
  gap: 0.75rem;
  padding: 0.75rem 0.875rem;
  border-radius: 0.625rem;
  background: oklch(var(--b1));
  border: 1px solid oklch(var(--b3));
  box-shadow: var(--shadow-2);
  font-size: 0.875rem;
  color: oklch(var(--bc));
  transform: translateX(120%);
  opacity: 0;
  transition:
    transform 250ms cubic-bezier(0.2, 0, 0, 1),
    opacity 250ms cubic-bezier(0.2, 0, 0, 1);
}
.toast-item[data-state="open"] {
  transform: translateX(0);
  opacity: 1;
}
.toast-item[data-state="leaving"] {
  transform: translateX(120%);
  opacity: 0;
}
.toast-icon {
  flex-shrink: 0;
  width: 1.25rem;
  height: 1.25rem;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  font-weight: 700;
  font-size: 0.95rem;
  border-radius: 999px;
  margin-top: 1px;
}
.toast-info    .toast-icon { color: oklch(var(--in)); background: oklch(var(--in) / 0.12); }
.toast-success .toast-icon { color: oklch(var(--su)); background: oklch(var(--su) / 0.12); }
.toast-warning .toast-icon { color: oklch(var(--wa)); background: oklch(var(--wa) / 0.14); }
.toast-error   .toast-icon { color: oklch(var(--er)); background: oklch(var(--er) / 0.12); }

.toast-info    { border-color: oklch(var(--in) / 0.25); }
.toast-success { border-color: oklch(var(--su) / 0.25); }
.toast-warning { border-color: oklch(var(--wa) / 0.30); }
.toast-error   { border-color: oklch(var(--er) / 0.30); }

.toast-body {
  flex: 1 1 auto;
  display: flex;
  flex-direction: column;
  gap: 0.25rem;
  min-width: 0;
}
.toast-message {
  line-height: 1.4;
  word-wrap: break-word;
}
.toast-action {
  align-self: flex-start;
  display: inline-flex;
  align-items: center;
  margin-top: 0.25rem;
  padding: 0.25rem 0.625rem;
  font-size: 0.8125rem;
  font-weight: 600;
  color: oklch(var(--p) / 0.85);
  background: oklch(var(--p) / 0.08);
  border: 1px solid oklch(var(--p) / 0.25);
  border-radius: 0.375rem;
  cursor: pointer;
  text-decoration: none;
  transition: background 150ms ease, border-color 150ms ease;
}
.toast-action:hover {
  background: oklch(var(--p) / 0.15);
  border-color: oklch(var(--p) / 0.4);
}
.toast-close {
  flex-shrink: 0;
  width: 1.5rem;
  height: 1.5rem;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  margin: -0.125rem -0.25rem -0.125rem 0;
  padding: 0;
  font-size: 1.25rem;
  line-height: 1;
  color: oklch(var(--bc) / 0.5);
  background: transparent;
  border: 0;
  border-radius: 0.25rem;
  cursor: pointer;
  transition: color 150ms ease, background 150ms ease;
}
.toast-close:hover {
  color: oklch(var(--bc));
  background: oklch(var(--bc) / 0.06);
}

/* Respect reduced motion: skip the slide animation; keep fade for state cue */
@media (prefers-reduced-motion: reduce) {
  .toast-item {
    transform: none !important;
    transition: opacity 150ms ease;
  }
  ::view-transition-old(root),
  ::view-transition-new(root) {
    animation: none !important;
  }
  .skeleton {
    animation: none;
  }
}

/* === Active Nav Indicator (Stage B) ===
 * Cyan 2px underline on the top-level nav link whose href matches the
 * current path. data-active is set by an inline script in layout.templ
 * at DOMContentLoaded and after each htmx:afterSettle.
 */
.nav-link {
  position: relative;
}
.nav-link[data-active="true"] {
  color: oklch(var(--bc));
}
.nav-link[data-active="true"]::after {
  content: "";
  position: absolute;
  left: 0.85rem;
  right: 0.85rem;
  bottom: -2px;
  height: 2px;
  background: oklch(var(--p));
  border-radius: 1px;
  pointer-events: none;
}
