/* play.ravivasavan.com — the shared skin.
   Everything every page has in common lives here once: the type, the theme
   tokens, the two-level chrome (site row + the experiment's tool row), and the
   glass material the controls float on. Pages keep only what is theirs.

   The material follows iOS's Liquid Glass principle: controls sit on a
   translucent layer ABOVE the content rather than in a panel cut out of it.
   It samples what is beneath (blur + saturation), carries a hairline edge and
   a brighter specular top, casts a soft ambient shadow, and is well rounded.
   The site's 64px pills were already that material; the rest now matches. */

@font-face {
  font-family: "Labil Grotesk";
  src: url("/assets/fonts/LabilGrotesk-Regular.woff2") format("woff2");
  font-weight: 400;
  font-style: normal;
  font-display: swap;
}

/* ---------------------------------------------------------------- tokens -- */

:root {
  /* Brand colours — inherited from ravivasavan.com */
  --night: #0d1b1e;
  --orange: #ff795c;
  --olive: #567c30;
  --white: #fff5f5;

  /* Day. --fg and --ink are the same colour under two names: the landing page
     grew up saying --fg, the experiments say --ink, and circadian.js publishes
     both. Keeping the pair means neither has to be rewritten. */
  --bg: var(--white);
  --fg: var(--night);
  --ink: var(--night);
  --muted: color-mix(in srgb, var(--night) 55%, var(--white));
  --field: color-mix(in srgb, var(--night) 4%, transparent);
  --separator: color-mix(in srgb, var(--night) 5%, transparent);
  --pill-bg: rgba(255, 245, 245, 0.2);
  --pill-filter: blur(10px) saturate(180%);
  --pill-edge: inset 0 0 0 1px var(--separator);

  --accent: var(--orange);

  /* Glass. --glass-mix and --glass-spec are the only two that move between
     day and night; everything else is derived, so a page never restates them. */
  --glass-mix: 62%;
  --glass-spec: 45%;
  --glass-bg: color-mix(in srgb, var(--bg) var(--glass-mix), transparent);
  --glass-blur: blur(24px) saturate(160%);
  --glass-edge: inset 0 0 0 1px color-mix(in srgb, var(--fg) 10%, transparent);
  --glass-specular: inset 0 1px 0 color-mix(in srgb, white var(--glass-spec), transparent);
  --glass-shadow: 0 1px 2px rgba(13, 27, 30, 0.06), 0 24px 64px rgba(13, 27, 30, 0.18);
  /* The same ambience at pill scale — 64px of drop under a 44px control reads
     as a mistake, so the small parts get a shorter throw. */
  --glass-shadow-sm: 0 1px 2px rgba(13, 27, 30, 0.06), 0 8px 24px rgba(13, 27, 30, 0.10);
  --glass-radius: 24px;

  /* The sheet is the one piece of glass that carries running text, and it lies
     directly over the experiment's own artwork at every width now that the
     content runs the full viewport under it. 62% of the background is not
     enough ground for a 13px label to survive teletext's saturated raster, so
     the sheet keeps the material's blur, edge and shadow but sits on a denser
     tint and a calmer saturation of its own — enough to read on, still thin
     enough for the canvas to show through it. */
  --sheet-mix: 86%;
  --sheet-spec: 34%;
  --sheet-blur: blur(24px) saturate(120%);

  /* Chrome geometry. Both rows and the rule are positioned off these, so a
     fold only has to restate the four numbers. */
  --chrome-top: 40px;
  --chrome-rule: 120px;
  --chrome-tools: 136px;
  --chrome-pad: 40px;

  /* The floating settings sheet. It is an overlay: the content underneath is
     the whole viewport and nothing reserves room for the glass, so the only
     geometry here is the sheet's own — how wide the pane is, how far it is
     inset, and how big the disc is once it is minimised. */
  --panel-w: 360px;
  --panel-gap: 16px;
  --sheet-min: 64px;
  /* Where the sheet's top edge sits. Overridden below on a page that has a
     tools row, because the two would otherwise start on the same line. */
  --sheet-top: var(--chrome-tools);

  /* Stacking, one place. Content < sheet < rule < chrome/tools. */
  --z-sheet: 450;
  --z-rule: 499;
  --z-chrome: 500;

  --chevron: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='24' height='24' viewBox='0 0 24 24' fill='none' stroke='%230d1b1e' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='m6 9 6 6 6-6'/%3E%3C/svg%3E");
}

