/* ============================================================================
   Deck — layout & components. Consumes css/tokens.css.
   ----------------------------------------------------------------------------
   DOM contract for render modules (Task 7 builds these subtrees):

   .card[data-provider="hinge"][data-status="pending|queued|liked|passed|failed|skipped"]
     .card-media
       img.card-photo
       .photo-strip > span(.on)              photo pager dots
       .media-top
         .media-top-left
           .provider-tag                     identity corner tag (accent text)
           .status-badge[data-status]        hidden while pending
         .corner-chips > .chip-corner(.verified)   verified / distance / height
       .photo-nav > button                   invisible prev/next click halves
       .card-scrim
       .card-identity > .card-name (+ .card-age) + .card-sub
       button.like-fab(.on)
     .card-body
       .card-job
       .card-decision
         .tag[data-career="strong|good|average|weak|unknown"]
         .income[data-unknown="true|false"]  不明 renders near-muted, never a claim
       .card-reason                          multi-line clamp (full text in focus)
       .evidence(.is-open)
         button.evidence-toggle[aria-expanded]
           .quality-badge[data-quality="likely_good|uncertain|miss_or_unavailable"]
           .evidence-confidence
         .evidence-body > .evidence-cand ...
       .card-foot

   .list-row[data-provider][data-status]     list view row (same data attrs)

   Chinese never appears in a selector: scoring vocabulary maps to data-
   attribute slugs so styles stay decoupled from the LLM's word list.
   ========================================================================== */

/* ---- reset & base --------------------------------------------------------- */

*,
*::before,
*::after {
  box-sizing: border-box;
}

html {
  height: 100%;
}

body {
  margin: 0;
  min-height: 100dvh;
  background: var(--bg);
  color: var(--text);
  font-family: var(--font-sans);
  font-size: var(--fs-base);
  line-height: var(--lh-normal);
  -webkit-font-smoothing: antialiased;
}

img {
  max-width: 100%;
  display: block;
}

button {
  font: inherit;
  color: inherit;
}

/* Links are ink with a faint rule under them, not the UA's #0000ee. They
   deliberately do NOT take the brand accent: --hinge is identity-only (see
   tokens.css), and --focus-ring is spoken for. Colour is therefore carried by
   the underline's weight rather than by hue, which is also what keeps a link
   inside a sentence from out-shouting the sentence. */
a {
  color: inherit;
  text-decoration: underline;
  text-decoration-thickness: 1px;
  text-underline-offset: 0.18em;
  text-decoration-color: color-mix(in srgb, currentColor 32%, transparent);
  transition: text-decoration-color var(--dur-1) var(--ease);
}

a:hover {
  text-decoration-color: currentColor;
}

[hidden] {
  display: none !important;
}

/* Counts and durations always align digit columns. */
.num {
  font-variant-numeric: tabular-nums;
}

.visually-hidden {
  position: absolute;
  width: 1px;
  height: 1px;
  margin: -1px;
  padding: 0;
  overflow: hidden;
  clip: rect(0 0 0 0);
  white-space: nowrap;
  border: 0;
}

.skip-link {
  position: absolute;
  left: var(--sp-3);
  top: calc(-1 * var(--sp-12));
  z-index: 100;
  padding: var(--sp-2) var(--sp-4);
  background: var(--surface);
  color: var(--text);
  border: 1px solid var(--border-strong);
  border-radius: var(--r-sm);
  box-shadow: var(--elev-2);
  transition: top var(--dur-2) var(--ease);
}

.skip-link:focus-visible {
  top: var(--sp-3);
}

/* ---- focus & motion policy ------------------------------------------------ */

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

:focus:not(:focus-visible) {
  outline: none;
}

@media (prefers-reduced-motion: reduce) {
  *,
  *::before,
  *::after {
    transition-duration: 0.01ms !important;
    animation-duration: 0.01ms !important;
    animation-iteration-count: 1 !important;
    scroll-behavior: auto !important;
  }
}

/* ---- view transitions ------------------------------------------------------
   Switching browse views morphs each card's photo between layouts. The photo
   groups get their own, slightly longer duration than the cross-fading page
   root: the morph is the thing worth watching, and matching them makes the
   photo look like it is merely fading with everything else.

   Still inside the product's motion band at the root; the group runs a touch
   past it because a position+size interpolation reads as abrupt at 180ms in a
   way an opacity fade does not. */
::view-transition-group(*) {
  animation-duration: 260ms;
  animation-timing-function: var(--ease);
}

::view-transition-old(root),
::view-transition-new(root) {
  animation-duration: var(--dur-3);
  animation-timing-function: var(--ease);
}

/* A spring for the release of a press. cubic-bezier cannot overshoot; linear()
   can, and the ~8% overshoot on the way back is what gives a button a feel.
   Consumed by .btn's transform below — the existing translateY(1px) press
   idiom is kept, this only changes how it comes back. */
:root {
  --ease-spring: linear(
    0, 0.402 7.4%, 0.734 15.3%, 0.968 23.4%, 1.045 28.9%, 1.093 34.4%,
    1.108 40%, 1.097 47.8%, 1.063 55.6%, 1.016 68.4%, 0.995 78.4%, 0.998 92.4%, 1
  );
}

/* ---- brand accent ---------------------------------------------------------
   One platform ships, so the accent is a root token rather than something that
   switches with an ancestor. Used only for the top-bar rule and the card corner
   tag — never as a fill. */

:root {
  --provider-accent: var(--hinge);
  --provider-soft: var(--hinge-soft);
  --provider-on-scrim: var(--hinge-on-scrim);
}

/* `hidden` must win over any display given to a fieldset. */
.filter-field[hidden] {
  display: none;
}

/* ---- shell ---------------------------------------------------------------- */

.shell {
  max-width: 1280px;
  margin-inline: auto;
  padding-inline: var(--sp-4);
  display: flex;
  flex-direction: column;
  min-height: 100dvh;
}

/* ---- buttons -------------------------------------------------------------- */

.btn {
  appearance: none;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: var(--sp-2);
  padding: var(--sp-2) var(--sp-4);
  border: 1px solid var(--border);
  border-radius: var(--r-sm);
  background: var(--surface);
  color: var(--text);
  font-size: var(--fs-sm);
  font-weight: 500;
  line-height: var(--lh-tight);
  cursor: pointer;
  transition:
    background var(--dur-1) var(--ease),
    border-color var(--dur-1) var(--ease),
    color var(--dur-1) var(--ease),
    /* The press itself stays inside the motion band; only the release runs
       long, on a spring that overshoots ~8%. That overshoot is the whole
       difference between "a state changed" and "I pushed something". */
    transform 320ms var(--ease-spring);
}

.btn:active:not(:disabled) {
  transition-duration: var(--dur-1);
}

.btn:hover:not(:disabled) {
  background: var(--surface-2);
  border-color: var(--border-strong);
}

.btn:active:not(:disabled) {
  transform: translateY(1px);
}

.btn:disabled {
  opacity: 0.45;
  cursor: not-allowed;
}

/* In flight. The button is disabled while busy, so without this it would fade
   to the same 45% as a button that is refusing to be pressed — two opposite
   meanings wearing one appearance. The spinner rides in .btn's own flex gap. */
.btn[data-busy="true"]:disabled {
  opacity: 0.8;
  cursor: progress;
}

.btn[data-busy="true"]::before {
  content: "";
  width: 0.85em;
  height: 0.85em;
  border-radius: 50%;
  border: 2px solid color-mix(in srgb, currentColor 28%, transparent);
  border-top-color: currentColor;
  animation: btn-spin 620ms linear infinite;
}

@keyframes btn-spin {
  to {
    transform: rotate(1turn);
  }
}

/* Neutral-primary: a dark button, so provider accents stay identity-only. */
.btn.primary {
  background: var(--text);
  border-color: transparent;
  color: var(--bg);
  font-weight: 600;
}

.btn.primary:hover:not(:disabled) {
  background: var(--text-2);
  border-color: transparent;
}

/* Destructive confirm (sign out). Outline, not a fill: it is a rare action. */
.btn.danger {
  color: var(--danger);
  border-color: color-mix(in srgb, var(--danger) 45%, var(--border));
  background: var(--danger-soft);
  font-weight: 600;
}

.btn.danger:hover:not(:disabled) {
  background: var(--danger-soft);
  border-color: var(--danger);
}

/* The one loud action. Like-semantic fill, not a provider accent. */
.btn.send {
  background: var(--like);
  border-color: transparent;
  color: #fff;
  font-weight: 600;
}

.btn.send:hover:not(:disabled) {
  filter: brightness(1.08);
  background: var(--like);
  border-color: transparent;
}

.btn.icon {
  min-width: 34px;
  padding-inline: var(--sp-2);
  font-size: var(--fs-base);
  line-height: 1;
}

