/* ============================================================
   EASING LAB  —  /playground/easing
   A full-size motion previewer on a phone. Every selector is prefixed
   .ease- so nothing here can leak into, or be caught by, the
   200KB site stylesheet this page also loads.

   Colour comes entirely from the site's :root tokens, so dark
   mode works for free via .dark-mode on <html>/<body>.
   ============================================================ */

.ease-body {
  --ease-nav: 100px;            /* overwritten from JS with the measured bar */
  --ease-rail: 340px;
  --ease-gutter: 40px;

  /* Instrument surfaces. Derived from the page ground so the tool reads as
     part of the site rather than a widget dropped on top of it. */
  --ease-well: #f2eeea;         /* the canvas the phone sits on */
  --ease-device: #232120;       /* the phone body */

  /* Wireframe palette: three greys and nothing else. Colour in the preview
     would compete with the motion, which is the only thing being judged here. */
  --ease-app-bg: #fbfaf9;       /* the screen */
  --ease-surface: #ffffff;      /* sheets, menus, cards — sit above the screen */
  --wf-1: #b9b4ad;              /* titles, primary buttons */
  --wf-2: #d5d0ca;              /* body, icons */
  --wf-3: #e9e5e0;              /* hairlines, faint copy */
  --ease-ink: var(--text-primary);

  /* Curve-editor purple. A tool affordance, deliberately outside the editorial
     palette — it marks the two draggable bezier handles, their arms, and the
     travelling playhead. One colour for the whole graph: no red, no blue.
     Brightened in dark mode to hold up on the darker well. */
  --ease-editor: #7c3aed;
}

.dark-mode .ease-body,
body.dark-mode {
  --ease-well: #1d1d1c;
  --ease-device: #000000;
  --ease-app-bg: #161615;
  --ease-surface: #232322;
  --wf-1: #55554e;
  --wf-2: #3d3d37;
  --wf-3: #2b2b26;
  --ease-editor: #a684ff;
}

/* The tool is a fixed instrument panel, not a document: no page scroll, and
   crucially no Lenis — this page deliberately does not load smooth.js. */
@media (min-width: 1000px) {
  .ease-body { overflow: hidden; }
}

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

.ease-app {
  display: grid;
  grid-template-columns: minmax(0, 1fr) var(--ease-rail);
  gap: 0;
  align-items: stretch;
  max-width: var(--wrap-width);
  margin: 0 auto;
  padding: 0 var(--ease-gutter) 0;
  min-height: calc(100vh - var(--ease-nav));
}

.ease-stage-col {
  display: flex;
  flex-direction: column;
  min-width: 0;
  padding: 4px 40px 28px 0;
}

/* ---------------- heading ---------------- */

.ease-stage-head { flex: none; margin-bottom: 18px; }

.ease-title {
  font-family: var(--font-display);
  font-weight: 400;
  font-size: 30px;
  line-height: 1.1;
  letter-spacing: -0.015em;
  color: var(--text-primary);
  margin: 0 0 6px;
}

.ease-sub {
  margin: 0;
  font-size: 14px;
  line-height: 1.45;
  color: var(--text-muted);
  max-width: 46ch;
}

/* ---------------- the stage: a phone ---------------- */

.ease-stage {
  flex: 1 1 auto;
  min-height: 300px;
  min-width: 0;
  display: grid;
  padding: 24px;
  overflow: hidden;
  background: var(--ease-well);
  border: 1px solid var(--rule-soft);
}

/* Height-led: the phone claims all the stage height going and derives its width
   from that, so a taller window buys a bigger preview. A portrait device in a
   landscape stage leaves air either side — that is the shape of the problem,
   not a layout bug. */
/* Height-led so a taller window buys a bigger preview, but capped near a real
   device's on-screen size — a phone rendered a metre tall stops reading as a
   phone, and "true to size" is the whole point of this note. Below the cap it
   shrinks with the stage; the content tracks it either way (see --pt below). */