/* Every theme block is anchored to :root on purpose. A bare [data-theme="…"]
   matches any element carrying the attribute, so a third-party widget that
   stamps its own (DialKit writes data-theme="system") would repaint itself in
   the wrong palette from the inside. */
:root[data-theme="night"] {
  --bg: var(--night);
  --fg: var(--white);
  --ink: var(--white);
  --muted: color-mix(in srgb, var(--white) 55%, var(--night));
  --field: color-mix(in srgb, var(--white) 6%, transparent);
  --separator: color-mix(in srgb, var(--white) 8%, transparent);
  --pill-bg: rgba(13, 27, 30, 0.5);
  --glass-mix: 52%;
  --glass-spec: 12%;
  --sheet-mix: 80%;
  --sheet-spec: 10%;
  --chevron: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='24' height='24' viewBox='0 0 24 24' fill='none' stroke='%23fff5f5' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='m6 9 6 6 6-6'/%3E%3C/svg%3E");
}

@media (prefers-color-scheme: dark) {
  :root[data-theme="system"] {
    --bg: var(--night);
    --fg: var(--white);
    --ink: var(--white);
    --muted: color-mix(in srgb, var(--white) 55%, var(--night));
    --field: color-mix(in srgb, var(--white) 6%, transparent);
    --separator: color-mix(in srgb, var(--white) 8%, transparent);
    --pill-bg: rgba(13, 27, 30, 0.5);
    --glass-mix: 52%;
    --glass-spec: 12%;
    --sheet-mix: 80%;
    --sheet-spec: 10%;
    --chevron: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='24' height='24' viewBox='0 0 24 24' fill='none' stroke='%23fff5f5' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='m6 9 6 6 6-6'/%3E%3C/svg%3E");
  }
}

/* Circadian slides continuously between light and dark, so neither chevron
   would be right for most of the day: it gets a mid-tone that reads on both. */
:root[data-theme="circadian"] {
  --chevron: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='24' height='24' viewBox='0 0 24 24' fill='none' stroke='%23888f92' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='m6 9 6 6 6-6'/%3E%3C/svg%3E");
}

/* ----------------------------------------------------------------- reset -- */

*, *::before, *::after { box-sizing: border-box; }
h1, h2, h3, h4, h5, h6, p, figure, blockquote, dl, dd, ul, ol { margin: 0; }
[hidden] { display: none !important; }

html {
  background: var(--bg);
  /* Reserve the gutter so the fixed chrome measures against the same box as
     ravivasavan.com — otherwise the centred pill lands half a scrollbar off
     on platforms with classic scrollbars. */
  scrollbar-gutter: stable;
}

body {
  margin: 0;
  padding: 0;
  background: var(--bg);
  color: var(--ink);
  font-family: "Labil Grotesk", system-ui, sans-serif;
  font-size: 16px;
  line-height: 1.3;
  transition: background-color 240ms ease, color 240ms ease;
}

a { color: inherit; }

/* ------------------------------------------------- level 1: the site row -- */
/* Back (or home) at the left, the identity pill dead centre on a 1fr auto 1fr
   grid, the theme switch hard right. A hairline across the full width divides
   it from the experiment below: everything above the line is the site's,
   everything under it belongs to the tool. */

.chrome,
.header-pills {
  position: fixed;
  top: var(--chrome-top);
  left: 0;
  right: 0;
  z-index: var(--z-chrome);
  display: grid;
  grid-template-columns: 1fr auto 1fr;
  align-items: center;
  pointer-events: none;
}
.chrome { padding: 0 var(--chrome-pad); }
.chrome > *,
.header-pills > * { pointer-events: auto; }

/* Experiment pages: back pill at the true left edge, theme at the true right. */
.chrome__back  { justify-self: start; }
.chrome__id    { justify-self: center; }
.chrome__theme { justify-self: end; }

/* Landing page: the two icon pills tuck in against the identity pill instead
   of sitting at the edges, because there is no tool row to align them with. */
.header-pills > .icon-pill--home  { grid-column: 1; justify-self: end;    margin-right: 8px; }
.header-pills > .header-pill      { grid-column: 2; justify-self: center; }
.header-pills > .icon-pill--theme { grid-column: 3; justify-self: start;  margin-left: 8px; }

.chrome-rule {
  position: fixed;
  top: var(--chrome-rule);
  left: 0;
  right: 0;
  border-top: 1px solid var(--separator);
  z-index: var(--z-rule);
  pointer-events: none;
}

/* --- the 64px glass pills --- */