.btn.tiny {
  padding: var(--sp-1) var(--sp-2);
  font-size: var(--fs-xs);
  border-radius: var(--r-xs);
}

.btn kbd {
  font-family: var(--font-mono);
  font-size: var(--fs-xs);
  opacity: 0.6;
}

/* ---- segmented control ---------------------------------------------------- */

.segmented {
  display: inline-flex;
  padding: 2px;
  gap: 2px;
  background: var(--surface-2);
  border: 1px solid var(--border);
  border-radius: var(--r-sm);
}

.seg-btn {
  appearance: none;
  border: 0;
  background: transparent;
  color: var(--text-3);
  padding: calc(var(--sp-1) + 1px) var(--sp-3);
  border-radius: calc(var(--r-sm) - 2px);
  font-size: var(--fs-sm);
  font-weight: 500;
  line-height: var(--lh-tight);
  cursor: pointer;
  display: inline-flex;
  align-items: center;
  gap: var(--sp-2);
  transition:
    background var(--dur-1) var(--ease),
    color var(--dur-1) var(--ease),
    box-shadow var(--dur-1) var(--ease);
}

.seg-btn:hover {
  color: var(--text);
}

.seg-btn.is-active,
.seg-btn[aria-pressed="true"] {
  background: var(--surface);
  color: var(--text);
  box-shadow: var(--elev-1);
  font-weight: 600;
}

/* ---- chips ---------------------------------------------------------------- */

.chip {
  appearance: none;
  display: inline-flex;
  align-items: center;
  gap: var(--sp-1);
  padding: var(--sp-1) var(--sp-3);
  border: 1px solid var(--border);
  border-radius: var(--r-full);
  background: var(--surface);
  color: var(--text-2);
  font-size: var(--fs-xs);
  font-weight: 500;
  line-height: var(--lh-tight);
  cursor: pointer;
  transition:
    background var(--dur-1) var(--ease),
    border-color var(--dur-1) var(--ease),
    color var(--dur-1) var(--ease);
}

.chip:hover {
  border-color: var(--border-strong);
  color: var(--text);
}

.chip.is-active,
.chip[aria-pressed="true"] {
  background: var(--text);
  border-color: var(--text);
  color: var(--bg);
}

.chip-group {
  display: flex;
  flex-wrap: wrap;
  gap: var(--sp-2);
}

/* Removable active-filter chip in the filter strip. */
.chip-removable {
  padding-right: var(--sp-2);
}

.chip-removable .chip-x {
  appearance: none;
  border: 0;
  background: transparent;
  color: inherit;
  cursor: pointer;
  font-size: var(--fs-sm);
  line-height: 1;
  padding: 0 2px;
  opacity: 0.7;
}

.chip-removable .chip-x:hover {
  opacity: 1;
}

/* ---- top bar -------------------------------------------------------------- */

.topbar {
  position: sticky;
  top: 0;
  z-index: 40;
  margin-inline: calc(-1 * var(--sp-4));
  padding: var(--sp-2) var(--sp-4);
  background: color-mix(in srgb, var(--bg) 86%, transparent);
  backdrop-filter: blur(12px) saturate(1.1);
  border-bottom: 1px solid var(--border);
}

/* ---- progress bars (refresh / enrich / send) ------------------------------ */
/* Classic left → right fill. Prefer determinate width; unknown-duration work
   uses a slow soft grow (still LTR), never a sliding chip. */

.progress-host {
  width: 100%;
  margin-top: var(--sp-2);
}

.progress-host--queue {
  margin-top: var(--sp-1);
  max-width: 16rem;
}

.progress {
  position: relative;
  height: 4px;
  border-radius: var(--r-full);
  background: var(--surface-3);
  overflow: hidden;
}

.progress-fill {
  display: block;
  height: 100%;
  width: 0%;
  max-width: 100%;
  margin: 0;
  border-radius: inherit;
  background: var(--ok);
  transform-origin: left center;
  transition: width var(--dur-3) var(--ease);
}

.progress.is-muted .progress-fill {
  background: var(--gray-400);
  opacity: 0.85;
}

/* Unknown duration (fetch / enrich): fill creeps L→R up to 90%, then holds. */
.progress.is-soft .progress-fill {
  width: 0%;
  transition: none;
  animation: progress-grow-ltr var(--progress-soft-ms, 75s) linear forwards;
}

@keyframes progress-grow-ltr {
  0% {
    width: 0%;
  }
  100% {
    width: 90%;
  }
}

@media (prefers-reduced-motion: reduce) {
  .progress.is-soft .progress-fill {
    animation: none;
    width: 40%;
    opacity: 0.75;
  }

  .progress-fill {
    transition: none;
  }
}

/* Evidence-panel strip (enrich running) */
.evidence-progress {
  margin: var(--sp-2) 0 var(--sp-3);
}

/* Provider identity rule: the one accent line in the chrome. */
.topbar::after {
  content: "";
  position: absolute;
  left: 0;
  right: 0;
  bottom: -1px;
  height: 2px;
  background: var(--provider-accent);
  transition: background var(--dur-3) var(--ease);
}

.topbar-inner {
  display: flex;
  align-items: center;
  gap: var(--sp-3);
  flex-wrap: wrap;
}

.brand {
  display: flex;
  align-items: baseline;
  gap: var(--sp-2);
  margin-right: var(--sp-2);
}

/* The wordmark and the focus-mode name are the only two places the serif
   earns its keep. A display serif needs size: at --fs-lg over a photo it
   just loses legibility for no gain, so .card-name stays sans. */
.brand-name {
  font-family: var(--font-serif);
  font-size: 1.1875rem;
  font-weight: 500;
  letter-spacing: -0.005em;
}

.brand-sub {
  font-size: var(--fs-xs);
  color: var(--text-3);
}

.status-capsule {
  display: inline-flex;
  align-items: center;
  gap: var(--sp-2);
  padding: var(--sp-1) var(--sp-3);
  border: 1px solid var(--border);
  border-radius: var(--r-full);
  background: var(--surface);
  font-size: var(--fs-xs);
  font-weight: 500;
  color: var(--text-2);
  min-height: 28px;
  max-width: 24rem;
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}

.status-capsule::before {
  content: "";
  width: 7px;
  height: 7px;
  border-radius: 50%;
  background: var(--ok);
  flex-shrink: 0;
}

/* The capsule is a button: with no session it is the way into setup. */
button.status-capsule {
  appearance: none;
  cursor: pointer;
  text-align: left;
  transition:
    background var(--dur-1) var(--ease),
    border-color var(--dur-1) var(--ease);
}

button.status-capsule:hover {
  background: var(--surface-2);
  border-color: var(--border-strong);
}

.status-capsule.is-action {
  border-color: color-mix(in srgb, var(--danger) 45%, var(--border));
  color: var(--text);
  font-weight: 600;
}

.status-capsule.is-busy::before {
  background: var(--warn);
  animation: capsule-pulse 1.2s ease-in-out infinite;
}

.status-capsule.is-error::before {
  background: var(--danger);
}

@keyframes capsule-pulse {
  50% {
    opacity: 0.35;
  }
}

.topbar-actions {
  display: flex;
  align-items: center;
  gap: var(--sp-2);
  margin-left: auto;
  flex-wrap: wrap;
}

/* ---- overflow menu -------------------------------------------------------- */

.menu-wrap {
  position: relative;
}

.menu-panel {
  position: absolute;
  right: 0;
  top: calc(100% + var(--sp-1));
  z-index: 50;
  /* Wide enough for the settings groups the panel now carries; "Metric |
     Imperial" side by side does not fit the old 12rem. */
  min-width: 14.5rem;
  padding: var(--sp-1);
  background: var(--surface);
  border: 1px solid var(--border);
  border-radius: var(--r-md);
  box-shadow: var(--elev-3);
  display: flex;
  flex-direction: column;
}

.menu-item {
  appearance: none;
  border: 0;
  background: transparent;
  text-align: left;
  padding: var(--sp-2) var(--sp-3);
  border-radius: var(--r-sm);
  font-size: var(--fs-sm);
  cursor: pointer;
}

/* One row of the menu is a link (the terms) rather than a button. It is a
   flex child of .menu-panel so it blockifies on its own; this only strips the
   global link treatment, which would otherwise underline one menu row. */
a.menu-item {
  color: inherit;
  text-decoration: none;
}

.menu-item:hover:not(:disabled) {
  background: var(--surface-2);
}

.menu-item:disabled {
  opacity: 0.45;
  cursor: not-allowed;
}

.menu-sep {
  height: 1px;
  margin: var(--sp-1) var(--sp-2);
  background: var(--border);
}

/* A setting inside the menu: a quiet caption over its control. Menu *items*
   are verbs and get the row treatment above; these are nouns with a current
   value, so they are labelled rather than pressed. */