.ease-phone {
  /* Capping the HEIGHT is what actually bounds the size: a max-width loses to
     the explicit height + aspect-ratio and gets ignored. 936px tall at 9:19.5
     lands the device around 430px wide — a real phone held at arm's length —
     and it shrinks below that with the stage. */
  height: min(100%, 936px);
  aspect-ratio: 9 / 19.5;
  justify-self: center;
  /* The FRAME is sized in px, deliberately NOT in --pt. --pt is 100cqw/393,
     and out here — the phone is not itself inside a query container — cqw has
     no container to resolve against and falls back to the viewport width,
     roughly tripling the value. That turned a slim bezel into a 28px slab and
     a phone corner into a 161px jellybean. The screen inside resolves --pt
     correctly because .ease-viewport IS the container; only the frame was
     wrong. A thin uniform bezel and a real phone's corner do not need to
     scale with device points anyway. */
  padding: 8px;
  background: var(--ease-device);
  border-radius: 44px;
  box-shadow: 0 20px 44px rgba(0, 0, 0, 0.16);
}
.dark-mode .ease-phone {
  box-shadow: 0 20px 44px rgba(0, 0, 0, 0.55);
  outline: 1px solid rgba(255, 255, 255, 0.09);
  outline-offset: -1px;
}

/* The device is a query container, and --pt is one iOS point mapped onto it:
   a reference iPhone is 393pt wide, so 1pt = 100cqw / 393. Every dimension
   inside the phone is then written in real points — 16pt gutters, 40pt
   avatars, 50pt buttons — so the UI is true to size at any phone scale, and
   scaling the phone scales the content with it instead of stranding it small.
   --pt is declared on .ease-phone too, because its own padding/radius resolve
   before the container context begins. */
.ease-viewport {
  position: relative;
  width: 100%;
  height: 100%;
  overflow: hidden;
  border-radius: 37px;              /* px, for the same reason as the frame */
  background: var(--ease-app-bg);
  container-type: size;
  --pt: calc(100cqw / 393);
}

/* ---------------- wireframe atoms ---------------- */

/* A bar stands in for a text line: regular = body (~15pt), strong = title
   (~20pt). Heights are the visual weight of that line, not its point size. */
.wf-bar {
  display: block;
  height: calc(11 * var(--pt));
  border-radius: calc(3 * var(--pt));
  background: var(--wf-2);
  flex: none;
}
.wf-bar.wf-strong { height: calc(15 * var(--pt)); }
.wf-bar.wf-faint { background: var(--wf-3); }

.wf-dot {                                   /* list avatar — 40pt, a real one */
  display: block;
  width: calc(40 * var(--pt)); height: calc(40 * var(--pt));
  border-radius: 50%;
  background: var(--wf-2);
  flex: none;
}
.wf-dot-sm { width: calc(24 * var(--pt)); height: calc(24 * var(--pt)); }
.wf-dot-lg { width: calc(56 * var(--pt)); height: calc(56 * var(--pt)); }

/* 50pt tall — the standard full-width iOS action button. */
.wf-btn {
  display: block;
  height: calc(50 * var(--pt));
  border-radius: calc(14 * var(--pt));
  background: var(--wf-1);
  flex: none;
}
.wf-btn-ghost { background: var(--wf-3); }

/* ---- richer wireframe atoms (patterned on real iOS references) ---- */

/* Title line — darker than body, the way a real title outranks its subtitle. */
.wf-bar.wf-title { height: calc(12 * var(--pt)); background: var(--wf-1); }
/* Large-title / nav-title weight. */
.wf-bar.wf-hero-title { height: calc(17 * var(--pt)); border-radius: calc(4 * var(--pt)); background: var(--wf-1); }

.wf-lines { flex: 1; display: flex; flex-direction: column; gap: calc(8 * var(--pt)); min-width: 0; }

/* Trailing timestamp, pinned to the top of a feed row. */
.wf-stamp {
  flex: none; align-self: flex-start; margin-top: calc(2 * var(--pt));
  width: calc(26 * var(--pt)); height: calc(8 * var(--pt));
  border-radius: calc(2 * var(--pt));
  background: var(--wf-3);
}

/* Status-bar glyphs: a time pill on the left, two signal blobs and a battery
   on the right. Reads as a status bar without pretending to be a real one. */
