/*
 * emojipick layout and motion.
 *
 * The central structural decision: an item has exactly ONE DOM node for its
 * whole life, and it is *reparented* between the scatter field and the
 * checklist. It is never cloned and never rendered twice.
 *
 * That is what makes the animation honest. A FLIP needs the same element
 * measured before and after the move; if the field chip and the list row were
 * two nodes, we would be measuring one element's position and animating a
 * different one's — a coincidence that happens to look right until the two ever
 * disagree. It also means the move is a real move: there is no moment where the
 * item exists in both places, or in neither.
 *
 * The cost is that `.chip` has to read as a scattered emoji tile *and* as a
 * checklist row. That is done with context selectors (`.field .chip` /
 * `.list .chip`) rather than a modifier class, so reparenting is genuinely the
 * only thing that has to happen.
 */

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

/*
 * The column is at least one screen tall so that the pile, pushed down by
 * `#stage`'s auto margin, lands on the bottom edge of the *first* screen rather
 * than wherever the hero happens to end. Without the floor, a short hero leaves
 * the pile floating in the middle of the page with a third of the screen of
 * nothing under it — which is the opposite of "scattered across the bottom".
 */
.page {
  display: flex;
  flex-direction: column;
  max-width: 1180px;
  min-height: 100vh;
  /* `vh` on a phone is the height with the address bar *hidden*, so a 100vh
     column is taller than the screen while the bar is showing. `dvh` tracks it.
     The `vh` above stays as the fallback for anything without `dvh`. */
  min-height: 100dvh;
  margin: 0 auto;
  padding: var(--s5) var(--s5) var(--s8);
}

main {
  display: flex;
  flex: 1;
  flex-direction: column;
}

/* The pile sits on the floor of the first screen; the hero keeps the ceiling.
   This is a push, not a pin — once the checklist opens between them the column
   is taller than the viewport and everything below simply follows in flow. */
#stage {
  margin-top: auto;
}

.topbar {
  display: flex;
  align-items: center;
  justify-content: space-between;
  padding: var(--s4) 0 var(--s7);
}

.brand {
  display: inline-flex;
  align-items: center;
  gap: var(--s3);
  font-size: var(--t5);
  font-weight: 650;
  letter-spacing: 0.01em;
  color: var(--text);
  text-decoration: none;
}

.brand-mark {
  flex: none;
  width: 22px;
  height: 22px;
  color: var(--brand);
  /* The tilt is the page's own language — everything in the pile is askew —
     so the mark answers the cursor with the same gesture. */
  transition: rotate var(--dur) var(--ease);
}

.brand:hover .brand-mark {
  rotate: -8deg;
}

/* --------------------------------------------------------------- prompt */

.hero {
  padding-bottom: var(--s6);
}

.hero h1 {
  margin: 0 0 var(--s5);
  font-size: var(--t7);
  font-weight: 650;
  letter-spacing: -0.02em;
}

.bar {
  display: flex;
  gap: var(--s3);
  align-items: stretch;
}

.bar input {
  flex: 1;
  min-width: 0;
  padding: var(--s5) var(--s6);
  font: inherit;
  font-size: var(--t5);
  color: var(--text);
  background: var(--bg-card);
  border: 1px solid var(--border);
  border-radius: var(--radius-lg);
  box-shadow: var(--shadow-card);
  transition: border-color var(--dur-fast) var(--ease);
}

.bar input::placeholder {
  color: var(--text-faint);
}

.bar input:focus {
  outline: none;
  border-color: var(--primary);
}

.btn {
  padding: var(--s5) var(--s7);
  font-size: var(--t4);
  font-weight: 600;
  border-radius: var(--radius-lg);
  white-space: nowrap;
  transition:
    background var(--dur-fast) var(--ease),
    opacity var(--dur-fast) var(--ease),
    transform var(--dur-fast) var(--ease);
}

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

.btn:disabled {
  opacity: 0.5;
  cursor: default;
}

.btn-primary {
  background: var(--send);
  color: #fff;
}

[data-theme='dark'] .btn-primary {
  background: var(--primary);
  color: #16211a;
}

.btn-primary:hover:not(:disabled) {
  filter: brightness(1.08);
}

.btn-ghost {
  background: var(--bg-card);
  color: var(--text-dim);
  border: 1px solid var(--border-soft);
}

.btn-ghost:hover {
  color: var(--text);
  background: var(--bg-hover);
}

.icon-btn {
  display: grid;
  place-items: center;
  width: 32px;
  height: 32px;
  font-size: var(--t5);
  color: var(--text-dim);
  border-radius: var(--radius);
  transition: background var(--dur-fast) var(--ease);
}