.menu-group {
  display: flex;
  flex-direction: column;
  gap: var(--sp-1);
  padding: var(--sp-2) var(--sp-3);
}

.menu-group-label {
  font-size: var(--fs-xs);
  color: var(--text-3);
}

.menu-group .segmented {
  align-self: stretch;
}

.menu-group .seg-btn {
  flex: 1;
  justify-content: center;
}

.menu-note {
  margin: var(--sp-1) var(--sp-3) var(--sp-2);
  font-size: var(--fs-xs);
  color: var(--text-3);
  line-height: 1.4;
}

/* ---- banners -------------------------------------------------------------- */

/* App-level informational notice (e.g. research disabled). Prominent but
   calm: a bordered strip, not a flashing alert. Neutral tint — informative,
   not a warning; the `--action` variant below is the one that wants a
   response. */
.notice-banner {
  display: flex;
  align-items: center;
  gap: var(--sp-2);
  margin-top: var(--sp-3);
  padding: var(--sp-2) var(--sp-4);
  background: var(--surface-2);
  border: 1px solid var(--border);
  border-radius: var(--r-md);
  font-size: var(--fs-sm);
  color: var(--text-2);
}

.notice-banner::before {
  content: "";
  width: 8px;
  height: 8px;
  border-radius: 50%;
  background: var(--text-4);
  flex-shrink: 0;
}

/* Connector "not paired yet" wants an action from the operator, so it takes
   the warn accent; "offline" (paired, just unreachable right now) stays the
   plain neutral .notice-banner above — nothing broke, no accent needed. Same
   shape either way: this is deliberately not styled as an error/outage
   banner. */
.notice-banner--action {
  background: var(--warn-soft);
  border-color: color-mix(in srgb, var(--warn) 45%, var(--border));
}

.notice-banner--action::before {
  background: var(--warn);
}

/* The queued-send banner carries an interactive button alongside its text
   (unlike the read-only banners above), so it wraps onto its own line
   instead of squeezing Cancel off-screen on a narrow viewport. */
#queued-banner {
  flex-wrap: wrap;
}

#queued-banner .btn {
  margin-left: auto;
}

.halt-banner {
  display: flex;
  align-items: baseline;
  gap: var(--sp-2);
  flex-wrap: wrap;
  margin-top: var(--sp-3);
  padding: var(--sp-3) var(--sp-4);
  background: var(--danger-soft);
  border: 1px solid color-mix(in srgb, var(--danger) 45%, var(--border));
  border-radius: var(--r-md);
  font-size: var(--fs-sm);
  color: var(--text);
}

.halt-banner strong {
  color: var(--danger);
  font-weight: 700;
}

/* ---- filter strip --------------------------------------------------------- */

.filter-strip {
  display: flex;
  align-items: center;
  gap: var(--sp-3);
  padding: var(--sp-3) 0;
  min-height: 52px;
}

.active-filters {
  display: flex;
  align-items: center;
  gap: var(--sp-2);
  flex-wrap: wrap;
  flex: 1 1 auto;
  min-width: 0;
}

/* Empty label comes from the markup (data-empty-label, kept current by the
   data-i18n-empty walk in app.js) so it follows the active locale. */
.active-filters:empty::after {
  content: attr(data-empty-label);
  color: var(--text-4);
  font-size: var(--fs-xs);
}

.filter-count {
  color: var(--text-3);
  font-size: var(--fs-xs);
  white-space: nowrap;
}

.sort-label {
  display: inline-flex;
  align-items: center;
  gap: var(--sp-2);
  color: var(--text-3);
  font-size: var(--fs-xs);
  white-space: nowrap;
}

/* `email` belongs on this list for the same reason `tel` does — the account
   pages ask for one. Without it the field fell through to the UA's 2px inset
   border, so the email box rendered as a visibly different control from the
   password box directly beneath it on every sign-in, sign-up and reset form. */
select,
input[type="search"],
input[type="text"],
input[type="email"],
input[type="number"],
input[type="tel"],
input[type="password"] {
  background: var(--surface);
  color: var(--text);
  border: 1px solid var(--border);
  border-radius: var(--r-sm);
  padding: var(--sp-1) var(--sp-2);
  font: inherit;
  font-size: var(--fs-sm);
  min-width: 0;
  transition: border-color var(--dur-1) var(--ease);
}

select:hover:not(:disabled),
input[type="search"]:hover:not(:disabled),
input[type="text"]:hover:not(:disabled),
input[type="email"]:hover:not(:disabled),
input[type="number"]:hover:not(:disabled),
input[type="tel"]:hover:not(:disabled),
input[type="password"]:hover:not(:disabled) {
  border-color: var(--border-strong);
}

/* Native controls, tinted to the theme. A hand-built checkbox would buy an
   animatable tick and cost the platform's own hit-target and VoiceOver
   behaviour — not a trade worth making for one sign-up box. */
input[type="checkbox"],
input[type="radio"] {
  accent-color: var(--text);
}

/* ---- views container ------------------------------------------------------ */

.views {
  flex: 1 0 auto;
  padding-block: var(--sp-2) var(--sp-6);
}

/* Grid view */
.card-grid {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(260px, 1fr));
  gap: var(--sp-4);
}

/* List view */
.card-list {
  display: flex;
  flex-direction: column;
  gap: var(--sp-2);
}

.list-row {
  display: grid;
  grid-template-columns: 56px minmax(0, 2fr) minmax(0, 1.5fr) auto auto;
  align-items: center;
  gap: var(--sp-3);
  padding: var(--sp-2) var(--sp-3);
  background: var(--surface);
  border: 1px solid var(--border);
  border-radius: var(--r-md);
  transition: border-color var(--dur-1) var(--ease);
}

.list-row:hover {
  border-color: var(--border-strong);
}

.list-row.is-like {
  border-color: color-mix(in srgb, var(--like) 55%, var(--border));
}

.list-thumb {
  width: 56px;
  height: 72px;
  object-fit: cover;
  border-radius: var(--r-sm);
  background: var(--surface-2);
}

.list-main {
  min-width: 0;
}

.list-name {
  font-weight: 600;
  font-size: var(--fs-md);
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}

.list-sub {
  color: var(--text-3);
  font-size: var(--fs-xs);
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}

.list-decision {
  display: flex;
  align-items: center;
  gap: var(--sp-2);
  min-width: 0;
}

/* ---- card ----------------------------------------------------------------- */

.card {
  position: relative;
  display: flex;
  flex-direction: column;
  background: var(--surface);
  border: 1px solid var(--border);
  border-radius: var(--r-lg);
  overflow: hidden;
  box-shadow: var(--elev-1);
  transition:
    border-color var(--dur-2) var(--ease),
    box-shadow var(--dur-2) var(--ease),
    transform var(--dur-2) var(--ease);
}

.card:hover {
  border-color: var(--border-strong);
  box-shadow: var(--elev-2);
  transform: translateY(-2px);
}

.card.is-like {
  border-color: color-mix(in srgb, var(--like) 60%, var(--border));
  box-shadow: 0 0 0 1px color-mix(in srgb, var(--like) 45%, transparent),
    var(--elev-2);
}

.card[data-status="liked"],
.card[data-status="passed"] {
  opacity: 0.72;
}

.card[data-status="failed"] {
  border-color: color-mix(in srgb, var(--danger) 50%, var(--border));
}

/* media block — photo-forward, 3:4 */
.card-media {
  position: relative;
  aspect-ratio: 3 / 4;
  background: var(--surface-2);
  overflow: hidden;
  user-select: none;
}

.card-photo {
  width: 100%;
  height: 100%;
  object-fit: cover;
  transition: opacity var(--dur-2) var(--ease);
}

/* ---- lazy photo skeleton ---------------------------------------------------
   Photos are ~90% of this board's area and every one of them is lazy, so a
   fresh grid painted as a wall of flat --surface-2 rectangles — visually
   identical to a board whose images had failed. The shimmer is painted as the
   <img>'s OWN background: an undecoded img is transparent, and a decoded one
   covers its box completely (object-fit: cover), so the skeleton is hidden the
   instant the photo lands with no extra element and no :has().

   `is-settled` is added by app.js on both load AND error — an image that
   failed must stop claiming it is still coming. Targeting the lazy attribute
   rather than a class covers grid, list, read and the review modal at once,
   which is exactly the set with the problem. */