.header-pill {
  background: var(--pill-bg);
  backdrop-filter: var(--pill-filter);
  -webkit-backdrop-filter: var(--pill-filter);
  border-radius: 32px;
  padding: 8px;
  box-shadow: var(--pill-edge);
  margin: 0;
}
.header-pill__inner { display: flex; align-items: center; gap: 12px; }
.header-pill__avatar {
  width: 48px;
  height: 48px;
  border-radius: 48px;
  background: var(--separator);
  overflow: hidden;
  flex-shrink: 0;
}
.header-pill__avatar svg { width: 100%; height: 100%; display: block; }
.header-pill__label { display: flex; flex-direction: column; gap: 4px; padding-right: 12px; }
.header-pill__name { font-size: 16px; line-height: 1.3; }
/* Experiment pages put the experiment's name here; the landing page leaves it
   out and the pill is just the identity. */
.header-pill__role { font-size: 16px; line-height: 1.3; opacity: 0.6; }

.icon-pill {
  background: var(--pill-bg);
  backdrop-filter: var(--pill-filter);
  -webkit-backdrop-filter: var(--pill-filter);
  border: 0;
  border-radius: 32px;
  padding: 8px;
  cursor: pointer;
  color: var(--ink);
  box-shadow: var(--pill-edge);
  display: inline-flex;
  font: inherit;
  position: relative;
  overflow: hidden;
  text-decoration: none;
}
/* The hover is the inner disc growing, not the pill — same as the main site. */
.icon-pill::before {
  content: '';
  position: absolute;
  top: 8px;
  left: 8px;
  width: 48px;
  height: 48px;
  background: var(--separator);
  border-radius: 48px;
  z-index: 0;
  transition:
    top           240ms cubic-bezier(0.4, 0, 0.2, 1),
    left          240ms cubic-bezier(0.4, 0, 0.2, 1),
    width         240ms cubic-bezier(0.4, 0, 0.2, 1),
    height        240ms cubic-bezier(0.4, 0, 0.2, 1),
    border-radius 240ms cubic-bezier(0.4, 0, 0.2, 1);
}
.icon-pill:hover::before,
.icon-pill:focus-visible::before {
  top: 3.2px;
  left: 3.2px;
  width: 57.6px;
  height: 57.6px;
  border-radius: 38.4px;
}
.icon-pill__icon {
  width: 48px;
  height: 48px;
  border-radius: 48px;
  display: grid;
  place-items: center;
  flex-shrink: 0;
  overflow: hidden;
  position: relative;
  z-index: 1;
}
/* 16px glyph on the 24 grid — the main site's optical weight. */
.icon-pill__icon svg {
  grid-area: 1 / 1;
  width: 16px;
  height: 16px;
  transition: opacity 240ms ease-out, transform 240ms cubic-bezier(0.4, 0, 0.2, 1);
}
.icon-pill.is-on { color: var(--accent); }

/* A two-glyph toggle (camera on/off, mute…) swaps its faces on the same grid
   cell. Used by both pill sizes. */
.icon-pill svg[data-state="off"],
.tool-pill svg[data-state="off"] { opacity: 0; transform: rotate(-90deg) scale(0.85); }
.icon-pill.is-on svg[data-state="on"],
.tool-pill.is-on svg[data-state="on"] { opacity: 0; transform: rotate(90deg) scale(0.85); }
.icon-pill.is-on svg[data-state="off"],
.tool-pill.is-on svg[data-state="off"] { opacity: 1; transform: rotate(0) scale(1); }

/* The theme pill shows the current mode and, on hover, the one you'd get by
   clicking: day → night → system → circadian. */
.icon-pill--theme .icon-pill__icon svg { opacity: 0; transform: rotate(-90deg) scale(0.85); }
:root[data-theme="day"]       .icon-pill--theme svg[data-tstate="day"],
:root[data-theme="night"]     .icon-pill--theme svg[data-tstate="night"],
:root[data-theme="system"]    .icon-pill--theme svg[data-tstate="system"],
:root[data-theme="circadian"] .icon-pill--theme svg[data-tstate="circadian"] {
  opacity: 1;
  transform: rotate(0deg) scale(1);
}
:root[data-theme="day"]       .icon-pill--theme:hover svg[data-tstate="day"],
:root[data-theme="night"]     .icon-pill--theme:hover svg[data-tstate="night"],
:root[data-theme="system"]    .icon-pill--theme:hover svg[data-tstate="system"],
:root[data-theme="circadian"] .icon-pill--theme:hover svg[data-tstate="circadian"] {
  opacity: 0;
  transform: rotate(90deg) scale(0.85);
}
:root[data-theme="day"]       .icon-pill--theme:hover svg[data-tstate="night"],
:root[data-theme="night"]     .icon-pill--theme:hover svg[data-tstate="system"],
:root[data-theme="system"]    .icon-pill--theme:hover svg[data-tstate="circadian"],
:root[data-theme="circadian"] .icon-pill--theme:hover svg[data-tstate="day"] {
  opacity: 1;
  transform: rotate(0deg) scale(1);
}