.icon-btn:hover {
  background: var(--bg-hover);
  color: var(--text);
}

/* ------------------------------------------------------------ quick picks */

/*
 * Example sentences under the input. See index.html for why they are whole
 * sentences and why these four.
 *
 * They are deliberately quiet — a hairline, no fill, muted ink — because the
 * input above them is where the page wants the eye. Their job is to answer
 * "what do I even type", and an answer to that question does not need to
 * compete with the question.
 */
.quick {
  display: flex;
  flex-wrap: wrap;
  gap: var(--s2);
  margin-top: var(--s4);
}

.quick-btn {
  padding: var(--s2) var(--s5);
  font-size: var(--t3);
  color: var(--text-dim);
  background: var(--bg-card);
  border: 1px solid var(--border-soft);
  /* Capsule rather than --radius: the shape says "preset", which is the one
     thing separating these from the buttons that actually do something new. */
  border-radius: 999px;
  transition:
    color var(--dur-fast) var(--ease),
    border-color var(--dur-fast) var(--ease),
    background var(--dur-fast) var(--ease),
    opacity var(--dur-fast) var(--ease);
}

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

/*
 * Dimmed while a pick is in flight. The click would be ignored regardless — see
 * the guard at the top of submit() — but a button that looks live and does
 * nothing is worse than one that admits it is busy.
 */
.quick-btn:disabled {
  opacity: 0.45;
  cursor: default;
}

/* ------------------------------------------------------------ status line */

.status {
  display: flex;
  align-items: center;
  gap: var(--s3);
  min-height: 20px;
  margin: var(--s4) 0 0;
  font-size: var(--t2);
  color: var(--text-faint);
}

.status[data-tone='warn'] {
  color: var(--text-dim);
}

/*
 * The thinking indicator. Three dots rising in sequence rather than a spinner:
 * the wait is 400ms on a good request and up to 9s on a bad one, and a spinner
 * at 9s reads as "hung" while this reads as "still working".
 */
.dots {
  display: inline-flex;
  gap: 3px;
  align-items: flex-end;
  height: 12px;
}

.dots i {
  width: 4px;
  height: 4px;
  border-radius: 50%;
  background: currentColor;
  animation: dot 1.1s var(--ease) infinite;
}

.dots i:nth-child(2) {
  animation-delay: 0.15s;
}

.dots i:nth-child(3) {
  animation-delay: 0.3s;
}

@keyframes dot {
  0%,
  60%,
  100% {
    transform: translateY(0);
    opacity: 0.35;
  }
  30% {
    transform: translateY(-5px);
    opacity: 1;
  }
}

@media (prefers-reduced-motion: reduce) {
  .dots i {
    animation: none;
    opacity: 0.6;
  }
}

/* -------------------------------------------------------------- the field */

/*
 * The scatter layer. Height is set by scatter.js from the row count rather than
 * being fixed, so the field is exactly as tall as its contents and no chip is
 * ever clipped. It is a positioned container only — the chips inside are
 * absolutely placed, which is what lets them fly out of it without dragging the
 * layout around.
 */
.field {
  position: relative;
  /* The height is written inline by positionField() from the row count, so it
     animates here rather than snapping. The pile sinking as things are taken
     out of it is the visible half of "those ones were chosen", and the chips
     inside are absolutely positioned, so this costs one box and no reflow. */
  transition:
    opacity var(--dur) var(--ease),
    height var(--dur) var(--ease);
}

.chip {
  display: flex;
  align-items: center;
  justify-content: center;
  background: var(--bg-card);
  border: 1px solid var(--border-soft);
  border-radius: var(--radius-lg);
  cursor: pointer;
  /* Only properties that can be composited. Animating width/left/top here would
     make every hover a layout pass across 50 chips. */
  transition:
    box-shadow var(--dur-fast) var(--ease),
    border-color var(--dur-fast) var(--ease),
    opacity var(--dur-fast) var(--ease),
    background var(--dur-fast) var(--ease);
}

/*
 * Absolute positioning belongs to the *field*, not to the chip.
 *
 * It was originally on `.chip` itself, which broke the moment a chip was
 * reparented into the list: scatter.js sets `left`/`top` inline, those survive
 * the move, and an absolutely-positioned list row with `width: 100%` lands at
 * its old scatter coordinates stretched to the full containing block — one chip
 * was reaching x=2337 on a 1280px viewport. Keeping the positioning here means
 * leaving the field is enough to stop being positioned.
 */