img[loading="lazy"]:not(.is-settled) {
  /* The highlight is a percentage of --text rather than the next step on the
     surface scale: --surface-2 to --surface-3 is a ~6% luminance step in dark
     theme, which is invisible in motion. Mixing toward the ink also makes the
     sweep self-inverting — a light band on the dark theme, a dark one on the
     light theme — instead of needing a second rule per theme. */
  background-image: linear-gradient(
    100deg,
    var(--surface-2) 25%,
    color-mix(in srgb, var(--text) 16%, var(--surface-2)) 50%,
    var(--surface-2) 75%
  );
  /* At 300% the highlight is half the image wide, and sweeping position from
     100% to 0 keeps it inside the box for half of every cycle. A narrower band
     on an ease-in-out curve — the first attempt — spent most of the loop
     off-screen and crossed at the curve's fastest point, so it measurably
     animated while being invisible. Linear, because a skeleton is a metronome,
     not a gesture. */
  background-size: 300% 100%;
  animation: photo-shimmer 1600ms linear infinite;
}

@keyframes photo-shimmer {
  from {
    background-position: 100% 0;
  }
  to {
    background-position: 0 0;
  }
}

.photo-placeholder {
  position: absolute;
  inset: 0;
  display: grid;
  place-items: center;
  color: var(--text-4);
  font-size: var(--fs-sm);
}

/* stories-style pager strip */
.photo-strip {
  position: absolute;
  top: var(--sp-2);
  left: var(--sp-2);
  right: var(--sp-2);
  z-index: 3;
  display: flex;
  gap: var(--sp-1);
  pointer-events: none;
}

.photo-strip span {
  flex: 1;
  height: 3px;
  border-radius: var(--r-full);
  background: rgba(255, 255, 255, 0.3);
}

.photo-strip span.on {
  background: #fff;
}

.media-top {
  position: absolute;
  top: var(--sp-4);
  left: var(--sp-2);
  right: var(--sp-2);
  z-index: 3;
  display: flex;
  align-items: flex-start;
  justify-content: space-between;
  gap: var(--sp-2);
  pointer-events: none;
}

.media-top-left {
  display: flex;
  align-items: center;
  gap: var(--sp-1);
  min-width: 0;
}

/* provider corner tag — identity accent as text, frosted pill behind */
.provider-tag {
  padding: 2px var(--sp-2);
  border-radius: var(--r-full);
  background: var(--scrim-chip-strong);
  backdrop-filter: blur(6px);
  color: var(--provider-on-scrim);
  font-size: 0.62rem;
  font-weight: 700;
  letter-spacing: 0.08em;
  text-transform: uppercase;
}

/* Status and corner chips sit on a photo, so they use the denser scrim and the
   *-on-scrim tints. The page-level semantic colors read near 1.2:1 here. */
.status-badge {
  padding: 2px var(--sp-2);
  border-radius: var(--r-full);
  background: var(--scrim-chip-strong);
  backdrop-filter: blur(6px);
  color: var(--scrim-text);
  font-size: var(--fs-xs);
  font-weight: 600;
  white-space: nowrap;
}

.status-badge[data-status="pending"] {
  display: none;
}

.status-badge[data-status="queued"] {
  color: var(--superlike-on-scrim);
}

.status-badge[data-status="liked"] {
  color: var(--like-on-scrim);
}

.status-badge[data-status="failed"] {
  color: var(--danger-on-scrim);
}

.status-badge[data-status="skipped"] {
  color: var(--warn-on-scrim);
}

.corner-chips {
  display: flex;
  flex-direction: column;
  align-items: flex-end;
  gap: var(--sp-1);
}

.chip-corner {
  padding: 2px var(--sp-2);
  border-radius: var(--r-full);
  background: var(--scrim-chip-strong);
  backdrop-filter: blur(6px);
  color: var(--scrim-text);
  font-size: var(--fs-xs);
  font-variant-numeric: tabular-nums;
  white-space: nowrap;
}

.chip-corner.verified {
  color: var(--ok-on-scrim);
  font-weight: 600;
}

/* invisible click halves for photo paging */
.photo-nav {
  position: absolute;
  inset: 0;
  z-index: 2;
  display: flex;
}

.photo-nav button {
  flex: 1;
  border: 0;
  background: transparent;
  cursor: pointer;
  padding: 0;
}

.photo-nav button:focus-visible {
  outline: 2px solid var(--focus-ring);
  outline-offset: -4px;
}

.card-scrim {
  position: absolute;
  left: 0;
  right: 0;
  bottom: 0;
  height: 45%;
  z-index: 2;
  background: linear-gradient(transparent, var(--scrim));
  pointer-events: none;
}

.card-identity {
  position: absolute;
  left: var(--sp-3);
  right: calc(var(--sp-12) + var(--sp-3));
  bottom: var(--sp-3);
  z-index: 3;
  color: var(--scrim-text);
  pointer-events: none;
}

.card-name {
  margin: 0;
  font-size: var(--fs-lg);
  font-weight: 700;
  letter-spacing: -0.01em;
  line-height: var(--lh-tight);
  text-shadow: 0 1px 8px rgba(0, 0, 0, 0.5);
}

.card-age {
  font-weight: 500;
  opacity: 0.9;
  font-variant-numeric: tabular-nums;
}

.card-sub {
  margin: 2px 0 0;
  font-size: var(--fs-xs);
  opacity: 0.85;
  text-shadow: 0 1px 6px rgba(0, 0, 0, 0.5);
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}

/* circular like FAB */
.like-fab {
  position: absolute;
  right: var(--sp-3);
  bottom: var(--sp-3);
  z-index: 4;
  width: 44px;
  height: 44px;
  border-radius: 50%;
  border: 1px solid rgba(255, 255, 255, 0.25);
  background: var(--scrim-chip);
  backdrop-filter: blur(8px);
  color: #fff;
  font-size: 1.15rem;
  line-height: 1;
  display: grid;
  place-items: center;
  cursor: pointer;
  transition:
    transform var(--dur-1) var(--ease),
    background var(--dur-1) var(--ease),
    box-shadow var(--dur-1) var(--ease);
}

.like-fab:hover:not(:disabled) {
  transform: scale(1.07);
}

.like-fab.on {
  background: var(--like);
  border-color: transparent;
  box-shadow: 0 6px 20px color-mix(in srgb, var(--like) 45%, transparent);
}

.like-fab:disabled {
  opacity: 0.4;
  cursor: not-allowed;
}

/* card body */
.card-body {
  display: flex;
  flex-direction: column;
  gap: var(--sp-2);
  padding: var(--sp-3) var(--sp-4) var(--sp-4);
  flex: 1;
}

.card-job {
  margin: 0;
  font-size: var(--fs-md);
  font-weight: 600;
  line-height: 1.35;
}

.card-secondary {
  margin: 0;
  font-size: var(--fs-xs);
  color: var(--text-3);
  line-height: 1.35;
}

/* decision row: career tag + income */
.card-decision {
  display: flex;
  align-items: center;
  gap: var(--sp-2);
  flex-wrap: wrap;
}

.tag {
  display: inline-flex;
  align-items: center;
  padding: 2px var(--sp-2);
  border-radius: var(--r-full);
  border: 1px solid transparent;
  font-size: var(--fs-xs);
  font-weight: 600;
  line-height: var(--lh-tight);
}

.tag[data-career="strong"] {
  color: var(--ok);
  background: var(--ok-soft);
}

.tag[data-career="good"] {
  color: var(--text);
  background: var(--surface-2);
  border-color: var(--border-strong);
}

.tag[data-career="average"] {
  color: var(--warn);
  background: var(--warn-soft);
}

.tag[data-career="weak"] {
  color: var(--danger);
  background: var(--danger-soft);
}

/* 不明 must read as absence, not as a claim: near-muted, no pill weight. */
.tag[data-career="unknown"] {
  color: var(--text-4);
  background: transparent;
  border-color: transparent;
  font-weight: 400;
  padding-inline: 0;
}

.income {
  font-size: var(--fs-sm);
  font-weight: 600;
  font-variant-numeric: tabular-nums;
  color: var(--text-2);
}

.income[data-unknown="true"] {
  color: var(--text-4);
  font-weight: 400;
}

/* Scoring reason: multi-line so Luna's 1–2 sentences stay readable.
   Hover still has the full string via title=; focus mode shows everything. */
.card-reason {
  margin: 0;
  font-size: var(--fs-sm);
  color: var(--text-2);
  line-height: 1.45;
  display: -webkit-box;
  -webkit-box-orient: vertical;
  -webkit-line-clamp: 4;
  line-clamp: 4;
  overflow: hidden;
  overflow-wrap: anywhere;
  word-break: break-word;
  white-space: normal;
}

/* ---- evidence block (enrichment) ------------------------------------------ */

.evidence {
  border: 1px solid var(--border);
  border-radius: var(--r-md);
  background: var(--surface-2);
  font-size: var(--fs-xs);
}

.evidence-toggle {
  appearance: none;
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--sp-2);
  width: 100%;
  padding: var(--sp-2) var(--sp-3);
  border: 0;
  background: transparent;
  cursor: pointer;
  text-align: left;
  border-radius: var(--r-md);
}

.evidence-toggle:hover {
  background: color-mix(in srgb, var(--text) 4%, transparent);
}