.wf-time {
  width: calc(34 * var(--pt)); height: calc(12 * var(--pt));
  border-radius: calc(3 * var(--pt));
  background: var(--wf-1);
}
.wf-sys { display: flex; align-items: center; gap: calc(5 * var(--pt)); }
.wf-glyph {
  display: block;
  width: calc(15 * var(--pt)); height: calc(11 * var(--pt));
  border-radius: calc(2 * var(--pt));
  background: var(--wf-2);
}
.wf-batt {
  position: relative;
  width: calc(24 * var(--pt)); height: calc(12 * var(--pt));
  border: calc(1.5 * var(--pt)) solid var(--wf-2);
  border-radius: calc(3 * var(--pt));
}
.wf-batt::after {
  content: ""; position: absolute; inset: calc(2 * var(--pt));
  right: auto; width: 62%;
  background: var(--wf-2);
  border-radius: calc(1 * var(--pt));
}

/* Leading glyph for menu / drawer / toast rows — an SF-symbol stand-in. */
.wf-ic {
  flex: none;
  width: calc(22 * var(--pt)); height: calc(22 * var(--pt));
  border-radius: calc(6 * var(--pt));
  background: var(--wf-2);
}
.wf-ic-ok { border-radius: 50%; }        /* the toast's status glyph reads as a check chip */

/* Trailing disclosure chevron. */
.wf-chev {
  flex: none;
  width: calc(8 * var(--pt)); height: calc(8 * var(--pt));
  border-top: calc(2 * var(--pt)) solid var(--wf-2);
  border-right: calc(2 * var(--pt)) solid var(--wf-2);
  transform: rotate(45deg);
}

/* Tab-bar item: a glyph, active one inked darker. */
.wf-tab { display: flex; }
.wf-tab i {
  display: block;
  width: calc(26 * var(--pt)); height: calc(26 * var(--pt));
  border-radius: calc(7 * var(--pt));
  background: var(--wf-2);
}
.wf-tab.is-on i { background: var(--wf-1); }

/* Selected-option card — the body of a bottom sheet. */
.wf-opt {
  display: flex; align-items: center; justify-content: space-between;
  gap: calc(12 * var(--pt));
  padding: calc(15 * var(--pt)) calc(16 * var(--pt));
  border: 1px solid var(--wf-3);
  border-radius: calc(12 * var(--pt));
}
.wf-opt.is-on { border-color: var(--wf-1); }
.wf-opt .wf-bar { flex: 1; }
.wf-radio {
  flex: none;
  width: calc(22 * var(--pt)); height: calc(22 * var(--pt));
  border-radius: 50%;
  border: calc(2 * var(--pt)) solid var(--wf-2);
}
.wf-radio.is-on { border-color: var(--wf-1); background: var(--wf-1); position: relative; }
.wf-radio.is-on::after {                 /* the tick inside a chosen radio */
  content: ""; position: absolute;
  left: calc(7 * var(--pt)); top: calc(3.5 * var(--pt));
  width: calc(5 * var(--pt)); height: calc(9 * var(--pt));
  border: solid var(--ease-surface);
  border-width: 0 calc(2 * var(--pt)) calc(2 * var(--pt)) 0;
  transform: rotate(45deg);
}

/* Sheet title row: title left, close affordance right. */
.wf-sheet-head {
  display: flex; align-items: center; justify-content: space-between;
  margin-bottom: calc(2 * var(--pt));
}
.wf-x {
  flex: none;
  width: calc(24 * var(--pt)); height: calc(24 * var(--pt));
  border-radius: 50%;
  background: var(--wf-3);
}

/* ---------------- base screen ---------------- */

.ease-base,
.ease-detail {
  position: absolute;
  inset: 0;
  display: flex;
  flex-direction: column;
  background: var(--ease-app-bg);
  will-change: transform;
  backface-visibility: hidden;
}

/* Only the screen push uses the second screen; every other component animates
   over the base one, so the two are mutually exclusive. */
.ease-detail { display: none; z-index: 3; }
.ease-viewport[data-kind="push"] .ease-detail { display: flex; }
.ease-viewport[data-kind="push"] .ease-comp { display: none; }