/* ------------------------------------------ the glass material, on its own -- */

.glass {
  background: var(--glass-bg);
  backdrop-filter: var(--glass-blur);
  -webkit-backdrop-filter: var(--glass-blur);
  border-radius: var(--glass-radius);
  box-shadow: var(--glass-edge), var(--glass-specular), var(--glass-shadow);
}

/* ---------------------------------------------- level 2: the tools row --- */
/* The experiment's own buttons, one word each. Right-aligned under the rule,
   running the full width: the sheet is an overlay that starts below this row,
   so there is nothing here to stop short of. */

.tools {
  position: fixed;
  top: var(--chrome-tools);
  left: var(--chrome-pad);
  right: var(--chrome-pad);
  z-index: var(--z-chrome);
  display: flex;
  align-items: center;
  gap: 8px;
  /* Right-aligned by an auto margin on the first pill rather than
     justify-content: flex-end. A flex-end row that outgrows its box spills
     past the *start* of the line — off the left of the screen, before the
     scroll origin, where nothing can reach it. An auto margin collapses to
     zero the moment the pills stop fitting, so the same row just scrolls. */
  overflow-x: auto;
  overscroll-behavior-x: contain;
  -webkit-overflow-scrolling: touch;
  touch-action: pan-x;
  scrollbar-width: none;
  /* A scroller clips at its padding edge, and the row's box is exactly one
     pill tall, so the pills' ambient shadow would be sliced off. The padding
     gives it room and the negative margin gives the room straight back, so
     the pills still sit on --chrome-tools. */
  padding-block: 32px;
  margin-block: -32px;
  /* The taller, wider box must not swallow clicks meant for the page beneath
     or for the sheet's head — same trick the level-1 chrome already uses. */
  pointer-events: none;
}
.tools::-webkit-scrollbar { display: none; }
.tools > :first-child { margin-left: auto; }
/* On a page with a sheet the row hangs off the same edge the glass does, so
   the pills and the pane below them share a right margin. It no longer stops
   short of anything: the sheet starts below the row, not beside it. */
body:has(.sheet) .tools { right: var(--panel-gap); }

.tool-pill {
  display: inline-flex;
  align-items: center;
  gap: 8px;
  flex: 0 0 auto;
  pointer-events: auto;
  height: 44px;
  padding: 0 16px 0 12px;
  border: 0;
  border-radius: 22px;
  background: var(--glass-bg);
  backdrop-filter: var(--glass-blur);
  -webkit-backdrop-filter: var(--glass-blur);
  box-shadow: var(--glass-edge), var(--glass-specular), var(--glass-shadow-sm);
  color: var(--ink);
  font: inherit;
  font-size: 14px;
  line-height: 1;
  cursor: pointer;
  text-decoration: none;
  transition:
    transform  200ms cubic-bezier(0.4, 0, 0.2, 1),
    box-shadow 200ms ease,
    color      200ms ease;
}
.tool-pill:hover,
.tool-pill:focus-visible {
  transform: translateY(-1px);
  box-shadow: var(--glass-edge), var(--glass-specular), var(--glass-shadow);
}
.tool-pill:active { transform: translateY(0) scale(0.98); }
.tool-pill__icon { display: grid; place-items: center; width: 16px; height: 16px; flex-shrink: 0; }
.tool-pill__icon svg {
  grid-area: 1 / 1;
  width: 16px;
  height: 16px;
  transition: opacity 240ms ease-out, transform 240ms cubic-bezier(0.4, 0, 0.2, 1);
}
.tool-pill.is-on {
  color: var(--accent);
  box-shadow:
    inset 0 0 0 1px color-mix(in srgb, var(--accent) 60%, transparent),
    var(--glass-specular),
    var(--glass-shadow-sm);
}
.tool-pill--danger:hover { color: var(--accent); }
.tool-pill[disabled] { opacity: 0.4; cursor: default; }
.tool-pill[disabled]:hover { transform: none; color: var(--ink); box-shadow: var(--glass-edge), var(--glass-specular), var(--glass-shadow-sm); }