.evidence-toggle-main {
  display: flex;
  align-items: center;
  /* Chips wrap as whole units at narrow card widths (260px grid column)
     instead of hard-clipping against the card's overflow:hidden. */
  flex-wrap: wrap;
  gap: var(--sp-2);
  row-gap: 2px;
  min-width: 0;
}

.evidence-chevron {
  flex-shrink: 0;
  width: 6px;
  height: 6px;
  border-right: 1.5px solid var(--text-3);
  border-bottom: 1.5px solid var(--text-3);
  transform: rotate(-45deg);
  transition: transform var(--dur-2) var(--ease);
}

.evidence.is-open .evidence-chevron {
  transform: rotate(45deg);
}

.quality-badge {
  padding: 1px var(--sp-2);
  border-radius: var(--r-full);
  font-weight: 600;
  white-space: nowrap;
  max-width: 100%;
  overflow: hidden;
  text-overflow: ellipsis;
  color: var(--text-3);
  background: var(--surface);
  border: 1px solid var(--border);
}

.quality-badge[data-quality="likely_good"] {
  color: var(--ok);
  background: var(--ok-soft);
  border-color: transparent;
}

.quality-badge[data-quality="uncertain"] {
  color: var(--warn);
  background: var(--warn-soft);
  border-color: transparent;
}

.quality-badge[data-quality="miss_or_unavailable"] {
  color: var(--text-4);
}

.evidence-confidence {
  color: var(--text-3);
  font-variant-numeric: tabular-nums;
  white-space: nowrap;
  max-width: 100%;
  overflow: hidden;
  text-overflow: ellipsis;
}

.evidence-body {
  display: none;
}

.evidence.is-open .evidence-body {
  display: flex;
  flex-direction: column;
  gap: var(--sp-2);
  padding: 0 var(--sp-3) var(--sp-3);
}

.evidence-cand {
  border: 1px solid var(--border);
  border-radius: var(--r-sm);
  background: var(--surface);
  padding: var(--sp-2) var(--sp-3);
  display: flex;
  flex-direction: column;
  gap: 2px;
}

.evidence-cand.is-confirmed {
  border-color: color-mix(in srgb, var(--ok) 45%, var(--border));
  background: color-mix(in srgb, var(--ok) 5%, var(--surface));
}

.evidence-cand-line {
  color: var(--text);
  line-height: 1.35;
}

.evidence-cand-sub {
  color: var(--text-3);
}

.evidence-link {
  color: var(--superlike);
  text-decoration: none;
  word-break: break-all;
}

.evidence-link:hover {
  text-decoration: underline;
}

.evidence-note {
  margin: 0;
  color: var(--text-3);
  line-height: 1.4;
}

.evidence-actions {
  display: flex;
  gap: var(--sp-2);
  flex-wrap: wrap;
}

.card-foot {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--sp-2);
  margin-top: auto;
  font-size: var(--fs-xs);
  color: var(--text-3);
}

.card-foot .status-note {
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}

.card-foot .likeable-no {
  color: var(--danger);
}

/* ---- focus mode (full-bleed overlay) -------------------------------------- */

.focus-overlay {
  position: fixed;
  inset: 0;
  z-index: 70;
  background: var(--bg);
  display: flex;
  flex-direction: column;
}

.focus-top {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--sp-3);
  padding: var(--sp-3) var(--sp-4);
  border-bottom: 1px solid var(--border);
}

.focus-progress {
  font-size: var(--fs-sm);
  color: var(--text-3);
}

.focus-body {
  flex: 1;
  min-height: 0;
  display: grid;
  grid-template-columns: minmax(0, 1.1fr) minmax(320px, 0.9fr);
}

.focus-stage {
  position: relative;
  /* Hardcoded #0a0a0a before; that was a cold black next to a warm palette.
     --gray-950 is the same value the dark theme's page uses, so the stage
     now sits in the family in both themes. */
  background: var(--gray-950);
  display: grid;
  place-items: center;
  overflow: hidden;
  user-select: none;
}

.focus-stage img {
  max-width: 100%;
  max-height: 100%;
  object-fit: contain;
}

.focus-detail {
  overflow-y: auto;
  padding: var(--sp-8) var(--sp-8) var(--sp-10);
  display: flex;
  flex-direction: column;
  gap: var(--sp-3);
}

/* ---- focus: editorial treatment -------------------------------------------
   Focus is the "actually decide" surface, so it is the one place that trades
   density for hierarchy. Scale contrast does the work: the name is ~4x the
   labels around it, which is what makes the panel read as laid out rather
   than as a form. The grid and list stay compact — they are for scanning. */

.focus-kicker {
  display: flex;
  align-items: center;
  flex-wrap: wrap;
  gap: var(--sp-2);
  margin-bottom: var(--sp-1);
}

.focus-kicker .fk-place {
  font-size: 0.62rem;
  font-weight: 600;
  letter-spacing: 0.14em;
  text-transform: uppercase;
  color: var(--text-3);
}

.focus-detail .focus-name {
  margin: 0;
  font-family: var(--font-serif);
  /* Was --fs-2xl (24px), which is timid for the one screen built to be read
     rather than scanned. Fluid so a narrow panel does not overflow it. */
  font-size: clamp(2rem, 4.4vw, 3.25rem);
  /* 400, not 700: at this size the serif carries itself, and New York at
     700 goes cramped. CJK names fall through to Song, which suits it. */
  font-weight: 400;
  letter-spacing: -0.028em;
  line-height: 1.02;
  display: flex;
  align-items: baseline;
  gap: 0.4em;
}

.focus-detail .focus-name .card-age {
  font-size: 0.36em;
  color: var(--text-3);
  letter-spacing: 0;
}

.focus-role {
  font-size: var(--fs-base);
  color: var(--text-2);
  margin-top: var(--sp-1);
}

/* Spec table: labels in a scannable column, values right of a hairline.
   Row padding is deliberately loose — this is the read screen. */
.focus-specs {
  margin-top: var(--sp-3);
  border-top: 1px solid var(--border);
}

.focus-specs .sr {
  display: flex;
  align-items: baseline;
  gap: var(--sp-5);
  padding: var(--sp-3) 0;
  border-bottom: 1px solid var(--border);
  transition: background var(--dur-3) var(--ease);
}

.focus-specs .sr:hover {
  background: color-mix(in srgb, var(--text) 4%, transparent);
}

.focus-specs .k {
  flex: 0 0 7.5rem;
  font-size: var(--fs-xs);
  color: var(--text-3);
  letter-spacing: 0.02em;
}

.focus-specs .v {
  flex: 1;
  min-width: 0;
  font-size: var(--fs-md);
  color: var(--text);
}

/* The self-description is the one piece of prose on the screen, so it gets
   the serif and a real measure instead of running the panel's full width. */
.focus-bio {
  margin: var(--sp-4) 0 0;
  font-family: var(--font-serif);
  font-size: 1.0625rem;
  line-height: 1.6;
  color: var(--text-2);
  max-width: 46ch;
}

/* focus shows the full reason, not the clamp */
.focus-detail .card-reason {
  display: block;
  -webkit-box-orient: unset;
  -webkit-line-clamp: unset;
  line-clamp: none;
  overflow: visible;
  max-height: none;
}

/* ---- read view: editorial browse -------------------------------------------
   One column, one person per entry, photo side alternating. The alternation
   is a system rather than a decoration: a long scroll of identical rows reads
   as a table, and swapping sides is what keeps it reading as a layout. The
   information is identical in both directions, so nothing about comparing
   people depends on which side a card happened to land on.

   This view deliberately shows FEWER fields than focus. It is a browse: the
   things you weigh, then a link into focus for the full audit. */

.card-read {
  max-width: 62rem;
  margin: 0 auto;
  padding: var(--sp-6) var(--sp-6) var(--sp-12);
}

.read-entry {
  display: grid;
  grid-template-columns: 22rem minmax(0, 1fr);
  gap: var(--sp-8);
  align-items: start;
  padding: var(--sp-10) 0;
  border-bottom: 1px solid var(--border);
}

/* The sequence number sits 2.2rem above its entry, so the first one needs
   headroom or the container clips it. */
.read-entry:first-child {
  padding-top: var(--sp-8);
}

/* Odd entries keep the photo left; even ones mirror. */
.read-entry[data-beat="b"] {
  grid-template-columns: minmax(0, 1fr) 18rem;
}
.read-entry[data-beat="b"] .read-plate {
  order: 2;
}
.read-entry[data-beat="b"] .read-col {
  order: 1;
}
.read-entry[data-beat="b"] .read-media {
  aspect-ratio: 1 / 1;
}
.read-entry[data-beat="b"] .read-seq {
  left: auto;
  right: -1.6rem;
}

.read-plate {
  position: relative;
}

