/* components.css — dropzone, cards, engine toggle, spinner, dialog */
@layer components {
  /* CSS override fix: the browser's user-agent rule `[hidden] { display: none }`
     has attribute-selector specificity (0,1,0). Any class rule with a custom
     `display` value (like `.status { display: inline-flex }`) has the same
     specificity — and because `@layer components` cascades AFTER user-agent
     styles, the class rule wins and `hidden` is silently ignored.
     Forcing every element with the `hidden` attribute to `display: none`
     inside our own layer restores the expected behavior without having to
     remember to override it per-component. */
  [hidden] {
    display: none !important;
  }

  /* Old dropzone label is removed in favor of the topbar search-pill plus
     a window-wide drop overlay. The class is retained as no-op for any
     stale references; the new affordances are .topbar / .searchbar /
     .toolbar / .drop-overlay below. */
  .dropzone {
    display: none;
  }

  /* ── Topbar — single row of liquid-glass pills, all 44px tall ────────── */
  .topbar {
    position: sticky;
    inset-block-start: 0;
    z-index: 50;
    display: flex;
    align-items: center;
    gap: var(--space-s);
    padding-inline: var(--topbar-pad-x);
    padding-block: var(--space-xs);
    padding-block-start: max(var(--space-xs), env(safe-area-inset-top));
    /* Fully transparent — no tint, no blur. The user has asked four
       times to drop the topbar's container chrome; the only visible
       elements should be the searchbar pill plus the bare icon buttons.
       The topbar itself is just a positioning context, nothing more. */
    background: transparent;
  }

  /* Search pill: pick button (left, opens file picker) + text input
     (middle, Enter fires a text-search on both engines) + clear button
     (right, appears when input has text). 8px inline padding + 4px grid
     gap give the icons room to breathe from the input edge — earlier
     versions sat at 4px / no gap and felt cramped. Centered in the
     topbar; pill grows to fill column 2 up to --searchbar-max-w. */
  .searchbar {
    flex: 1 1 auto;
    justify-self: center;
    display: grid;
    grid-template-columns: auto minmax(0, 1fr) auto;
    align-items: center;
    gap: var(--space-xs);
    block-size: var(--searchbar-h);
    padding-inline: var(--space-s);
    border-radius: var(--searchbar-radius);
    color: var(--fg);
    inline-size: min(var(--searchbar-max-w), 100%);
    margin-inline: auto;
    /* Background, backdrop-filter, and the layered inset highlights come
       from the .liquid-glass mixin applied alongside .searchbar in the
       HTML — no explicit border, no flat background here. The outline
       hairline from the glass class draws the rim. */
  }

  .searchbar:focus-within {
    /* Subtle accent ring on focus, drawn outside the glass outline */
    box-shadow:
      inset 0 1px 0 var(--glass-highlight),
      inset 0 -1px 0 var(--glass-shadow-inset),
      0 0 0 0.5px var(--glass-hairline),
      var(--glass-drop),
      0 0 0 4px color-mix(in oklab, var(--accent) 30%, transparent);
  }

  @media (prefers-reduced-motion: no-preference) {
    .searchbar {
      transition: box-shadow 180ms var(--ease-out-quint);
    }
  }

  .searchbar-pick,
  .searchbar-clear {
    display: grid;
    place-items: center;
    inline-size: 2rem;
    block-size: 2rem;
    padding: 0;
    border: 0;
    border-radius: 999px;
    background: transparent;
    color: var(--muted);
    cursor: pointer;
    transition: color 0.14s ease, background-color 0.14s ease;
  }

  .searchbar-pick:hover,
  .searchbar-clear:hover {
    color: var(--fg);
    background: color-mix(in oklab, var(--fg) 7%, transparent);
  }

  .searchbar-pick:focus-visible,
  .searchbar-clear:focus-visible {
    outline: 2px solid var(--accent);
    outline-offset: 2px;
    color: var(--fg);
  }

  .searchbar-pick svg,
  .searchbar-clear svg {
    inline-size: var(--searchbar-icon);
    block-size: var(--searchbar-icon);
  }

  .searchbar-text {
    background: transparent;
    border: 0;
    outline: 0;
    font: inherit;
    color: var(--fg);
    min-inline-size: 0;
    padding-inline: var(--space-s);
  }

  .searchbar-text::placeholder {
    color: var(--muted);
  }

  /* Hide the native search-input cancel "X" so we don't double-show with
     our custom clear button. */
  .searchbar-text::-webkit-search-cancel-button,
  .searchbar-text::-webkit-search-decoration {
    appearance: none;
  }

  /* Right-side cluster: account label + settings + logout — same height
     as the searchbar pill, liquid-glass surface. */
  .topbar-meta {
    flex: 0 0 auto;
    display: inline-flex;
    align-items: center;
    gap: var(--space-xs);
    block-size: var(--searchbar-h);
    padding-inline: var(--space-s);
    border-radius: var(--searchbar-radius);
    color: var(--fg);
    font-size: var(--type-small);
    white-space: nowrap;
  }

  /* Compact user-glyph: a small circular initial + tooltip showing the
     full username on hover. Replaces the long "Signed in as <name>"
     string that was making the right pill way too verbose. */
  .topbar-meta .auth-user {
    display: grid;
    place-items: center;
    inline-size: 1.75rem;
    block-size: 1.75rem;
    border-radius: 50%;
    background: color-mix(in oklab, var(--accent) 22%, transparent);
    color: var(--fg);
    font-size: 0.78rem;
    font-weight: 600;
    letter-spacing: 0;
  }

  .topbar-meta .auth-user:empty {
    display: none;
  }

  .topbar-meta .icon-btn {
    inline-size: 2rem;
    block-size: 2rem;
    padding: 0;
    display: grid;
    place-items: center;
    border-radius: 50%;
    background: transparent;
    border: 0;
    color: var(--muted);
    cursor: pointer;
    transition: color 0.12s ease, background-color 0.12s ease;
  }

  .topbar-meta .icon-btn:hover {
    color: var(--fg);
    background: color-mix(in oklab, var(--fg) 8%, transparent);
  }

  .topbar-meta .icon-btn svg {
    inline-size: 1.125rem;
    block-size: 1.125rem;
  }

  /* Left-side cluster: view-mode segmented control + zoom buttons inside a
     single liquid-glass pill of matching height. */
  .topbar > .view-controls {
    flex: 0 0 auto;
    display: inline-flex;
    align-items: center;
    gap: var(--space-xs);
    block-size: var(--searchbar-h);
    padding-inline: var(--space-xs);
    border-radius: var(--searchbar-radius);
    margin-block-end: 0;
    color: var(--fg);
  }

  .topbar > .view-controls .view-mode {
    display: inline-flex;
    align-items: center;
    gap: 0;
    padding: 0;
    margin: 0;
    border: 0;
    border-radius: 999px;
    background: transparent;
  }

  .topbar > .view-controls .view-mode label {
    display: grid;
    place-items: center;
    inline-size: 2rem;
    block-size: 2rem;
    padding: 0;
    border-radius: 999px;
    color: var(--muted);
    cursor: pointer;
    transition: color 0.12s ease, background-color 0.12s ease;
  }

  .topbar > .view-controls .view-mode label svg {
    inline-size: 1.05rem;
    block-size: 1.05rem;
  }

  .topbar > .view-controls .view-mode label:has(input:checked) {
    background: color-mix(in oklab, var(--accent) 22%, transparent);
    color: var(--fg);
  }

  .topbar > .view-controls .view-mode label:hover {
    color: var(--fg);
  }

  .topbar > .view-controls .zoom-control button {
    inline-size: 1.65rem;
    block-size: 1.65rem;
    border: 0;
    background: transparent;
    color: var(--fg);
    font-size: 0.95rem;
    line-height: 1;
    border-radius: 999px;
    cursor: pointer;
    transition: background-color 0.12s ease;
  }

  .topbar > .view-controls .zoom-control button:hover {
    background: color-mix(in oklab, var(--fg) 8%, transparent);
  }

  /* Mobile (portrait): two-row topbar.
     Row 1 — view-controls + meta (icon-only, no pill chrome).
     Row 2 — searchbar full-width at the BOTTOM, thumb-reach for typing.
     Pattern matches iOS Safari / Photos navigation conventions. */
  @media (aspect-ratio < 3/4) {
    .topbar {
      flex-wrap: wrap;
      row-gap: var(--space-s);
    }

    .topbar > .view-controls {
      order: 0;
      flex: 0 0 auto;
    }

    .topbar-meta {
      order: 1;
      flex: 0 0 auto;
      margin-inline-start: auto;
    }

    .searchbar {
      order: 2;
      flex: 1 1 100%;
    }

    .searchbar-label {
      min-inline-size: 0;
    }
  }

  /* ── Secondary toolbar (limits + re-run) ───────────────────────────── */
  .toolbar {
    display: inline-flex;
    flex-wrap: wrap;
    gap: var(--space-s);
    align-items: center;
    margin-inline-start: auto;
    margin-inline-end: auto;
    color: var(--muted);
    font-size: var(--type-small);
  }

  /* ── Liquid-glass mixin (per research-liquid-glass.md) ──────────────── */
  .liquid-glass {
    background-color: var(--glass-tint);
    backdrop-filter: var(--glass-blur);
    -webkit-backdrop-filter: var(--glass-blur);
    box-shadow:
      inset 0 1px 0 var(--glass-highlight),
      inset 0 -1px 0 var(--glass-shadow-inset),
      0 0 0 0.5px var(--glass-hairline),
      var(--glass-drop);
    isolation: isolate;
    position: relative;
  }

  /* Specular sheen — soft wedge, low alpha. Depth comes from layered
     contact (inset highlight + hairline + drop), not from a paint streak. */
  .liquid-glass::before {
    content: "";
    position: absolute;
    inset: 0;
    border-radius: inherit;
    background: linear-gradient(
      155deg,
      rgba(255, 255, 255, 0.22) 0%,
      rgba(255, 255, 255, 0.05) 30%,
      rgba(255, 255, 255, 0) 50%
    );
    mix-blend-mode: overlay;
    pointer-events: none;
  }

  @supports not ((backdrop-filter: blur(1px)) or (-webkit-backdrop-filter: blur(1px))) {
    .liquid-glass {
      background-color: var(--glass-fallback);
      backdrop-filter: none;
      -webkit-backdrop-filter: none;
    }
  }

  @media (prefers-reduced-motion: reduce) {
    .liquid-glass::before {
      display: none;
    }
  }

  @media (prefers-contrast: more) {
    .liquid-glass {
      background-color: light-dark(#fff, #000);
      backdrop-filter: none;
      -webkit-backdrop-filter: none;
    }
    .liquid-glass::before {
      display: none;
    }
  }

  /* ── Empty-state placeholder (rendered in <main> when no tiles exist) ─ */
  .empty-state {
    display: flex;
    flex-direction: column;
    align-items: center;
    justify-content: center;
    gap: var(--space-l);
    min-block-size: 60dvh;
    color: var(--fg);
    text-align: center;
    cursor: pointer;
    border-radius: var(--radius-l);
    transition: background-color 200ms var(--ease-out-quint);
  }

  @media (hover: hover) {
    .empty-state:hover {
      background-color: color-mix(in oklab, var(--accent) 6%, transparent);
    }
  }

  .empty-state:focus-visible {
    outline: 2px solid var(--accent);
    outline-offset: 4px;
  }

  .empty-state[hidden] {
    display: none;
  }

  .empty-state-glyph {
    inline-size: clamp(3.5rem, 8vw, 6rem);
    block-size: clamp(3.5rem, 8vw, 6rem);
    color: var(--accent);
    animation: drop-pulse 1.8s ease-in-out infinite;
  }

  .empty-state-glyph svg {
    inline-size: 100%;
    block-size: 100%;
  }

  .empty-state-title {
    font-size: clamp(1.25rem, 2vw, 1.5rem);
    font-weight: 600;
    letter-spacing: -0.01em;
    margin: 0;
  }

  .empty-state-sub {
    color: var(--muted);
    font-size: var(--type-small);
  }

  /* ── Window-wide drop overlay (full-bleed, no inner card) ──────────── */
  .drop-overlay {
    position: fixed;
    inset: 0;
    z-index: 100;
    display: flex;
    flex-direction: column;
    align-items: center;
    justify-content: center;
    gap: var(--space-l);
    pointer-events: none;
    color: var(--fg);
    background-color: var(--glass-tint-strong);
    backdrop-filter: var(--glass-blur-strong);
    -webkit-backdrop-filter: var(--glass-blur-strong);
    animation: drop-fade-in 0.22s var(--ease-out-quint);
    text-align: center;
  }

  .drop-overlay[hidden] {
    display: none;
  }

  .drop-overlay-glyph {
    inline-size: clamp(3.5rem, 8vw, 6rem);
    block-size: clamp(3.5rem, 8vw, 6rem);
    color: var(--accent);
    animation: drop-pulse 1.8s ease-in-out infinite;
  }

  .drop-overlay-glyph svg {
    inline-size: 100%;
    block-size: 100%;
  }

  .drop-overlay-title {
    font-size: clamp(1.25rem, 2vw, 1.5rem);
    font-weight: 600;
    letter-spacing: -0.01em;
    margin: 0;
  }

  .drop-overlay-sub {
    color: var(--muted);
    font-size: var(--type-small);
  }

  @keyframes drop-fade-in {
    from {
      opacity: 0;
    }
    to {
      opacity: 1;
    }
  }

  @keyframes drop-pulse {
    0%, 100% {
      transform: translateY(0);
      opacity: 0.85;
    }
    50% {
      transform: translateY(-0.4rem);
      opacity: 1;
    }
  }

  @media (prefers-reduced-motion: reduce) {
    .drop-overlay {
      animation: none;
    }
    .drop-overlay-glyph {
      animation: none;
    }
  }

  /* ── Always-hot selection circle (top-left of every card) ──────────── */
  /* Selection ring + download icon — both Liquid Glass pills with the
     SVG/check glyph stacked over the visible circle via grid-area:1/1
     overlap. The 44 px hit area sits transparent around the visual
     22 px circle so touch targets stay HIG-compliant. */
  .card-select,
  .card-download {
    position: absolute;
    inset-block-start: var(--space-xs);
    inline-size: var(--select-hit);
    block-size: var(--select-hit);
    display: grid;
    place-items: center;
    background: transparent;
    border: 0;
    cursor: pointer;
    z-index: 3;
    padding: 0;
    color: #ffffff;
  }

  .card-select {
    inset-inline-start: var(--space-xs);
  }

  .card-download {
    inset-inline-end: var(--space-xs);
  }

  /* Visual glass circle — both ::before and the inner glyph occupy
     grid-area 1/1 so they perfectly overlap. align-self/justify-self
     pin to cell center so a hover scale on the parent doesn't shift
     anything off-axis. mattemotto: the icon and its frame should
     breathe the same air. */
  .card-select::before,
  .card-download::before {
    content: "";
    grid-area: 1 / 1;
    align-self: center;
    justify-self: center;
    inline-size: var(--select-visual);
    block-size: var(--select-visual);
    border-radius: 50%;
    background-color: var(--glass-tint-strong);
    backdrop-filter: blur(14px) saturate(1.6);
    -webkit-backdrop-filter: blur(14px) saturate(1.6);
    border: 0.5px solid rgba(255, 255, 255, 0.5);
    box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.32), 0 1px 3px rgba(0, 0, 0, 0.35);
    transition:
      background-color 160ms var(--ease-out-quint),
      transform 220ms var(--ease-spring),
      box-shadow 160ms var(--ease-out-quint);
  }

  /* Check glyph (stroke), white on accent fill when selected. Pure
     rotate around the cell center — no manual translate fudge factor
     (the previous translate was a leftover from an older layout that
     no longer aligns with the current grid centering). */
  .card-select::after {
    content: "";
    grid-area: 1 / 1;
    align-self: center;
    justify-self: center;
    inline-size: 0.65rem;
    block-size: 0.36rem;
    border-inline-start: 2px solid #ffffff;
    border-block-end: 2px solid #ffffff;
    transform: rotate(-45deg) translate3d(0, -1px, 0);
    transform-origin: center;
    opacity: 0;
    z-index: 1;
    transition: opacity 140ms ease;
    pointer-events: none;
  }

  .card[aria-selected="true"] .card-select::before {
    background-color: var(--accent);
    border-color: rgba(255, 255, 255, 0.6);
    transform: scale(1.06);
    box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.4), 0 2px 6px rgba(0, 122, 255, 0.4);
  }

  .card[aria-selected="true"] .card-select::after {
    opacity: 1;
  }

  @media (prefers-reduced-motion: reduce) {
    .card[aria-selected="true"] .card-select::before {
      transform: none;
    }
  }

  /* Download SVG — same grid cell as the glass circle, explicit
     align/justify-self so it pins to the cell center even when the
     cell auto-sizes to the larger ::before circle. */
  .card-download svg {
    grid-area: 1 / 1;
    align-self: center;
    justify-self: center;
    inline-size: 0.85rem;
    block-size: 0.85rem;
    z-index: 1;
    /* Subtle contact shadow so the white glyph reads on busy thumbs */
    filter: drop-shadow(0 1px 1px rgba(0, 0, 0, 0.4));
  }

  .card-download:hover::before {
    background-color: var(--accent);
    border-color: rgba(255, 255, 255, 0.6);
  }

  .card-download:active {
    transform: scale(0.94);
  }

  /* ── Provider badge (bottom-left, sits on caption gradient) ────────── */
  .card-provider {
    position: absolute;
    inset-block-end: var(--space-xs);
    inset-inline-start: var(--space-xs);
    inline-size: 1.25rem;
    block-size: 1.25rem;
    display: grid;
    place-items: center;
    border-radius: 50%;
    background: rgba(255 255 255 / 0.9);
    pointer-events: none;
    z-index: 2;
    box-shadow: 0 1px 2px rgba(0 0 0 / 0.4);
  }

  .card-provider svg {
    inline-size: 0.85rem;
    block-size: 0.85rem;
  }

  /* ── Bulk action bar (bottom-center floating Liquid Glass pill) ────── */
  .action-bar {
    position: fixed;
    inset-inline: 0;
    inset-block-end: max(var(--space-m), env(safe-area-inset-bottom));
    margin-inline: auto;
    inline-size: max-content;
    max-inline-size: calc(100vw - 2 * var(--space-m));
    padding: var(--space-xs) var(--space-s);
    border-radius: 999px;
    display: inline-flex;
    gap: var(--space-s);
    align-items: center;
    z-index: 60;
    font-size: var(--type-small);
    color: light-dark(#0a0a0a, #f2f2f0);
  }

  .action-bar[hidden] {
    display: none;
  }

  .action-count {
    font-weight: 500;
    padding-inline-start: var(--space-xs);
    white-space: nowrap;
  }

  .action-primary {
    padding-inline: var(--space-m);
    padding-block: calc(var(--space-xs) + 0.1rem);
    background: var(--accent);
    color: var(--accent-ink);
    border: 0;
    border-radius: 999px;
    font: inherit;
    font-weight: 600;
    cursor: pointer;
    transition: filter 0.12s ease, transform 0.12s ease;
  }

  .action-primary:hover {
    filter: brightness(1.08);
  }

  .action-primary:active {
    transform: scale(0.97);
  }

  .action-clear {
    inline-size: 1.75rem;
    block-size: 1.75rem;
    border-radius: 50%;
    border: 0;
    background: light-dark(rgba(0 0 0 / 0.06), rgba(255 255 255 / 0.08));
    color: inherit;
    font-size: 1.1rem;
    line-height: 1;
    cursor: pointer;
    transition: background 0.12s ease;
  }

  .action-clear:hover {
    background: light-dark(rgba(0 0 0 / 0.12), rgba(255 255 255 / 0.16));
  }

  /* tile — one per dropped image. The container is invisible per user
     spec: no border, no background, no padding-inline. The full viewport
     is the canvas; results breathe edge-to-edge. The head row is the
     only piece with chrome. */
  .tile {
    display: grid;
    gap: var(--space-m);
    min-inline-size: 0;
    padding-block: var(--space-l);
  }

  .tile + .tile {
    border-block-start: var(--hairline) solid var(--border);
  }

  /* Status colors live on the engine chips now, not the tile shell. */
  .tile[data-status="error"] {
    /* keep the per-tile error signal subtle on the head only */
  }

  .tile-head {
    display: grid;
    grid-template-columns: auto minmax(0, 1fr) auto;
    gap: var(--space-m);
    align-items: center;
  }

  .tile-thumb {
    inline-size: 5rem;
    block-size: 5rem;
    object-fit: cover;
    border-radius: var(--radius-m);
    background: color-mix(in oklab, var(--fg) 5%, var(--bg));
  }

  /* Text-query tile: same footprint as the file-thumb, but renders a
     centered magnifier glyph instead of an image. */
  .tile-thumb-text {
    display: grid;
    place-items: center;
    color: var(--accent);
  }

  .tile-thumb-text svg {
    inline-size: 60%;
    block-size: 60%;
  }

  .tile-info {
    display: grid;
    gap: calc(var(--space-xs) / 2);
    min-inline-size: 0;
  }

  .tile-name {
    font-weight: 600;
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
  }

  .tile-sub {
    color: var(--muted);
    font-size: var(--type-small);
  }

  .tile-status-line {
    color: var(--muted);
    font-size: var(--type-small);
    font-variant-numeric: tabular-nums;

    .tile[data-status="running"] &,
    .tile[data-status="partial"] & {
      color: var(--accent);
    }

    .tile[data-status="done"] & {
      color: color-mix(in oklab, var(--fg) 75%, var(--bg));
    }

    .tile[data-status="error"] & {
      color: var(--danger);
    }
  }

  .tile-actions {
    display: inline-flex;
    gap: var(--space-xs);
    align-items: center;
  }

  /* Per-batch icon buttons reuse .icon-btn for cohesion with the topbar.
     Subtle hover + focus states. */
  .tile-actions .icon-btn {
    color: var(--muted);
  }

  .tile-actions .icon-btn:hover {
    color: var(--fg);
  }

  .tile-action-rerun:hover {
    color: var(--accent);
  }

  .tile-action-remove:hover {
    color: var(--danger);
  }

  .tile-action-collapse svg {
    transition: transform 0.2s var(--ease-out-quint);
  }

  .tile[data-collapsed="true"] .tile-action-collapse svg {
    transform: rotate(-90deg);
  }

  .tile[data-collapsed="true"] .tile-engine-status,
  .tile[data-collapsed="true"] .tile-results,
  .tile[data-collapsed="true"] .result-grid {
    display: none;
  }

  /* ── Per-tile engine status row + unified results grid ──────────── */
  .tile-engine-status {
    display: inline-flex;
    flex-wrap: wrap;
    gap: var(--space-s);
    align-items: center;
    color: var(--muted);
    font-size: var(--type-small);
  }

  .engine-chip {
    display: inline-flex;
    align-items: center;
    gap: var(--space-xs);
    padding-inline: var(--space-s);
    padding-block: calc(var(--space-xs) / 1.5);
    border-radius: 999px;
    background: var(--card-bg);
    border: var(--hairline) solid var(--border);
    font-variant-numeric: tabular-nums;
    transition: border-color 0.12s ease, color 0.12s ease;
  }

  .engine-chip-icon {
    display: grid;
    place-items: center;
    inline-size: 1rem;
    block-size: 1rem;
  }

  .engine-chip-icon svg {
    inline-size: 100%;
    block-size: 100%;
  }

  .engine-chip[data-state="running"] {
    border-color: var(--accent);
    color: var(--accent);
  }

  .engine-chip[data-state="done"] {
    color: color-mix(in oklab, var(--fg) 75%, var(--bg));
  }

  .engine-chip[data-state="error"] {
    border-color: var(--danger);
    color: var(--danger);
  }

  .tile-results {
    /* unified-grid lives in the same .result-grid layout used elsewhere. */
    min-inline-size: 0;
  }

  /* Keep old bucket rules around in case something still references them
     (links, old states), but the new tiles never render this DOM. */
  .tile-buckets {
    display: grid;
    grid-template-columns: minmax(0, 1fr);
    gap: var(--space-m);
    min-inline-size: 0;
  }

  .tile-buckets .bucket {
    display: grid;
    gap: var(--space-s);
    min-inline-size: 0;
  }

  .tile-buckets .bucket-head {
    display: flex;
    align-items: baseline;
    gap: var(--space-s);
    flex-wrap: wrap;
  }

  .tile-buckets .bucket-head h3 {
    font-size: calc(var(--type-h2) * 0.85);
    letter-spacing: -0.01em;
  }

  .tile-buckets .bucket-meta {
    color: var(--muted);
    font-size: var(--type-small);
    font-variant-numeric: tabular-nums;
  }

  .tile-buckets .bucket[data-state="running"] .bucket-meta {
    color: var(--accent);
  }

  .tile-buckets .bucket[data-state="error"] .bucket-meta {
    color: var(--danger);
  }

  .tile-retry {
    color: var(--danger);
  }

  .grid-sentinel {
    grid-column: 1 / -1;
    block-size: 1px;
    inline-size: 100%;
  }

  .engine-toggle {
    display: flex;
    flex-wrap: wrap;
    gap: var(--space-xs);
    padding: var(--space-xs);
    background: var(--card-bg);
    border: var(--hairline) solid var(--border);
    border-radius: 999px;

    & legend {
      position: absolute;
      inline-size: 1px;
      block-size: 1px;
      overflow: hidden;
      clip: rect(0 0 0 0);
    }

    & label {
      display: inline-flex;
      align-items: center;
      gap: var(--space-xs);
      padding-inline: var(--space-m);
      padding-block: var(--space-xs);
      border-radius: 999px;
      cursor: pointer;
      font-size: var(--type-small);
      color: var(--muted);
      transition: background-color 140ms ease, color 140ms ease;
    }

    & label:has(input:checked) {
      background: var(--accent);
      color: var(--accent-ink);
    }

    & input {
      appearance: none;
      inline-size: 0;
      block-size: 0;
      margin: 0;
    }
  }

  .limit-field {
    display: inline-flex;
    align-items: center;
    gap: var(--space-s);
    padding-inline: var(--space-m);
    padding-block: var(--space-xs);
    background: var(--card-bg);
    border: var(--hairline) solid var(--border);
    border-radius: 999px;
    font-size: var(--type-small);
    color: var(--muted);

    & .limit-label {
      white-space: nowrap;
    }

    & input[type="number"] {
      inline-size: 4.5rem;
      padding-inline: var(--space-xs);
      padding-block: calc(var(--space-xs) / 2);
      background: transparent;
      border: var(--hairline) solid var(--border);
      border-radius: var(--radius-s);
      color: var(--fg);
      font: inherit;
      text-align: center;

      &:focus-visible {
        outline: 2px solid var(--accent);
        outline-offset: 2px;
      }

      /* Hide browser default spinner buttons on webkit */
      &::-webkit-inner-spin-button,
      &::-webkit-outer-spin-button {
        appearance: none;
        margin: 0;
      }
    }
  }

  .submit-btn {
    padding-inline: var(--space-l);
    padding-block: var(--space-s);
    background: var(--accent);
    color: var(--accent-ink);
    border-radius: 999px;
    font-weight: 600;
    transition: transform 120ms ease, opacity 120ms ease;

    &:disabled {
      opacity: 0.6;
      cursor: progress;
    }

    &:active {
      transform: scale(0.98);
    }
  }

  .status {
    display: inline-flex;
    align-items: center;
    gap: var(--space-s);
    padding: var(--space-s) var(--space-m);
    background: var(--card-bg);
    border: var(--hairline) solid var(--border);
    border-radius: var(--radius-m);
    color: var(--muted);
    font-size: var(--type-small);
    justify-self: start;

    &.error {
      color: var(--danger);
      border-color: var(--danger);
    }
  }

  .spinner {
    inline-size: 1rem;
    block-size: 1rem;
    border-radius: 50%;
    border: 2px solid var(--border);
    border-block-start-color: var(--accent);
    animation: spin 0.9s linear infinite;
  }

  @keyframes spin {
    to {
      transform: rotate(360deg);
    }
  }

  /* ── View-mode toggle + zoom (segmented control) ─────────────────── */
  .sr-only {
    position: absolute;
    inline-size: 1px;
    block-size: 1px;
    overflow: hidden;
    clip: rect(0 0 0 0);
    white-space: nowrap;
  }

  .view-controls {
    display: inline-flex;
    flex-wrap: wrap;
    gap: var(--space-m);
    align-items: center;
    margin-block-end: var(--space-s);
    color: var(--muted);
    font-size: var(--type-small);
  }

  .view-controls[hidden] {
    display: none;
  }

  .view-mode {
    display: inline-flex;
    align-items: stretch;
    gap: 0;
    padding: 0.15rem;
    margin: 0;
    border: var(--hairline) solid var(--border);
    border-radius: 999px;
    background: var(--card-bg);
  }

  .view-mode legend {
    position: absolute;
    inline-size: 1px;
    block-size: 1px;
    overflow: hidden;
    clip: rect(0 0 0 0);
    white-space: nowrap;
  }

  .view-mode label {
    display: inline-flex;
    align-items: center;
    padding-inline: var(--space-m);
    padding-block: calc(var(--space-xs) / 1.2);
    border-radius: 999px;
    cursor: pointer;
    color: var(--muted);
    transition: background-color 0.12s ease, color 0.12s ease;
  }

  .view-mode label:has(input:checked) {
    background: var(--accent);
    color: var(--accent-ink);
  }

  .view-mode input[type="radio"] {
    position: absolute;
    inline-size: 1px;
    block-size: 1px;
    overflow: hidden;
    clip: rect(0 0 0 0);
    appearance: none;
    margin: 0;
  }

  .zoom-control {
    display: inline-flex;
    gap: var(--space-xs);
    align-items: center;
  }

  .zoom-control button {
    inline-size: 1.75rem;
    block-size: 1.75rem;
    border-radius: 50%;
    border: var(--hairline) solid var(--border);
    background: var(--card-bg);
    color: var(--fg);
    font-size: 1rem;
    line-height: 1;
    cursor: pointer;
    transition: border-color 0.12s ease, background 0.12s ease;
  }

  .zoom-control button:hover {
    border-color: var(--border-strong);
  }

  /* Zoom is available in every view mode; semantics differ per mode:
     grid → mutates --card-min, masonry-v → mutates --masonry-cols,
     masonry-h → mutates --row-height. JS handles which token. */

  /* ── Per-view layouts ─────────────────────────────────────────────── */
  /* Grid (default) — auto-fill minmax driven by --card-min token. */
  body[data-view="grid"] .result-grid {
    display: grid;
    grid-template-columns:
      repeat(
        auto-fill,
        minmax(var(--card-min, 10rem), 1fr)
      );
    gap: var(--space-s);
  }

  body[data-view="grid"] .result-grid .card {
    aspect-ratio: 1 / 1;
  }

  /* Pinterest-style vertical masonry — CSS Grid + per-card row-span.
     `column-count` looks right but the spec balances column heights
     against unconstrained height (it equalizes by pushing shorter
     columns DOWN), which is exactly the misalignment users see.
     Grid anchors every column to the top by construction; JS sets
     each card's --card-row-span from its measured height after the
     thumbnail loads. mattemotto: top row, flush, no negotiation. */
  body[data-view="masonry-v"] .result-grid {
    display: grid;
    grid-template-columns: repeat(var(--masonry-cols, 3), minmax(0, 1fr));
    grid-auto-rows: 0.5rem; /* 8 px row unit — matches GAP_PX in app.js */
    gap: var(--space-s);
    padding-block-start: 0;
  }

  body[data-view="masonry-v"] .result-grid .card {
    display: block;
    /* Default span = 1 (one 8 px sliver). The card grows to its
       analytical span as soon as setMasonryRowSpan runs — pre-seeded
       from meta.w/meta.h on render, refined on image load. The old
       default of 50 produced 792 px black tails for any card that
       hadn't loaded yet. */
    grid-row: span var(--card-row-span, 1);
    margin: 0;
    aspect-ratio: auto;
    inline-size: 100%;
  }

  body[data-view="masonry-v"] .result-grid .card img {
    aspect-ratio: auto;
    inline-size: 100%;
    block-size: auto;
    object-fit: contain;
  }

  /* Horizontal masonry — flex-basis from aspect ratio (the user's recipe).
     --w and --h come from img.naturalWidth/Height set on load. */
  body[data-view="masonry-h"] .result-grid {
    display: flex;
    flex-wrap: wrap;
    gap: var(--space-xs);
  }

  body[data-view="masonry-h"] .result-grid .card {
    --ratio: calc(var(--w, 1) / var(--h, 1));
    flex-basis: calc(var(--ratio, 1) * var(--row-height, 14rem));
    flex-grow: calc(var(--ratio, 1) * 100);
    min-inline-size: 6rem;
    block-size: var(--row-height, 14rem);
    aspect-ratio: auto;
  }

  @media (aspect-ratio < 3/4) {
    body[data-view="masonry-h"] .result-grid {
      --row-height: 9rem;
    }
  }

  body[data-view="masonry-h"] .result-grid .card img {
    aspect-ratio: auto;
    inline-size: 100%;
    block-size: 100%;
    object-fit: cover;
  }

  @media (aspect-ratio < 3/4) {
    body[data-view="masonry-h"] .result-grid {
      --row-height: 9rem;
    }
  }

  /* HD badge color tokens — tweakable in one place. */
  :root {
    --hd-on-bg: var(--accent);
    --hd-on-ink: var(--accent-ink);
    --hd-off-border: rgba(255 255 255 / 0.85);
    --hd-off-ink: rgba(255 255 255 / 0.95);
    --hd-loading-ring: var(--accent);
  }

  .card {
    /* --w/--h default placeholder aspect (4:5 portrait — close to the
       cosmos + Lens median). Real values are pre-seeded from meta.w/h
       on render and refined on image load. The masonry span formula
       reads these so the card occupies plausible space immediately
       instead of collapsing to an 8 px sliver. */
    --w: 4;
    --h: 5;
    position: relative;
    display: block;
    aspect-ratio: 1 / 1;
    overflow: hidden;
    border-radius: var(--radius-m);
    background: var(--card-bg);
    border: var(--hairline) solid var(--border);
    transition: border-color 150ms ease;

    & img {
      inline-size: 100%;
      block-size: 100%;
      object-fit: cover;
      opacity: 0;
    }

    /* Image visible once the load handler tags the card. The card-
       img-settle keyframe animation (in the motion-polish block below)
       choreographs the appear with scale + opacity ramp; this rule
       defines the resting state. */
    &.has-loaded img {
      opacity: 1;
    }

    /* Skeleton — quietly breathing diagonal until the image arrives.
       Painted via ::before so it stacks BEHIND the img (CSS paint order
       for pseudo-elements: ::before, content, ::after). The image fades
       in on .has-loaded; the skeleton vanishes when its content rule
       is no longer satisfied. mattemotto: the wait should never feel empty. */
    &:not(.has-loaded)::before {
      content: "";
      position: absolute;
      inset: 0;
      background: linear-gradient(
        135deg,
        color-mix(in oklab, var(--card-bg) 92%, var(--fg) 8%),
        color-mix(in oklab, var(--card-bg) 96%, var(--fg) 4%) 50%,
        color-mix(in oklab, var(--card-bg) 92%, var(--fg) 8%)
      );
      background-size: 200% 200%;
      pointer-events: none;
    }

    @media (prefers-reduced-motion: no-preference) {
      &:not(.has-loaded)::before {
        animation: card-skeleton 1.6s ease-in-out infinite;
      }
    }

    & .hd-badge {
      position: absolute;
      inset-block-start: var(--space-xs);
      inset-inline-end: var(--space-xs);
      inline-size: 1.6rem;
      block-size: 1.6rem;
      display: grid;
      place-items: center;
      border-radius: 999px;
      font-size: 0.6rem;
      font-weight: 700;
      letter-spacing: 0.04em;
      line-height: 1;
      font-family: ui-sans-serif, system-ui, sans-serif;
      user-select: none;
      pointer-events: none;
      backdrop-filter: blur(4px);
      background: rgba(0 0 0 / 0.45);
      color: var(--hd-off-ink);
      border: 1px solid var(--hd-off-border);
      transition: background-color 120ms ease, color 120ms ease, border-color 120ms ease;
    }

    &[data-hd="on"] .hd-badge {
      background: var(--hd-on-bg);
      color: var(--hd-on-ink);
      border-color: transparent;
    }

    &[data-hd="loading"] .hd-badge {
      color: transparent;
      border-color: var(--border);
      background: rgba(0 0 0 / 0.35);
    }

    &[data-hd="loading"] .hd-badge::after {
      content: "";
      position: absolute;
      inline-size: 0.9rem;
      block-size: 0.9rem;
      border-radius: 50%;
      border: 2px solid rgba(255 255 255 / 0.25);
      border-block-start-color: var(--hd-loading-ring);
      animation: spin 0.9s linear infinite;
    }

    & .card-caption {
      position: absolute;
      inset-inline: 0;
      inset-block-end: 0;
      display: grid;
      gap: 0.125rem;
      padding: var(--space-s);
      background: linear-gradient(to top, rgba(0 0 0 / 0.85), rgba(0 0 0 / 0));
      color: #fff;
      font-size: var(--type-small);
    }

    & .card-title {
      font-weight: 600;
      overflow: hidden;
      display: -webkit-box;
      -webkit-line-clamp: 2;
      -webkit-box-orient: vertical;
    }

    & .card-source {
      color: rgba(255 255 255 / 0.75);
      font-size: 0.75rem;
      overflow: hidden;
      white-space: nowrap;
      text-overflow: ellipsis;
    }

    &:hover {
      border-color: var(--accent);
    }

    /* No transform on hover — translate shifts cause masonry reflow that
       reads as a "glitchy disappear" on Pinterest column-count layouts.
       Color/border-only hover is plenty of feedback. */
  }

  .link-btn {
    color: var(--muted);
    font-size: var(--type-small);
    text-decoration: underline;
    text-underline-offset: 0.2em;

    &:hover {
      color: var(--accent);
    }
  }

  .raw-dialog {
    inline-size: min(60rem, 92vw);
    max-block-size: 80dvh;
    padding: var(--space-m);
    border: var(--hairline) solid var(--border);
    border-radius: var(--radius-m);
    background: var(--card-bg);
    color: var(--fg);

    &::backdrop {
      background: rgba(0 0 0 / 0.5);
      backdrop-filter: blur(4px);
    }
  }

  /* Generic icon button used in topbar (gear, sign-out) and dialog chrome. */
  .icon-btn {
    display: grid;
    place-items: center;
    inline-size: 1.875rem;
    block-size: 1.875rem;
    padding: 0;
    border: 0;
    border-radius: 50%;
    background: transparent;
    color: var(--muted);
    cursor: pointer;
    transition: color 0.14s ease, background-color 0.14s ease;
  }

  .icon-btn:hover {
    color: var(--fg);
    background: color-mix(in oklab, var(--fg) 7%, transparent);
  }

  .icon-btn:focus-visible {
    outline: 2px solid var(--accent);
    outline-offset: 2px;
    color: var(--fg);
  }

  .icon-btn svg {
    inline-size: 1.125rem;
    block-size: 1.125rem;
  }

  /* ── Settings dialog ─────────────────────────────────────────────── */
  .settings-dialog {
    inline-size: min(28rem, 92vw);
    padding: var(--space-l);
    border: var(--hairline) solid var(--border);
    border-radius: var(--radius-l);
    background: var(--card-bg);
    color: var(--fg);
    box-shadow: 0 4px 12px rgba(0 0 0 / 0.08), 0 24px 60px rgba(0 0 0 / 0.18);
    transition: opacity 0.16s var(--ease-out-quint), transform 0.16s var(--ease-out-quint);
  }

  .settings-dialog::backdrop {
    background: color-mix(in oklab, var(--bg) 60%, transparent);
    backdrop-filter: blur(8px) saturate(140%);
    -webkit-backdrop-filter: blur(8px) saturate(140%);
  }

  .settings-dialog[open] {
    animation: settings-pop 0.18s var(--ease-out-quint);
  }

  @keyframes settings-pop {
    from {
      opacity: 0;
      transform: translateY(0.5rem) scale(0.98);
    }
    to {
      opacity: 1;
      transform: translateY(0) scale(1);
    }
  }

  .settings-dialog-head {
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: var(--space-m);
    margin-block-end: var(--space-l);
  }

  .settings-dialog-head h2 {
    font-size: var(--type-h2);
    font-weight: 600;
    letter-spacing: -0.01em;
    margin: 0;
  }

  .settings-section {
    border: 0;
    margin: 0;
    padding: 0;
    display: grid;
    gap: var(--space-m);
  }

  .settings-section legend {
    padding: 0;
    color: var(--muted);
    font-size: var(--type-small);
    font-weight: 600;
    text-transform: uppercase;
    letter-spacing: 0.04em;
    margin-block-end: var(--space-xs);
  }

  .settings-row {
    display: grid;
    gap: var(--space-xs);
    align-items: center;
  }

  /* Custom toggle replaces the default checkbox for a more cohesive look. */
  .settings-toggle {
    grid-template-columns: auto auto 1fr;
    gap: var(--space-s);
    cursor: pointer;
    user-select: none;
  }

  .settings-toggle input[type="checkbox"] {
    position: absolute;
    inline-size: 1px;
    block-size: 1px;
    overflow: hidden;
    clip: rect(0 0 0 0);
    appearance: none;
    margin: 0;
  }

  .settings-toggle-track {
    display: inline-flex;
    align-items: center;
    inline-size: 2.4rem;
    block-size: 1.4rem;
    padding: 2px;
    border-radius: 999px;
    background: color-mix(in oklab, var(--fg) 14%, transparent);
    transition: background-color 0.18s var(--ease-out-quint);
  }

  .settings-toggle-thumb {
    inline-size: 1.05rem;
    block-size: 1.05rem;
    border-radius: 50%;
    background: var(--card-bg);
    box-shadow: 0 1px 2px rgba(0 0 0 / 0.25), 0 0 0 1px rgba(0 0 0 / 0.05);
    transition: transform 0.18s var(--ease-out-quint);
  }

  .settings-toggle input[type="checkbox"]:checked + .settings-toggle-track {
    background: var(--accent);
  }

  .settings-toggle input[type="checkbox"]:checked + .settings-toggle-track .settings-toggle-thumb {
    transform: translateX(0.95rem);
  }

  .settings-toggle input[type="checkbox"]:focus-visible + .settings-toggle-track {
    box-shadow: 0 0 0 2px var(--accent);
  }

  .settings-toggle-label {
    color: var(--fg);
  }

  .settings-pair {
    grid-template-columns: 1fr 1fr;
    gap: var(--space-m);
  }

  .settings-pair-field {
    display: grid;
    gap: var(--space-xs);
  }

  .settings-pair-field span {
    color: var(--muted);
    font-size: var(--type-small);
  }

  .settings-pair-field input[type="number"] {
    inline-size: 100%;
    padding-inline: var(--space-s);
    padding-block: var(--space-xs);
    border: var(--hairline) solid var(--border);
    border-radius: var(--radius-s);
    background: var(--bg);
    color: var(--fg);
    font: inherit;
    font-variant-numeric: tabular-nums;

    &:focus-visible {
      outline: 2px solid var(--accent);
      outline-offset: 2px;
      border-color: var(--accent);
    }

    &::-webkit-inner-spin-button,
    &::-webkit-outer-spin-button {
      appearance: none;
      margin: 0;
    }
  }

  .settings-quality-row {
    grid-template-columns: 1fr;
    gap: var(--space-xs);
  }

  .settings-quality-label {
    display: flex;
    justify-content: space-between;
    color: var(--muted);
    font-size: var(--type-small);
  }

  .settings-quality-value {
    color: var(--fg);
    font-weight: 600;
    font-variant-numeric: tabular-nums;
  }

  .settings-quality-row input[type="range"] {
    inline-size: 100%;
    accent-color: var(--accent);
  }

  .settings-quality-scale {
    display: flex;
    justify-content: space-between;
    color: var(--muted);
    font-size: 0.7rem;
    font-variant-numeric: tabular-nums;
  }

  .settings-hint {
    color: var(--muted);
    font-size: var(--type-small);
    margin: 0;
    line-height: 1.45;
  }

  .settings-hint code {
    font-family: ui-monospace, "SF Mono", Menlo, Consolas, monospace;
    background: color-mix(in oklab, var(--fg) 8%, transparent);
    padding: 0.05rem 0.35rem;
    border-radius: var(--radius-s);
    font-size: 0.85em;
  }

  .raw-dialog-head {
    display: flex;
    align-items: baseline;
    justify-content: space-between;
    gap: var(--space-m);
    margin-block-end: var(--space-s);
  }

  .raw-body {
    max-block-size: 60dvh;
    overflow: auto;
    padding: var(--space-s);
    background: color-mix(in oklab, var(--fg) 5%, var(--bg));
    border-radius: var(--radius-s);
    font-family: ui-monospace, "SF Mono", Menlo, Consolas, monospace;
    font-size: 0.8rem;
    white-space: pre-wrap;
    word-break: break-word;
  }

  /* lightbox — full-viewport image viewer with keyboard nav. No navigation
     out of the app by default; the source link is secondary. */
  .lightbox {
    padding: 0;
    border: none;
    background: rgba(0 0 0 / 0.94);
    color: #fff;
    max-inline-size: 100vw;
    max-block-size: 100dvh;
    inline-size: 100vw;
    block-size: 100dvh;

    &::backdrop {
      background: rgba(0 0 0 / 0.6);
    }

    &[open] {
      display: grid;
      place-items: center;
      animation: lightbox-enter var(--duration-modal) var(--ease-spring);
    }

    &[open]::backdrop {
      animation: lightbox-backdrop-enter 360ms var(--ease-out-quint);
    }

    /* Slide content arrives a hair after the dialog frame — scale-up
       from 0.94 + opacity ramp so the image "rises into" the viewport.
       iOS spring curve for a coordinated entrance. */
    &[open] .lightbox-slide {
      animation: lightbox-slide-rise var(--duration-modal) var(--ease-spring) backwards;
    }

    /* Scrubber slides up from below as the photo settles. */
    &[open] .lightbox-scrubber {
      animation: lightbox-scrubber-rise 460ms var(--ease-spring) 100ms backwards;
    }

    /* Top control buttons fade + drop in subtly. */
    &[open] .lightbox-btn {
      animation: lightbox-chrome-fade 360ms var(--ease-out-quint) 80ms backwards;
    }
  }

  @keyframes lightbox-enter {
    from {
      opacity: 0;
      transform: scale(0.96);
    }
    to {
      opacity: 1;
      transform: scale(1);
    }
  }

  @keyframes lightbox-backdrop-enter {
    from {
      opacity: 0;
    }
    to {
      opacity: 1;
    }
  }

  @keyframes lightbox-slide-rise {
    from {
      opacity: 0;
      transform: translate3d(0, 16px, 0) scale(0.96);
    }
    to {
      opacity: 1;
      transform: translate3d(0, 0, 0) scale(1);
    }
  }

  @keyframes lightbox-scrubber-rise {
    from {
      opacity: 0;
      transform: translate3d(0, 24px, 0);
    }
    to {
      opacity: 1;
      transform: translate3d(0, 0, 0);
    }
  }

  @keyframes lightbox-chrome-fade {
    from {
      opacity: 0;
      transform: translate3d(0, -6px, 0);
    }
    to {
      opacity: 1;
      transform: translate3d(0, 0, 0);
    }
  }

  @media (prefers-reduced-motion: reduce) {
    .lightbox[open],
    .lightbox[open]::backdrop,
    .lightbox[open] .lightbox-slide,
    .lightbox[open] .lightbox-scrubber,
    .lightbox[open] .lightbox-btn {
      animation: none;
    }
  }

  /* ── Lightbox horizontal scroll-snap canvas (THICCR Tracks pattern) ─── */
  /* Each slide is a full-viewport-width snap target. Trackpad/touch
     swipe naturally moves between images via native scroll; the scrubber
     at the bottom mirrors via JS scroll-end sync. */
  .lightbox-canvas {
    position: absolute;
    inset: 0;
    z-index: 1;
    overflow-x: auto;
    overflow-y: hidden;
    overscroll-behavior-x: contain;
    scroll-snap-type: x mandatory;
    scrollbar-width: none;
    touch-action: pan-x;
  }

  .lightbox-canvas::-webkit-scrollbar {
    display: none;
  }

  .lightbox-strip {
    display: flex;
    block-size: 100%;
    min-block-size: 0;
  }

  .lightbox-slide {
    flex: 0 0 100vw;
    scroll-snap-align: center;
    scroll-snap-stop: always;
    display: grid;
    place-items: center;
    block-size: 100%;
    /* No padding here — the lightbox-img-frame computes its own dimensions
       in JS based on viewport size minus an inset margin. Adding padding on
       the slide would double-account for the breathing room. */
  }

  /* THICCR Tracks pattern: definite-size frame whose pixel dimensions are
     set inline by JS (fitFrame) on every viewport resize. With definite
     sizing, the percentage chain that broke earlier (canvas
     position:absolute → strip 100% → slide 100% → img max-block-size:100%
     resolved to indefinite → bitmap rendered at intrinsic height and
     overflowed) is short-circuited: the frame is N pixels tall and the
     img inside fills it exactly via 100% + contain. Tall portraits
     letterbox cleanly inside the frame; landscapes fit width. */
  .lightbox-img-frame {
    position: relative;
    display: grid;
    place-items: center;
    overflow: hidden;
    border-radius: var(--radius-m);
  }

  .lightbox-slide img {
    inline-size: 100%;
    block-size: 100%;
    object-fit: contain;
    object-position: center center;
    background: rgba(255 255 255 / 0.04);
    user-select: none;
    -webkit-user-drag: none;
  }

  /* Per-slide spinner / status label. */
  .lightbox-slide-status {
    position: absolute;
    inset-block-start: var(--space-m);
    inset-inline-start: 50%;
    transform: translateX(-50%);
    padding: calc(var(--space-xs) / 1.5) var(--space-s);
    background: rgba(0 0 0 / 0.55);
    border-radius: 999px;
    font-size: 0.7rem;
    letter-spacing: 0.04em;
    color: rgba(255 255 255 / 0.85);
    backdrop-filter: blur(4px);
    -webkit-backdrop-filter: blur(4px);
    pointer-events: none;
    opacity: 0;
    transform: translateX(-50%) translateY(-2px);
    transition: opacity 0.18s var(--ease-out-quint), transform 0.18s var(--ease-out-quint);
  }

  .lightbox-slide[data-loading="true"] .lightbox-slide-status {
    opacity: 1;
    transform: translateX(-50%) translateY(0);
  }

  @media (prefers-reduced-motion: reduce) {
    .lightbox-slide-status {
      transition: none;
      transform: translateX(-50%);
    }
    .lightbox-slide[data-loading="true"] .lightbox-slide-status {
      transform: translateX(-50%);
    }
  }

  /* Info bar sits at the bottom, riding on top of the scrubber band — iOS
     Photos style. Scrubber is 5rem tall + safe-area; the bar lands a hair
     above its top edge so the rounded pill kisses the scrubber's frosted
     surface. Capped width so widescreen doesn't make it gigantic; ellipsis
     truncation when content overflows. Hidden on narrow / portrait — on
     mobile the picture is the source of truth. z-index above scrubber so
     it visually overlays. */
  .lightbox-info-bar {
    position: absolute;
    inset-inline: 0;
    inset-block-end: calc(5rem + var(--space-xs) + env(safe-area-inset-bottom));
    z-index: 12;
    display: inline-flex;
    align-items: center;
    gap: var(--space-m);
    padding: var(--space-xs) var(--space-m);
    margin-inline: auto;
    inline-size: max-content;
    max-inline-size: clamp(16rem, 60vw, 36rem);
    border-radius: 999px;
    color: light-dark(#0a0a0a, #f2f2f0);
    font-size: var(--type-small);
    pointer-events: none;
  }

  .lightbox-info-bar a,
  .lightbox-info-bar span {
    pointer-events: auto;
  }

  .lightbox-info-bar .lightbox-title {
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
    max-inline-size: 22ch;
  }

  /* On portrait / narrow, hide the info bar entirely — the image is the
     content; the source/position can live in the URL bar / share sheet
     post-download. */
  @media (aspect-ratio < 3/4) {
    .lightbox-info-bar {
      display: none;
    }
  }

  .lightbox-status {
    position: absolute;
    inset-block-end: var(--space-s);
    inset-inline-start: var(--space-s);
    padding: calc(var(--space-xs) / 1.5) var(--space-s);
    background: rgba(0 0 0 / 0.55);
    border-radius: 999px;
    font-size: 0.7rem;
    letter-spacing: 0.04em;
    color: rgba(255 255 255 / 0.85);
    backdrop-filter: blur(4px);
    pointer-events: none;
  }

  .lightbox-info {
    display: grid;
    grid-auto-flow: column;
    grid-auto-columns: max-content;
    justify-content: center;
    gap: var(--space-m);
    align-items: center;
    font-size: var(--type-small);
    color: rgba(255 255 255 / 0.88);
    text-wrap: balance;
    max-inline-size: 90vw;
  }

  .lightbox-title {
    max-inline-size: 50ch;
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
    font-weight: 600;
  }

  .lightbox-source {
    color: var(--accent);
    text-decoration: underline;
    text-decoration-thickness: 1px;
    text-underline-offset: 2px;

    &:empty {
      display: none;
    }
  }

  .lightbox-position {
    color: rgba(255 255 255 / 0.55);
    font-variant-numeric: tabular-nums;
  }

  /* Lightbox close + chevrons all use the .liquid-glass class for the
     vibrancy material; this rule layers positioning + sizing on top.
     User asked for these to be ~half size — visual chrome stays subtle so
     the photo dominates. Hit target stays >= 28px (touch-tolerable, below
     HIG 44pt but matches the iOS Photos lightbox affordance). */
  .lightbox-btn {
    position: absolute;
    display: grid;
    place-items: center;
    inline-size: clamp(28px, 3vw, 36px);
    block-size: clamp(28px, 3vw, 36px);
    padding: 0;
    border: 0;
    border-radius: 999px;
    color: light-dark(#1c1c1e, #f5f5f7);
    cursor: pointer;
    transition: transform 180ms cubic-bezier(0.2, 0.8, 0.2, 1);
    z-index: 12;
  }

  .lightbox-btn svg {
    inline-size: 14px;
    block-size: 14px;
  }

  .lightbox-btn:hover {
    transform: scale(1.06);
  }

  .lightbox-btn:active {
    transform: scale(0.96);
  }

  .lightbox-btn:focus-visible {
    outline: 2px solid AccentColor;
    outline-offset: 2px;
  }

  .lightbox-close {
    inset-block-start: max(var(--space-m), env(safe-area-inset-top));
    inset-inline-end: var(--space-m);
  }

  .lightbox-select {
    inset-block-start: max(var(--space-m), env(safe-area-inset-top));
    inset-inline-start: var(--space-m);
  }

  .lightbox-select .lightbox-select-check {
    opacity: 0;
    transition: opacity 0.18s var(--ease-out-quint);
  }

  .lightbox-select[aria-pressed="true"] {
    color: var(--accent);
  }

  .lightbox-select[aria-pressed="true"] .lightbox-select-check {
    opacity: 1;
  }

  .lightbox-download-btn {
    inset-block-start: max(var(--space-m), env(safe-area-inset-top));
    inset-inline-start: calc(var(--space-m) + clamp(28px, 3vw, 36px) + var(--space-xs));
  }

  .lightbox-chevron {
    inset-block: 0;
    margin-block: auto;
  }

  .lightbox-chevron[data-side="start"] {
    inset-inline-start: clamp(12px, 2vw, 24px);
  }

  .lightbox-chevron[data-side="end"] {
    inset-inline-end: clamp(12px, 2vw, 24px);
  }

  /* Mobile (aspect-ratio < 3/4): chevrons hide entirely and the bottom
     scrubber takes over for navigation. Per the user's spec: "the two
     left and right chevrons should only appear on desktop or big white
     screens." */
  @media (aspect-ratio < 3/4) {
    .lightbox-chevron {
      display: none;
    }
  }

  /* ── iOS Photos-style scrubber (CSS scroll-snap, no JS swipe) ─────── */
  .lightbox-scrubber {
    position: absolute;
    inset-inline: 0;
    inset-block-end: 0;
    display: flex;
    gap: var(--space-xs);
    overflow-x: auto;
    overflow-y: hidden;
    overscroll-behavior-x: contain;
    scroll-snap-type: x mandatory;
    scroll-padding-inline: 50%;
    scrollbar-width: none;
    touch-action: pan-x;
    block-size: 5rem;
    padding-block: var(--space-xs);
    padding-block-end: max(var(--space-xs), env(safe-area-inset-bottom));
    padding-inline: var(--space-m);
    /* Translucent glass over the image's bottom band — the photo bleeds
       through underneath, scrubber thumbs stay legible thanks to the
       blur + saturate filter. Strong tint was a tray that walled off the
       image. Light tint is an overlay; iOS Photos pattern. */
    background-color: var(--glass-tint);
    backdrop-filter: var(--glass-blur);
    -webkit-backdrop-filter: var(--glass-blur);
    /* Subtle inner top hairline for visual separation against the bleeding
       image, plus a soft top edge to anchor the scrubber band. */
    box-shadow: inset 0 1px 0 rgba(255 255 255 / 0.12), 0 -1px 12px rgba(0 0 0 / 0.18);
    z-index: 11;
  }

  .lightbox-scrubber::-webkit-scrollbar {
    display: none;
  }

  .lightbox-scrubber .thumb {
    /* position: relative anchors the [aria-selected]::after corner dot
       to the thumb itself. Without this the absolute dot escaped to the
       scrubber's positioning context and rendered at the scrubber's
       left edge — that was the "blue dot glitch." */
    position: relative;
    flex: 0 0 auto;
    inline-size: 3.25rem;
    block-size: 3.25rem;
    scroll-snap-align: center;
    scroll-snap-stop: always;
    border-radius: var(--radius-s);
    border: 2px solid transparent;
    padding: 0;
    background: none;
    cursor: pointer;
    overflow: hidden;
    opacity: 0.55;
    transition: opacity 0.12s ease, border-color 0.12s ease, transform 0.12s ease;
  }

  .lightbox-scrubber .thumb img {
    display: block;
    inline-size: 100%;
    block-size: 100%;
    object-fit: cover;
    border-radius: inherit;
  }

  /* Active thumb: ring INSIDE the thumb edge (inset shadow), not outside.
     Outset rings on the leftmost thumb were getting clipped by the
     scrubber's overflow-x; inset keeps them contained per thumb. */
  .lightbox-scrubber .thumb[aria-current="true"] {
    opacity: 1;
    border-color: rgba(255 255 255 / 0.95);
    box-shadow: inset 0 0 0 2px var(--accent);
  }

  /* Selection corner dot — anchored to the thumb's top-left via the
     `position: relative` above. Mirrors the grid selection state. */
  .lightbox-scrubber .thumb[aria-selected="true"]::after {
    content: "";
    position: absolute;
    inset-block-start: 2px;
    inset-inline-start: 2px;
    inline-size: 0.5rem;
    block-size: 0.5rem;
    border-radius: 50%;
    background: var(--accent);
    box-shadow: 0 0 0 2px rgba(0 0 0 / 0.6);
  }

  /* mattemotto: a hover is a held breath. release it on motion. */
  /* ── Motion polish — iOS Photos-feeling micro-animations ───────────── */
  @media (prefers-reduced-motion: no-preference) {
    /* 1. Tile cards (grid view only — masonry transforms reflow columns
       and were explicitly forbidden upstream). The base `.card` already
       carries `transition: border-color 150ms ease`; we extend it here. */
    body[data-view="grid"] .result-grid .card {
      transition:
        border-color 150ms ease,
        transform 180ms var(--ease-out-quint),
        box-shadow 180ms var(--ease-out-quint);
      will-change: transform;
    }

    @media (hover: hover) {
      body[data-view="grid"] .result-grid .card:hover {
        transform: translate3d(0, -2px, 0);
        box-shadow: 0 6px 18px rgba(0 0 0 / 0.10);
      }
    }

    /* 2. View-mode radio toggle pop. End state == start state, so this
       is a one-shot keyframe rather than a transition. */
    .view-mode label:has(input:checked) {
      animation: view-mode-pop 220ms var(--ease-out-back);
    }

    @keyframes view-mode-pop {
      0% {
        transform: scale(1);
      }
      55% {
        transform: scale(1.04);
      }
      100% {
        transform: scale(1);
      }
    }

    /* 5. Lightbox close. JS sets [data-closing]; we mirror the open
       keyframe in reverse. */
    .lightbox[data-closing] {
      animation: lightbox-exit 0.18s var(--ease-out-quint) forwards;
    }

    @keyframes lightbox-exit {
      from {
        opacity: 1;
        transform: scale(1);
      }
      to {
        opacity: 0;
        transform: scale(0.985);
      }
    }

    /* 6. Drop overlay — keep the existing fade-in keyframe, but layer a
       gentle backdrop transition on the visible body so the blur ramps
       in/out instead of snapping when the overlay mounts/unmounts. */
    .drop-overlay {
      transition:
        opacity 200ms var(--ease-out-quint),
        backdrop-filter 200ms var(--ease-out-quint),
        -webkit-backdrop-filter 200ms var(--ease-out-quint);
    }

    /* 7. Settings dialog close (mirror of settings-pop). Triggered via
       `[data-closing]` attribute on the dialog before .close() fires. */
    .settings-dialog[data-closing] {
      animation: settings-pop-out 0.18s var(--ease-out-quint) forwards;
    }

    @keyframes settings-pop-out {
      from {
        opacity: 1;
        transform: translateY(0) scale(1);
      }
      to {
        opacity: 0;
        transform: translateY(0.4rem) scale(0.98);
      }
    }

    /* 8. Engine chip running-state pulse. Subtle accent halo that breathes
       only while work is in flight. */
    .engine-chip[data-state="running"] {
      animation: chip-pulse 1.6s ease-in-out infinite;
    }

    @keyframes chip-pulse {
      0%, 100% {
        box-shadow: 0 0 0 0 color-mix(in oklab, var(--accent) 0%, transparent);
      }
      50% {
        box-shadow: 0 0 0 4px color-mix(in oklab, var(--accent) 25%, transparent);
      }
    }

    /* Liquid Glass note: real Liquid Glass (Safari 26 / iOS 26) does not
       inflate on hover. The material reads steady regardless of pointer
       state; tap reactions are subtle inward press, never outward lift.
       The press batch below is the entire feedback surface. */

    /* Logout button press — inherits the shared press batch below.
       No hover lift, no halo: matches the rest of the topbar. */
    #logout-btn {
      transition: color 120ms ease, background-color 120ms ease, transform 220ms var(--ease-spring);
    }

    #logout-btn:active {
      transform: scale(0.96);
      transition-duration: 90ms;
    }

    /* ── Entrances — nothing pops into existence ──────────────────── */

    /* Skeleton breathing for cards still loading their thumbnail. */
    @keyframes card-skeleton {
      0%, 100% {
        background-position: 0% 50%;
      }
      50% {
        background-position: 100% 50%;
      }
    }

    /* Card entrance — iOS-Photos signature: scale from 0.94 + a touch
       of vertical drift + opacity, staggered so the wave reads as
       intentional cascade rather than a thundering herd of pop-ins.
       backwards fill applies the start state before paint so cards
       don't flash visible-then-hidden. */
    @keyframes card-enter {
      from {
        opacity: 0;
        transform: translate3d(0, 6px, 0) scale(0.94);
      }
      to {
        opacity: 1;
        transform: translate3d(0, 0, 0) scale(1);
      }
    }
    .result-grid > .card {
      animation: card-enter 360ms var(--ease-out-quint) backwards;
    }
    /* Stagger up to 16 cards — anything beyond enters at delay 0
       so big result sets don't drag a half-second tail. */
    .result-grid > .card:nth-child(1) {
      animation-delay: 0ms;
    }
    .result-grid > .card:nth-child(2) {
      animation-delay: 30ms;
    }
    .result-grid > .card:nth-child(3) {
      animation-delay: 60ms;
    }
    .result-grid > .card:nth-child(4) {
      animation-delay: 90ms;
    }
    .result-grid > .card:nth-child(5) {
      animation-delay: 120ms;
    }
    .result-grid > .card:nth-child(6) {
      animation-delay: 150ms;
    }
    .result-grid > .card:nth-child(7) {
      animation-delay: 180ms;
    }
    .result-grid > .card:nth-child(8) {
      animation-delay: 210ms;
    }
    .result-grid > .card:nth-child(9) {
      animation-delay: 240ms;
    }
    .result-grid > .card:nth-child(10) {
      animation-delay: 270ms;
    }
    .result-grid > .card:nth-child(11) {
      animation-delay: 300ms;
    }
    .result-grid > .card:nth-child(12) {
      animation-delay: 330ms;
    }
    .result-grid > .card:nth-child(13) {
      animation-delay: 360ms;
    }
    .result-grid > .card:nth-child(14) {
      animation-delay: 390ms;
    }
    .result-grid > .card:nth-child(15) {
      animation-delay: 420ms;
    }
    .result-grid > .card:nth-child(16) {
      animation-delay: 450ms;
    }

    /* Photo settles into its frame — slight scale-down on load so the
       image arrives with intent. The skeleton fades, the photo zooms
       gently from 1.04 → 1 as opacity ramps 0 → 1. */
    @keyframes card-img-settle {
      from {
        opacity: 0;
        transform: scale(1.04);
      }
      to {
        opacity: 1;
        transform: scale(1);
      }
    }
    .card.has-loaded img {
      animation: card-img-settle 420ms var(--ease-out-quint) backwards;
    }

    /* Tile entrance — the parent container that wraps engine status
       + result grid for each dropped image / text query. */
    @keyframes tile-enter {
      from {
        opacity: 0;
        transform: translate3d(0, 14px, 0);
      }
      to {
        opacity: 1;
        transform: translate3d(0, 0, 0);
      }
    }
    .tile {
      animation: tile-enter 320ms var(--ease-out-quint) backwards;
    }

    /* Engine chip entrance — slide in from outside the chip row, one
       after the other. Each chip starts ~24 px to the left, faded; it
       slides into place with a soft ease-out. backwards fill applies
       the start state before paint so chips never pop visible-then-
       hidden. mattemotto: a row of chips is a row, not a thunderclap. */
    @keyframes chip-slide-in {
      from {
        opacity: 0;
        transform: translate3d(-24px, 0, 0);
      }
      to {
        opacity: 1;
        transform: translate3d(0, 0, 0);
      }
    }
    .engine-chip {
      animation: chip-slide-in 320ms var(--ease-out-quint) backwards;
    }
    /* Stagger via nth-child so chips arrive in sequence rather than as
       a slab. Most tiles have 2-3 chips, but this scales up to 6. */
    .engine-chip:nth-child(1) {
      animation-delay: 0ms;
    }
    .engine-chip:nth-child(2) {
      animation-delay: 80ms;
    }
    .engine-chip:nth-child(3) {
      animation-delay: 160ms;
    }
    .engine-chip:nth-child(4) {
      animation-delay: 240ms;
    }
    .engine-chip:nth-child(5) {
      animation-delay: 320ms;
    }
    .engine-chip:nth-child(6) {
      animation-delay: 400ms;
    }

    /* Lightbox filmstrip thumbs — sequenced cascade when the lightbox
       opens. Same ethos as engine chips and result cards: nothing
       pops into existence. iOS spring + 40 ms stagger reads as
       deliberate, not a herd. */
    @keyframes thumb-rise {
      from {
        opacity: 0;
        transform: translate3d(0, 4px, 0) scale(0.92);
      }
      to {
        opacity: 0.55;
        transform: translate3d(0, 0, 0) scale(1);
      }
    }
    .lightbox-scrubber .thumb {
      animation: thumb-rise 320ms var(--ease-spring) backwards;
    }
    .lightbox-scrubber .thumb:nth-child(1) {
      animation-delay: 0ms;
    }
    .lightbox-scrubber .thumb:nth-child(2) {
      animation-delay: 40ms;
    }
    .lightbox-scrubber .thumb:nth-child(3) {
      animation-delay: 80ms;
    }
    .lightbox-scrubber .thumb:nth-child(4) {
      animation-delay: 120ms;
    }
    .lightbox-scrubber .thumb:nth-child(5) {
      animation-delay: 160ms;
    }
    .lightbox-scrubber .thumb:nth-child(6) {
      animation-delay: 200ms;
    }
    .lightbox-scrubber .thumb:nth-child(7) {
      animation-delay: 240ms;
    }
    .lightbox-scrubber .thumb:nth-child(8) {
      animation-delay: 280ms;
    }
    .lightbox-scrubber .thumb:nth-child(9) {
      animation-delay: 320ms;
    }
    .lightbox-scrubber .thumb:nth-child(10) {
      animation-delay: 360ms;
    }
    .lightbox-scrubber .thumb:nth-child(11) {
      animation-delay: 400ms;
    }
    .lightbox-scrubber .thumb:nth-child(12) {
      animation-delay: 440ms;
    }

    /* ── Liquid-glass press — the bouncy reaction on every tap ─────
       Subtle inward scale on :active with spring release. The 90 ms
       press / 220 ms release asymmetry is the iOS feel: quick to
       compress, slow to recover. mattemotto: glass remembers the touch. */
    .card,
    .lightbox-btn,
    .lightbox-select,
    .searchbar-pick,
    .searchbar-clear,
    .topbar-meta .icon-btn,
    .topbar > .view-controls .view-mode label,
    .topbar > .view-controls .zoom-control button,
    .icon-btn,
    .action-primary,
    .action-clear,
    .submit-btn,
    .engine-toggle label,
    .tile-actions .icon-btn,
    .empty-state-cta,
    .card-select,
    .card-download {
      transition-property: transform, background-color, color, box-shadow, border-color;
      transition-duration: 220ms;
      transition-timing-function: var(--ease-spring);
    }

    /* Liquid Glass press — the WWDC25 session 219 "flex + energize
       with light" reaction. Subtle inward scale paired with a
       brightness flash. The :active duration is short (tap-in 120ms);
       release uses the longer transition above (200ms) for the slow
       recover. iOS-feel asymmetry: quick to compress, slow to relax. */
    .card:active,
    .empty-state:active {
      transform: scale(0.99);
      filter: brightness(1.04);
      transition-duration: var(--duration-tap);
    }

    .lightbox-btn:active,
    .lightbox-select:active,
    .searchbar-pick:active,
    .searchbar-clear:active,
    .topbar-meta .icon-btn:active,
    .topbar > .view-controls .view-mode label:active,
    .topbar > .view-controls .zoom-control button:active,
    .icon-btn:active,
    .action-primary:active,
    .action-clear:active,
    .submit-btn:active,
    .engine-toggle label:active,
    .tile-actions .icon-btn:active,
    .empty-state-cta:active,
    .card-select:active,
    .card-download:active {
      transform: scale(0.985);
      filter: brightness(1.06);
      transition-duration: var(--duration-tap);
    }
  }

  /* Reduced-motion companions — explicit kill-switch for every animation
     the polish block introduces. Anything CSS already pinned (selection
     ring, lightbox enter, settings pop, drop overlay enter) keeps its
     existing reduce-motion guards higher in the file. */
  @media (prefers-reduced-motion: reduce) {
    body[data-view="grid"] .result-grid .card {
      transition: border-color 150ms ease;
    }
    body[data-view="grid"] .result-grid .card:hover {
      transform: none;
      box-shadow: none;
    }
    .view-mode label:has(input:checked) {
      animation: none;
    }
    .lightbox[data-closing] {
      animation: none;
    }
    .drop-overlay {
      transition: none;
    }
    .settings-dialog[data-closing] {
      animation: none;
    }
    .engine-chip[data-state="running"] {
      animation: none;
    }
    .searchbar,
    .topbar > .view-controls,
    .topbar-meta,
    .topbar-meta .auth-user,
    #logout-btn {
      transition: none;
    }
    .searchbar:hover,
    .topbar > .view-controls:hover,
    .topbar-meta:hover,
    #logout-btn:active {
      transform: none;
    }

    /* New entrance + press kill-switches */
    .result-grid > .card,
    .tile,
    .engine-chip,
    .lightbox-scrubber .thumb,
    .lightbox[open],
    .lightbox[open]::backdrop,
    .lightbox[open] .lightbox-slide,
    .lightbox[open] .lightbox-scrubber,
    .lightbox[open] .lightbox-btn {
      animation: none;
    }
    .card:not(.has-loaded)::before {
      animation: none;
    }
    .card,
    .lightbox-btn,
    .lightbox-select,
    .searchbar-pick,
    .searchbar-clear,
    .topbar-meta .icon-btn,
    .topbar > .view-controls .view-mode label,
    .topbar > .view-controls .zoom-control button,
    .icon-btn,
    .action-primary,
    .action-clear,
    .submit-btn,
    .engine-toggle label,
    .tile-actions .icon-btn,
    .empty-state-cta,
    .card-select,
    .card-download {
      transition: none;
    }
    .card:active,
    .empty-state:active,
    .lightbox-btn:active,
    .lightbox-select:active,
    .searchbar-pick:active,
    .searchbar-clear:active,
    .topbar-meta .icon-btn:active,
    .topbar > .view-controls .view-mode label:active,
    .topbar > .view-controls .zoom-control button:active,
    .icon-btn:active,
    .action-primary:active,
    .action-clear:active,
    .submit-btn:active,
    .engine-toggle label:active,
    .tile-actions .icon-btn:active,
    .empty-state-cta:active,
    .card-select:active,
    .card-download:active {
      transform: none;
      filter: none;
    }
    .empty-state {
      transition: none;
    }
    .card img {
      transition: none;
    }
    .card.has-loaded img {
      animation: none;
    }
    .result-grid > .card:nth-child(n) {
      animation-delay: 0ms;
    }
  }
}