/* ------------------------------------------------------------- the sheet -- */
/* Settings float over the content on their own pane of glass, inset from the
   viewport rather than butted against it. It is a pure overlay: the artwork
   underneath runs the full width and is centred on the viewport, and the glass
   lies on top of it. Covering some of the artwork is what minimise is for. */

/* A page with a tools row drops the sheet below the row — both would otherwise
   start at --chrome-tools and the glass would sit on the pills. */
body:has(.tools) { --sheet-top: calc(var(--chrome-tools) + 44px + var(--panel-gap)); }

.sheet {
  position: fixed;
  top: var(--sheet-top);
  right: var(--panel-gap);
  width: var(--panel-w);
  /* Sized rather than bottom-anchored, so width and height can both
     interpolate when the pane folds into the minimised disc. A percentage on
     a fixed element measures the viewport, scrollbar already deducted. */
  height: calc(100% - var(--sheet-top) - var(--panel-gap));
  z-index: var(--z-sheet);
  padding: 20px;
  display: flex;
  flex-direction: column;
  gap: 20px;
  overflow-y: auto;
  scrollbar-width: none;
  overscroll-behavior: contain;
  /* Secondary type inside the sheet, redefined once for everything in it —
     the shared labels, DialKit's, and each experiment's own. :root's --muted
     is 55% of the ink mixed into the page, which measures 3.87:1 on flat
     ground and collapses to under 2:1 the moment a saturated canvas is behind
     the glass on a phone. 82% of the ink clears 4.5:1 against the darkest and
     the lightest backdrop the experiments put under the sheet. */
  --muted: color-mix(in srgb, var(--fg) 82%, transparent);
  /* The denser tint, at every width: there is always artwork behind it. */
  background: color-mix(in srgb, var(--bg) var(--sheet-mix), transparent);
  backdrop-filter: var(--sheet-blur);
  -webkit-backdrop-filter: var(--sheet-blur);
  border-radius: var(--glass-radius);
  box-shadow:
    var(--glass-edge),
    inset 0 1px 0 color-mix(in srgb, white var(--sheet-spec), transparent),
    var(--glass-shadow);
  transition:
    width         240ms cubic-bezier(0.4, 0, 0.2, 1),
    height        240ms cubic-bezier(0.4, 0, 0.2, 1),
    border-radius 240ms cubic-bezier(0.4, 0, 0.2, 1);
}
.sheet::-webkit-scrollbar { display: none; }

/* The grabber only exists folded; play.js adds it if the markup left it out. */
.sheet__grab {
  display: none;
  position: absolute;
  top: 8px;
  left: 50%;
  width: 36px;
  height: 4px;
  margin-left: -18px;
  border-radius: 2px;
  background: color-mix(in srgb, var(--fg) 22%, transparent);
}

.sheet__head {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 12px;
  flex: 0 0 auto;
}
.sheet__title { font-size: 14px; }
/* The minimise button is the head's hard-right item, so the summary takes the
   slack to its left and stays where it always was. */
.sheet__summary { font-size: 13px; color: var(--muted); font-variant-numeric: tabular-nums; margin-left: auto; }

/* The minimise control — play.js appends it to the head if the markup has
   none, the way it appends the grabber. Glass-inset disc, 16px glyph. */
.sheet__min {
  flex: 0 0 auto;
  width: 32px;
  height: 32px;
  display: grid;
  place-items: center;
  border: 0;
  border-radius: 16px;
  padding: 0;
  background: color-mix(in srgb, var(--fg) 6%, transparent);
  box-shadow: inset 0 0 0 1px color-mix(in srgb, var(--fg) 10%, transparent);
  color: var(--ink);
  cursor: pointer;
  transition: background-color 160ms ease;
}
.sheet__min svg { width: 16px; height: 16px; display: block; }
.sheet__min:hover { background: color-mix(in srgb, var(--fg) 12%, transparent); }
.sheet__min:focus-visible {
  outline: none;
  box-shadow: inset 0 0 0 1px color-mix(in srgb, var(--accent) 60%, transparent),
              0 0 0 3px color-mix(in srgb, var(--accent) 25%, transparent);
}

/* And what is left of the sheet once it is minimised: the disc's whole face is
   this button, so the glass is still the sheet — nothing else moves. */
.sheet__icon {
  display: none;
  position: absolute;
  inset: 0;
  place-items: center;
  border: 0;
  border-radius: inherit;
  padding: 0;
  background: transparent;
  color: var(--ink);
  cursor: pointer;
  transition: background-color 160ms ease;
}
.sheet__icon svg { width: 20px; height: 20px; display: block; }
.sheet__icon:hover { background: color-mix(in srgb, var(--fg) 8%, transparent); }
.sheet__icon:focus-visible {
  outline: none;
  box-shadow: inset 0 0 0 2px color-mix(in srgb, var(--accent) 60%, transparent);
}