/* The sequence number breaks the column edge on purpose. Half of what makes a
   grid feel rigid is every element staying politely inside its own column. */
.read-seq {
  position: absolute;
  top: -2.2rem;
  left: -1.9rem;
  z-index: 2;
  font-family: var(--font-serif);
  font-size: 5.5rem;
  line-height: 1;
  color: var(--hinge);
  opacity: 0.17;
  pointer-events: none;
  transition: opacity var(--dur-3) var(--ease), transform var(--dur-3) var(--ease);
}

.read-entry:hover .read-seq {
  opacity: 0.32;
  transform: translateY(-0.3rem);
}

.read-media {
  position: relative;
  aspect-ratio: 3 / 4;
  overflow: hidden;
  border-radius: var(--r-md);
  background: var(--surface-2);
  isolation: isolate;
}

.read-photo {
  width: 100%;
  height: 100%;
  object-fit: cover;
  display: block;
  transition: transform 1.1s cubic-bezier(0.16, 0.84, 0.44, 1);
}

.read-entry:hover .read-photo {
  transform: scale(1.035);
}

.read-name {
  margin: 0;
  font-family: var(--font-serif);
  font-size: clamp(1.75rem, 3.4vw, 2.5rem);
  font-weight: 400;
  letter-spacing: -0.026em;
  line-height: 1.04;
  display: flex;
  align-items: baseline;
  gap: 0.4em;
}

.read-name .card-age {
  font-size: 0.4em;
  color: var(--text-3);
  letter-spacing: 0;
}

.read-acts {
  display: flex;
  align-items: center;
  gap: var(--sp-3);
  margin-top: var(--sp-5);
}

.read-deeper {
  appearance: none;
  border: 0;
  background: none;
  cursor: pointer;
  padding: var(--sp-1) 0;
  font: inherit;
  font-size: var(--fs-sm);
  color: var(--hinge);
  border-bottom: 1px solid color-mix(in srgb, var(--hinge) 32%, transparent);
  transition: border-color var(--dur-2) var(--ease);
}

.read-deeper:hover {
  border-bottom-color: var(--hinge);
}

@media (max-width: 860px) {
  .read-entry,
  .read-entry[data-beat="b"] {
    grid-template-columns: 1fr;
    gap: var(--sp-5);
  }
  .read-entry[data-beat="b"] .read-plate {
    order: 0;
  }
  .read-entry[data-beat="b"] .read-col {
    order: 0;
  }
  .read-seq,
  .read-entry[data-beat="b"] .read-seq {
    font-size: 3.5rem;
    top: -1.2rem;
    left: -0.5rem;
    right: auto;
  }
}

.focus-actions {
  display: flex;
  justify-content: center;
  gap: var(--sp-4);
  padding: var(--sp-3) var(--sp-4) var(--sp-5);
  border-top: 1px solid var(--border);
}

.focus-actions .btn {
  min-width: 9rem;
  padding-block: var(--sp-3);
}

.action-like {
  background: var(--like);
  border-color: transparent;
  color: #fff;
  font-weight: 600;
}

.action-like:hover:not(:disabled) {
  filter: brightness(1.08);
  background: var(--like);
  border-color: transparent;
}

/* ---- decision bar (sticky bottom) ----------------------------------------- */

.decision-bar {
  position: sticky;
  bottom: 0;
  z-index: 40;
  display: flex;
  align-items: center;
  gap: var(--sp-4);
  margin-inline: calc(-1 * var(--sp-4));
  padding: var(--sp-3) var(--sp-5);
  background: color-mix(in srgb, var(--surface) 88%, transparent);
  backdrop-filter: blur(12px);
  border-top: 1px solid var(--border);
}

.decision-main {
  display: flex;
  flex-direction: column;
  gap: var(--sp-1);
  min-width: 0;
  flex: 1;
}

.decision-counts {
  display: flex;
  align-items: center;
  gap: var(--sp-4);
  font-size: var(--fs-sm);
  color: var(--text-3);
  flex-wrap: wrap;
}

.decision-counts .count {
  display: inline-flex;
  align-items: baseline;
  gap: var(--sp-1);
  white-space: nowrap;
}

.decision-counts .count.like {
  color: var(--like);
  font-weight: 600;
}

.decision-counts .num {
  font-size: var(--fs-base);
  font-weight: 700;
}

.decision-bar .btn.send {
  margin-left: auto;
  min-width: 8rem;
  padding-block: var(--sp-2);
}

/* ---- filter drawer (slides from the right) -------------------------------- */

.drawer-backdrop {
  position: fixed;
  inset: 0;
  z-index: 59;
  background: rgba(8, 8, 8, 0.4);
  opacity: 0;
  pointer-events: none;
  transition: opacity var(--dur-3) var(--ease);
}

.drawer-backdrop.is-open {
  opacity: 1;
  pointer-events: auto;
}

.drawer {
  position: fixed;
  top: 0;
  right: 0;
  bottom: 0;
  z-index: 60;
  width: min(380px, 100vw);
  background: var(--surface);
  border-left: 1px solid var(--border);
  transform: translateX(100%);
  display: flex;
  flex-direction: column;
  /* visibility flips after the slide-out completes, so the closed drawer
     still animates yet leaves the tab order */
  visibility: hidden;
  transition:
    transform var(--dur-3) var(--ease),
    visibility 0s linear var(--dur-3);
}

.drawer.is-open {
  transform: none;
  box-shadow: var(--elev-3);
  visibility: visible;
  transition: transform var(--dur-3) var(--ease);
}

.drawer-head {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--sp-3);
  padding: var(--sp-4) var(--sp-5) var(--sp-3);
}

.drawer-head h2 {
  margin: 0;
  font-size: var(--fs-lg);
  font-weight: 700;
}

.drawer-note {
  margin: 0;
  padding: 0 var(--sp-5) var(--sp-3);
  font-size: var(--fs-xs);
  color: var(--text-3);
}

.drawer-body {
  flex: 1;
  overflow-y: auto;
  padding: 0 var(--sp-5) var(--sp-4);
  display: flex;
  flex-direction: column;
  gap: var(--sp-4);
}

.filter-field {
  margin: 0;
  padding: var(--sp-3) var(--sp-3) var(--sp-3);
  border: 1px solid var(--border);
  border-radius: var(--r-md);
  background: var(--surface-2);
  min-width: 0;
}

.filter-field legend {
  padding: 0 var(--sp-1);
  font-size: var(--fs-xs);
  font-weight: 650;
  color: var(--text-2);
  letter-spacing: 0.02em;
}

.filter-inline {
  display: flex;
  flex-wrap: wrap;
  gap: var(--sp-2) var(--sp-3);
  align-items: flex-end;
}

.filter-inline label,
.field-stack {
  display: flex;
  flex-direction: column;
  gap: var(--sp-1);
  font-size: var(--fs-xs);
  color: var(--text-3);
  min-width: 0;
}

.field-stack + .field-stack {
  margin-top: var(--sp-2);
}

/* The label row can carry a trailing tag ("Optional") pushed to the far edge,
   which is how a field says it may be skipped without spending a whole line
   and without the parenthetical wrapping under the label text. */
.field-label {
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  gap: var(--sp-2);
  font-size: var(--fs-sm);
  font-weight: 500;
  color: var(--text-2);
}

.field-tag {
  font-size: var(--fs-xs);
  font-weight: 400;
  color: var(--text-4);
}

/* Belongs to the field above it — see login.html for why it is a child of the
   label rather than a sibling of it. */
.field-hint {
  font-size: var(--fs-xs);
  color: var(--text-3);
}

.filter-field input[type="number"] {
  width: 4.5rem;
}

.filter-field input[type="search"] {
  width: 100%;
}

.check-label {
  display: flex;
  align-items: center;
  gap: var(--sp-2);
  margin-top: var(--sp-2);
  font-size: var(--fs-xs);
  color: var(--text-2);
  cursor: pointer;
}

/* Secondary filter dimensions (lifestyle, background). Ten chip groups would
   bury the ones that actually open a wave — age, height, distance, intent — so
   they collapse behind one disclosure and stay out of the tab order until the
   operator asks for them. */
.filter-more {
  border: 1px solid var(--border);
  border-radius: var(--r-md);
  background: var(--surface-2);
}

.filter-more > summary {
  padding: var(--sp-3);
  font-size: var(--fs-xs);
  font-weight: 650;
  color: var(--text-2);
  letter-spacing: 0.02em;
  cursor: pointer;
  list-style: none;
}

.filter-more > summary::-webkit-details-marker {
  display: none;
}

.filter-more > summary::before {
  content: "▸ ";
  color: var(--text-3);
}

.filter-more[open] > summary::before {
  content: "▾ ";
}

.filter-more-body {
  display: flex;
  flex-direction: column;
  gap: var(--sp-3);
  padding: 0 var(--sp-3) var(--sp-3);
}