.wf-status {
  display: flex; align-items: center; justify-content: space-between;
  padding: calc(16 * var(--pt)) calc(20 * var(--pt)) calc(6 * var(--pt));
  flex: none;
}
.wf-status span { display: flex; gap: calc(4 * var(--pt)); }
.wf-tick {
  display: block;
  width: calc(4 * var(--pt)); height: calc(11 * var(--pt));
  background: var(--wf-2);
}

.wf-header {
  display: flex; align-items: center; justify-content: space-between;
  gap: calc(12 * var(--pt));
  padding: calc(12 * var(--pt)) calc(16 * var(--pt)) calc(14 * var(--pt));
  border-bottom: 1px solid var(--wf-3);
  flex: none;
}
.wf-header-back { justify-content: flex-start; gap: calc(14 * var(--pt)); }
.wf-back {
  display: block;
  width: calc(12 * var(--pt)); height: calc(12 * var(--pt));
  border-left: calc(2.5 * var(--pt)) solid var(--wf-1);
  border-bottom: calc(2.5 * var(--pt)) solid var(--wf-1);
  transform: rotate(45deg);
  flex: none;
}

.wf-list { flex: 1; overflow: hidden; }

/* 60pt row — 12+12 padding around a 40pt avatar, iOS-standard. */
.wf-row {
  display: flex; align-items: center;
  gap: calc(14 * var(--pt));
  padding: calc(12 * var(--pt)) calc(16 * var(--pt));
}
.wf-list .wf-row { border-bottom: 1px solid var(--wf-3); }
.wf-row span {
  flex: 1; display: flex; flex-direction: column;
  gap: calc(8 * var(--pt)); min-width: 0;
}

.wf-tabs {
  display: flex; justify-content: space-around; align-items: center;
  padding: calc(12 * var(--pt)) 0 calc(16 * var(--pt));
  border-top: 1px solid var(--wf-3);
  flex: none;
}
.wf-tabs i {
  display: block;
  width: calc(28 * var(--pt)); height: calc(28 * var(--pt));
  background: var(--wf-2);
}
.wf-tabs i:first-child { background: var(--wf-1); }

.wf-detail-body {
  flex: 1;
  display: flex; flex-direction: column; gap: calc(16 * var(--pt));
  padding: calc(16 * var(--pt));
}
.wf-hero { display: block; height: 30%; background: var(--wf-3); flex: none; }
/* The detail screen's action sits near the bottom edge, as it would on a real
   one, instead of trailing the copy with half a screen of white beneath it. */
.wf-detail-body .wf-btn { margin-top: auto; }

.wf-nav { display: flex; align-items: center; gap: calc(14 * var(--pt)); padding: calc(12 * var(--pt)) 0; }

/* ---------------- the moving component ---------------- */

.ease-scrim {
  position: absolute;
  inset: 0;
  background: #000;
  opacity: 0;
  z-index: 1;
}

.ease-comp {
  position: absolute;
  z-index: 2;
  background: var(--ease-surface);
  will-change: transform, opacity;
  backface-visibility: hidden;
}

/* Each component is anchored flush to the edge it travels from, so hiding it is
   always exactly translate(+/-100%) — no magic offsets to keep in sync with the
   padding. Modal and toast get a transparent positioning band for the same
   reason: the band is what travels, the card just rides on it. */
.ease-viewport[data-kind="sheet"] .ease-comp {
  left: 0; right: 0; bottom: 0;
  height: 56%;
  padding: calc(10 * var(--pt)) calc(18 * var(--pt)) calc(20 * var(--pt));
  display: flex; flex-direction: column; gap: calc(14 * var(--pt));
  border-radius: calc(20 * var(--pt)) calc(20 * var(--pt)) 0 0;
  box-shadow: 0 -10px 30px rgba(0, 0, 0, 0.13);
}
/* The sheet's action sits on the bottom edge, where a real one does, rather
   than floating directly under the last row with dead space beneath it. */
.ease-viewport[data-kind="sheet"] .ease-comp > .wf-btn { margin-top: auto; }