.sheet__body {
  display: flex;
  flex-direction: column;
  gap: 20px;
  flex: 1 1 auto;
  min-height: 0;
}

/* Uppercase and letter-spaced, but not dimmed a second time: 60% of an
   already-secondary ink measured 1.4:1 over teletext's raster. */
.sheet__section {
  font-size: 11px;
  text-transform: uppercase;
  letter-spacing: 0.08em;
  font-weight: 400;
  color: var(--muted);
  margin-top: 8px;
}
.sheet__section:first-child { margin-top: 0; }

.sheet__note { font-size: 13px; line-height: 1.45; color: var(--muted); }

/* ------------------------------------------------------------- controls -- */
/* One shape for every setting on every experiment: a label row with the value
   right-aligned in tabular figures, and the control under it. */

.field { display: grid; gap: 8px; }
.field__top {
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  gap: 12px;
  font-size: 13px;
}
.field__label { color: var(--muted); }
.field__value { font-variant-numeric: tabular-nums; color: var(--ink); }
.field__row { display: flex; gap: 8px; }
.field__row > * { flex: 1; min-width: 0; }

/* --- range --- */
/* WebKit has no ::-moz-range-progress, so the filled half is a gradient sized
   by --dial-fill, which play.js keeps in step with the value. */
input[type="range"].dial {
  -webkit-appearance: none;
  appearance: none;
  width: 100%;
  height: 16px;
  margin: 0;
  display: block;
  background: transparent;
  cursor: pointer;
}
.dial::-webkit-slider-runnable-track {
  height: 2px;
  border-radius: 2px;
  background:
    linear-gradient(var(--accent), var(--accent)) 0 / var(--dial-fill, 0%) 100% no-repeat,
    color-mix(in srgb, var(--fg) 18%, transparent);
}
.dial::-webkit-slider-thumb {
  -webkit-appearance: none;
  appearance: none;
  width: 16px;
  height: 16px;
  border: 0;
  border-radius: 16px;
  background: var(--accent);
  box-shadow: 0 0 0 2px var(--bg);
  margin-top: -7px;
  transition: transform 140ms ease;
}
.dial:hover::-webkit-slider-thumb { transform: scale(1.08); }
.dial::-moz-range-track { height: 2px; border-radius: 2px; background: color-mix(in srgb, var(--fg) 18%, transparent); }
.dial::-moz-range-progress { height: 2px; border-radius: 2px; background: var(--accent); }
.dial::-moz-range-thumb {
  width: 16px;
  height: 16px;
  border: 0;
  border-radius: 16px;
  background: var(--accent);
  box-shadow: 0 0 0 2px var(--bg);
}
.dial:focus-visible { outline: none; }
.dial:focus-visible::-webkit-slider-thumb { box-shadow: 0 0 0 2px var(--bg), 0 0 0 6px color-mix(in srgb, var(--accent) 40%, transparent); }
.dial:focus-visible::-moz-range-thumb { box-shadow: 0 0 0 2px var(--bg), 0 0 0 6px color-mix(in srgb, var(--accent) 40%, transparent); }
.dial:disabled { opacity: 0.4; cursor: default; }

/* --- text, number, select, textarea --- */
.input,
.select,
.textarea {
  font: inherit;
  font-size: 14px;
  color: var(--ink);
  background: color-mix(in srgb, var(--fg) 6%, transparent);
  border: 0;
  box-shadow: inset 0 0 0 1px color-mix(in srgb, var(--fg) 10%, transparent);
  border-radius: 10px;
  height: 40px;
  padding: 0 12px;
  width: 100%;
}
.select {
  -webkit-appearance: none;
  appearance: none;
  padding-right: 34px;
  background-image: var(--chevron);
  background-repeat: no-repeat;
  background-position: right 10px center;
  background-size: 14px 14px;
  cursor: pointer;
}
.textarea {
  height: auto;
  min-height: 116px;
  padding: 10px 12px;
  line-height: 1.45;
  resize: vertical;
}
.input:focus-visible,
.select:focus-visible,
.textarea:focus-visible {
  outline: none;
  box-shadow: inset 0 0 0 1px color-mix(in srgb, var(--accent) 60%, transparent),
              0 0 0 3px color-mix(in srgb, var(--accent) 25%, transparent);
}
/* A placeholder is not content, so it keeps the lighter grey the rest of the
   family uses rather than the sheet's raised --muted. */
