/* Magpie: boutique-minimal, mobile-first, no framework */

:root {
  /* ----- primitives: the raw palette -----
     Components never reference these directly; they go through the semantic
     aliases below, so a retheme (or dark mode) is an alias edit, not a hunt. */
  --ivory: #faf7f2;
  --parchment: #f0e9dd;
  --card: #fffdf9;
  --ink: #221f1b;
  --smoke: #6f675c;
  --blush: #c98a8a;
  --blush-deep: #ad616a;
  --gold: #b08d57;
  --gold-deep: #8a6b3d;
  --sage: #7d907d;
  --danger: #a4544e;
  --paper: #fdfbf7; /* off-white for text and controls on photos/dark ground */

  /* ink and paper at fixed opacities, the only translucencies allowed */
  --ink-a05: rgb(34 31 27 / 0.05);
  --ink-a06: rgb(34 31 27 / 0.06);
  --ink-a12: rgb(34 31 27 / 0.12);
  --ink-a55: rgb(34 31 27 / 0.55);
  --ink-a72: rgb(34 31 27 / 0.72);
  --paper-a12: rgb(253 251 247 / 0.12);
  --paper-a16: rgb(253 251 247 / 0.16);
  --paper-a22: rgb(253 251 247 / 0.22);
  --paper-a28: rgb(253 251 247 / 0.28);
  --paper-a70: rgb(253 251 247 / 0.7);
  --paper-a85: rgb(253 251 247 / 0.85);
  --paper-a90: rgb(253 251 247 / 0.9);

  /* ----- product color swatches -----
     Not theme color: these are data, the canonical colours in the lexicon
     drawn so the picker shows a color instead of the word for one. The chips
     read them directly, with no semantic alias, because there is no semantic
     meaning to alias: each one just says "roughly what a shopper means by
     blush". Muted to sit inside the boutique palette while staying
     recognizable at 15px. */
  --swatch-white: #ffffff;
  --swatch-ivory: #f2ead9;
  --swatch-champagne: #e3d3b3;
  --swatch-gold: #b08d57;
  --swatch-silver: #c2c4c6;
  --swatch-grey: #9a9691;
  --swatch-brown: #7b5a3f;
  --swatch-black: #1c1a17;
  --swatch-blush: #cf9494;
  --swatch-red: #9d3b3b;
  --swatch-orange: #b8683f;
  --swatch-yellow: #c0a144;
  --swatch-green: #5d7a5d;
  --swatch-blue: #3f5878;
  --swatch-purple: #75587e;

  /* the near-black behind and over photos, warmer than ink */
  --scrim-solid: #14120f;
  --scrim-heavy: rgb(20 17 14 / 0.82);
  --scrim-mid: rgb(20 17 14 / 0.5);
  --scrim-soft: rgb(20 17 14 / 0.45);

  /* ----- semantics: what components actually consume ----- */
  --surface: var(--ivory);
  --surface-raised: var(--card);
  --surface-sunken: var(--parchment);
  --surface-frost: rgb(250 247 242 / 0.92);
  --surface-inverse: var(--ink); /* filled controls: primary buttons, toasts, pressed chips */
  --text: var(--ink);
  --text-muted: var(--smoke);
  --text-inverse: var(--paper);
  --hairline: rgb(34 31 27 / 0.12);
  --accent: var(--blush-deep);
  --accent-soft: rgb(201 138 138 / 0.16);
  --positive: var(--sage);
  --positive-soft: rgb(125 144 125 / 0.55);
  --destructive: var(--danger);
  --destructive-soft: rgb(164 84 78 / 0.1);
  --destructive-loud: rgb(164 84 78 / 0.55);
  --highlight: var(--gold);
  --highlight-soft: rgb(176 141 87 / 0.15);
  --highlight-loud: rgb(176 141 87 / 0.6);
  /* on-photo verdict colors: brighter than sage/danger so they read on imagery */
  --stamp-like: #9fb59f;
  --stamp-pass: #d98f88;

  --shadow-card: 0 12px 32px -10px rgb(34 31 27 / 0.3);
  --shadow-tile: 0 4px 14px -6px rgb(34 31 27 / 0.22);

  --radius: 18px; /* swipe cards */
  --radius-lg: 16px; /* hero imagery, big pills */
  --radius-md: 14px; /* buttons, tiles, toasts, inputs */
  --radius-sm: 8px; /* labels that describe rather than offer: criteria, notes */
  --radius-pill: 999px;

  --ease-spring: cubic-bezier(0.2, 0.9, 0.3, 1.2); /* overshoots: snap-back, playful returns */
  --ease-settle: cubic-bezier(0.2, 0.9, 0.3, 1); /* fast start, gentle landing */

  --z-tabbar: 40;
  --z-overlay: 60; /* sheet backdrop; the sheet itself sits one above */
  --z-lightbox: 62; /* opens from inside the sheet, so it has to clear it */
  --z-toast: 70;

  --tabbar-h: 58px;
  --serif: 'Didot', 'Playfair Display', 'Cormorant Garamond', Georgia, 'Times New Roman', serif;
  --sans: -apple-system, BlinkMacSystemFont, 'Segoe UI', Inter, 'Helvetica Neue', Arial, sans-serif;
}

* {
  box-sizing: border-box;
  -webkit-tap-highlight-color: transparent;
}

html,
body {
  margin: 0;
  padding: 0;
  height: 100%;
  overscroll-behavior: none;
}

body {
  font-family: var(--sans);
  background: var(--surface);
  color: var(--text);
  -webkit-font-smoothing: antialiased;
  position: fixed;
  inset: 0;
  overflow: hidden;
}

button {
  font-family: inherit;
  border: 0;
  background: none;
  color: inherit;
  padding: 0;
  cursor: pointer;
}

input,
textarea {
  font-family: inherit;
  color: inherit;
}

/* Inline Phosphor icons (js/ui/icons.js). 1em square so each icon inherits
   the font-size the glyph it replaced was sized by. */
.icon {
  width: 1em;
  height: 1em;
  display: inline-block;
  vertical-align: -0.125em;
  flex: none;
}

/* ---------- component library: buttons ----------
   Compose one of each group on the element:
     base      .btn
     tone      --primary | --soft | --danger-soft | --raised | --scrim | --say | --link
     tint      --accent | --danger | --muted        (glyph color over a tone)
     shape     --circle + (--sm | --md | --lg)      round icon buttons
               --big                                full-bleed pill CTA
               --compact                            small button with a label
   e.g. h('button.btn.btn--circle.btn--lg.btn--raised.btn--accent', …)
   --sm/--md/--lg are fixed square boxes sized for one glyph. A word does not
   fit in one: use --compact when the button says something. */

.btn {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: 8px;
  border-radius: var(--radius-md);
  padding: 14px;
  font-size: 15px;
  font-weight: 600;
  text-align: center;
  text-decoration: none;
  transition: transform 0.1s;
}

.btn:active {
  transform: scale(0.97);
}

.btn[disabled] {
  opacity: 0.4;
  pointer-events: none;
}

/* an author `display` outranks the UA's [hidden] rule, so nothing built on .btn
   could hide itself. The sheet's zoom button showed up before it had a gallery
   to open, parked under the close button. */
.btn[hidden] {
  display: none;
}

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

/* tones */

.btn--primary {
  background: var(--surface-inverse);
  color: var(--text-inverse);
}

.btn--soft {
  background: var(--ink-a06);
  color: var(--text);
}

.btn--soft:active {
  background: var(--ink-a12);
}

.btn--danger-soft {
  background: var(--destructive-soft);
  color: var(--destructive);
}

.btn--raised {
  background: var(--surface-raised);
  box-shadow: var(--shadow-tile);
}

/* sits on photography: translucent ink, blurred */
.btn--scrim {
  background: var(--ink-a55);
  color: var(--text-inverse);
  backdrop-filter: blur(6px);
}

.btn--say {
  background: var(--highlight-soft);
  color: var(--gold-deep);
}

/* text-only link button */
.btn--link {
  padding: 0;
  border-radius: 0;
  background: none;
  color: var(--accent);
  font-size: 14px;
}

.btn--link:active {
  transform: none;
  opacity: 0.7;
}

/* glyph tints, composed over a tone */

.btn--accent {
  color: var(--accent);
}

.btn--danger {
  color: var(--destructive);
}

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

/* shapes & sizes */

.btn--circle {
  padding: 0;
  border-radius: 50%;
  flex: none;
}

.btn--sm {
  width: 36px;
  height: 36px;
  font-size: 16px;
}

.btn--md {
  width: 44px;
  height: 44px;
  font-size: 18px;
}

.btn--lg {
  width: 62px;
  height: 62px;
  font-size: 26px;
}

/* Sized by its label, not by a box: these ride at the end of a row next to the
   text they act on, so they take the height of the pill beside them and no
   more. Kept above the 36px the icon sizes hold, so the target stays honest. */
.btn--compact {
  flex: none;
  min-height: 36px;
  padding: 8px 12px;
  font-size: 13px;
  border-radius: var(--radius-pill);
  white-space: nowrap;
}

.btn--big {
  padding: 16px;
  font-size: 16px;
  letter-spacing: 0.02em;
  border-radius: var(--radius-lg);
  box-shadow: var(--shadow-tile);
}

/* context: the search form's CTA spans the form */
.search-wrap .btn--big {
  width: 100%;
  margin-top: 10px;
}

#app {
  height: 100dvh;
  display: flex;
  flex-direction: column;
}

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

#topbar {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 8px;
  padding: calc(env(safe-area-inset-top, 0px) + 10px) 18px 8px;
}

#topbar .brandline {
  display: flex;
  align-items: baseline;
  gap: 8px;
  min-width: 0;
}

#topbar .brand {
  font-family: var(--serif);
  font-size: 22px;
  letter-spacing: 0.04em;
}

/* Says the app is invite-only on every screen, so a tester reads a rough edge
   as the stage it's at rather than as something broken, and knows the link
   isn't theirs to forward. Toned to sit beside the wordmark, not compete with
   it: the gold is the quietest accent in the palette. */
#topbar .beta-badge {
  flex: none;
  font-family: var(--sans);
  font-size: 10px;
  font-weight: 500;
  letter-spacing: 0.08em;
  line-height: 1;
  padding: 4px 8px;
  border-radius: var(--radius-pill);
  color: var(--gold-deep);
  background: var(--highlight-soft);
  white-space: nowrap;
}

#topbar .brand .accent {
  color: var(--accent);
  font-style: italic;
}

#topbar .top-actions {
  display: flex;
  gap: 10px;
  align-items: center;
}

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

#views {
  flex: 1;
  min-height: 0;
  position: relative;
}

.view {
  position: absolute;
  inset: 0;
  overflow-y: auto;
  -webkit-overflow-scrolling: touch;
  padding-bottom: calc(var(--tabbar-h) + env(safe-area-inset-bottom, 0px) + 12px);
}