/* Nested one level deep, so the inner fieldsets drop their own frame rather
   than drawing a box inside a box. */
.filter-more-body .filter-field {
  border: 0;
  padding: 0;
  background: none;
}

.drawer-foot {
  display: flex;
  justify-content: space-between;
  gap: var(--sp-3);
  padding: var(--sp-3) var(--sp-5) var(--sp-5);
  border-top: 1px solid var(--border);
}

/* ---- setup / login panel ---------------------------------------------------
   Takes over the card area when the active provider has no session. A single
   narrow column: this is a form, not a board. Filter strip and decision bar are
   hidden while it is up — neither means anything without a session. */

body[data-setup="open"] .filter-strip,
body[data-setup="open"] .decision-bar {
  display: none;
}

.setup {
  width: min(34rem, 100%);
  margin: var(--sp-6) auto var(--sp-10);
  display: flex;
  flex-direction: column;
  gap: var(--sp-4);
}

.setup-head {
  display: flex;
  align-items: flex-start;
  justify-content: space-between;
  gap: var(--sp-3);
}

/* h1 as well as h2: the inline board panel uses h2 (the page already has an
   h1), reset.html uses h1. Without this the reset heading fell through to the
   UA's 2em and towered over everything around it. */
.setup-head h1,
.setup-head h2 {
  margin: 0 0 var(--sp-1);
  font-size: var(--fs-xl);
  font-weight: 700;
  letter-spacing: -0.01em;
}

.setup-sub {
  margin: 0;
  font-size: var(--fs-sm);
  color: var(--text-3);
  line-height: 1.5;
}

.setup-pane {
  display: flex;
  flex-direction: column;
  gap: var(--sp-4);
  padding: var(--sp-5);
  background: var(--surface);
  border: 1px solid var(--border);
  border-radius: var(--r-lg);
  box-shadow: var(--elev-1);
}

.setup-state {
  margin: 0;
}

.setup-badge {
  display: inline-flex;
  align-items: center;
  gap: var(--sp-2);
  padding: 2px var(--sp-3);
  border-radius: var(--r-full);
  border: 1px solid var(--border);
  background: var(--surface-2);
  color: var(--text-2);
  font-size: var(--fs-xs);
  font-weight: 600;
}

.setup-badge::before {
  content: "";
  width: 7px;
  height: 7px;
  border-radius: 50%;
  background: var(--text-4);
}

.setup-badge[data-ready="true"] {
  color: var(--ok);
  background: var(--ok-soft);
  border-color: transparent;
}

.setup-badge[data-ready="true"]::before {
  background: var(--ok);
}

.setup-para,
.setup-note,
.setup-hint {
  margin: 0;
  line-height: 1.5;
}

.setup-para {
  font-size: var(--fs-sm);
  color: var(--text-2);
}

.setup-para code {
  font-family: var(--font-mono);
  font-size: 0.78em;
}

.setup-note {
  padding: var(--sp-2) var(--sp-4);
  background: var(--warn-soft);
  border: 1px solid color-mix(in srgb, var(--warn) 45%, var(--border));
  border-radius: var(--r-md);
  font-size: var(--fs-sm);
  color: var(--text);
}

.setup-hint {
  font-size: var(--fs-xs);
  color: var(--text-3);
}

.field-inline {
  margin: 0;
  padding: 0;
  border: 0;
  display: flex;
  flex-wrap: wrap;
  gap: var(--sp-3);
  align-items: center;
}

.field-inline > legend {
  padding: 0;
  font-size: var(--fs-xs);
  color: var(--text-3);
}

.field-radio {
  display: inline-flex;
  flex: 0 0 auto;
  align-items: center;
  gap: var(--sp-2);
  white-space: nowrap;
  font-size: var(--fs-sm);
  color: var(--text);
  cursor: pointer;
}

.btn.link {
  padding: 0;
  min-height: 0;
  background: none;
  border: 0;
  color: var(--accent);
  font-size: inherit;
  text-decoration: underline;
  cursor: pointer;
}

/* The experimental phone-login path. Collapsed and visually secondary on
   purpose: it reproduces the iOS app's login, which carries a device
   attestation Deck cannot produce. The web-session handoff above it needs no
   such blob, so it stays the recommended route. */
.setup-advanced {
  margin-top: var(--sp-4);
  padding: var(--sp-3) var(--sp-4);
  background: var(--surface-2);
  border: 1px solid var(--border);
  border-radius: var(--r-md);
}

.setup-advanced > summary {
  cursor: pointer;
  font-size: var(--fs-sm);
  font-weight: 600;
  color: var(--text-2);
}

.setup-advanced > summary:focus-visible {
  outline: 2px solid var(--focus-ring);
  outline-offset: 2px;
  border-radius: var(--r-xs);
}

.setup-advanced[open] > summary {
  margin-bottom: var(--sp-3);
  color: var(--text);
}

.setup-advanced textarea {
  width: 100%;
  resize: vertical;
  padding: var(--sp-2);
  background: var(--surface);
  color: var(--text);
  border: 1px solid var(--border);
  border-radius: var(--r-sm);
  font-family: var(--font-mono);
  font-size: var(--fs-xs);
  line-height: 1.4;
}

.setup-advanced textarea:focus-visible {
  outline: 2px solid var(--focus-ring);
  outline-offset: 1px;
}

.setup-form {
  display: flex;
  flex-direction: column;
  gap: var(--sp-2);
}

/* The rule is a *separator*, so it only exists when there is something above
   it to separate from. In the inline setup panel a form follows a badge and a
   paragraph; on login.html the form is the pane's first child, where the same
   declaration drew a hairline floating under 20px of empty card. */
.setup-form:not(:first-child) {
  padding-top: var(--sp-3);
  border-top: 1px solid var(--border);
}

.setup-form .field-stack {
  font-size: var(--fs-sm);
  color: var(--text-2);
}

/* Text fields only. Unqualified, this also stretched the accept-terms
   checkbox to the full form width and padded it, which pushed its label
   into a narrow column halfway across the card — the tick still worked, so
   nothing failed, it just looked broken. */
.setup-form input:not([type="checkbox"]):not([type="radio"]) {
  width: 100%;
  padding: var(--sp-2) var(--sp-3);
  font-size: var(--fs-base);
}

.setup-form .btn {
  align-self: flex-start;
  min-width: 9rem;
  padding-block: var(--sp-2);
}

/* Reserves its line so a message appearing does not shift the buttons. */
.setup-msg {
  margin: 0;
  min-height: 1.3em;
  font-size: var(--fs-sm);
  line-height: 1.3;
  color: var(--text-2);
}

.setup-msg[data-kind="ok"] {
  color: var(--ok);
}

.setup-msg[data-kind="error"] {
  color: var(--danger);
}

.setup-danger {
  display: flex;
  flex-direction: column;
  gap: var(--sp-2);
  padding-top: var(--sp-3);
  border-top: 1px solid var(--border);
  align-items: flex-start;
}

.setup-confirm {
  display: flex;
  flex-direction: column;
  gap: var(--sp-2);
  width: 100%;
  padding: var(--sp-3);
  background: var(--danger-soft);
  border: 1px solid color-mix(in srgb, var(--danger) 35%, var(--border));
  border-radius: var(--r-md);
}

.setup-confirm-actions {
  display: flex;
  gap: var(--sp-2);
  flex-wrap: wrap;
}

/* ---- account page (login.html) --------------------------------------------
   Reuses .shell/.setup/.setup-form/.field-stack/.check-label wholesale —
   this is the same "one narrow form card" language the inline setup panel
   above already uses, just on its own page instead of taking over the
   board. Only the handful of rules below are new. */

.login-shell {
  justify-content: center;
}

/* On the board the wordmark is one item in a horizontal bar. Here it is the
   page's title, so it stacks and takes display size. */
.login-brand {
  flex-direction: column;
  align-items: center;
  gap: var(--sp-1);
  margin: 0 0 var(--sp-5);
}

.login-brand .brand-name {
  margin: 0;
  font-size: 1.75rem;
  font-weight: 500;
}

.login-brand .brand-sub {
  margin: 0;
}

/* Two fields do not need 34rem. The shared .setup width is sized for the
   board's inline session panel, which carries far more. */
.login-card {
  width: min(23rem, 100%);
  margin-block: 0;
  gap: var(--sp-3);
}

.login-tabs .seg-btn {
  flex: 1;
  justify-content: center;
}

.login-pane {
  gap: var(--sp-3);
  padding: var(--sp-5) var(--sp-5) var(--sp-4);
}

/* The pane that just became visible slides up as it fades. Tab switching is
   otherwise instantaneous in a way that reads as the page having reloaded. */
.login-pane:not([hidden]) {
  animation: pane-in var(--dur-3) var(--ease) both;
}