/*
 * A field chip is a bare emoji: no card, no border, no name.
 *
 * It started as a white tile with the item's name under the glyph. That is the
 * right shape for a list row and the wrong one for a pile — 109 near-identical
 * cards read as a wall, and the wall is what the eye lands on instead of the
 * things in it. Dropping to the glyph alone is most of what makes the field
 * look strewn rather than laid out, and it is what lets the chip shrink far
 * enough below its cell for the jitter to be visible.
 *
 * The name is not lost: `buildNode` sets it as `aria-label` (announced) and as
 * `title` (on hover). Only the resting state is wordless.
 */
.field .chip {
  position: absolute;
  /* Both fall back to the tokens.css value until positionField() has run and
     written the computed sizes onto the field. */
  width: var(--chip, 46px);
  height: var(--chip, 46px);
  padding: 0;
  background: none;
  border: 0;
  box-shadow: none;
  /* Kept round at rest — invisible without a background — so the hover tint
     fades in instead of snapping from a square to a circle. */
  border-radius: 50%;
  /*
   * The tilt is what separates "scattered" from "laid out", and it is written
   * with the standalone `rotate` property rather than folded into `transform`.
   * `fly()` animates `transform`, so a tilt living there would vanish for the
   * whole flight and snap back on landing. Set per chip by positionField();
   * scoped to the field, so the same node is dead straight the moment it is
   * reparented into the list.
   */
  rotate: var(--rot, 0deg);
}

.field .chip:hover {
  /* --bg-hover is a near-invisible nudge against the page background, which
     is right for a list row and useless for a bare glyph that needs to read as
     "this one is under the cursor". The soft brand tint is the smallest thing
     that actually registers. */
  background: var(--primary-soft);
  z-index: 2;
}

.field .chip-emoji {
  /* Scales with the pitch, so a pile squeezed into the bottom quarter of a
     short screen shrinks its glyphs instead of overlapping them. */
  font-size: var(--glyph, 30px);
  line-height: 1;
  /* `scale` is a standalone property for the same reason `rotate` is:
     `transform` on this subtree belongs to the idle float, and a hover scale
     written there would simply be overwritten by it. */
  transition: scale var(--dur-fast) var(--ease);
}

.field .chip:hover .chip-emoji {
  scale: 1.18;
}

/*
 * The name belongs to the checklist reading of the chip, not the pile — but it
 * cannot be absent from the pile either. Several emoji are near-neighbours at
 * this size (the medicine faces, the two paperclip glyphs), and a bare glyph
 * with no way to ask what it is would be a guessing game.
 *
 * So it is deferred rather than removed: hidden at rest, revealed on hover as
 * a small floating label under the glyph. The pile stays wordless; the answer
 * is one hover away.
 */
.field .chip-name {
  position: absolute;
  top: calc(100% + 2px);
  left: 50%;
  translate: -50% 0;
  /* The chip is tilted; its label must not be. `--rot` is inherited from the
     chip's inline style, and the standalone `rotate` property composes after
     `translate`, so the label cancels the tilt about its own centre and stays
     put. */
  rotate: calc(-1 * var(--rot, 0deg));
  padding: 1px 5px;
  font-size: var(--t1);
  line-height: 1.3;
  color: var(--text-dim);
  white-space: nowrap;
  background: var(--bg-card);
  border-radius: var(--radius);
  box-shadow: var(--shadow-lift);
  opacity: 0;
  /* The label must not steal the hover that is showing it. */
  pointer-events: none;
  transition: opacity var(--dur-fast) var(--ease);
}

.field .chip:hover .chip-name {
  opacity: 1;
}

/* The checkbox belongs to the checklist reading of the chip, not the tile. */
.field .chip-box {
  display: none;
}

/*
 * Idle float. Kept slow (9s) and shallow (3px) — 50 chips moving at once is
 * only atmosphere if it is barely perceptible; any faster and it competes with
 * the thing the user is trying to read.
 *
 * Scoped to the idle phase: during a result the field holds the leftovers, and
 * motion there would pull attention away from the list that just arrived.
 */
@keyframes float {
  0%,
  100% {
    transform: translateY(0);
  }
  50% {
    transform: translateY(-3px);
  }
}

@media (prefers-reduced-motion: no-preference) {
  [data-phase='idle'] .field .chip {
    animation: float 9s ease-in-out infinite;
    animation-delay: var(--float-delay, 0s);
  }
}

/*
 * Visually hidden, still announced. `display: none` would remove it from the
 * accessibility tree entirely, which defeats the point.
 */
.sr-only {
  position: absolute;
  width: 1px;
  height: 1px;
  margin: -1px;
  padding: 0;
  overflow: hidden;
  clip-path: inset(50%);
  white-space: nowrap;
  border: 0;
}