.view[hidden] {
  display: none;
}

/* Changing view is a display swap: one section is hidden and the next shown in
   the same frame, which lands as a snap with nothing connecting the two. A
   short fade and rise on the arriving view gives the change somewhere to go.
   Coming back from [hidden] is a fresh render, so this restarts by itself on
   every switch without any JavaScript to retrigger it. */
/* 200ms and 6px was the first attempt and it read as nothing at all: far
   enough to measure, not far enough to notice. The rise is what carries it,
   so that is what grew. */
.view:not([hidden]) {
  animation: view-in 0.28s var(--ease-settle);
}

@keyframes view-in {
  from { opacity: 0; transform: translateY(16px); }
  to   { opacity: 1; transform: none; }
}

/* The bar comes and goes with the view under it, and a bar that appears in one
   frame while the view beneath it fades reads as two separate events. */
#nestbar:not([hidden]) {
  animation: view-in 0.2s var(--ease-settle);
}

@media (prefers-reduced-motion: reduce) {
  .view:not([hidden]),
  #nestbar:not([hidden]) {
    animation: none;
  }
}

/* Flex/grid display rules on components would otherwise beat the UA's
   [hidden] style, and hidden must always win. */
[hidden] {
  display: none !important;
}

.view--deck {
  overflow: hidden;
  display: flex;
  flex-direction: column;
}

/* ---------- search view ---------- */

.search-wrap {
  padding: 8px 22px 40px;
  max-width: 520px;
  margin: 0 auto;
}

.hero {
  margin: 14px 0 26px;
}

.hero h1 {
  font-family: var(--serif);
  font-weight: 500;
  font-size: 34px;
  line-height: 1.15;
  margin: 0 0 10px;
}

.hero h1 em {
  color: var(--accent);
}

.hero p {
  color: var(--text-muted);
  margin: 0;
  font-size: 15px;
  line-height: 1.45;
}

.field {
  margin-bottom: 18px;
}

.field label {
  display: block;
  font-size: 12px;
  font-weight: 500;
  letter-spacing: 0.02em;
  color: var(--text-muted);
  margin-bottom: 8px;
}

.textinput {
  width: 100%;
  border: 1px solid var(--hairline);
  background: var(--surface-raised);
  border-radius: var(--radius-md);
  padding: 14px 16px;
  font-size: 17px;
  outline: none;
}

.textinput:focus {
  border-color: var(--highlight);
}

.chiprow {
  display: flex;
  flex-wrap: wrap;
  gap: 8px;
}

.chip {
  border: 1px solid var(--hairline);
  background: var(--surface-raised);
  border-radius: var(--radius-pill);
  padding: 9px 15px;
  font-size: 14px;
  color: var(--text);
  transition: all 0.15s;
}

.chip[aria-pressed='true'] {
  background: var(--surface-inverse);
  border-color: var(--surface-inverse);
  color: var(--text-inverse);
}

/* Color chips carry the color itself, so they opt out of the filled pressed
   look above: a pressed "white" chip would put pale text on near-black and
   fight the color it names. Selection is a ring in the color instead, backed
   by a faint ink outline so ivory and white still show an edge against the
   card. Ring thickness, not just hue, carries the state, so it survives
   grayscale and color blindness. */
.chip--color {
  display: inline-flex;
  align-items: center;
  gap: 8px;
  padding-left: 10px;
}

.chip--color .swatch {
  width: 15px;
  height: 15px;
  border-radius: 50%;
  flex: none;
  background: var(--swatch);
  /* inset, so the pale swatches read as a disc and not a hole in the chip */
  box-shadow: inset 0 0 0 1px var(--ink-a12);
}

/* The outer ring is ink at 55%, not the 12% this shipped with. At 12% it was
   the same value as --hairline, so selecting a pale swatch swapped a faint
   edge at radius 0 for an identically faint edge at radius 2: white, ivory,
   champagne and silver looked no different selected than not. The ring in the
   swatch colour still does the talking for the darker half of the palette;
   this is what carries the state when the swatch itself cannot. */
.chip--color[aria-pressed='true'] {
  background: var(--surface-raised);
  border-color: var(--swatch);
  color: var(--text);
  box-shadow: 0 0 0 1px var(--swatch), 0 0 0 2px var(--ink-a55);
}

.chip--color[data-color='white'] { --swatch: var(--swatch-white); }
.chip--color[data-color='ivory'] { --swatch: var(--swatch-ivory); }
.chip--color[data-color='champagne'] { --swatch: var(--swatch-champagne); }
.chip--color[data-color='gold'] { --swatch: var(--swatch-gold); }
.chip--color[data-color='silver'] { --swatch: var(--swatch-silver); }
.chip--color[data-color='grey'] { --swatch: var(--swatch-grey); }
.chip--color[data-color='brown'] { --swatch: var(--swatch-brown); }
.chip--color[data-color='black'] { --swatch: var(--swatch-black); }
.chip--color[data-color='blush'] { --swatch: var(--swatch-blush); }
.chip--color[data-color='red'] { --swatch: var(--swatch-red); }
.chip--color[data-color='orange'] { --swatch: var(--swatch-orange); }
.chip--color[data-color='yellow'] { --swatch: var(--swatch-yellow); }
.chip--color[data-color='green'] { --swatch: var(--swatch-green); }
.chip--color[data-color='blue'] { --swatch: var(--swatch-blue); }
.chip--color[data-color='purple'] { --swatch: var(--swatch-purple); }

/* A "never show" term. The whole pill is the remove button, so the target is
   the chip and not the 16px glyph inside it; the x is a marker of what the tap
   will do, which is why it carries no label of its own. */
.exclude-strip {
  margin-bottom: 10px;
}

.chip--token {
  display: inline-flex;
  align-items: center;
  gap: 8px;
  padding-right: 11px;
}

.chip--token .chip-x {
  display: grid;
  place-items: center;
  width: 16px;
  height: 16px;
  border-radius: 50%;
  flex: none;
  background: var(--ink-a12);
  color: var(--text-muted);
  font-size: 9px;
}

.chip--token:hover .chip-x,
.chip--token:focus-visible .chip-x {
  background: var(--ink-a72);
  color: var(--text-inverse);
}

.pricerow {
  display: flex;
  gap: 8px;
  align-items: center;
}

.pricerow .textinput {
  padding: 11px 12px;
  font-size: 15px;
}

.keep-taste {
  display: flex;
  align-items: center;
  gap: 10px;
  margin-top: 16px;
  font-size: 14px;
  color: var(--text-muted);
}

.note {
  margin-top: 18px;
  font-size: 13px;
  color: var(--text-muted);
  line-height: 1.5;
}

/* ---------- deck view ---------- */

.deck-status {
  padding: 2px 20px 6px;
  display: flex;
  gap: 8px;
  align-items: center;
  min-height: 30px;
  flex-wrap: wrap;
}

.badge {
  font-size: 12px;
  padding: 5px 11px;
  border-radius: var(--radius-pill);
  background: var(--highlight-soft);
  color: var(--gold-deep);
  font-weight: 600;
}

.badge--similar {
  background: var(--accent-soft);
  color: var(--accent);
  display: inline-flex;
  align-items: center;
  gap: 6px;
}

.badge--similar button {
  font-weight: 800;
  font-size: 13px;
}

.deck-stage {
  flex: 1;
  min-height: 0;
  position: relative;
  margin: 2px 16px 8px;
}

.swipecard {
  position: absolute;
  inset: 0;
  border-radius: var(--radius);
  background: var(--surface-raised);
  box-shadow: var(--shadow-card);
  overflow: hidden;
  touch-action: none;
  will-change: transform;
  user-select: none;
  -webkit-user-select: none;
}

.swipecard img {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  object-fit: cover;
  pointer-events: none;
}

/* The photo scales inside the card while the card keeps the swipe transform.
   Two elements, two transforms: that is what lets a pinch interrupt a drag
   without the two of them fighting over one style property.

   Fit whole rather than cropped to fill: retailers shoot full-length at 2:3 or
   taller and the card is nearer 5:6, so covering it cost the top of the head
   and the hem of the skirt. The sheet already made this trade; the deck is
   where the verdict actually gets made, so it matters more here. */
.swipecard .card-photo {
  object-fit: contain;
  transform-origin: center;
  will-change: transform;
}

/* The leftover box, filled by the same frame stretched to cover and blurred
   past recognition: the sheet's .hero-fill, at card size. Studio white blurs
   to studio white and the seam disappears; a shot on a dark ground gets a dark
   surround instead of a bar that fights it. The scale pushes the blur's own
   soft edge outside the card, which crops it. No extra request: the browser
   already has this bitmap. */
.swipecard .card-fill {
  object-fit: cover;
  transform: scale(1.15);
  filter: blur(24px) saturate(1.15);
  opacity: 0.9;
}

/* One segment per photo. A lone segment is a true statement too, and the point
   of showing it: this is all there is, stop looking for a second angle. */
.card-pips {
  position: absolute;
  top: 10px;
  left: 12px;
  right: 12px;
  display: flex;
  gap: 4px;
  z-index: 3;
  pointer-events: none;
}

.card-pips[hidden],
.card-busy[hidden] {
  display: none;
}

.card-pip {
  flex: 1;
  height: 3px;
  border-radius: var(--radius-pill);
  background: var(--paper-a28);
  box-shadow: 0 1px 2px rgb(20 17 14 / 0.25);
  transition: background 0.18s ease, transform 0.18s ease;
}

/* Brightness alone did not carry it. These sit on top of the photo, and the
   photos here are mostly pale (ivory dresses on white backdrops), which
   flattens 90% white against 28% white to almost nothing. So the one you are
   on is a different size as well as a different tone, and shape survives a
   background that tone does not. scaleY rather than height, so growing it
   cannot nudge the row it sits in. */
.card-pip.is-on {
  background: var(--paper);
  transform: scaleY(2);
  box-shadow: 0 1px 3px rgb(20 17 14 / 0.45);
}

@media (prefers-reduced-motion: reduce) {
  .card-pip {
    transition: none;
  }
}

/* still asking the retailer how many there are */
.card-pip.is-looking {
  animation: pulse-pip 1.15s ease-in-out infinite;
}

@keyframes pulse-pip {
  0%,
  100% {
    opacity: 0.4;
  }
  50% {
    opacity: 1;
  }
}

/* Only ever shown for an angle slow enough to notice: a cached frame swaps in
   under a frame's time and a spinner over the top of it looks like a fault. */
.card-busy {
  position: absolute;
  inset: 0;
  display: grid;
  place-items: center;
  z-index: 2;
  pointer-events: none;
}

/* over photography, not over the app's own ground */
.card-busy .spinner {
  border-color: var(--paper-a28);
  border-top-color: var(--paper-a90);
  filter: drop-shadow(0 1px 3px rgb(20 17 14 / 0.4));
}