@keyframes pane-in {
  from {
    opacity: 0;
    transform: translateY(5px);
  }
}

.login-pane .setup-form {
  gap: var(--sp-4);
}

/* Full width: the submit is the only thing to do on this card, and a 9rem
   button under a 100%-wide field looks like it lost an argument with the
   layout. The shared rule stays auto-width for the board's session panel,
   where several buttons share a row. */
.login-pane .setup-form .btn {
  align-self: stretch;
  width: 100%;
  margin-top: var(--sp-1);
  padding-block: var(--sp-3);
}

.login-alt {
  margin: 0;
  text-align: center;
}

/* Centred to sit under the centred wordmark. Only reset.html has a heading
   block here — login.html's title is the wordmark itself. */
.login-card .setup-head {
  justify-content: center;
  text-align: center;
}

/* Quiet tail on the settings panel: the terms are there to be found on
   purpose, not to compete with the sign-in controls above them. */
.setup-foot {
  margin-top: var(--sp-6);
  padding-top: var(--sp-4);
  border-top: 1px solid var(--border);
  font-size: var(--fs-sm);
}

.setup-foot a {
  color: var(--text-3);
}

.setup-foot a:hover {
  color: var(--accent);
}

/* ---- empty states ---------------------------------------------------------- */

.empty-state {
  text-align: center;
  padding: var(--sp-12) var(--sp-4);
  color: var(--text-3);
}

.empty-title {
  margin: 0 0 var(--sp-1);
  font-size: var(--fs-lg);
  font-weight: 600;
  color: var(--text-2);
}

.empty-sub {
  margin: 0 0 var(--sp-4);
  font-size: var(--fs-sm);
}

.empty-actions {
  display: flex;
  justify-content: center;
  gap: var(--sp-2);
}

/* ---- footer ---------------------------------------------------------------- */

.page-foot {
  padding: var(--sp-6) 0 var(--sp-4);
  text-align: center;
  font-size: var(--fs-xs);
  color: var(--text-4);
}

.page-foot p {
  margin: var(--sp-1) 0;
}


/* ---- dialogs ---------------------------------------------------------------- */

.modal {
  border: 0;
  padding: 0;
  background: transparent;
  color: var(--text);
  max-width: calc(100vw - 2rem);
}

.modal::backdrop {
  background: rgba(8, 8, 8, 0.55);
  backdrop-filter: blur(3px);
}

.modal-panel {
  width: min(26rem, calc(100vw - 2rem));
  margin: auto;
  background: var(--surface);
  border: 1px solid var(--border);
  border-radius: var(--r-lg);
  box-shadow: var(--elev-3);
  padding: var(--sp-5);
}

.modal-wide .modal-panel {
  width: min(46rem, calc(100vw - 2rem));
}

.modal-panel h2 {
  margin: 0 0 var(--sp-3);
  font-size: var(--fs-xl);
  font-weight: 700;
  letter-spacing: -0.01em;
}

.modal-body {
  display: flex;
  flex-direction: column;
  gap: var(--sp-3);
  font-size: var(--fs-md);
}

.modal-body p,
.modal-body ul {
  margin: 0;
  line-height: var(--lh-normal);
}

.modal-muted {
  color: var(--text-3);
  font-size: var(--fs-sm);
}

.modal-note {
  font-size: var(--fs-xs);
  color: var(--text-3);
  margin: var(--sp-3) 0 0;
}

.modal-actions {
  display: flex;
  justify-content: flex-end;
  gap: var(--sp-2);
  margin-top: var(--sp-4);
}

.modal-actions.center {
  justify-content: center;
}

/* send review: two-column per-person checklists */
.review-columns {
  display: grid;
  grid-template-columns: 1fr 1fr;
  gap: var(--sp-4);
}

.review-col h3 {
  margin: 0 0 var(--sp-2);
  font-size: var(--fs-sm);
  font-weight: 650;
  color: var(--text-2);
}

.review-col[data-side="like"] h3 {
  color: var(--like);
}

.review-list {
  list-style: none;
  margin: 0;
  padding: var(--sp-1);
  max-height: 40vh;
  overflow-y: auto;
  border: 1px solid var(--border);
  border-radius: var(--r-md);
  background: var(--surface-2);
  display: flex;
  flex-direction: column;
  gap: 2px;
}

.review-list:empty::after {
  content: attr(data-empty-label);
  display: block;
  text-align: center;
  padding: var(--sp-4);
  color: var(--text-4);
  font-size: var(--fs-xs);
}

.review-item {
  display: flex;
  align-items: center;
  gap: var(--sp-2);
  padding: var(--sp-1) var(--sp-2);
  border-radius: var(--r-sm);
  font-size: var(--fs-sm);
}

.review-item:hover {
  background: color-mix(in srgb, var(--text) 5%, transparent);
}

.review-thumb {
  width: 36px;
  height: 48px;
  object-fit: cover;
  border-radius: var(--r-xs);
  background: var(--surface-3);
  flex-shrink: 0;
}

.review-name {
  flex: 1;
  min-width: 0;
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}

.review-move {
  flex-shrink: 0;
}

/* keyboard help */
.keys-grid {
  display: grid;
  grid-template-columns: auto 1fr;
  gap: var(--sp-2) var(--sp-4);
  align-items: center;
  margin: 0;
  font-size: var(--fs-sm);
}

.keys-grid dt {
  display: flex;
  gap: var(--sp-1);
  justify-content: flex-end;
}

.keys-grid dd {
  margin: 0;
  color: var(--text-2);
}

kbd {
  font-family: var(--font-mono);
  font-size: var(--fs-xs);
  background: var(--surface-2);
  border: 1px solid var(--border);
  border-bottom-width: 2px;
  border-radius: var(--r-xs);
  padding: 1px 6px;
  color: var(--text-2);
  white-space: nowrap;
}

/* ---- breakpoints ------------------------------------------------------------ */

@media (max-width: 900px) {
  /* Two columns down from the auto-fill default, so a mid-width tablet
     never lands on an awkward 1-then-jump-to-3 layout as it rotates. */
  .card-grid {
    grid-template-columns: repeat(2, 1fr);
  }

  .focus-body {
    grid-template-columns: 1fr;
    grid-template-rows: minmax(0, 1.2fr) minmax(0, 1fr);
  }

  .list-row {
    grid-template-columns: 56px minmax(0, 1fr) auto;
  }

  .list-decision {
    display: none;
  }
}

@media (max-width: 640px) {
  .topbar-inner {
    gap: var(--sp-2);
  }

  .status-capsule {
    order: 5;
    flex: 1 1 100%;
    max-width: none;
  }

  .topbar-actions {
    margin-left: 0;
  }

  .filter-strip {
    flex-wrap: wrap;
  }

  .sort-label {
    margin-left: auto;
  }

  /* One column: a phone-width card at 260px+ next to a second one has no
     room left worth using. */
  .card-grid {
    grid-template-columns: 1fr;
  }

  .decision-bar {
    flex-wrap: wrap;
    gap: var(--sp-2) var(--sp-4);
    /* Sticky-bottom bar would otherwise sit under the home indicator in a
       standalone PWA launch (index.html's viewport-fit=cover is what makes
       env() resolve to a real value here). env() is 0 everywhere else —
       every desktop browser and any non-notched phone — so this is a no-op
       there. */
    padding-bottom: calc(var(--sp-3) + env(safe-area-inset-bottom, 0px));
  }

  .decision-bar .btn.send {
    flex: 1 1 100%;
    margin-left: 0;
  }

  .review-columns {
    grid-template-columns: 1fr;
  }

  .drawer {
    width: 100vw;
    border-left: 0;
  }

  .focus-actions .btn {
    flex: 1;
    min-width: 0;
  }

  /* Touch target floor (WCAG 2.5.5 / Apple HIG: 44px). Below this width the
     device is a phone in one hand — every surviving tap target, including
     the small decision buttons on card/list rows (.btn.tiny) and the
     topbar's segmented controls, must clear it. .like-fab is already 44x44
     unconditionally (app.css's card DOM contract comment), so it needs no
     override here. */
  .btn,
  .seg-btn {
    min-height: 44px;
  }

  .btn.icon {
    min-width: 44px;
  }
}

/* No hidden-behind-hover functionality exists in this UI (photo-nav's click
   halves and every disclosure are always present, never hover-gated) — the
   one thing hover *does* add is a couple of lift/scale transforms that read
   as a genuine bug on a touch device: iOS Safari applies :hover on tap and
   holds it until the next tap lands elsewhere, so a tapped card would stay
   visibly "lifted" instead of settling back down. Neutralizing rather than
   deleting the rules keeps them for a touchscreen laptop with a mouse
   plugged in. */
@media (hover: none) {
  .card:hover {
    transform: none;
  }

  .like-fab:hover:not(:disabled) {
    transform: none;
  }
}