/* -------------------------------------------------------------- checklist */

.panel {
  /* Below the prompt, above the pile — so the gap it needs is underneath it. */
  margin: 0 0 var(--s7);
  padding: var(--s5);
  background: var(--bg-card);
  border: 1px solid var(--border-soft);
  border-radius: var(--radius-lg);
  box-shadow: var(--shadow-card);
}

.panel-head {
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  padding: var(--s2) var(--s3) var(--s5);
}

.panel-head h2 {
  margin: 0;
  font-size: var(--t5);
  font-weight: 650;
}

.panel-head .count {
  font-size: var(--t2);
  color: var(--text-faint);
  font-variant-numeric: tabular-nums;
}

/*
 * Multi-column, and that is load-bearing rather than cosmetic.
 *
 * The list sits between the prompt and the pile, so its height is what pushes
 * the pile down. One row per item meant 37 items produced a 1900px list, which
 * put the pile a screen and a half below the chips that were flying into it —
 * the animation happened almost entirely off-screen. Filling columns instead
 * keeps a full result to roughly 400px, so both ends of every flight stay in
 * view.
 */
.list {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(180px, 1fr));
  gap: var(--s2);
  margin: 0;
  padding: 0;
  list-style: none;
}

.list .chip {
  width: 100%;
  min-width: 0;
  gap: var(--s4);
  padding: var(--s4) var(--s5);
  border-radius: var(--radius);
  text-align: left;
}

.list .chip:hover {
  background: var(--bg-hover);
}

.list .chip-emoji {
  font-size: 20px;
  line-height: 1;
}

.list .chip-name {
  flex: 1;
  /* min-width:0 is required for the ellipsis: without it a flex item refuses to
     shrink below its content, and one long name would widen the whole track. */
  min-width: 0;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
  font-size: var(--t4);
  color: var(--text-dim);
}

/*
 * Checked rows keep the card surface; the emphasis goes on the *unchecked*
 * ones.
 *
 * The first version tinted every checked row with the brand colour, which on a
 * real result meant 25 of 36 rows filled green — a wall the eye slides off. The
 * state actually worth finding is the exception ("the model said no and I
 * disagree"), so the unchecked rows are the ones that get a distinct surface.
 */
.list .chip[aria-pressed='true'] .chip-name {
  color: var(--text);
  font-weight: 550;
}

.list .chip[aria-pressed='true']:hover {
  background: var(--primary-soft);
}

.list .chip[aria-pressed='false'] {
  background: var(--bg-sunken);
  border-color: transparent;
}

.list .chip[aria-pressed='false'] .chip-name {
  color: var(--text-faint);
}

.list .chip[aria-pressed='false']:hover {
  background: var(--bg-hover);
}

.chip-box {
  display: grid;
  place-items: center;
  flex: none;
  width: 18px;
  height: 18px;
  border: 1.5px solid var(--border);
  border-radius: 4px;
  transition:
    background var(--dur-fast) var(--ease),
    border-color var(--dur-fast) var(--ease);
}

.chip-box::after {
  content: '';
  width: 9px;
  height: 5px;
  margin-top: -2px;
  border-left: 2px solid var(--primary-ink);
  border-bottom: 2px solid var(--primary-ink);
  transform: rotate(-45deg) scale(0);
  transition: transform var(--dur-fast) var(--ease);
}

.list .chip[aria-pressed='true'] .chip-box {
  background: var(--primary);
  border-color: var(--primary);
}

.list .chip[aria-pressed='true'] .chip-box::after {
  transform: rotate(-45deg) scale(1);
}

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

.panel-foot {
  display: flex;
  gap: var(--s3);
  padding-top: var(--s6);
}

.panel-foot .btn {
  padding: var(--s4) var(--s6);
}

/*
 * Manual additions live in the same list as model picks, so the chip needs no
 * separate state — but the user should be able to tell which is which after
 * the fact. A quiet left rule is enough; a badge would be louder than the
 * information deserves.
 */
.list .chip[data-manual='true'] {
  box-shadow: inset 2px 0 0 var(--border);
}

/* --------------------------------------------------------------- narrow */

@media (max-width: 560px) {
  .page {
    padding: var(--s4) var(--s4) var(--s8);
  }

  .hero h1 {
    font-size: var(--t6);
  }

  .bar {
    flex-wrap: wrap;
  }

  .bar .btn {
    flex: 1;
  }

  .panel-foot {
    flex-direction: column;
  }

  .panel-foot .btn {
    width: 100%;
  }
}