@media (prefers-reduced-motion: reduce) {
  .card-pip.is-looking { animation: none; opacity: 0.6; }
}

.swipecard .scrim {
  position: absolute;
  inset: auto 0 0 0;
  height: 55%;
  background: linear-gradient(to top, var(--scrim-heavy), rgba(20, 17, 14, 0));
  pointer-events: none;
}

.swipecard .meta {
  position: absolute;
  left: 18px;
  right: 18px;
  bottom: 16px;
  color: var(--text-inverse);
  pointer-events: none;
}

.swipecard .meta .price {
  font-family: var(--serif);
  font-size: 30px;
  line-height: 1;
  margin-bottom: 6px;
}

.swipecard .meta .price .approx {
  font-size: 15px;
  opacity: 0.75;
  font-family: var(--sans);
}

.swipecard .meta .title {
  font-size: 15px;
  line-height: 1.35;
  display: -webkit-box;
  -webkit-line-clamp: 2;
  -webkit-box-orient: vertical;
  overflow: hidden;
  margin-bottom: 8px;
  text-shadow: 0 1px 6px rgba(0, 0, 0, 0.4);
}

.swipecard .meta .sub {
  font-size: 12.5px;
  opacity: 0.85;
  margin-bottom: 8px;
}

.whychips {
  display: flex;
  gap: 6px;
  flex-wrap: wrap;
}

.whychip {
  font-size: 11.5px;
  font-weight: 600;
  padding: 4px 10px;
  border-radius: var(--radius-pill);
  background: var(--paper-a22);
  backdrop-filter: blur(6px);
  color: var(--text-inverse);
}

.whychip--pos {
  background: var(--positive-soft);
}

.whychip--neg {
  background: var(--destructive-loud);
}

.whychip--wild {
  background: var(--highlight-loud);
}

/* A chip on the top card is tappable: "this is the bit I didn't like". The
   affordance stays quiet (it must not compete with the photo) but the
   pressed state has to be unmistakable, because it changes what gets learned. */
.whychip--tap {
  font: inherit;
  font-size: 11.5px;
  font-weight: 600;
  border: 1px solid var(--paper-a28);
  cursor: pointer;
  /* .swipecard .meta is pointer-events: none so drags pass through to the card;
     these chips are the one thing in there that has to be clickable */
  pointer-events: auto;
  touch-action: manipulation;
  transition: transform 0.12s ease, background 0.12s ease;
}

.whychip--tap:active {
  transform: scale(0.94);
}

.whychip--tap.is-blamed {
  background: var(--destructive);
  border-color: var(--text-inverse);
  color: var(--text-inverse);
  text-decoration: line-through;
  text-decoration-thickness: 1.5px;
}

.whychip--tap.is-blamed::after {
  content: ' ✕';
  text-decoration: none;
  display: inline-block;
}

.whychip--tap:focus-visible {
  outline: 2px solid var(--text-inverse);
  outline-offset: 2px;
}

@media (prefers-reduced-motion: reduce) {
  .whychip--tap { transition: none; }
  .whychip--tap:active { transform: none; }
}

/* Sentence case, so the tracking comes down with it: 0.08em was set for caps,
   where the extra air is what keeps them readable, and the same value on
   lowercase just pulls the word apart. */
.stamp {
  position: absolute;
  top: 26px;
  font-family: var(--serif);
  font-size: 34px;
  letter-spacing: 0.02em;
  padding: 6px 18px;
  border: 3px solid;
  border-radius: 10px;
  opacity: 0;
  pointer-events: none;
  background: var(--paper-a12);
  backdrop-filter: blur(2px);
}

.stamp--like {
  left: 20px;
  color: var(--stamp-like);
  border-color: var(--stamp-like);
  transform: rotate(-14deg);
}

.stamp--pass {
  right: 20px;
  color: var(--stamp-pass);
  border-color: var(--stamp-pass);
  transform: rotate(14deg);
}

.deck-actions {
  display: flex;
  justify-content: center;
  align-items: center;
  gap: 22px;
  padding: 6px 0 10px;
}

/* empty / loading states */

.deck-empty {
  position: absolute;
  inset: 0;
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  text-align: center;
  padding: 30px;
  gap: 10px;
}

.deck-empty h2 {
  font-family: var(--serif);
  font-weight: 500;
  font-size: 26px;
  margin: 0;
}

.deck-empty p {
  color: var(--text-muted);
  font-size: 14.5px;
  line-height: 1.5;
  margin: 0;
  max-width: 300px;
}

.deck-empty .btn--big {
  padding: 13px 26px;
  margin-top: 8px;
}

.spinner {
  width: 34px;
  height: 34px;
  border-radius: 50%;
  border: 3px solid var(--surface-sunken);
  border-top-color: var(--highlight);
  animation: spin 0.9s linear infinite;
}

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

/* inside a card, where 34px would read as an error rather than a wait */
.spinner--sm {
  width: 22px;
  height: 22px;
  border-width: 2px;
}

/* Sized to sit on a line of text rather than in a button of its own. */
.spinner--xs {
  width: 13px;
  height: 13px;
  border-width: 2px;
  flex: none;
}

/* ---------- board view ---------- */

.board-wrap {
  padding: 4px 14px 20px;
}

.board-head {
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  padding: 6px 6px 12px;
}

.board-head h2 {
  font-family: var(--serif);
  font-weight: 500;
  font-size: 24px;
  margin: 0;
  flex: 1;
  min-width: 0;
}

.board-head .count {
  color: var(--text-muted);
  font-size: 13.5px;
  flex: none;
  padding-left: 12px;
}

.masonry {
  column-count: 2;
  column-gap: 12px;
}

.tile {
  break-inside: avoid;
  margin-bottom: 12px;
  border-radius: var(--radius-md);
  overflow: hidden;
  background: var(--surface-raised);
  box-shadow: var(--shadow-tile);
  position: relative;
  width: 100%;
  text-align: left;
  display: block;
}

.tile .tilephoto {
  position: relative;
  aspect-ratio: 3 / 4;
  overflow: hidden;
  background: var(--surface-sunken);
}

.tile .tilephoto img {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  display: block;
}

.tile .tile-img {
  object-fit: contain;
}

/* Half the deck card's width, so half its blur: 24px here would smear a tile
   into a colour field with no sense of what it came from. */
.tile .tile-fill {
  object-fit: cover;
  transform: scale(1.15);
  filter: blur(14px) saturate(1.15);
  opacity: 0.9;
}

.tile .tilebody {
  padding: 9px 11px 11px;
}

.tile .tileprice {
  font-family: var(--serif);
  font-size: 17px;
}

.tile .tiletitle {
  font-size: 12px;
  color: var(--text-muted);
  line-height: 1.35;
  display: -webkit-box;
  -webkit-line-clamp: 2;
  -webkit-box-orient: vertical;
  overflow: hidden;
  margin-top: 2px;
}

.tile .match {
  position: absolute;
  top: 8px;
  right: 8px;
  font-size: 11px;
  font-weight: 700;
  background: var(--surface-frost);
  border-radius: var(--radius-pill);
  padding: 4px 9px;
  color: var(--accent);
}

.board-empty {
  text-align: center;
  padding: 60px 30px;
  color: var(--text-muted);
}

.board-empty .bigheart {
  font-size: 44px;
  margin-bottom: 12px;
}

/* ---------- taste view ---------- */

.taste-wrap {
  padding: 4px 20px 30px;
  max-width: 560px;
  margin: 0 auto;
}

.taste-head h2 {
  font-family: var(--serif);
  font-weight: 500;
  font-size: 24px;
  margin: 6px 0 4px;
}

.taste-head p {
  color: var(--text-muted);
  font-size: 13.5px;
  line-height: 1.5;
  margin: 0 0 14px;
}

/* Each panel is a card. The screen used to be one column of muted paragraphs
   and only the headings said where a section began; the card edge does that
   now, so the words inside can stay quiet. */
.tgroup {
  background: var(--surface-raised);
  border: 1px solid var(--hairline);
  border-radius: var(--radius-lg);
  padding: 14px 16px 16px;
  margin-bottom: 12px;
}

.tgroup h3 {
  font-size: 13.5px;
  letter-spacing: 0.01em;
  color: var(--text);
  margin: 0 0 10px;
  font-weight: 650;
}

/* Inside a card the default 18px note gap reads as a hole. */
.tgroup .note {
  margin-top: 10px;
}

.tchips {
  display: flex;
  flex-wrap: wrap;
  gap: 8px;
}

.tchip {
  position: relative;
  border: 1px solid var(--hairline);
  border-radius: 12px;
  background: var(--surface-raised);
  padding: 8px 12px 10px;
  font-size: 13.5px;
  overflow: hidden;
  min-width: 74px;
  text-align: left;
}

.tchip .bar {
  position: absolute;
  left: 0;
  bottom: 0;
  height: 3.5px;
  border-radius: 2px;
  background: var(--positive);
  transition: width 0.3s;
}

.tchip .bar--neg {
  background: var(--destructive);
}

.tchip--boost {
  border-color: var(--highlight);
  box-shadow: 0 0 0 1.5px var(--highlight);
}

.tchip--ban {
  opacity: 0.55;
  text-decoration: line-through;
}

.tchip .ovricon {
  font-size: 10.5px;
  margin-left: 5px;
  color: var(--highlight);
  font-weight: 700;
}

/* The numbers as small pills rather than a sentence of loose spans, so the
   strip reads at a glance and stops adding to the paragraph pile. */
.taste-meta {
  display: flex;
  gap: 6px;
  flex-wrap: wrap;
  padding: 2px 0 14px;
}

.taste-meta span {
  border: 1px solid var(--hairline);
  background: var(--surface-raised);
  border-radius: var(--radius-pill);
  padding: 5px 11px;
  font-size: 12.5px;
  color: var(--text-muted);
  font-variant-numeric: tabular-nums;
}

/* Longer copy lives behind these; see more() in taste.js. Chevron and reset
   summary marker match details.passes on the board. */
details.more {
  margin-top: 12px;
}

.taste-head details.more {
  margin: -4px 0 16px;
}

details.more > summary {
  list-style: none;
  cursor: pointer;
  display: inline-flex;
  align-items: center;
  gap: 7px;
  font-size: 12.5px;
  font-weight: 600;
  color: var(--text-muted);
  -webkit-tap-highlight-color: transparent;
}

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

details.more > summary::after {
  content: '';
  width: 6px;
  height: 6px;
  margin-top: -3px;
  border-right: 1.5px solid var(--text-muted);
  border-bottom: 1.5px solid var(--text-muted);
  transform: rotate(45deg);
  transition: transform 0.15s;
}

details.more[open] > summary::after {
  transform: rotate(-135deg);
  margin-top: 3px;
}

details.more > .note {
  margin: 8px 0 0;
}

/* ---------- bottom tab bar ---------- */