.wf-grab {
  display: block;
  width: calc(36 * var(--pt)); height: calc(5 * var(--pt));
  border-radius: calc(2.5 * var(--pt));
  background: var(--wf-2);
  align-self: center;
  margin-bottom: calc(4 * var(--pt));
  flex: none;
}

.ease-viewport[data-kind="drawer"] .ease-comp {
  top: 0; bottom: 0;
  width: 78%;
  padding: calc(18 * var(--pt)) calc(16 * var(--pt));
  display: flex; flex-direction: column; gap: calc(4 * var(--pt));
  box-shadow: 0 0 34px rgba(0, 0, 0, 0.2);
}
.ease-viewport[data-kind="drawer"][data-edge="left"] .ease-comp { left: 0; }
.ease-viewport[data-kind="drawer"][data-edge="right"] .ease-comp { right: 0; }
.wf-drawer-top {
  display: flex; align-items: center; gap: calc(14 * var(--pt));
  padding-bottom: calc(16 * var(--pt)); margin-bottom: calc(8 * var(--pt));
  border-bottom: 1px solid var(--wf-3);
}

.ease-viewport[data-kind="menu"] .ease-comp {
  top: calc(64 * var(--pt)); right: calc(14 * var(--pt));
  width: 58%;
  padding: calc(8 * var(--pt));
  display: flex; flex-direction: column;
  transform-origin: top right;
  border: 1px solid var(--wf-3);
  border-radius: calc(12 * var(--pt));
  box-shadow: 0 12px 28px rgba(0, 0, 0, 0.18);
}
.ease-comp .wf-nav { padding: calc(12 * var(--pt)) calc(10 * var(--pt)); }
/* Hairline between rows inside the menu and the drawer, like a grouped list. */
.ease-viewport[data-kind="menu"] .wf-nav + .wf-nav,
.ease-viewport[data-kind="drawer"] .wf-nav + .wf-nav { border-top: 1px solid var(--wf-3); }

.ease-viewport[data-kind="modal"] .ease-comp {
  inset: 0;
  padding: 0 calc(24 * var(--pt));
  display: flex; align-items: center; justify-content: center;
  background: transparent;
}
.wf-card {
  width: 100%;
  background: var(--ease-surface);
  padding: calc(22 * var(--pt)) calc(20 * var(--pt));
  display: flex; flex-direction: column; align-items: center; gap: calc(12 * var(--pt));
  border-radius: calc(16 * var(--pt));
  box-shadow: 0 14px 34px rgba(0, 0, 0, 0.22);
}
/* Alert copy is centred; the title sits a touch clear of the body. */
.wf-card .wf-bar { align-self: center; }
.wf-card .wf-title { margin-bottom: calc(2 * var(--pt)); }
.wf-card .wf-actions { width: 100%; }
.wf-actions { display: flex; gap: calc(10 * var(--pt)); margin-top: calc(6 * var(--pt)); }
.wf-actions .wf-btn { flex: 1; }

.ease-viewport[data-kind="toast"] .ease-comp {
  left: 0; right: 0;
  background: transparent;
  padding: 0 calc(12 * var(--pt));
}
.ease-viewport[data-kind="toast"][data-edge="top"] .ease-comp { top: 0; padding-top: calc(44 * var(--pt)); }
.ease-viewport[data-kind="toast"][data-edge="bottom"] .ease-comp { bottom: 0; padding-bottom: calc(72 * var(--pt)); }
.wf-toast {
  display: flex; align-items: center; gap: calc(12 * var(--pt));
  padding: calc(14 * var(--pt)) calc(14 * var(--pt));
  background: var(--ease-surface);
  border: 1px solid var(--wf-3);
  border-radius: calc(12 * var(--pt));
  box-shadow: 0 8px 22px rgba(0, 0, 0, 0.16);
}

/* ---------------- transport ---------------- */

.ease-transport {
  flex: none;
  display: flex;
  align-items: center;
  gap: 18px;
  margin-top: 18px;
}