.input::placeholder, .textarea::placeholder { color: color-mix(in srgb, var(--ink) 55%, transparent); }

/* --- switch --- */
/* The checkbox is still the control — it keeps the keyboard, the label, and
   the form semantics; the pill is just what you see. */
.switch {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 12px;
  font-size: 13px;
  cursor: pointer;
}
.switch input {
  position: absolute;
  width: 1px;
  height: 1px;
  opacity: 0;
  pointer-events: none;
}
.switch__track {
  flex: 0 0 auto;
  position: relative;
  width: 36px;
  height: 20px;
  border-radius: 10px;
  background: color-mix(in srgb, var(--fg) 14%, transparent);
  box-shadow: inset 0 0 0 1px color-mix(in srgb, var(--fg) 8%, transparent);
  transition: background-color 200ms ease;
}
.switch__track::after {
  content: '';
  position: absolute;
  top: 2px;
  left: 2px;
  width: 16px;
  height: 16px;
  border-radius: 16px;
  background: var(--bg);
  box-shadow: 0 1px 2px rgba(13, 27, 30, 0.25);
  transition: transform 200ms cubic-bezier(0.4, 0, 0.2, 1);
}
.switch input:checked + .switch__track { background: var(--accent); }
.switch input:checked + .switch__track::after { transform: translateX(16px); }
.switch input:focus-visible + .switch__track { box-shadow: 0 0 0 3px color-mix(in srgb, var(--accent) 35%, transparent); }
.switch input:disabled + .switch__track { opacity: 0.4; }

/* --- buttons --- */
.btn {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: 8px;
  height: 36px;
  padding: 0 14px;
  border: 0;
  border-radius: 10px;
  background: color-mix(in srgb, var(--fg) 6%, transparent);
  box-shadow: inset 0 0 0 1px color-mix(in srgb, var(--fg) 10%, transparent);
  color: var(--ink);
  font: inherit;
  font-size: 14px;
  line-height: 1;
  cursor: pointer;
  transition: background-color 160ms ease, color 160ms ease;
}
.btn svg { width: 14px; height: 14px; }
.btn:hover { background: color-mix(in srgb, var(--fg) 12%, transparent); }
.btn:focus-visible { outline: none; box-shadow: inset 0 0 0 1px color-mix(in srgb, var(--accent) 60%, transparent), 0 0 0 3px color-mix(in srgb, var(--accent) 25%, transparent); }
.btn--primary { background: var(--accent); color: var(--night); box-shadow: none; }
.btn--primary:hover { background: color-mix(in srgb, var(--accent) 88%, var(--night)); }
.btn[disabled] { opacity: 0.4; cursor: default; }
.btn[disabled]:hover { background: color-mix(in srgb, var(--fg) 6%, transparent); }

/* --- swatches --- */
.swatches { display: flex; gap: 4px; }
.swatch {
  width: 100%;
  height: 20px;
  border-radius: 4px;
  box-shadow: inset 0 0 0 1px color-mix(in srgb, var(--fg) 12%, transparent);
}

/* ---------------------------------------------------------------- folds -- */
/* ≤900 the chrome tightens to ravivasavan.com's phone numbers, the tools row
   scrolls sideways rather than shedding its labels, and the sheet swings down
   to the bottom of the screen. */