#tabbar {
  position: fixed;
  left: 0;
  right: 0;
  bottom: 0;
  height: calc(var(--tabbar-h) + env(safe-area-inset-bottom, 0px));
  padding-bottom: env(safe-area-inset-bottom, 0px);
  background: var(--surface-frost);
  backdrop-filter: blur(14px);
  border-top: 1px solid var(--hairline);
  display: flex;
  z-index: var(--z-tabbar);
}

.tab {
  position: relative;
  flex: 1;
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  gap: 3px;
  color: var(--text-muted);
  font-size: 11px;
  letter-spacing: 0.01em;
  font-weight: 600;
}

.tab .glyph {
  font-size: 20px;
  line-height: 1;
}

.tab[aria-selected='true'] {
  color: var(--text);
}

/* Anchored to the tab, not translated off its own static position: the count
   changes width at 10 and at 100, and only a real anchor keeps it put. Sits
   clear of the bar's top hairline rather than straddling it. */
.tab .badge-dot {
  position: absolute;
  top: 7px;
  left: 50%;
  margin-left: 5px;
  min-width: 19px;
  height: 19px;
  padding: 0 6px;
  box-sizing: border-box;
  border-radius: var(--radius-pill);
  background: var(--accent);
  color: var(--text-inverse);
  font-size: 10.5px;
  font-weight: 600;
  line-height: 1;
  letter-spacing: 0;
  font-variant-numeric: tabular-nums;
  display: grid;
  place-items: center;
}

/* ---------- detail sheet ---------- */

#sheet-root:empty {
  display: none;
}

.sheet-backdrop {
  position: fixed;
  inset: 0;
  background: var(--scrim-soft);
  z-index: var(--z-overlay);
  animation: fadein 0.2s;
}

@keyframes fadein {
  from {
    opacity: 0;
  }
}

.sheet {
  position: fixed;
  left: 0;
  right: 0;
  bottom: 0;
  z-index: calc(var(--z-overlay) + 1);
  background: var(--surface);
  border-radius: 22px 22px 0 0;
  max-height: 86dvh;
  display: flex;
  flex-direction: column;
  animation: slideup 0.25s var(--ease-settle);
  padding-bottom: env(safe-area-inset-bottom, 0px);
}

@keyframes slideup {
  from {
    transform: translateY(60%);
  }
}

/* Above everything the hero puts on top of itself. .sheet-scroll is not a
   stacking context, so the hero's overlays land in this same one, and at an
   equal z-index the later element in the document wins. That put the tap zone
   over the close button: the corner that dismisses the sheet paged the gallery
   instead. Keep this the highest layer inside the sheet. */
.sheet-close {
  position: absolute;
  top: 14px;
  right: 14px;
  z-index: 4;
}

.sheet .grip {
  width: 40px;
  height: 4.5px;
  background: var(--hairline);
  border-radius: 3px;
  margin: 10px auto 4px;
  flex-shrink: 0;
  position: relative;
  cursor: grab;
  /* the drag is ours; without this the sheet's own scroller takes the gesture */
  touch-action: none;
}

/* The bar is 4.5px tall. What you can actually catch hold of should not be. */
.sheet .grip::after {
  content: '';
  position: absolute;
  inset: -14px -70px;
}

.sheet .grip:active {
  cursor: grabbing;
}

/* Set to none while a pull is in progress, so the sheet tracks the finger
   exactly, and restored on release so a spring back is animated. */
.sheet {
  transition: transform 0.22s var(--ease-settle);
}

.sheet .sheet-scroll {
  overflow-y: auto;
  -webkit-overflow-scrolling: touch;
  padding: 6px 20px 20px;
}

.sheet .hero-slide img {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  display: block;
}

/* The photo, whole. Retailers shoot full-length gowns at 2:3 or taller, so
   cropping them to fit a 3:4 slot took the model's head off the top and the
   hem off the bottom: the two things you opened the sheet to look at. */
.sheet img.hero-img {
  object-fit: contain;
  z-index: 1;
}

/* What fills the leftover box: the same frame, stretched to cover and blurred
   past recognition. Studio white blurs to studio white and the seam disappears;
   an editorial shot on a dark ground gets a dark surround instead of a bar that
   fights it. Costs no extra request, since the browser already has this
   bitmap. */
.sheet img.hero-fill {
  object-fit: cover;
  transform: scale(1.15);
  filter: blur(24px) saturate(1.15);
  opacity: 0.9;
}

.sheet h2 {
  font-family: var(--serif);
  font-weight: 500;
  font-size: 21px;
  line-height: 1.3;
  margin: 14px 0 4px;
}

.sheet .sheet-sub {
  color: var(--text-muted);
  font-size: 14px;
  margin-bottom: 10px;
}

.sheet .sheet-price {
  font-family: var(--serif);
  font-size: 28px;
  margin: 2px 0 12px;
}

.sheet .attr-chips {
  display: flex;
  flex-wrap: wrap;
  gap: 7px;
  margin-bottom: 16px;
}

.sheet .attr-chips .chip {
  padding: 6px 12px;
  font-size: 12.5px;
}

/* Returns: a quiet block on sunken ground, so it reads as reference material
   next to the item rather than as another thing to act on. It carries three
   states off one head: checking, the policy, or that we could not read one.
   All three keep the same frame, so the block settles in place rather than
   swapping itself out for a different-looking thing. */
.sheet .returns {
  background: var(--surface-sunken);
  border: 1px solid var(--hairline);
  border-radius: var(--radius-md);
  padding: 12px 14px;
  margin-bottom: 16px;
}

.sheet .returns-head {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 8px;
  font-size: 13px;
  color: var(--text-muted);
}

.sheet .returns-head svg {
  flex: none;
}

.sheet .returns-head .chip--sm {
  padding: 3px 9px;
  font-size: 12px;
  background: var(--surface);
}

/* Sits on the head's own line, so waiting costs one row and the block does not
   change height when the answer replaces it. */
.sheet .returns-wait {
  display: inline-flex;
  align-items: center;
  gap: 7px;
}

/* The spinner's track is the sunken surface this block is already painted on,
   which would leave the arc turning against nothing. */
.sheet .returns-wait .spinner {
  border-color: var(--hairline);
  border-top-color: var(--highlight);
}

.sheet .returns-summary {
  margin: 8px 0 0;
  font-size: 14.5px;
  line-height: 1.4;
}

/* Not knowing is stated plainly but never at the weight of a policy: this is
   the one line in the block that is about us rather than about the store. */
.sheet .returns-none {
  margin: 8px 0 0;
  font-size: 14px;
  line-height: 1.4;
  color: var(--text-muted);
}

.sheet .returns-note {
  margin: 6px 0 0;
  font-size: 13px;
  line-height: 1.4;
  color: var(--text-muted);
}

/* The attribution carries the weight of the claim, so it is always visible
   rather than tucked behind a disclosure. */
.sheet .returns-src {
  display: inline-block;
  margin-top: 10px;
  font-size: 12.5px;
  color: var(--text-muted);
  text-decoration: underline;
  text-underline-offset: 2px;
}

.sheet .sheet-actions {
  display: flex;
  flex-direction: column;
  gap: 10px;
  padding-top: 4px;
}

.sheet .sheet-actions .row {
  display: flex;
  gap: 10px;
}

.sheet .sheet-actions .row .btn {
  flex: 1;
}

/* ---------- banning a brand or a shop ---------- */

.banwrap {
  border-top: 1px solid var(--hairline);
  padding-top: 12px;
}

.banpanel {
  display: flex;
  flex-direction: column;
  gap: 12px;
}

/* Sentence case, with weight and letter-spacing carrying the emphasis instead
   of capitals. */
.banlabel {
  font-size: 12.5px;
  letter-spacing: 0.04em;
  color: var(--text-muted);
  font-weight: 650;
}

.banscope {
  display: flex;
  flex-direction: column;
  gap: 8px;
}

.banrow {
  display: flex;
  gap: 10px;
  align-items: center;
}

.banrow .textinput {
  padding: 11px 12px;
  font-size: 15px;
}

.banrow .btn {
  flex: 0 0 auto;
  white-space: nowrap;
}

.bansite {
  flex: 1;
  font-size: 15px;
  font-weight: 600;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

.banpanel .note {
  margin-top: 0;
}

/* ---------- taste: the rules in force ----------
   The heading used to be bumped a step above the other group headings; now
   that every panel is a card at the same level, it sits with the rest. */

.rules .statgrid {
  margin-bottom: 12px;
}

.rulesec {
  display: flex;
  flex-direction: column;
  gap: 8px;
  margin-bottom: 12px;
}

.rulechips {
  display: flex;
  flex-wrap: wrap;
  gap: 8px;
}

.rulechip {
  display: inline-flex;
  align-items: center;
  gap: 6px;
  border: 1px solid var(--hairline);
  background: var(--destructive-soft);
  color: var(--destructive);
  border-radius: var(--radius-pill);
  padding: 7px 8px 7px 14px;
  font-size: 13.5px;
}

.rulex {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 20px;
  height: 20px;
  border-radius: var(--radius-pill);
  color: inherit;
  opacity: 0.65;
  font-size: 12px;
}

.rulex:hover {
  opacity: 1;
  background: var(--destructive-soft);
}

/* ---------- toasts ---------- */

#toast-root {
  position: fixed;
  left: 14px;
  right: 14px;
  bottom: calc(var(--tabbar-h) + env(safe-area-inset-bottom, 0px) + 14px);
  z-index: var(--z-toast);
  display: flex;
  flex-direction: column;
  gap: 8px;
  pointer-events: none;
}

/* Taps pass through a toast now, but a message printed over the swipe controls
   still hides the thing it is asking you to reconsider. On the deck the toast
   clears the row and covers the foot of the card instead; every other view
   keeps the tighter anchor just above the tab bar. */
body:has(#view-deck:not([hidden])) #toast-root {
  bottom: calc(var(--tabbar-h) + env(safe-area-inset-bottom, 0px) + 100px);
}

/* The deck parks its controls in a row at the bottom of the view, which is
   exactly where a toast lands. An interactive toast body sat on top of undo,
   pass and love and swallowed every tap for as long as it was up, so the body
   takes no pointer events and only the toast's own buttons do. */
.toast {
  pointer-events: none;
  background: var(--surface-inverse);
  color: var(--text-inverse);
  border-radius: var(--radius-md);
  padding: 13px 16px;
  font-size: 14px;
  line-height: 1.45;
  box-shadow: var(--shadow-card);
  display: flex;
  gap: 12px;
  /* centred, not flex-start: the action and the dismiss are single-line
     controls sitting beside a message that can wrap to two, and pinning them
     to the top left them riding above it. */
  align-items: center;
  animation: slideup 0.25s;
}

.toast button {
  pointer-events: auto;
}