.ease-play {
  flex: none;
  display: inline-flex;
  align-items: center;
  gap: 8px;
  height: 38px;
  padding: 0 18px;
  border: 1px solid var(--text-primary);
  background: var(--text-primary);
  color: var(--bg-cream);
  font-family: var(--font-sans);
  font-size: 13px;
  font-weight: 600;
  letter-spacing: 0.01em;
  cursor: pointer;
  transition: opacity 0.15s ease;
}
.ease-play:hover { opacity: 0.84; }
.ease-play .ease-ico-stop { display: none; }
.ease-play.is-playing .ease-ico-play { display: none; }
.ease-play.is-playing .ease-ico-stop { display: block; }

/* ---- the scrubber: the reason this page exists ---- */

.ease-scrub {
  flex: 1 1 auto;
  min-width: 0;
  display: flex;
  align-items: center;
  gap: 14px;
  cursor: ew-resize;
  touch-action: none;
  outline: none;
}
.ease-scrub:focus-visible .ease-scrub-track { box-shadow: 0 0 0 2px var(--accent); }

.ease-scrub-track {
  position: relative;
  flex: 1 1 auto;
  height: 3px;
  background: var(--rule);
}

.ease-scrub-fill {
  position: absolute;
  inset: 0 auto 0 0;
  width: 0;
  background: var(--accent);
}

.ease-scrub-knob {
  position: absolute;
  top: 50%;
  left: 0;
  width: 13px;
  height: 13px;
  margin: -6.5px 0 0 -6.5px;
  border-radius: 50%;
  background: var(--accent);
}

.ease-scrub-read {
  flex: none;
  font-variant-numeric: tabular-nums;
  font-size: 12px;
  color: var(--text-muted);
  min-width: 96px;
  text-align: right;
}
.ease-scrub-read i { opacity: 0.4; font-style: normal; }

.ease-transport-right {
  flex: none;
  display: flex;
  align-items: center;
  gap: 10px;
}

/* ---------------- segmented controls ---------------- */

.ease-seg {
  display: inline-flex;
  border: 1px solid var(--rule);
}
.ease-seg button {
  appearance: none;
  border: 0;
  border-left: 1px solid var(--rule);
  background: transparent;
  color: var(--text-muted);
  font-family: var(--font-sans);
  font-size: 13px;
  line-height: 1;
  height: 34px;
  min-width: 44px;
  padding: 0 10px;
  cursor: pointer;
  transition: background 0.15s ease, color 0.15s ease;
}
.ease-seg button:first-child { border-left: 0; }
.ease-seg button:hover { color: var(--text-hover); }
.ease-seg button.is-on {
  background: var(--text-primary);
  color: var(--bg-cream);
}
.ease-seg-dir { width: 100%; }
.ease-seg-dir button { flex: 1; font-size: 15px; }
.ease-seg-speed button { min-width: 40px; height: 36px; font-size: 12px; }

.ease-ghost {
  appearance: none;
  height: 36px;
  padding: 0 14px;
  border: 1px solid var(--rule);
  background: transparent;
  color: var(--text-muted);
  font-family: var(--font-sans);
  font-size: 13px;
  cursor: pointer;
  transition: background 0.15s ease, color 0.15s ease;
}
.ease-ghost:hover { color: var(--text-hover); }
.ease-ghost[aria-pressed="true"] {
  background: var(--text-primary);
  color: var(--bg-cream);
  border-color: var(--text-primary);
}

.ease-hint {
  flex: none;
  margin: 12px 0 0;
  font-size: 11.5px;
  color: var(--text-muted);
  opacity: 0.8;
}
.ease-hint i { font-style: normal; opacity: 0.45; margin: 0 4px; }
.ease-hint kbd {
  font-family: var(--font-sans);
  font-size: 11px;
  border: 1px solid var(--rule);
  padding: 1px 5px;
  margin: 0 1px;
}

/* ---------------- control rail ---------------- */

.ease-rail {
  border-left: 1px solid var(--rule-soft);
  padding: 4px 0 28px 28px;
  display: flex;
  flex-direction: column;
  gap: 13px;
  overflow-y: auto;
  max-height: calc(100vh - var(--ease-nav));
  scrollbar-width: thin;
}