@media (max-width: 900px) {
  :root {
    --chrome-top: 16px;
    --chrome-rule: 96px;
    --chrome-tools: 112px;
    --chrome-pad: 16px;
    /* A thumb's disc rather than a pointer's. */
    --sheet-min: 56px;
  }

  /* On an experiment the role is the experiment's name and the tool row says
     it again; on the landing page it is the tagline and nothing else does, so
     that copy only goes at 640. */
  .chrome .header-pill__role { display: none; }

  /* Both selectors, because the sheet-page rule above is the more specific
     one and would otherwise keep its desktop inset here. */
  .tools,
  body:has(.sheet) .tools {
    left: 0;
    right: 0;
    padding-inline: var(--chrome-pad);
  }
  /* Folded there's no sheet beside the row to align against, so it sits at
     the left edge rather than hanging off the right. */
  .tools > :first-child { margin-left: 0; }

  /* --- bottom sheet --- */
  /* The material is the same as the pane's; only the geometry swings down. */
  .sheet {
    top: auto;
    /* Right-anchored and sized, not stretched between two edges: the same
       reason the pane is, so the width can travel to the minimised disc. */
    left: auto;
    right: var(--panel-gap);
    bottom: var(--panel-gap);
    width: calc(100% - var(--panel-gap) * 2);
    height: 60px;
    padding: 0;
    gap: 0;
    overflow: hidden;
    transition:
      height        320ms cubic-bezier(0.4, 0, 0.2, 1),
      width         240ms cubic-bezier(0.4, 0, 0.2, 1),
      border-radius 240ms cubic-bezier(0.4, 0, 0.2, 1);
  }
  /* --sheet-full is measured by play.js when the sheet opens, so the glass
     grows to its content and no further — and, on a landscape phone, no
     higher than the tools row, which draws above the sheet and would
     otherwise land on top of its own head. */
  .sheet.is-open {
    height: min(
      72vh,
      var(--sheet-full, 72vh),
      max(60px, calc(100vh - var(--chrome-tools) - 44px - var(--panel-gap) * 2))
    );
  }
  .sheet__grab { display: block; }
  .sheet__head {
    flex: 0 0 60px;
    height: 60px;
    padding: 14px var(--panel-gap) 0;
    cursor: pointer;
    user-select: none;
    -webkit-user-select: none;
  }
  .sheet__body {
    overflow-y: auto;
    overscroll-behavior: contain;
    scrollbar-width: none;
    padding: 4px var(--panel-gap) var(--panel-gap);
  }
  .sheet__body::-webkit-scrollbar { display: none; }
}

@media (max-width: 640px) {
  :root {
    --chrome-top: 12px;
    --chrome-rule: 88px;
    --chrome-tools: 104px;
    --chrome-pad: 12px;
    --panel-gap: 12px;
  }
  /* Folded hardest, the landing row is a flex bar edge to edge rather than a
     centred grid — the same collapse the main site makes. */
  .header-pills {
    display: flex;
    align-items: center;
    justify-content: space-between;
    grid-template-columns: none;
    padding: 0 var(--chrome-pad);
    gap: 8px;
  }
  .header-pills > .icon-pill--home,
  .header-pills > .icon-pill--theme,
  .header-pills > .header-pill { grid-column: auto; justify-self: auto; margin: 0; }
  .header-pills .header-pill__role { display: none; }
  .header-pill__inner { gap: 8px; }
  .header-pill__label { line-height: 1; }
  .header-pill__name { font-size: 14px; }
}

/* ------------------------------------------------------------ minimised -- */
/* The whole sheet folds into one disc of the same glass, anchored where its
   top-right corner was — 64px on a pointer, 56px on a thumb — and the disc's
   face is the button that brings it back.

   The state lives on <html>, not on the sheet: play.js stamps the attribute
   the moment it runs, which is before first paint and before a page that
   builds its sheet in script even has one to stamp, so a sheet that was left
   minimised never flashes open. Written after the folds so it wins at every
   width against .sheet and .sheet.is-open alike. */

:root[data-play-sheet="min"] .sheet {
  width: var(--sheet-min);
  height: var(--sheet-min);
  border-radius: calc(var(--sheet-min) / 2);
  padding: 0;
  gap: 0;
  overflow: hidden;
}
:root[data-play-sheet="min"] .sheet > :not(.sheet__icon) { display: none; }
:root[data-play-sheet="min"] .sheet__icon { display: grid; }

@media (max-width: 900px) {
  /* Folded, the disc keeps the bottom sheet's right-hand end and lifts clear
     of the home indicator. */
  :root[data-play-sheet="min"] .sheet {
    bottom: calc(var(--panel-gap) + env(safe-area-inset-bottom, 0px));
  }
}

/* ----------------------------------------------------------- preferences -- */

/* Some people get motion sick from a blur that samples a moving page, and some
   just want the text solid. Both get an opaque panel instead. */
@media (prefers-reduced-transparency: reduce) {
  .glass,
  .sheet,
  .tool-pill,
  .header-pill,
  .icon-pill {
    background: color-mix(in srgb, var(--bg) 96%, transparent);
    backdrop-filter: none;
    -webkit-backdrop-filter: none;
  }
}

/* Scoped to the chrome deliberately: an experiment's own motion is the point of
   the page, and each one handles reduced motion itself. */
@media (prefers-reduced-motion: reduce) {
  body,
  .sheet,
  .sheet__min,
  .sheet__icon,
  .switch__track,
  .switch__track::after,
  .tool-pill,
  .icon-pill::before,
  .icon-pill__icon svg,
  .tool-pill__icon svg,
  .dial::-webkit-slider-thumb { transition: none; }
}