/* Filled rather than a gold text label: on the dark toast the two right-hand
   controls read as one row of text, and the one that does something should not
   look like the one that dismisses it. Ink on gold is 5.3:1, above the 4.5:1
   the label size needs. Kept as toast chrome rather than a button tone, since
   nothing else in the app wears solid gold. */
.toast .toast-action {
  flex: none;
  background: var(--highlight);
  color: var(--text);
  font-weight: 600;
  font-size: 13.5px;
  padding: 6px 14px;
  border-radius: var(--radius-pill);
}

.toast .toast-action:active {
  opacity: 0.85;
}

.toast .close {
  color: var(--paper-a70);
  font-size: 16px;
  font-weight: 700;
  padding: 2px;
}

/* wider screens: keep it phone-shaped */
@media (min-width: 560px) {
  #views,
  #topbar,
  #nestbar,
  #tabbar {
    /* width: 100% is load-bearing, not belt and braces. #topbar and #views are
       flex items of the #app column, and an auto margin on the cross axis
       cancels the stretch that would otherwise size them. Left to shrink-fit,
       #views measures its children, all of which are position: absolute and so
       contribute nothing, and collapses to zero width: every view invisible,
       the app a blank page at any window 560px or wider. */
    width: 100%;
    max-width: 480px;
    margin-left: auto;
    margin-right: auto;
  }

  #tabbar {
    left: 50%;
    /* the base rule pins right: 0, which leaves left, right and width all set
       at once. The auto margins above then absorb the slack instead of
       collapsing to zero, and the bar lands half a gutter right of the column
       it belongs under. It centres itself from left and the transform. */
    right: auto;
    transform: translateX(-50%);
    width: 100%;
    border-radius: 18px 18px 0 0;
    border-left: 1px solid var(--hairline);
    border-right: 1px solid var(--hairline);
  }

  /* Toasts were pinned to the viewport rather than to the column, so on a
     desktop window one stretched the full width of the screen while the app it
     belonged to sat in a phone-shaped strip in the middle. Centred like the
     tab bar, and held a gutter inside the column so it lines up with the
     cards rather than with the window. */
  #toast-root {
    left: 50%;
    right: auto;
    transform: translateX(-50%);
    width: calc(100% - 28px);
    max-width: 452px;
  }

  /* Same story as the toast, one layer up. The product sheet lives in
     #sheet-root, a sibling of #app rather than a child, so it never saw this
     cap and ran the full width of the window: a 1600px photo hanging under a
     480px column.

     Centred with auto margins rather than the transform used above, because
     this is the one overlay whose transform is already spoken for. The
     entrance animation and the pull to dismiss both write to it, and a
     translateX(-50%) sitting in the stylesheet would be overwritten by the
     first frame of either, throwing the sheet against the left edge halfway
     through a drag. left: 0 and right: 0 with a max-width leaves the
     horizontal position over-constrained, which is precisely the case auto
     margins are there to resolve.

     The backdrop stays full-bleed on purpose: a scrim that covered only the
     column would leave the rest of the window looking live. */
  .sheet {
    max-width: 480px;
    margin-left: auto;
    margin-right: auto;
  }
}

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

.composer-row {
  display: flex;
  gap: 8px;
  align-items: center;
  padding: 0 16px 8px;
  animation: comment-in 0.16s ease-out;
}

@keyframes comment-in {
  from { opacity: 0; transform: translateY(6px); }
  to   { opacity: 1; transform: none; }
}

.comment-input {
  flex: 1;
  min-width: 0;
  border: 1px solid var(--hairline);
  background: var(--surface-raised);
  border-radius: var(--radius-pill);
  padding: 12px 16px;
  font-size: 16px; /* keeps iOS from zooming the viewport on focus */
  outline: none;
}

.comment-input:focus {
  border-color: var(--highlight);
}

.comment-input[disabled] {
  opacity: 0.6;
}

/* The send button becomes the wait while the remark is with the model. On the
   inverse ground the ring has to be paper, same as the one over photography. */
.composer-row .spinner {
  border-color: var(--paper-a28);
  border-top-color: var(--paper-a90);
}

/* A disabled control drops to 0.4, which is right for one you can't use yet
   and wrong for one that is working: the spinner is the whole message. */
.composer-row .btn[aria-busy='true'][disabled] {
  opacity: 1;
}

/* ---------- details sheet gallery ---------- */

.sheet-hero {
  margin-bottom: 12px;
  position: relative;
}

/* One slide per angle, swiped rather than tapped, since this is a swipe-first app
   and a gallery you have to poke at reads as the odd one out. Snap keeps the
   thumbnail row and the scroll position honest with each other. */
.hero-track {
  display: flex;
  overflow-x: auto;
  scroll-snap-type: x mandatory;
  -webkit-overflow-scrolling: touch;
  scrollbar-width: none;
  border-radius: var(--radius-lg);
  /* the sheet scrolls vertically; keep the two axes from fighting */
  overscroll-behavior-x: contain;
  touch-action: pan-x pan-y;
}

.hero-track::-webkit-scrollbar {
  display: none;
}

.hero-track:focus-visible {
  outline: 2px solid var(--highlight);
  outline-offset: 2px;
}

.hero-slide {
  flex: 0 0 100%;
  scroll-snap-align: center;
  position: relative;
  aspect-ratio: 3 / 4;
  overflow: hidden;
  border-radius: var(--radius-lg);
  background: var(--surface-sunken);
}

/* the track, the tap zones and the zoom button share one frame; the thumbnail
   strip sits outside it so full-height overlays stop at the photo's edge */
.hero-stage {
  position: relative;
}

/* bottom-left: the sheet's own close button owns the top-right corner, and the
   two were landing on top of each other */
.hero-zoom {
  position: absolute;
  left: 10px;
  bottom: 10px;
  z-index: 3;
}

/* Tap the left edge for the previous photo, the right edge for the next.
   Transparent on purpose: the thumbnail strip is the visible affordance.

   These are edges rather than halves because they used to be halves, and a
   half covers the whole photo. touch-action: pan-x was set here in the hope
   that a swipe would fall through to the track, but pan-x only offers the
   gesture to a scrollable *ancestor*, and .hero-track is a sibling of this
   overlay rather than a parent. So there was nothing to pan and every swipe
   died on a transparent button. Keeping the zones to the outer fifth leaves
   the middle of the frame sitting directly on the scroller, where a swipe is
   the browser's own and arrives with its snapping and momentum intact. */
.hero-taps {
  position: absolute;
  inset: 0;
  display: flex;
  justify-content: space-between;
  z-index: 2;
  pointer-events: none;
}

.hero-tap {
  flex: 0 0 20%;
  pointer-events: auto;
  background: none;
  border: 0;
  padding: 0;
  cursor: pointer;
  -webkit-tap-highlight-color: transparent;
  /* the zone itself never scrolls; a swipe belongs to the track in the middle */
  touch-action: pan-y;
}

.hero-tap:disabled {
  pointer-events: none;
}

.hero-tap:focus-visible {
  outline: 2px solid var(--highlight);
  outline-offset: -4px;
  border-radius: var(--radius-lg);
}

/* ---------- fullscreen viewer ---------- */

.lightbox {
  position: fixed;
  inset: 0;
  z-index: var(--z-lightbox);
  display: grid;
  place-items: center;
  overscroll-behavior: contain;
}

.lb-backdrop {
  position: absolute;
  inset: 0;
  background: var(--scrim-solid);
}

.lb-img {
  position: relative;
  max-width: 100vw;
  max-height: 100dvh;
  object-fit: contain;
  touch-action: none;
  will-change: transform;
  user-select: none;
  -webkit-user-select: none;
}

/* on the lightbox's near-black ground the scrim tone vanishes; frost with
   paper instead */
.lb-close {
  position: absolute;
  top: max(12px, env(safe-area-inset-top));
  right: 12px;
  background: var(--paper-a16);
  backdrop-filter: blur(8px);
  color: var(--text-inverse);
}

.lb-count {
  position: absolute;
  bottom: max(18px, env(safe-area-inset-bottom));
  left: 50%;
  transform: translateX(-50%);
  color: var(--paper-a85);
  font-size: 12.5px;
  font-variant-numeric: tabular-nums;
  letter-spacing: 0.06em;
  background: var(--scrim-mid);
  padding: 4px 12px;
  border-radius: var(--radius-pill);
}

.lb-nav {
  position: absolute;
  inset: 0;
  display: flex;
  align-items: center;
  justify-content: space-between;
  pointer-events: none;
  padding: 0 4px;
}

.lb-arrow {
  pointer-events: auto;
  color: var(--paper-a90);
  font-size: 26px;
}

.lb-close:focus-visible,
.lb-arrow:focus-visible,
.hero-zoom:focus-visible {
  outline: 2px solid var(--text-inverse);
  outline-offset: 2px;
}

.hero-taps[hidden],
.hero-thumbs[hidden] {
  display: none;
}

.hero-thumbs {
  display: flex;
  gap: 8px;
  margin-top: 8px;
  overflow-x: auto;
  -webkit-overflow-scrolling: touch;
  scrollbar-width: none;
}

.hero-thumbs::-webkit-scrollbar {
  display: none;
}

.hero-thumb {
  flex: none;
  width: 56px;
  height: 72px;
  border-radius: 8px;
  overflow: hidden;
  border: 2px solid transparent;
  opacity: 0.6;
  transition: opacity 0.15s, border-color 0.15s;
}

.hero-thumb.is-active {
  border-color: var(--highlight);
  opacity: 1;
}

.hero-thumb img {
  width: 100%;
  height: 100%;
  object-fit: cover;
  display: block;
}

/* A pending gallery lookup and a product that simply has one photo look
   identical in the frame, so the difference gets said out loud here. */
.hero-status {
  display: flex;
  align-items: center;
  gap: 7px;
  margin-top: 9px;
  color: var(--text-muted);
  font-size: 12.5px;
}

.hero-status.is-settled {
  opacity: 0.7;
}

.hero-status-dot {
  width: 6px;
  height: 6px;
  flex: none;
  border-radius: 50%;
  background: var(--accent);
  animation: pulse-dot 1.15s ease-in-out infinite;
}

@keyframes pulse-dot {
  0%,
  100% {
    opacity: 0.3;
    transform: scale(0.75);
  }
  50% {
    opacity: 1;
    transform: scale(1);
  }
}

@media (prefers-reduced-motion: reduce) {
  .hero-status-dot { animation: none; opacity: 0.7; }
}

.taste-danger {
  display: flex;
  flex-wrap: wrap;
  gap: 10px;
  padding-top: 14px;
}

/* ---------- saved hunts ---------- */

/* ---------- the wish box ---------- */

/* The one control that matters on the setup screen: everything else folds
   away under .tune. Serif and roomy so a wish reads like a sentence, not a
   search field. */