.ease-group { display: flex; flex-direction: column; gap: 6px; }

/* Curve / spring panels are single flex items in the rail; give them their own
   tighter internal rhythm so the preset and its number band read as one block. */
.ease-panel { display: flex; flex-direction: column; gap: 11px; }

/* `hidden` is only `display: block` in the UA sheet, so any element given a
   display here — every .ease-group is a flex column — silently ignores it.
   The rail toggles Direction, the two easing panels and the two duration
   blocks via .hidden, so this rule is what actually makes them disappear. */
.ease-app [hidden] { display: none !important; }

.ease-label {
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  gap: 10px;
  font-size: 11px;
  font-weight: 600;
  letter-spacing: 0.09em;
  text-transform: uppercase;
  color: var(--text-muted);
}
.ease-label output {
  font-weight: 500;
  letter-spacing: 0;
  text-transform: none;
  font-size: 12.5px;
  font-variant-numeric: tabular-nums;
  color: var(--text-primary);
}

/* selects */
.ease-select { position: relative; display: block; }
.ease-select select {
  appearance: none;
  width: 100%;
  height: 34px;
  padding: 0 34px 0 12px;
  border: 1px solid var(--rule);
  background: transparent;
  color: var(--text-primary);
  font-family: var(--font-sans);
  font-size: 14px;
  cursor: pointer;
}
.ease-select svg {
  position: absolute;
  right: 12px;
  top: 50%;
  margin-top: -7px;
  pointer-events: none;
  color: var(--text-muted);
}

/* tabs */
.ease-tabs { display: flex; border: 1px solid var(--rule); }
.ease-tabs button {
  appearance: none;
  flex: 1;
  border: 0;
  border-left: 1px solid var(--rule);
  background: transparent;
  color: var(--text-muted);
  font-family: var(--font-sans);
  font-size: 13px;
  height: 34px;
  cursor: pointer;
  transition: background 0.15s ease, color 0.15s ease;
}
.ease-tabs button:first-child { border-left: 0; }
.ease-tabs button.is-on { background: var(--text-primary); color: var(--bg-cream); }

/* ---------------- curve graph ---------------- */

.ease-graph-wrap { display: flex; flex-direction: column; gap: 5px; }

/* The graph is square on purpose: the unit box inside it is a true square, so
   the slope you read off the curve is the slope the motion actually has. An
   earlier pass stretched the viewBox to a wide box, which squashed the curve
   and turned the handle dots into ellipses. */
.ease-graph {
  width: 100%;
  max-width: 212px;
  aspect-ratio: 1;
  height: auto;
  display: block;
  background: var(--ease-well);
  border: 1px solid var(--rule-soft);
  touch-action: none;
}

/* Dotted plot field — evenly spaced dots tiled across the graph, the way the
   easing.dev editor reads. Replaces the old dashed unit rectangle. */
.ease-dotgrid circle { fill: var(--rule-soft); }
.ease-dots { pointer-events: none; }

/* Keep the 0 and 1 reference lines as faint guides; the dashed unit box is
   retired in favour of the dot field (the element stays for JS but is invisible). */
.ease-grid line { stroke: var(--rule-soft); stroke-width: 1; }
.ease-graph-unit { fill: none; stroke: none; }

.ease-curve { fill: none; stroke: var(--text-primary); stroke-width: 2.2; stroke-linecap: round; }

/* The signature easing.dev look: two straight blue arms out to filled blue
   handles. A thin well-coloured ring separates each handle from its arm. */
.ease-handle-arm { stroke: var(--ease-editor); stroke-width: 1.4; opacity: 1; }
.ease-handle { fill: var(--ease-editor); stroke: var(--ease-well); stroke-width: 1.6; cursor: grab; }
.ease-handle:active { cursor: grabbing; }
.ease-handles[hidden] { display: none; }

/* Playhead stays the red accent so it never reads as a third blue handle. */
.ease-playhead { fill: var(--ease-editor); stroke: var(--ease-well); stroke-width: 1.5; }

.ease-graph-note { font-size: 11px; color: var(--text-muted); opacity: 0.8; }

/* ---------------- numbers & sliders ---------------- */