.wishbox {
  display: block;
  width: 100%;
  border: 1px solid var(--hairline);
  background: var(--surface-raised);
  border-radius: var(--radius-lg);
  padding: 16px 18px 52px;
  font-family: var(--serif);
  font-size: 19px;
  line-height: 1.45;
  color: var(--text);
  resize: none;
  outline: none;
  min-height: 132px;
}

.wishbox::placeholder {
  color: var(--text-muted);
  opacity: 0.75;
}

.wishbox:focus {
  border-color: var(--highlight);
}

/* What the parser lifted out of the wish, shown back immediately. */
.caught-row {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 6px;
  margin-top: 10px;
}

.caught-label {
  display: inline-flex;
  align-items: center;
  gap: 4px;
  font-size: 12px;
  letter-spacing: 0.02em;
  color: var(--highlight);
}

/* Squared off, not a pill: these describe a nest rather than offering a choice,
   and a pill reads as something to tap next to the chips that are. */
.caught {
  font-size: 13px;
  padding: 4px 10px;
  border-radius: var(--radius-sm);
  background: var(--highlight-soft);
  color: var(--text);
}

/* The fine-tune panel. Opened by .tune-toggle in the corner of the wish box,
   so the whole strip is hidden outright rather than teased by a summary row. */
.tune {
  border-top: 1px solid var(--hairline);
  margin: 20px 0;
  padding-top: 18px;
}

.tune-hint {
  margin-bottom: 16px;
}

/* Parks the toggle inside the wish box. The box reserves room for it in its
   own bottom padding, so a long wish scrolls behind nothing. */
.wishbox-wrap {
  position: relative;
}

/* A chip rather than a .btn: opaque, so a wish long enough to scroll passes
   behind it cleanly, and the open state can borrow the pressed-chip fill. */
.tune-toggle {
  position: absolute;
  right: 10px;
  bottom: 10px;
  display: inline-flex;
  align-items: center;
  gap: 6px;
  font-weight: 600;
}

.tune-toggle[aria-expanded='true'] {
  background: var(--surface-inverse);
  border-color: var(--surface-inverse);
  color: var(--text-inverse);
}

/* Collapsible passed-on section. */
details.passes {
  border-top: 1px solid var(--hairline);
  margin: 20px 0;
  padding-top: 4px;
}

details.passes > summary {
  list-style: none;
  cursor: pointer;
  display: flex;
  align-items: baseline;
  gap: 10px;
  padding: 12px 0;
  -webkit-tap-highlight-color: transparent;
}

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

details.passes > summary::after {
  content: '';
  margin-left: auto;
  width: 8px;
  height: 8px;
  border-right: 1.5px solid var(--text-muted);
  border-bottom: 1.5px solid var(--text-muted);
  transform: rotate(45deg);
  transition: transform 0.15s;
}

details.passes[open] > summary::after {
  transform: rotate(-135deg);
}

.tune-title {
  font-size: 12px;
  font-weight: 500;
  letter-spacing: 0.02em;
  color: var(--text);
}

.tune-sub {
  font-size: 12.5px;
  color: var(--text-muted);
}

/* ---------- nest home ---------- */

.nest-head {
  padding-bottom: 4px;
}

.nest-crit {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 6px;
  padding: 0 6px 14px;
}

.nest-crit-q {
  font-size: 13.5px;
  color: var(--text-muted);
}

.chip--edit {
  padding: 4px 10px;
  font-size: 12.5px;
  display: inline-flex;
  align-items: center;
  gap: 4px;
  color: var(--text-muted);
}

.collect-cta {
  display: flex;
  align-items: center;
  justify-content: center;
  gap: 8px;
  margin: 0 6px 18px;
  width: calc(100% - 12px);
}

/* On the frame, not the images inside it: the blurred backdrop carries its own
   filter, and setting the property twice on that element would drop the blur
   rather than add to it. */
.masonry--passes .tile--pass .tilephoto {
  filter: saturate(0.55) brightness(0.97);
}

.masonry--passes {
  margin-top: 4px;
}

/* The heading doubles as the rename control, so it carries a pencil rather than
   a separate button parked next to it. */
/* Block, not flex: the pencil has to sit in the same text flow as the name so
   it follows the last word of a title that wraps, rather than parking itself
   beside the treasure count. */
.board-title {
  font: inherit;
  text-align: left;
  display: block;
  width: 100%;
}

.board-title-pencil {
  font-size: 14px;
  color: var(--text-muted);
  margin-left: 8px;
  white-space: nowrap;
}

.board-title-input {
  font-family: var(--serif);
  font-weight: 500;
  font-size: 24px;
  width: 100%;
  min-width: 0;
  background: none;
  border: 0;
  border-bottom: 1px solid var(--highlight);
  padding: 0 0 2px;
}

.board-title-input:focus {
  outline: none;
}

/* ---------- renaming a nest ---------- */

/* The field and the two buttons that end the edit, shared by the gallery tile
   and the board heading. The filled one keeps what was typed and the bare one
   throws it away, which is the same pair of tones every other destructive
   choice in here uses. */
/* A form only so the phone keyboard's return key has something to submit, so
   it gives back the margin a form is otherwise given. */
.nameedit {
  margin: 0;
}

.nameedit--inline {
  display: flex;
  align-items: flex-end;
  gap: 6px;
}

.nameedit--inline > input {
  flex: 1;
  min-width: 0;
}

.nameedit-acts {
  display: flex;
  gap: 2px;
  flex: none;
}

/* The tile reserves two lines for a name whether or not it needs them, and an
   edit that gave one of them back would bounce the whole grid. Sized like the
   name it stands in for, so the two lines are the same two lines. */
.nesttile-foot .nameedit {
  font-size: 18px;
  line-height: 1.25;
  min-height: calc(2 * 1.25em);
}

/* ---------- the nest bar ---------- */

/* Sits under the wordmark on every view inside a nest: which nest this is, and
   the way out of it. */
#nestbar {
  padding: 0 14px 8px;
}

.nestbar-back {
  display: flex;
  align-items: center;
  gap: 8px;
  width: 100%;
  padding: 9px 12px;
  border: 1px solid var(--hairline);
  border-radius: var(--radius-md);
  background: var(--surface-raised);
  text-align: left;
}

.nestbar-back:active {
  background: var(--ink-a05);
}

.nestbar-caret {
  font-size: 15px;
  color: var(--text-muted);
  display: inline-flex;
  flex: none;
}

.nestbar-name {
  flex: 1;
  min-width: 0;
  font-size: 15px;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

.nestbar-all {
  flex: none;
  font-size: 12px;
  color: var(--text-muted);
  letter-spacing: 0.02em;
}

/* ---------- the nests gallery ---------- */

.nests-wrap {
  padding: 8px 18px 30px;
  max-width: 560px;
  margin: 0 auto;
}

.nest-grid {
  display: grid;
  grid-template-columns: repeat(2, minmax(0, 1fr));
  gap: 16px 14px;
}

.nesttile {
  position: relative;
  min-width: 0;
}

.nesttile-open {
  display: block;
  width: 100%;
  border-radius: var(--radius-md);
  overflow: hidden;
  background: var(--surface-raised);
  box-shadow: var(--shadow-tile);
  position: relative;
}

.nesttile-open:active {
  transform: scale(0.98);
}

.nesttile.is-active .nesttile-open {
  outline: 2px solid var(--highlight);
  outline-offset: 2px;
}

/* Four finds as one square. Fewer than four still fills it: the cells span
   whatever is left over, so a young nest reads as young rather than as broken. */
/* minmax(0, 1fr) and the min-height: 0 pair are what hold the square. A grid
   container and its items both take an automatic minimum size from their
   content, so a row of 800px-tall product photos grows the box to fit them and
   aspect-ratio loses: measured 190x485 on a deployed preview before this. */
.nest-mosaic {
  display: grid;
  grid-template-columns: minmax(0, 1fr) minmax(0, 1fr);
  grid-template-rows: minmax(0, 1fr) minmax(0, 1fr);
  gap: 2px;
  aspect-ratio: 1 / 1;
  width: 100%;
  min-height: 0;
  background: var(--surface-sunken);
}

.nest-mosaic img {
  width: 100%;
  height: 100%;
  min-width: 0;
  min-height: 0;
  object-fit: cover;
  display: block;
  background: var(--surface-sunken);
}

.nest-mosaic--1 img {
  grid-area: 1 / 1 / 3 / 3;
}

.nest-mosaic--2 img {
  grid-row: 1 / 3;
}

.nest-mosaic--3 img:first-child {
  grid-area: 1 / 1 / 3 / 2;
}

.nesttile-flag {
  position: absolute;
  left: 8px;
  bottom: 8px;
  font-size: 11px;
  letter-spacing: 0.02em;
  padding: 4px 8px;
  border-radius: var(--radius-sm);
  background: var(--surface-frost);
  color: var(--text);
}

.nesttile-row {
  display: flex;
  align-items: center;
  gap: 6px;
  min-height: 34px;
}

.nesttile-acts {
  display: flex;
  gap: 2px;
  margin-left: auto;
  flex: none;
}

/* The caption's buttons are glyphs, not controls to be noticed; 36px keeps the
   target honest while the tile stays a photograph with a name under it. */
.nesttile-acts .btn--sm {
  width: 34px;
  height: 34px;
  font-size: 15px;
}

.nesttile-foot {
  padding: 9px 2px 0;
}

/* Two lines, and the room for two whether or not it needs them: a nest named
   after the wish she typed is a sentence, and one line of it identifies
   nothing. The reserved height keeps the grid's rows level. */
.nesttile-name {
  font-family: var(--serif);
  font-size: 18px;
  line-height: 1.25;
  text-align: left;
  display: -webkit-box;
  -webkit-line-clamp: 2;
  -webkit-box-orient: vertical;
  overflow: hidden;
  width: 100%;
  min-height: calc(2 * 1.25em);
}

.nesttile-input {
  font-family: var(--serif);
  font-size: 18px;
  width: 100%;
  min-width: 0;
  background: none;
  border: 0;
  border-bottom: 1px solid var(--highlight);
  padding: 0 0 2px;
}

.nesttile-input:focus {
  outline: none;
}

.nesttile-meta {
  font-size: 12px;
  color: var(--text-muted);
  letter-spacing: 0.02em;
  min-width: 0;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

/* The one tile that isn't a nest yet, so it is drawn as an opening rather than
   as a thing: no photo, no shadow, just the outline of one. */
.nesttile--new .nesttile-open {
  background: none;
  box-shadow: none;
  border: 1px dashed var(--hairline);
}

.nesttile--new .nest-mosaic {
  background: none;
  display: flex;
  align-items: center;
  justify-content: center;
}

.nesttile-plus {
  font-size: 30px;
  color: var(--text-muted);
  display: inline-flex;
}

/* Manage mode's delete: the one destructive thing in a row of quiet glyphs, so
   it carries the colour without the filled pill a real danger button gets. */
.btn--danger-ghost {
  color: var(--destructive);
}

/* ---------- gallery sections ---------- */

/* The grid and the feed are two sections of one screen, and each carries its
   label on the left with whatever acts on it on the right. Instagram's Saved
   page is the reference, and the part worth taking is that the controls live
   up here rather than on every tile below. */
.sec-head {
  display: flex;
  align-items: baseline;
  gap: 12px;
  margin: 26px 0 12px;
}

/* The hero names the grid, so Manage hangs off it rather than off a second
   heading that would just say "Nests" under "Your nests". */
.hero--gallery {
  display: flex;
  align-items: flex-start;
  gap: 14px;
}

.hero--gallery .hero-text {
  min-width: 0;
  flex: 1;
}

.hero-manage {
  flex: none;
  margin-top: 10px;
}

.sec-count {
  margin-left: auto;
  flex: none;
  font-size: 13px;
  color: var(--text-muted);
  font-variant-numeric: tabular-nums;
}

.sec-title {
  font-family: var(--serif);
  font-size: 19px;
  font-weight: 500;
  margin: 0;
  min-width: 0;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

.sec-link {
  font-size: 14px;
  color: var(--accent);
  padding: 2px 0;
}

.sec-link:active {
  opacity: 0.6;
}

/* ---------- the cross-nest wall ---------- */

.tres-wrap {
  margin-bottom: 4px;
}

.tres-search {
  display: flex;
  align-items: center;
  gap: 8px;
  padding: 0 12px;
  margin-bottom: 12px;
  border-radius: var(--radius-md);
  background: var(--surface-sunken);
  border: 1px solid var(--hairline);
}

.tres-search-glyph {
  display: inline-flex;
  font-size: 16px;
  color: var(--text-muted);
  flex: none;
}

.tres-search-input {
  flex: 1;
  min-width: 0;
  border: 0;
  background: none;
  padding: 11px 0;
  font-size: 15px;
  color: var(--text);
}

.tres-search-input:focus {
  outline: none;
}

/* Three across, square, gapless. The wall is for recognising something you
   already chose, and at this size the photograph is the only thing carrying
   that, so nothing is spent on chrome between the tiles. */
.tres-grid {
  display: grid;
  grid-template-columns: repeat(3, minmax(0, 1fr));
  gap: 3px;
}

.tres {
  position: relative;
  aspect-ratio: 1 / 1;
  min-width: 0;
  overflow: hidden;
  border-radius: var(--radius-sm);
  background: var(--surface-sunken);
  display: block;
  padding: 0;
}

.tres:active {
  transform: scale(0.97);
}

.tres-img {
  width: 100%;
  height: 100%;
  object-fit: cover;
  display: block;
}

/* Which nest it came out of, banded across the foot of the tile. This is the
   answer to the question the wall exists to answer, so it is on the photograph
   rather than one tap inside it. */
.tres-nest {
  position: absolute;
  left: 0;
  right: 0;
  bottom: 0;
  padding: 10px 6px 4px;
  font-size: 10px;
  line-height: 1.3;
  color: var(--text-inverse);
  background: linear-gradient(to top, rgb(34 31 27 / 0.72), transparent);
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
  text-align: left;
}

.tres-empty {
  margin-top: 14px;
}

/* ---------- taste anchors ---------- */

.ref-strip {
  display: flex;
  flex-wrap: wrap;
  gap: 8px;
  margin-bottom: 10px;
}

/* Live status for the strip above it, so it sits closer than the standing
   help text and reads as something the app is telling you right now. */
.ref-note {
  margin-top: 8px;
  color: var(--text);
}

.ref-chip {
  position: relative;
  width: 58px;
  height: 74px;
  border-radius: 10px;
  overflow: hidden;
  border: 1px solid var(--hairline);
  background: var(--surface-raised);
}

.ref-chip img {
  width: 100%;
  height: 100%;
  object-fit: cover;
  display: block;
}

/* a photo that has been picked but not yet read off disk */
.ref-chip--pending {
  display: grid;
  place-items: center;
  background: var(--surface-sunken);
}

.ref-spin {
  width: 18px;
  height: 18px;
  border-radius: 50%;
  border: 2px solid var(--hairline);
  border-top-color: var(--highlight);
  animation: spin 0.9s linear infinite;
}

.ref-chip--link {
  width: auto;
  min-width: 74px;
  height: 74px;
  padding: 0 12px;
  display: grid;
  place-items: center;
}

.ref-host {
  font-size: 11px;
  color: var(--text-muted);
  max-width: 90px;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

.ref-x {
  position: absolute;
  top: 2px;
  right: 2px;
  width: 18px;
  height: 18px;
  border-radius: 50%;
  background: var(--ink-a72);
  color: var(--text-inverse);
  font-size: 10px;
  display: grid;
  place-items: center;
}

/* ---------- taste: how it's doing ---------- */

.statgrid {
  display: grid;
  grid-template-columns: 1fr auto;
  gap: 7px 14px;
  align-items: baseline;
}

.statk {
  font-size: 13.5px;
  color: var(--text-muted);
}

.statv {
  font-size: 14px;
  font-weight: 650;
  font-variant-numeric: tabular-nums;
  text-align: right;
}

.taste-export {
  display: flex;
  flex-direction: column;
  align-items: flex-start;
  gap: 2px;
  margin-top: 18px;
}

.exporthint {
  font-size: 12px;
  color: var(--text-muted);
}

/* Which nest this screen is describing. Taste sits in the tab bar alongside
   Swipe and Nest, which reads app-level, and nearly everything on it is
   scoped to one nest. */
.taste-scope {
  font-size: 13px;
  color: var(--text-muted);
  margin: 2px 0 10px;
}

.taste-scope strong {
  color: var(--text);
  font-weight: 600;
}

/* Set apart from .taste-danger below it: a pivot loses nothing, and reading
   as destructive would keep people away from it. */
.taste-pivot {
  display: flex;
  flex-direction: column;
  gap: 4px;
  align-items: flex-start;
  margin-top: 22px;
  padding-top: 16px;
  border-top: 1px solid var(--hairline);
}

.pivothint {
  font-size: 12px;
  color: var(--text-muted);
  max-width: 46ch;
  line-height: 1.45;
}

.whychip--said {
  background: var(--highlight-loud);
  font-style: italic;
  font-weight: 500;
  max-width: 100%;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

.fitrow {
  display: grid;
  grid-template-columns: 1fr 1fr 2fr;
  gap: 8px;
  margin-top: 10px;
}

.fitrow .textinput {
  padding: 11px 12px;
  font-size: 15px;
}

/* Sits inside a note, so it has to match that line rather than the button
   scale it inherits from .btn. */
.fit-edit {
  font-size: inherit;
  font-weight: 600;
  vertical-align: baseline;
}

/* ---------- the queue ---------- */

.admin {
  padding: 18px 18px calc(var(--tabbar-h) + env(safe-area-inset-bottom, 0px) + 24px);
  display: flex;
  flex-direction: column;
  gap: 10px;
}

.admin h2 {
  font-family: var(--serif);
  font-size: 24px;
  font-weight: 400;
  margin: 0;
}

.admin .note {
  margin: 0;
}

.admin-row {
  display: flex;
  align-items: center;
  gap: 10px;
  padding: 12px 14px;
  border-radius: var(--radius-md);
  background: var(--surface-raised);
  box-shadow: var(--shadow-tile);
}

/* Takes the slack so the status and the buttons stay put down the list, and
   a long address truncates rather than pushing them off the row. */
.admin-who {
  flex: 1;
  min-width: 0;
}

.admin-name,
.admin-email {
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

.admin-name {
  font-size: 15px;
}

.admin-email {
  font-size: 12px;
  color: var(--text-muted);
}

.admin-status {
  flex: none;
  font-size: 11px;
  letter-spacing: 0.04em;
  padding: 4px 8px;
  border-radius: var(--radius-pill);
  background: var(--surface-sunken);
  color: var(--text-muted);
}

/* Waiting is the only one that wants the eye: the others are a record of a
   decision already made. */
.admin-status[data-status='waitlist'] {
  background: var(--highlight-soft);
  color: var(--gold-deep);
}

.admin-status[data-status='approved'],
.admin-status[data-status='admin'] {
  background: rgb(125 144 125 / 0.18);
  color: var(--positive);
}

.admin-status[data-status='blocked'] {
  background: var(--destructive-soft);
  color: var(--destructive);
}

.admin-actions {
  flex: none;
  display: flex;
  gap: 6px;
}

/* ---------- friends ---------- */

.friends {
  padding: 18px 18px calc(var(--tabbar-h) + env(safe-area-inset-bottom, 0px) + 24px);
  display: flex;
  flex-direction: column;
  gap: 10px;
}

.friends h2 {
  font-family: var(--serif);
  font-size: 24px;
  font-weight: 400;
  margin: 0;
}

.friends .note {
  margin: 0;
}

.friend-section h3 {
  font-size: 14px;
  font-weight: 600;
  margin: 14px 0 0;
}

.friend-search {
  display: flex;
  flex-direction: column;
  gap: 10px;
}

.friend-input {
  font-size: 15px;
  padding: 12px 14px;
}

.friend-searching {
  display: flex;
  align-items: center;
  gap: 8px;
}

.friend-list {
  display: flex;
  flex-direction: column;
  gap: 8px;
}

.friend-row {
  display: flex;
  align-items: center;
  gap: 10px;
  padding: 10px 12px;
  border-radius: var(--radius-md);
  background: var(--surface-raised);
  box-shadow: var(--shadow-tile);
}

/* A face for the row: the Google avatar, or the name's first letter standing
   in for one. Referrer policy is set where the img is built, since Google's
   CDN refuses hotlinks that carry one. */
.friend-ava {
  flex: none;
  width: 34px;
  height: 34px;
  border-radius: 50%;
  overflow: hidden;
  background: var(--surface-sunken);
  display: grid;
  place-items: center;
  font-size: 15px;
  font-weight: 600;
  color: var(--text-muted);
}

.friend-ava img {
  width: 100%;
  height: 100%;
  object-fit: cover;
  display: block;
}

/* Takes the slack so buttons stay put down the list, and a long name
   truncates rather than pushing them off the row. */
.friend-who {
  flex: 1;
  min-width: 0;
}

.friend-name {
  font-size: 15px;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

.friend-actions {
  flex: none;
  display: flex;
  gap: 6px;
}

.friend-chip {
  flex: none;
  font-size: 11px;
  letter-spacing: 0.04em;
  padding: 4px 8px;
  border-radius: var(--radius-pill);
  background: var(--surface-sunken);
  color: var(--text-muted);
}

.friend-chip[data-kind='in'] {
  background: rgb(125 144 125 / 0.18);
  color: var(--positive);
}

.friend-handle {
  font-size: 12px;
  color: var(--text-muted);
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

/* ---------- you: the topbar face and the profile ---------- */

.topbar-ava {
  display: grid;
  place-items: center;
}

.topbar-ava-face {
  width: 30px;
  height: 30px;
  font-size: 13px;
  box-shadow: var(--shadow-tile);
}

.profilev {
  max-width: 560px;
  margin: 0 auto;
  width: 100%;
  padding: 18px 18px calc(var(--tabbar-h) + env(safe-area-inset-bottom, 0px) + 24px);
  display: flex;
  flex-direction: column;
  gap: 14px;
}

.profilev h2 {
  font-family: var(--serif);
  font-size: 24px;
  font-weight: 400;
  margin: 0;
}

.profilev .field {
  margin-bottom: 0;
}

.profile-top {
  display: flex;
  align-items: center;
  gap: 14px;
}

/* The face is the button: tapping who you are is how you change it. */
.profile-face {
  position: relative;
  flex: none;
}

.profile-ava {
  width: 76px;
  height: 76px;
  font-size: 30px;
}

.profile-face-badge {
  position: absolute;
  right: -2px;
  bottom: -2px;
  width: 26px;
  height: 26px;
  border-radius: 50%;
  background: var(--surface-raised);
  box-shadow: var(--shadow-tile);
  display: grid;
  place-items: center;
  font-size: 13px;
  color: var(--text-muted);
}

.profile-who {
  min-width: 0;
}

.profile-name {
  font-family: var(--serif);
  font-size: 20px;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

.profile-handle {
  font-size: 13px;
  color: var(--text-muted);
}

.profile-hint {
  margin: 6px 0 0;
}

.profile-facts {
  display: flex;
  flex-direction: column;
  gap: 4px;
}

.profile-facts .note {
  margin: 0;
}

.profile-signout {
  align-self: flex-start;
}

/* The @ lives outside the box so what is typed is exactly what is claimed. */
.gate-handle {
  display: flex;
  align-items: center;
  gap: 8px;
}

.gate-at {
  font-size: 17px;
  color: var(--text-muted);
  flex: none;
}

.gate-handle-input {
  flex: 1;
  min-width: 0;
}

.gate-claim-error {
  margin-top: -6px;
}

/* ---------- sharing a nest ---------- */

.share-veil {
  position: fixed;
  inset: 0;
  z-index: var(--z-overlay);
  background: rgb(30 26 20 / 0.4);
  display: flex;
  align-items: flex-end;
  justify-content: center;
}

.share-panel {
  width: 100%;
  max-width: 560px;
  max-height: 72vh;
  overflow: auto;
  background: var(--surface);
  border-radius: var(--radius-lg) var(--radius-lg) 0 0;
  padding: 20px 18px calc(env(safe-area-inset-bottom, 0px) + 20px);
  display: flex;
  flex-direction: column;
  gap: 12px;
}

.share-panel h3 {
  font-family: var(--serif);
  font-size: 20px;
  font-weight: 500;
  margin: 0;
}

.share-panel .note {
  margin: 0;
}

.share-done {
  align-self: stretch;
}

/* The line under a section heading that says what the section is for. */
.sec-note {
  margin: 0;
}

/* ---------- a friend's board, and reactions ---------- */

.sharedb-head {
  display: flex;
  flex-direction: column;
  gap: 6px;
  margin-bottom: 4px;
}

.sharedb-byline {
  margin: 0;
}

/* The photo link stands where the tile button stands on your own board. */
.tile--shared {
  cursor: default;
}

.tile--shared .tilephoto {
  display: block;
}

.react-row {
  display: flex;
  flex-wrap: wrap;
  gap: 6px;
  margin-top: 8px;
}

.react-chip {
  display: inline-flex;
  align-items: center;
  gap: 4px;
  font-size: 12px;
  padding: 5px 10px;
  border-radius: var(--radius-pill);
  background: var(--surface-sunken);
  color: var(--text-muted);
}

.react-chip[aria-pressed='true'] {
  background: var(--highlight-soft);
  color: var(--gold-deep);
  font-weight: 600;
}

.react-note {
  margin-top: 6px;
  font-size: 12px;
  color: var(--text-muted);
  line-height: 1.4;
}

.react-note-edit {
  display: flex;
  flex-direction: column;
  gap: 6px;
  margin-top: 6px;
}

.react-note-input {
  font-size: 14px;
  padding: 10px 12px;
  resize: none;
}

/* What friends said, on the owner's own tiles: one quiet line per voice. */
.tilereact {
  display: flex;
  flex-direction: column;
  gap: 2px;
  margin-top: 6px;
}

.tilereact-row {
  font-size: 11.5px;
  color: var(--text-muted);
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

/* ---------- the door ---------- */

/* Covers the app rather than replacing it, so no view underneath has to know
   sign-in exists. Opaque, because a half-visible deck behind a sign-in panel
   reads as something you could reach if you tried. */
#gate-root {
  position: fixed;
  inset: 0;
  z-index: var(--z-lightbox);
  background: var(--surface);
}

#gate-root[hidden] {
  display: none;
}

.gate {
  height: 100%;
  overflow-y: auto;
  display: flex;
  align-items: center;
  justify-content: center;
  padding: calc(env(safe-area-inset-top, 0px) + 24px) 24px
           calc(env(safe-area-inset-bottom, 0px) + 24px);
}

.gate-panel {
  width: 100%;
  max-width: 360px;
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 14px;
  text-align: center;
}

/* The deck, drawn. Three cards fanned and breathing, so the first thing on
   screen is the shape of the thing rather than a paragraph about it. Purely
   decorative, hidden from screen readers, and it holds its own height so
   nothing below it moves as it animates. */
.gate-deck {
  position: relative;
  width: 190px;
  height: 184px;
  margin-bottom: 2px;
  flex: none;
}

/* Sits above the fan and overlapping it, tail crossing the cards, so it reads
   as a bird that has just landed on them rather than a logo parked nearby.
   Ink coloured via currentColor, with the shoulder cut in the page's own
   background, which is how the same paths serve the light page and the dark
   app icon without a second drawing. */
.gate-bird {
  position: absolute;
  right: -30px;
  top: -16px;
  color: var(--text);
  z-index: 1;
}

@media (prefers-reduced-motion: no-preference) {
  .gate-bird { animation: gate-bird-settle 7s var(--ease-settle) infinite 0.1s; }
}

@keyframes gate-bird-settle {
  0%, 100% { transform: translate(0, 0) rotate(0deg); }
  46%      { transform: translate(-3px, -5px) rotate(-2.5deg); }
  72%      { transform: translate(1px, -1px) rotate(0.5deg); }
}

/* Portrait, because that is the shape of a garment photographed on a person,
   and the whole point of drawing the deck is that it is recognisable at a
   glance as the thing the app is made of. */
.gate-card {
  position: absolute;
  left: 50%;
  top: 8px;
  width: 100px;
  height: 140px;
  margin-left: -50px;
  border-radius: 13px;
  box-shadow: var(--shadow-card);
  transform-origin: 50% 92%;
}

/* Each card takes a different tone from the swatch end of the palette, which
   is roughly what a rail of clothes looks like from across a room. Every card
   carries its fan angle inside its own keyframes rather than in a static
   transform, because an animation on the same property would otherwise drop
   the fan the moment it started and the three would collapse into one. */
.gate-card.is-back {
  background: linear-gradient(158deg, #e6d9bd, #cdb891);
  opacity: 0.92;
  transform: rotate(-13deg) translateX(-15px) scale(0.93);
}

.gate-card.is-mid {
  background: linear-gradient(158deg, #ecd8d3, #d6b5ae);
  opacity: 0.95;
  transform: rotate(9deg) translateX(15px) scale(0.96);
}

.gate-card.is-front {
  background: linear-gradient(158deg, var(--card), #f0e4d4);
  border: 1px solid rgb(34 31 27 / 0.07);
  transform: rotate(-2deg);
}

@media (prefers-reduced-motion: no-preference) {
  .gate-card.is-back { animation: gate-fan-back 7s var(--ease-settle) infinite; }
  .gate-card.is-mid { animation: gate-fan-mid 7s var(--ease-settle) infinite 0.4s; }
  .gate-card.is-front { animation: gate-fan-front 7s var(--ease-settle) infinite 0.2s; }
}

/* A hand weighing a card, not a card being thrown: it leans, hesitates, comes
   back. Small enough to read as breathing rather than as something asking to
   be watched. */
@keyframes gate-fan-front {
  0%, 100% { transform: rotate(-2deg) translate(0, 0); }
  42%      { transform: rotate(-7deg) translate(-7px, -7px); }
  68%      { transform: rotate(0.5deg) translate(3px, -2px); }
}

@keyframes gate-fan-mid {
  0%, 100% { transform: rotate(9deg) translateX(15px) scale(0.96); }
  50%      { transform: rotate(12deg) translate(20px, -4px) scale(0.965); }
}

@keyframes gate-fan-back {
  0%, 100% { transform: rotate(-13deg) translateX(-15px) scale(0.93); }
  50%      { transform: rotate(-16deg) translate(-20px, -3px) scale(0.935); }
}

.gate-brand {
  font-family: var(--serif);
  font-size: 30px;
  letter-spacing: 0.04em;
  margin-bottom: 6px;
}

/* The cycling example. Fixed height so the button underneath never moves, and
   italic serif so it reads as the app's own voice rather than as a caption. */
.gate-hunt {
  margin: -6px 0 2px;
  height: 44px;
  display: flex;
  align-items: center;
  justify-content: center;
}

.gate-hunt-line {
  font-family: var(--serif);
  font-style: italic;
  font-size: 19px;
  line-height: 1.25;
  color: var(--accent);
  transition: opacity 0.26s ease, transform 0.26s ease;
}

.gate-hunt-line.is-out {
  opacity: 0;
  transform: translateY(-4px);
}

/* Says invite-only without saying unfinished. Letter-spaced small caps sit
   between the promise above it and the housekeeping below. */
.gate-beta {
  margin: 2px 0 0;
  font-size: 11px;
  letter-spacing: 0.1em;
  color: var(--gold-deep);
}

.gate-brand .accent {
  color: var(--accent);
  font-style: italic;
}

.gate-panel h1 {
  font-family: var(--serif);
  font-size: 26px;
  font-weight: 400;
  line-height: 1.25;
  margin: 0;
}

.gate-lede {
  margin: 0;
  color: var(--text-muted);
  font-size: 15px;
  line-height: 1.5;
}

.gate-panel .btn--big {
  width: 100%;
  margin-top: 6px;
}

.gate-panel .note {
  margin: 0;
  color: var(--text-muted);
  font-size: 12px;
  line-height: 1.45;
}

.gate-error {
  margin: 0;
  width: 100%;
  padding: 10px 12px;
  border-radius: var(--radius-sm);
  background: var(--destructive-soft);
  color: var(--destructive);
  font-size: 13px;
  line-height: 1.4;
}