.ease-nums { display: grid; grid-template-columns: repeat(4, 1fr); gap: 6px; }
.ease-nums input {
  width: 100%;
  height: 34px;
  padding: 0 6px;
  border: 1px solid var(--rule);
  background: transparent;
  color: var(--text-primary);
  font-family: var(--font-sans);
  font-variant-numeric: tabular-nums;
  font-size: 13px;
  text-align: center;
}
.ease-nums input:focus-visible { outline: 1px solid var(--accent); outline-offset: -1px; }

.ease-slider-row input[type="range"] {
  appearance: none;
  width: 100%;
  height: 20px;
  background: transparent;
  cursor: pointer;
}
.ease-slider-row input[type="range"]::-webkit-slider-runnable-track {
  height: 3px; background: var(--rule);
}
.ease-slider-row input[type="range"]::-moz-range-track { height: 3px; background: var(--rule); }
.ease-slider-row input[type="range"]::-webkit-slider-thumb {
  appearance: none;
  width: 13px; height: 13px; margin-top: -5px;
  border: 0; border-radius: 50%;
  background: var(--text-primary);
}
.ease-slider-row input[type="range"]::-moz-range-thumb {
  width: 13px; height: 13px; border: 0; border-radius: 50%; background: var(--text-primary);
}

.ease-derived-val { margin: 0; font-size: 13px; color: var(--text-muted); line-height: 1.4; }
.ease-derived-val strong {
  display: block;
  font-family: var(--font-display);
  font-weight: 400;
  font-size: 26px;
  color: var(--text-primary);
  font-variant-numeric: tabular-nums;
}

/* ---------------- output ---------------- */

.ease-out { gap: 0; border-top: 1px solid var(--rule-soft); padding-top: 14px; margin-top: 2px; }
.ease-tabs-out { margin-bottom: 8px; }

.ease-code {
  margin: 0 0 8px;
  padding: 10px 12px;
  border: 1px solid var(--rule);
  background: var(--ease-well);
  color: var(--text-body);
  font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
  font-size: 11.5px;
  line-height: 1.55;
  white-space: pre-wrap;
  word-break: break-word;
  max-height: 116px;
  overflow: auto;
}

.ease-copy {
  display: inline-flex;
  align-items: center;
  gap: 7px;
  height: 34px;
  padding: 0 14px;
  border: 1px solid var(--rule);
  background: transparent;
  color: var(--text-primary);
  font-family: var(--font-sans);
  font-size: 13px;
  cursor: pointer;
  align-self: flex-start;
  transition: background 0.15s ease;
}
.ease-copy:hover { background: var(--rule-soft); }

.ease-back {
  margin-top: auto;
  padding-top: 14px;
  font-size: 13px;
  color: var(--text-muted);
}
.ease-back:hover { color: var(--text-hover); }

/* ---------------- responsive ---------------- */

@media (max-width: 999px) {
  .ease-app {
    grid-template-columns: minmax(0, 1fr);
    min-height: 0;
    padding-bottom: 60px;
  }
  .ease-stage-col { padding: 4px 0 24px; }
  .ease-rail {
    border-left: 0;
    border-top: 1px solid var(--rule-soft);
    padding: 24px 0 0;
    max-height: none;
    overflow: visible;
  }
  .ease-stage { min-height: 430px; }
  .ease-graph { max-width: 320px; }
}

@media (max-width: 620px) {
  /* Explicit grid rather than flex-wrap: left to wrap on its own the three
     items broke onto three ragged rows. Play and the speed/loop cluster share
     the top row, the scrubber gets the full width below it. */
  .ease-transport {
    display: grid;
    grid-template-columns: auto auto;
    justify-content: space-between;
    row-gap: 14px;
  }
  .ease-play { justify-self: start; }
  .ease-scrub { grid-column: 1 / -1; }
  .ease-stage { padding: 14px; min-height: 400px; }
  .ease-hint { display: none; }
}

/* Reduced motion: the page is *about* motion, so nothing here is disabled.
   What changes is that it never starts on its own — see easing.js, which
   refuses to autoplay or loop. An explicit press of Play still runs. */
